Skip to content

Commit

Permalink
feat: actual random mob movement
Browse files Browse the repository at this point in the history
  • Loading branch information
pikdum committed Sep 13, 2024
1 parent 678009b commit add0120
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 29 deletions.
4 changes: 3 additions & 1 deletion lib/game.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/game/chat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions lib/game/combat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
<<guid::little-size(64)>> = body
{:continue, Map.put(state, :target, guid)}
end

def handle_attack_swing(state) do
case Map.fetch(state, :attacking) do
{:ok, target_guid} ->
Expand Down
181 changes: 153 additions & 28 deletions lib/game/mob.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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}

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions lib/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,39 @@ defmodule ThistleTea.Util do
{:binary.decode_unsigned(<<guid::64>>), 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, <<>>}

Expand Down
14 changes: 14 additions & 0 deletions test/thistle_tea_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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>>
Expand All @@ -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

0 comments on commit add0120

Please sign in to comment.