diff --git a/lib/game.ex b/lib/game.ex index cd788f6..9706a0a 100644 --- a/lib/game.ex +++ b/lib/game.ex @@ -44,11 +44,13 @@ defmodule ThistleTea.Game do @cmsg_attackswing 0x141 @cmsg_attackstop 0x142 @cmsg_setsheathed 0x1E0 + @cmsg_set_selection 0x13D @combat_opcodes [ @cmsg_attackswing, @cmsg_attackstop, - @cmsg_setsheathed + @cmsg_setsheathed, + @cmsg_set_selection ] @cmsg_player_login 0x03D diff --git a/lib/game/chat.ex b/lib/game/chat.ex index 4ddf432..ef61efd 100644 --- a/lib/game/chat.ex +++ b/lib/game/chat.ex @@ -142,6 +142,17 @@ defmodule ThistleTea.Game.Chat do end end + def handle_chat(state, _, _, ".move" <> _, _) do + with target <- Map.get(state, :target), + pid <- :ets.lookup_element(:entities, target, 2, nil), + %{x: x, y: y, z: z} <- Map.get(state.character, :movement) do + GenServer.cast(pid, {:move_to, x, y, z}) + state + else + nil -> state + end + end + def handle_chat(state, chat_type, language, message, _target_name) when chat_type in [@chat_type_say, @chat_type_yell, @chat_type_emote] do packet = messagechat_packet(chat_type, language, message, state.guid, nil) diff --git a/lib/game/combat.ex b/lib/game/combat.ex index 44a4bab..85bdfcb 100644 --- a/lib/game/combat.ex +++ b/lib/game/combat.ex @@ -9,6 +9,8 @@ defmodule ThistleTea.Game.Combat do @cmsg_attackstop 0x142 @cmsg_setsheathed 0x1E0 + @cmsg_set_selection 0x13D + @smsg_attackstart 0x143 @smsg_attackstop 0x144 @@ -72,6 +74,11 @@ defmodule ThistleTea.Game.Combat do {:continue, Map.put(state, :character, character)} end + def handle_packet(@cmsg_set_selection, body, state) do + <> = body + {:continue, Map.put(state, :target, guid)} + end + def handle_attack_swing(state) do case Map.fetch(state, :attacking) do {:ok, target_guid} -> diff --git a/lib/game/mob.ex b/lib/game/mob.ex index 6e6bbfe..b916c3b 100644 --- a/lib/game/mob.ex +++ b/lib/game/mob.ex @@ -2,7 +2,13 @@ defmodule ThistleTea.Mob do use GenServer import ThistleTea.Game.UpdateObject, only: [generate_packet: 4] - import ThistleTea.Util, only: [pack_guid: 1, random_int: 2] + + import ThistleTea.Util, + only: [ + pack_guid: 1, + random_int: 2, + calculate_movement_duration: 3 + ] require Logger @@ -19,10 +25,12 @@ defmodule ThistleTea.Mob do @update_flag_living 0x20 # @movement_flag_forward 0x00000001 - @movement_flag_fixed_z 0x00000800 + # @movement_flag_fixed_z 0x00000800 @smsg_attackerstateupdate 0x14A + @smsg_monster_move 0x0DD + def start_link(creature) do GenServer.start_link(__MODULE__, creature) end @@ -37,16 +45,46 @@ defmodule ThistleTea.Mob do {x_new, y_new} end + def move_packet(state, {x0, y0, z0}, {x1, y1, z1}, duration) do + packed_guid = state.packed_guid + + move_type = 0 + spline_id = random_int(1, 10_000_000) + spline_flags = 0 + spline_count = 1 + + # TODO: figure out how to do multiple spline points + packed_guid <> + << + # initial position + x0::little-float-size(32), + y0::little-float-size(32), + z0::little-float-size(32), + spline_id::little-size(32), + move_type::little-size(8), + spline_flags::little-size(32), + duration::little-size(32), + spline_count::little-size(32), + # target position + x1::little-float-size(32), + y1::little-float-size(32), + z1::little-float-size(32) + >> + end + def random_movement(state) do - o2 = :rand.uniform(2 * 31415) / 10000.0 + with [] <- Map.get(state, :path, []), + nil <- Map.get(state, :path_timer) do + %{map: map} = state.creature + %{x0: x0, y0: y0, z0: z0} = state - %{ - state - | creature: %{ - state.creature - | orientation: o2 - } - } + {x1, y1, z1} = + ThistleTea.Pathfinding.find_random_point_around_circle(map, {x0, y0, z0}, 10.0) + + state |> move_to({x1, y1, z1}) + else + _ -> state + end end def take_damage(state, damage) do @@ -115,6 +153,12 @@ defmodule ThistleTea.Mob do end end + defp randomize_rate(state) do + # TODO: test different rates + update_rate = :rand.uniform(90_000) + state |> Map.put(:update_rate, update_rate) + end + @impl GenServer def init(creature) do creature = Map.put(creature, :guid, creature.guid + @creature_guid_offset) @@ -129,8 +173,6 @@ defmodule ThistleTea.Mob do creature.position_z ) - update_rate = :rand.uniform(4_000) + 1_000 - # :idle - do nothing # :random_movement - move around randomly @@ -141,20 +183,26 @@ defmodule ThistleTea.Mob do :idle end - {:ok, - %{ - default_behavior: default_behavior, - current_behavior: :idle, - creature: creature, - packed_guid: pack_guid(creature.guid), - # extract out some initial values? - movement_flags: @movement_flag_fixed_z, - max_health: creature.curhealth, - max_mana: creature.curmana, - update_rate: update_rate, - level: - random_int(creature.creature_template.min_level, creature.creature_template.max_level) - }} + state = + %{ + default_behavior: default_behavior, + current_behavior: :idle, + creature: creature, + packed_guid: pack_guid(creature.guid), + # extract out some initial values? + # movement_flags: @movement_flag_fixed_z, + movement_flags: 0, + max_health: creature.curhealth, + max_mana: creature.curmana, + level: + random_int(creature.creature_template.min_level, creature.creature_template.max_level), + x0: creature.position_x, + y0: creature.position_y, + z0: creature.position_z + } + |> randomize_rate() + + {:ok, state} end @impl GenServer @@ -165,8 +213,7 @@ defmodule ThistleTea.Mob do {:noreply, state} {_, :random_movement} -> - state = random_movement(state) - send_updates(state) + state = random_movement(state) |> randomize_rate() Process.send_after(self(), :behavior_event, state.update_rate) {:noreply, state} @@ -175,6 +222,12 @@ defmodule ThistleTea.Mob do end end + @impl GenServer + def handle_info(:follow_path, state) do + state = follow_path(state) + {:noreply, state} + end + @impl GenServer def handle_info(:respawn, state) do if state.current_behavior != :idle do @@ -218,6 +271,12 @@ defmodule ThistleTea.Mob do end end + @impl GenServer + def handle_cast({:move_to, x, y, z}, state) do + state = state |> move_to({x, y, z}) + {:noreply, state} + end + @impl GenServer def handle_cast({:receive_spell, caster, _spell_id}, state) do # TODO: look up and apply spell effects @@ -264,6 +323,72 @@ defmodule ThistleTea.Mob do {:noreply, state} end + def send_movement_packet(state, payload) do + %{position_x: x0, position_y: y0, position_z: z0, map: map} = state.creature + nearby_players = SpatialHash.query(:players, map, x0, y0, z0, 250) + + for {_guid, pid, _distance} <- nearby_players do + GenServer.cast(pid, {:send_packet, @smsg_monster_move, payload}) + end + + state + end + + def move_to(state, {x, y, z}) do + %{position_x: x0, position_y: y0, position_z: z0, map: map} = state.creature + path = ThistleTea.Pathfinding.find_path(map, {x0, y0, z0}, {x, y, z}) + state |> Map.put(:path, path) |> follow_path() + end + + def queue_follow_path(state, delay) do + if Map.get(state, :path, []) |> Enum.count() > 0 do + path_timer = Process.send_after(self(), :follow_path, delay) + state |> Map.put(:path_timer, path_timer) + else + state |> Map.delete(:path_timer) + end + end + + def follow_path(state) do + case Map.get(state, :path) do + [{x1, y1, z1} | rest] -> + %{position_x: x0, position_y: y0, position_z: z0} = state.creature + speed = state.creature.creature_template.speed_walk + + duration = + (calculate_movement_duration({x0, y0, z0}, {x1, y1, z1}, speed) * 1_000) |> trunc() + + packet = move_packet(state, {x0, y0, z0}, {x1, y1, z1}, duration) + + # TODO: this will be ahead of actual movement + # could maybe start a timer to update in intervals? + creature = + state.creature + |> Map.put(:position_x, x1) + |> Map.put(:position_y, y1) + |> Map.put(:position_z, z1) + + SpatialHash.update( + :mobs, + creature.guid, + self(), + creature.map, + creature.position_x, + creature.position_y, + creature.position_z + ) + + state + |> Map.put(:path, rest) + |> Map.put(:creature, creature) + |> send_movement_packet(packet) + |> queue_follow_path(duration) + + _ -> + state + end + end + def update_packet(state, movement_flags \\ 0) do fields = %{ object_guid: state.creature.guid, diff --git a/lib/util.ex b/lib/util.ex index 0f22e6b..ad68665 100644 --- a/lib/util.ex +++ b/lib/util.ex @@ -82,6 +82,39 @@ defmodule ThistleTea.Util do {:binary.decode_unsigned(<>), remaining_data} end + def pack_vector({x, y, z}) do + x_packed = Bitwise.band(trunc(x / 0.25), 0x7FF) + y_packed = Bitwise.band(trunc(y / 0.25), 0x7FF) + z_packed = Bitwise.band(trunc(z / 0.25), 0x3FF) + + x_packed + |> Bitwise.bor(Bitwise.bsl(y_packed, 11)) + |> Bitwise.bor(Bitwise.bsl(z_packed, 22)) + end + + def unpack_vector(packed) do + x = Bitwise.band(packed, 0x7FF) / 4 + y = Bitwise.band(Bitwise.bsr(packed, 11), 0x7FF) / 4 + z = Bitwise.band(Bitwise.bsr(packed, 22), 0x3FF) / 4 + + {x, y, z} + end + + def calculate_movement_duration({x0, y0, z0}, {x1, y1, z1}, speed) + when is_float(speed) and speed > 0 do + distance = :math.sqrt(:math.pow(x1 - x0, 2) + :math.pow(y1 - y0, 2) + :math.pow(z1 - z0, 2)) + duration = distance / speed + duration + end + + def calculate_total_duration(path_list, speed) + when is_list(path_list) and length(path_list) > 1 do + path_list + |> Enum.chunk_every(2, 1, :discard) + |> Enum.map(fn [start, finish] -> calculate_movement_duration(start, finish, speed) end) + |> Enum.sum() + end + def parse_string(payload, pos \\ 1) def parse_string(payload, _pos) when byte_size(payload) == 0, do: {:ok, payload, <<>>} diff --git a/test/thistle_tea_test.exs b/test/thistle_tea_test.exs index bce38b5..efa6cae 100644 --- a/test/thistle_tea_test.exs +++ b/test/thistle_tea_test.exs @@ -3,6 +3,8 @@ defmodule ThistleTeaTest do import ThistleTea.Mob import ThistleTea.Util + require Logger + test "future_position" do assert future_position(0, 0, 0, 1, 1) == {1, 0} assert future_position(0, 0, 0, 10, 1) == {10, 0} @@ -11,6 +13,12 @@ defmodule ThistleTeaTest do assert x == -10 end + test "movement duration" do + assert calculate_movement_duration({0.0, 0.0, 0.0}, {3.0, 4.0, 0.0}, 1.0) == 5.0 + path = [{0.0, 0.0, 0.0}, {3.0, 4.0, 0.0}, {3.0, 4.0, 5.0}] + assert calculate_total_duration(path, 1.0) === 10.0 + end + test "pack guid" do guid = 0x123 extra = <<0xAA>> @@ -19,4 +27,10 @@ defmodule ThistleTeaTest do assert unpacked == guid assert rest == extra end + + test "pack vector" do + vector = {1, 2, 3} + packed = pack_vector(vector) + assert unpack_vector(packed) == vector + end end