Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d72012
Skeleton
EtienneDesticourt Aug 8, 2016
e9913c3
Seek logic rework, removed channel callback
EtienneDesticourt Aug 9, 2016
59fa130
Fixed seek logic and added tests, fixed movement update, added ref to…
EtienneDesticourt Aug 10, 2016
3792cbd
Minor rename for clarity
EtienneDesticourt Aug 10, 2016
1fd1a23
Modified Seek to listen for :entity_change on Position
EtienneDesticourt Aug 10, 2016
239f2cc
:pos -> :coord
EtienneDesticourt Aug 10, 2016
165397c
Modified way AI behaviours are chosen for an npc
EtienneDesticourt Aug 10, 2016
f382506
Removed added whitespace
EtienneDesticourt Aug 10, 2016
e9c9693
Merge remote-tracking branch 'refs/remotes/origin/master' into AI_Pro…
EtienneDesticourt Aug 10, 2016
6fdee5b
:pos -> :coord
EtienneDesticourt Aug 10, 2016
f15a24f
Whitespace removal
EtienneDesticourt Aug 11, 2016
b2a2252
Added auto pos update skeleton
EtienneDesticourt Aug 11, 2016
12af7b6
Cleaned up seeking handler, fixed movement update handler
EtienneDesticourt Aug 15, 2016
260666f
Removed excess linebreak
EtienneDesticourt Aug 15, 2016
f718ddf
Naming changes
EtienneDesticourt Aug 16, 2016
38b65ca
Updated deps
EtienneDesticourt Oct 10, 2016
f819c1d
Put better default aggro/escape dist
EtienneDesticourt Oct 10, 2016
f7ab019
Set seeking as default npc behaviour, started auto updates at behavio…
EtienneDesticourt Oct 10, 2016
0c18581
Fixed tests
EtienneDesticourt Oct 10, 2016
d93eeb8
Changed event from map to atom, fixed seeks option
EtienneDesticourt Oct 11, 2016
e6dc0a6
Added geom deps, replaced Coord with Vector2D, added NavMesh to maps
EtienneDesticourt Oct 14, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions lib/entice/logic/ai/seek.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Entice.Logic.Seek do
alias Entice.Entity
alias Entice.Logic.{Seek, Player.Position, Npc, Movement}
alias Geom.Shape.{Path, Vector}
alias Geom.Ai.Astar

defstruct target: nil, aggro_distance: 1000, escape_distance: 2000, path: []

def register(entity),
do: Entity.put_behaviour(entity, Seek.Behaviour, [])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens with this behaviour if no params is given? Does it even work then?


def register(entity, aggro_distance, escape_distance)
when is_integer(aggro_distance) and is_integer(escape_distance),
do: Entity.put_behaviour(entity, Seek.Behaviour, %{aggro_distance: aggro_distance, escape_distance: escape_distance})

def unregister(entity),
do: Entity.remove_behaviour(entity, Seek.Behaviour)

#TODO: Add team attr to determine who should be attacked by whom
defmodule Behaviour do
use Entice.Entity.Behaviour

def init(entity, %{aggro_distance: aggro_distance, escape_distance: escape_distance}),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to check already here if all the attributes we require being present are actually there...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean by that. We match on aggro_distance and escape_distance, if they're not there we get the default init, no?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I meant Attributes, as in if the entity has the attributes we need, i.e. Movement etc.

do: {:ok, entity |> put_attribute(%Seek{aggro_distance: aggro_distance, escape_distance: escape_distance})}

def init(entity, _args),
do: {:ok, entity |> put_attribute(%Seek{})}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, no args = what behaviour?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right I'll add an init that only take an entity

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I meant: Do we need this case? Is this actually any use without parameters?


#No introspection for npcs ;)
def handle_event({:entity_change, %{entity_id: eid}}, %Entity{id: eid} = entity),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we can actually receive our own updates, can you verify?

do: {:ok, entity}

def handle_event({:entity_change, %{changed: %{Position => %Position{coord: mover_coord}}, entity_id: moving_entity_id}},
%Entity{attributes: %{Position => %Position{coord: my_coord},
Movement => _,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we check for Movement here? I mean, if we need it for the behaviour, why not using the deconstructed variable?

Npc => %Npc{init_coord: init_coord},
Seek => %Seek{aggro_distance: aggro_distance, escape_distance: escape_distance, target: target}}} = entity) do
case target do
nil ->
if in_aggro_range?(my_coord, mover_coord, aggro_distance) do
{:ok, entity |> seek_target_current_coord(moving_entity_id, mover_coord)}
else
{:ok, entity}
end

^moving_entity_id ->
if past_escape_range?(init_coord, mover_coord, escape_distance) do
{:ok, entity |> return_to_spawn(my_coord, init_coord)}
else
{:ok, entity |> seek_target_current_coord(moving_entity_id, mover_coord)}
end

_ -> {:ok, entity}
end
end

def terminate(_reason, entity),
do: {:ok, entity |> remove_attribute(Seek)}

defp in_aggro_range?(my_coord, mover_coord, aggro_distance),
do: Vector.dist(my_coord, mover_coord) <= aggro_distance

defp past_escape_range?(init_coord, mover_coord, escape_distance),
do: Vector.dist(init_coord, mover_coord) >= escape_distance

defp seek_target_current_coord(%Entity{attributes: %{Position => %Position{coord: my_coord},
Npc => %Npc{map: map}, Seek => %Seek{path: path}}} = entity, target_id, target_coord) do
{success, result} = Astar.get_path(map.nav_mesh, my_coord, target_coord)

%Path{vertices: new_path} = case success do
:ok -> result

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting this function and the other below would be nice ;)

:error -> Path.empty
end

entity |> update_attribute(Seek, fn(s) -> %Seek{s | target: target_id, path: new_path} end)
end

defp return_to_spawn(%Entity{attributes: %{Npc => %Npc{map: map}}} = entity, my_coord, spawn_coord) do
{success, result} = Astar.get_path(map.nav_mesh, my_coord, spawn_coord)

%Path{vertices: new_path} = case success do
:ok -> result

:error -> Path.empty
end

entity |> update_attribute(Seek, fn(s) -> %Seek{s | target: nil, path: new_path} end)
end
end
end
2 changes: 1 addition & 1 deletion lib/entice/logic/attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Entice.Logic.Attributes do

defmacro __using__(_) do
quote do
alias Entice.Utils.Geom.Coord
alias Geom.Shape.Vector2D
alias Entice.Logic.Player.Name
alias Entice.Logic.Player.Position
alias Entice.Logic.Player.MapInstance
Expand Down
2 changes: 1 addition & 1 deletion lib/entice/logic/map_instance.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defmodule Entice.Logic.MapInstance do
def handle_event(
{:map_instance_npc_add, %{name: name, model: model, position: position}},
%Entity{attributes: %{MapInstance => %MapInstance{map: map}}} = entity) do
{:ok, eid, _pid} = Npc.spawn(name, model, position)
{:ok, eid, _pid} = Npc.spawn(map, name, model, position, seeks: true)
Coordination.register(eid, map) # TODO change map to something else if we have multiple instances
{:ok, entity}
end
Expand Down
13 changes: 8 additions & 5 deletions lib/entice/logic/maps/map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Entice.Logic.Map do
Is mainly used in area.ex where all the maps are defined.
"""
import Inflex
alias Entice.Utils.Geom.Coord
alias Geom.Shape.Vector2D

defmacro __using__(_) do
quote do
Expand All @@ -17,16 +17,18 @@ defmodule Entice.Logic.Map do


defmacro defmap(mapname, opts \\ []) do
spawn = Keyword.get(opts, :spawn, quote do %Coord{} end)
spawn = Keyword.get(opts, :spawn, quote do %Vector2D{} end)
outpost = Keyword.get(opts, :outpost, quote do true end)
nav_mesh = Keyword.get(opts, :nav_mesh, quote do nil end)

map_content = content(Macro.to_string(mapname))
quote do
defmodule unquote(mapname) do
alias Entice.Utils.Geom.Coord
alias Geom.Shape.Vector2D
unquote(map_content)
def spawn, do: unquote(spawn)
def is_outpost?, do: unquote(outpost)
def nav_mesh, do: unquote(nav_mesh)
end
@maps [ unquote(mapname) | @maps ]
end
Expand Down Expand Up @@ -58,10 +60,11 @@ defmodule Entice.Logic.Map do
def name, do: unquote(name)
def underscore_name, do: unquote(uname)

def spawn, do: %Coord{}
def spawn, do: %Vector2D{}
def is_outpost?, do: true
def nav_mesh, do: nil

defoverridable [spawn: 0, is_outpost?: 0]
defoverridable [spawn: 0, is_outpost?: 0, nav_mesh: 0]
end
end
end
12 changes: 7 additions & 5 deletions lib/entice/logic/maps/maps.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
defmodule Entice.Logic.Maps do
use Entice.Logic.Map
alias Geom.Shape.NavigationMesh, as: NavMesh
alias Geom.Utils.Serializer


# Lobby is for special client entities that represent a logged in client.
defmap Lobby

# Outposts...
defmap HeroesAscent, spawn: %Coord{x: 2017, y: -3241}
defmap RandomArenas, spawn: %Coord{x: 3854, y: 3874}
defmap TeamArenas, spawn: %Coord{x: -1873, y: 352}
defmap HeroesAscent, spawn: %Vector2D{x: 2017, y: -3241}, nav_mesh: Serializer.deserialize(%NavMesh{}, "data/heroes_ascent.map"), outpost: true
defmap RandomArenas, spawn: %Vector2D{x: 3854, y: 3874}
defmap TeamArenas, spawn: %Vector2D{x: -1873, y: 352}

# Explorables...
defmap GreatTempleOfBalthazar, spawn: %Coord{x: -6558, y: -6010}, outpost: false # faked for testing purpose
defmap IsleOfTheNameless, spawn: %Coord{x: -6036, y: -2519}, outpost: false
defmap GreatTempleOfBalthazar, spawn: %Vector2D{x: -6558, y: -6010}, outpost: false # faked for testing purpose
defmap IsleOfTheNameless, spawn: %Vector2D{x: -6036, y: -2519}, outpost: false


def default_map, do: HeroesAscent
Expand Down
81 changes: 76 additions & 5 deletions lib/entice/logic/movement.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
defmodule Entice.Logic.Movement do
alias Entice.Entity
alias Entice.Utils.Geom.Coord
alias Entice.Logic.{Movement, Player.Position}
alias Entice.Logic.{Movement, Player.Position, Seek}
alias Geom.Shape.{Vector, Vector2D}

@update_interval 50
@speed 288 #TODO: Figure out the velocity/speed naming business
@epsilon 10

@doc """
Note that velocity is actually a coefficient for the real velocity thats used inside
the client, but for simplicities sake we used velocity as a name.
"""
defstruct goal: %Coord{}, plane: 1, move_type: 9, velocity: 1.0
defstruct goal: %Vector2D{x: 0, y: 0}, plane: 1, move_type: 9, velocity: 1.0, auto_updating?: false


def register(entity),
do: Entity.put_behaviour(entity, Movement.Behaviour, [])

def register(entity, auto_updating?: auto_updating?),
do: Entity.put_behaviour(entity, Movement.Behaviour, auto_updating?: auto_updating?)

def unregister(entity),
do: Entity.remove_behaviour(entity, Movement.Behaviour)
Expand All @@ -30,19 +35,85 @@ defmodule Entice.Logic.Movement do
end)
end

def update_interval, do: @update_interval
def speed, do: @speed
def epsilon, do: @epsilon


defmodule Behaviour do
use Entice.Entity.Behaviour

def init(%Entity{attributes: %{Movement => _}} = entity, _args),
do: {:ok, entity}

def init(%Entity{attributes: %{Position => %Position{pos: pos, plane: plane}}} = entity, _args),
do: {:ok, entity |> put_attribute(%Movement{goal: pos, plane: plane})}
def init(%Entity{attributes: %{Position => %Position{coord: coord, plane: plane}}} = entity, auto_updating?: true) do
self |> Process.send_after(:movement_calculate_next, 1)
{:ok, entity |> put_attribute(%Movement{goal: coord, plane: plane, auto_updating?: true})}
end

def init(%Entity{attributes: %{Position => %Position{coord: coord, plane: plane}}} = entity, _args),
do: {:ok, entity |> put_attribute(%Movement{goal: coord, plane: plane})}

def init(entity, _args),
do: {:ok, entity |> put_attribute(%Movement{})}

#TODO: Move all this logic to seek
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea ;)

def handle_event(:movement_calculate_next,
%Entity{attributes: %{Movement => %Movement{velocity: velocity, auto_updating?: auto_updating?, goal: goal},
Seek => %Seek{path: path},
Position => %Position{coord: current_pos}}} = entity) do
#Determine next goal
{entity, goal} = case Enum.count(path) do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we can also simply match on the List like:

[_ | _] -> ...
_ -> {entity, goal}

0 ->
{entity, goal} #Empty path

1 ->
{entity, goal} #Path only has starting pos left

_ ->
cond do
Vector.equal(goal, current_pos, Movement.epsilon) -> #Reached goal
new_goal = Enum.at(path, 1) #len path >= 2 so we know next is not nil
entity = entity
|> update_attribute(Movement, fn(m) -> %Movement{m | goal: new_goal} end)
|> update_attribute(Seek, fn(s) -> %Seek{s | path: Enum.drop(path, 1)} end)
{entity, new_goal}

true ->
{entity, goal}
end
end

#Advance pos if not at new (or unchanged) goal
next_pos = cond do
Vector.equal(goal, current_pos, Movement.epsilon) ->
current_pos

true ->
{:ok, next_pos} = calc_next_pos(current_pos, goal, velocity)
next_pos
end

entity = entity |> update_attribute(Position, fn(p) -> %Position{p | coord: next_pos} end)
if auto_updating?, do: self |> Process.send_after(:movement_calculate_next, Movement.update_interval)
{:ok, entity}
end

defp calc_next_pos(%Vector2D{} = current_pos, %Vector2D{} = goal, velocity) do
direction = Vector.sub(goal, current_pos)
cond do
#Convoluted cond because somehow %Vector2D{x: 0, y: 0} != %Vector2D{x: 0.0, y: 0.0} in case
Vector.equal(direction, %Vector2D{x: 0, y: 0}, 0) ->
{:ok, current_pos}

true ->
unit = Vector.unit(direction)
offset = Vector.mul(unit, velocity * Movement.speed * Movement.update_interval / 1000)
{:ok, Vector.add(current_pos, offset)}
end
end

defp calc_next_pos(_,_,_), do: {:error, :wrong_arguments}

def terminate(_reason, entity),
do: {:ok, entity |> remove_attribute(Movement)}
Expand Down
22 changes: 14 additions & 8 deletions lib/entice/logic/npc.ex
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
defmodule Entice.Logic.Npc do
use Entice.Logic.Map
alias Entice.Entity
alias Entice.Logic.{Npc, Vitals}
alias Entice.Logic.{Npc, Vitals, Movement, Seek}
alias Entice.Logic.Player.{Name, Position, Level}

defstruct npc_model_id: :dhuum, init_coord: %Position{}, map: nil

defstruct(npc_model_id: :dhuum)


def spawn(name, model, %Position{} = position)
#I'd rather pass something like fn(pos1, pos2) -> Geom.Ai.Astar.findpath(map.nav_mesh, pos1, pos2) end
#from map_instance and then override func in this module but can't figure it out
def spawn(map, name, model, %Position{} = position, opts \\ [])
when is_binary(name) and is_atom(model) do
{:ok, id, pid} = Entity.start()
Npc.register(id, name, model, position)
Npc.register(id, map, name, model, position)
Vitals.register(id)
if opts[:seeks] do
Seek.register(id)
Movement.register(id, auto_updating?: true)
else
Movement.register(id)
end
{:ok, id, pid}
end


def register(entity, name, model, %Position{} = position)
def register(entity, map, name, model, %Position{} = position)
when is_binary(name) and is_atom(model) do
entity |> Entity.attribute_transaction(fn (attrs) ->
attrs
|> Map.put(Name, %Name{name: name})
|> Map.put(Position, position)
|> Map.put(Npc, %Npc{npc_model_id: model})
|> Map.put(Npc, %Npc{npc_model_id: model, init_coord: position.coord, map: map})
|> Map.put(Level, %Level{level: 20})
end)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/entice/logic/player.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ defmodule Entice.Logic.Player do
Responsible for the basic player stats.
"""
alias Entice.Entity
alias Entice.Utils.Geom.Coord
alias Geom.Shape.Vector2D


defmodule Name, do: defstruct(
name: "Unknown Entity")

defmodule Position, do: defstruct(
pos: %Coord{},
coord: %Vector2D{},
plane: 1)

defmodule Appearance, do: defstruct(
Expand All @@ -31,7 +31,7 @@ defmodule Entice.Logic.Player do
entity |> Entity.attribute_transaction(fn (attrs) ->
attrs
|> Map.put(Name, %Name{name: name})
|> Map.put(Position, %Position{pos: map.spawn})
|> Map.put(Position, %Position{coord: map.spawn})
|> Map.put(Appearance, appearance)
|> Map.put(Level, %Level{level: 20})
end)
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Entice.Logic.Mixfile do
[{:entice_entity, github: "entice/entity", ref: "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a"},
{:uuid, "~> 1.1"},
{:inflex, "~> 1.5"},
{:pipe, "~> 0.0.2"}]
{:pipe, "~> 0.0.2"},
{:geom, "~> 1.0"}]
end
end
8 changes: 5 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
%{"entice_entity": {:git, "https://github.com/entice/entity.git", "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a", [ref: "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a"]},
"entice_utils": {:git, "https://github.com/entice/utils.git", "79ead4dca77324b4c24f584468edbaff2029eeab", [ref: "79ead4dca77324b4c24f584468edbaff2029eeab"]},
"inflex": {:hex, :inflex, "1.5.0"},
"pipe": {:hex, :pipe, "0.0.2"},
"uuid": {:hex, :uuid, "1.1.3"}}
"geom": {:hex, :geom, "1.0.0", "b95356b34025b71096db92a6c3be47e7729db6983223a6d03cdff85aec01c3f5", [:mix], [{:poison, "~> 2.0 or ~>3.0", [hex: :poison, optional: false]}]},
"inflex": {:hex, :inflex, "1.5.0", "e4ff5d900280b2011b24d1ac1c4590986ee5add2ea644c9894e72213cf93ff0b", [:mix], []},
"pipe": {:hex, :pipe, "0.0.2", "eff98a868b426745acef103081581093ff5c1b88100f8ff5949b4a30e81d0d9f", [:mix], []},
"poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []},
"uuid": {:hex, :uuid, "1.1.3", "06ca38801a1a95b751701ca40716bb97ddf76dfe7e26da0eec7dba636740d57a", [:mix], []}}
Loading