├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── entice │ └── logic │ ├── attributes.ex │ ├── casting.ex │ ├── group.ex │ ├── map_instance.ex │ ├── map_registry.ex │ ├── maps │ ├── map.ex │ └── maps.ex │ ├── movement.ex │ ├── npc.ex │ ├── player.ex │ ├── skillbar.ex │ ├── skills │ ├── skill.ex │ └── skills.ex │ └── vitals.ex ├── mix.exs ├── mix.lock └── test ├── entice └── logic │ ├── casting_test.exs │ ├── group_test.exs │ ├── map_instance_test.exs │ ├── map_registry_test.exs │ ├── maps │ └── maps_test.exs │ ├── movement_test.exs │ ├── npc_test.exs │ ├── player_test.exs │ ├── skillbar_test.exs │ ├── skills │ └── skills_test.exs │ └── vitals_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | sudo: false 3 | 4 | otp_release: 5 | - 18.2 6 | 7 | before_install: 8 | - wget http://s3.hex.pm/builds/elixir/v1.2.4.zip 9 | - unzip -d elixir v1.2.4.zip 10 | 11 | before_script: 12 | - export PATH=`pwd`/elixir/bin:$PATH 13 | - mix local.hex --force 14 | - mix deps.get 15 | 16 | script: 17 | - MIX_ENV=test mix test 18 | 19 | notifications: 20 | irc: irc.rizon.net#gwlpr 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/entice/logic.svg)](https://travis-ci.org/entice/logic) 2 | 3 | Entice.Logic 4 | ======== 5 | 6 | Provides the gameplay logic. 7 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/entice/logic/attributes.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Attributes do 2 | @moduledoc """ 3 | Simple convenience macro for common attribute imports. 4 | """ 5 | 6 | defmacro __using__(_) do 7 | quote do 8 | alias Entice.Utils.Geom.Coord 9 | alias Entice.Logic.Player.Name 10 | alias Entice.Logic.Player.Position 11 | alias Entice.Logic.Player.MapInstance 12 | alias Entice.Logic.Player.Appearance 13 | alias Entice.Logic.Player.Level 14 | alias Entice.Logic.Vitals.Health 15 | alias Entice.Logic.Vitals.Energy 16 | alias Entice.Logic.Vitals.Morale 17 | alias Entice.Logic.Movement 18 | alias Entice.Logic.Npc 19 | alias Entice.Logic.Group.Leader 20 | alias Entice.Logic.Group.Member 21 | alias Entice.Logic.SkillBar 22 | alias Entice.Logic.MapInstance 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/entice/logic/casting.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Casting do 2 | @moduledoc """ 3 | This handles the casting process of arbitrary skills. 4 | Does not validate if an entity has the skills unlocked or w/e. 5 | Keeps a timer for casting, and an association from 6 | skill -> recharge-timer. 7 | 8 | You can pass in a listener PID or nil, and you will get notified 9 | of the following events: 10 | 11 | {:skill_casted, %{entity_id: entity, skill: skill, slot: slot, target_entity_id: target}} 12 | {:skill_cast_interrupted, %{entity_id: entity, skill: skill, slot: slot, target_entity_id: target, reason: reason}} 13 | {:skill_recharged, %{entity_id: entity, skill: skill, slot: slot}} 14 | {:after_cast_delay_ended, %{entity_id: entity}} 15 | 16 | TODO propagate these events in the local coordination instead to only one receiver 17 | """ 18 | use Pipe 19 | alias Entice.Logic.Casting 20 | alias Entice.Logic.Vitals.Energy 21 | alias Entice.Entity 22 | 23 | 24 | @after_cast_delay 250 25 | 26 | 27 | defstruct( 28 | cast_timer: nil, 29 | after_cast_timer: nil, 30 | recharge_timers: %{}) 31 | 32 | 33 | def register(entity_id), 34 | do: Entity.put_behaviour(entity_id, Casting.Behaviour, %Casting{}) 35 | 36 | 37 | def unregister(entity_id), 38 | do: Entity.remove_behaviour(entity_id, Casting) 39 | 40 | 41 | @doc "Deals with timing and thus might fail. Should be called by the Skillbar" 42 | def cast_skill(entity, skill, slot, target, report_to_pid \\ nil) 43 | when is_atom(skill) and (is_nil(report_to_pid) or is_pid(report_to_pid)), 44 | do: Entity.call_behaviour(entity, Casting.Behaviour, {:casting_cast_start, report_to_pid, %{target: target, skill: skill, slot: slot}}) 45 | 46 | 47 | @doc "Is there a better way to export this value out of this module?" 48 | def after_cast_delay, do: @after_cast_delay 49 | 50 | 51 | defmodule Behaviour do 52 | use Entice.Entity.Behaviour 53 | 54 | 55 | def init(entity, %Casting{} = casting), 56 | do: {:ok, entity |> put_attribute(casting)} 57 | 58 | 59 | def handle_call( 60 | {:casting_cast_start, report_to_pid, %{target: target, skill: skill, slot: slot}}, 61 | %Entity{attributes: %{ 62 | Casting => %Casting{}, 63 | Energy => %Energy{mana: mana}}} = entity) do 64 | cast_time = skill.cast_time 65 | 66 | check_able_to_cast(skill, target, entity) 67 | |> case do 68 | {:error, _reason} = msg -> {:ok, msg, entity} 69 | {:ok, skill} -> 70 | timer = cast_start(cast_time, skill, slot, target, report_to_pid) 71 | # TODO propagate locally for other entities to see 72 | {:ok, {:ok, skill, cast_time}, 73 | entity 74 | |> update_attribute(Casting, fn c -> %Casting{c | cast_timer: timer} end) 75 | |> reduce_mana(mana - skill.energy_cost)} 76 | end 77 | end 78 | 79 | 80 | def handle_call(event, entity), do: super(event, entity) 81 | 82 | 83 | @doc "This event triggers when the cast ends, it resets the casting timer, calls the skill's callback, and triggers recharge_end after a while." 84 | def handle_event({:casting_cast_end, skill, slot, target, report_to_pid}, entity) do 85 | do_report = if report_to_pid, do: true, else: false # nil/other to boolean 86 | recharge_time = skill.recharge_time 87 | recharge_timer = recharge_start(recharge_time, skill, slot, report_to_pid) 88 | after_cast_timer = after_cast_start(Entice.Logic.Casting.after_cast_delay, report_to_pid) 89 | 90 | skill.apply_effect(target, entity.id) 91 | |> handle_cast_result(skill) 92 | |> prepare_cast_message(entity, skill, slot, target, recharge_time) 93 | |> case do 94 | message when do_report -> report_to_pid |> send(message) 95 | _ -> nil 96 | end 97 | 98 | {:ok, entity |> update_attribute(Casting, 99 | fn c -> 100 | %Casting{c | 101 | cast_timer: nil, 102 | after_cast_timer: after_cast_timer, 103 | recharge_timers: c.recharge_timers |> Map.put(skill, recharge_timer)} 104 | end)} 105 | end 106 | 107 | 108 | @doc "This event triggers when a skill's recharge period ends, it resets the recharge timer for the skill." 109 | def handle_event({:casting_recharge_end, skill, slot, recharge_time, report_to_pid}, entity) do 110 | do_report = if report_to_pid, do: true, else: false # nil/other to boolean 111 | 112 | if do_report and recharge_time > 0, 113 | do: report_to_pid |> send({:skill_recharged, %{entity_id: entity.id, skill: skill, slot: slot}}) 114 | 115 | {:ok, entity |> update_attribute(Casting, fn c -> %Casting{c | recharge_timers: c.recharge_timers |> Map.delete(skill)} end)} 116 | end 117 | 118 | 119 | def handle_event({:casting_after_cast_end, report_to_pid}, entity) do 120 | if report_to_pid, do: report_to_pid |> send({:after_cast_delay_ended, %{entity_id: entity.id}}) 121 | {:ok, entity |> update_attribute(Casting, fn c -> %Casting{c | after_cast_timer: nil} end)} 122 | end 123 | 124 | 125 | def terminate(_reason, entity), 126 | do: {:ok, entity |> remove_attribute(Casting)} 127 | 128 | 129 | # Internal 130 | 131 | 132 | defp reduce_mana(entity, new_mana), 133 | do: entity |> update_attribute(Energy, fn e -> %Energy{e | mana: new_mana} end) 134 | 135 | 136 | defp cast_start(cast_time, skill, slot, target, report_to_pid), 137 | do: start_timer({:casting_cast_end, skill, slot, target, report_to_pid}, cast_time) 138 | 139 | 140 | defp recharge_start(recharge_time, skill, slot, report_to_pid), 141 | do: start_timer({:casting_recharge_end, skill, slot, recharge_time, report_to_pid}, recharge_time) 142 | 143 | 144 | defp after_cast_start(after_cast_time, report_to_pid), 145 | do: start_timer({:casting_after_cast_end, report_to_pid}, after_cast_time) 146 | 147 | 148 | defp start_timer(message, time) do 149 | if time == 0 do 150 | self |> send(message) 151 | nil 152 | else 153 | self |> Process.send_after(message, time) 154 | end 155 | end 156 | 157 | 158 | defp check_able_to_cast(skill, target, %Entity{attributes: %{ 159 | Casting => %Casting{cast_timer: cast_timer, after_cast_timer: after_cast_timer, recharge_timers: recharge_timers}, 160 | Energy => %Energy{mana: mana}}} = entity) do 161 | 162 | case skill.cast_time do 163 | cast_time when cast_time == 0 -> 164 | pipe_matching {:ok, _}, 165 | {:ok, skill} 166 | |> enough_energy?(mana - skill.energy_cost) 167 | |> not_recharging?(recharge_timers[skill]) 168 | |> check_requirements(target, entity) 169 | _cast_time -> 170 | pipe_matching {:ok, _}, 171 | {:ok, skill} 172 | |> enough_energy?(mana - skill.energy_cost) 173 | |> not_recharging?(recharge_timers[skill]) 174 | |> check_requirements(target, entity) 175 | |> not_casting?(cast_timer, after_cast_timer) 176 | end 177 | end 178 | 179 | # Take local entity data when we are the target 180 | defp check_requirements(input, target_eid, %Entity{id: target_eid} = entity), 181 | do: check_requirements(input, entity, entity) 182 | 183 | defp check_requirements({:ok, skill}, target, entity) do 184 | case skill.check_requirements(target, entity) do 185 | :ok -> {:ok, skill} 186 | error -> error 187 | end 188 | end 189 | 190 | defp enough_energy?({:ok, skill}, mana) when mana > 0, do: {:ok, skill} 191 | defp enough_energy?({:ok, _skill}, _mana), do: {:error, :not_enough_energy} 192 | 193 | 194 | defp not_recharging?({:ok, skill}, nil = _recharge_timer), do: {:ok, skill} 195 | defp not_recharging?(_skill, _recharge_timer), do: {:error, :still_recharging} 196 | 197 | 198 | defp not_casting?({:ok, skill}, nil = _cast_timer, nil = _after_cast_timer), do: {:ok, skill} 199 | defp not_casting?(_skill, _cast_timer, _after_cast_timer), do: {:error, :still_casting} 200 | 201 | 202 | defp handle_cast_result(:ok, _skill), do: :ok 203 | defp handle_cast_result({:error, reason}, _skill), do: {:error, reason} 204 | 205 | defp handle_cast_result(result, skill), 206 | do: raise "Corrupted result after applying effect of skill #{skill.underscore_name}. Got: #{result} - should be :ok or {:error, reason}" 207 | 208 | 209 | defp prepare_cast_message(:ok, entity, skill, slot, target, recharge_time) do 210 | {:skill_casted, %{ 211 | entity_id: entity.id, 212 | skill: skill, 213 | slot: slot, 214 | target_entity_id: target, 215 | recharge_time: recharge_time}} 216 | end 217 | 218 | defp prepare_cast_message({:error, reason}, entity, skill, slot, target, recharge_time) do 219 | {:skill_cast_interrupted, %{ 220 | entity_id: entity.id, 221 | skill: skill, 222 | slot: slot, 223 | target_entity_id: target, 224 | recharge_time: recharge_time, 225 | reason: reason}} 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/entice/logic/group.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Group do 2 | @moduledoc """ 3 | Implements a distributed grouping behaviour for entities. 4 | This has two different kinds of implementations, one for leaders of a group, 5 | and one for members. These different behaviours react to the same events 6 | according to their nature. 7 | """ 8 | alias Entice.Logic.Group.Leader 9 | alias Entice.Logic.Group.Member 10 | alias Entice.Logic.Group.LeaderBehaviour 11 | alias Entice.Logic.Group.MemberBehaviour 12 | alias Entice.Entity.Coordination 13 | alias Entice.Entity 14 | 15 | 16 | defmodule Leader, do: defstruct( 17 | members: [], 18 | invited: []) 19 | 20 | defmodule Member, do: defstruct( 21 | leader: "") 22 | 23 | 24 | # External API 25 | 26 | 27 | @doc """ 28 | Enable grouping behaviour for an entity. 29 | """ 30 | def register(entity_id), 31 | do: Entity.put_behaviour(entity_id, LeaderBehaviour, []) 32 | 33 | 34 | @doc """ 35 | Removes any kind of grouping behaviour from this entity. 36 | Works reasonably: If you're in a group (member or leader), 37 | you will leave the group. 38 | """ 39 | def unregister(entity_id) do 40 | Entity.remove_behaviour(entity_id, LeaderBehaviour) 41 | Entity.remove_behaviour(entity_id, MemberBehaviour) 42 | end 43 | 44 | 45 | @doc """ 46 | Check if a given entity is the leader of my group. 47 | If this is called by the leader with its own id, then the result is true. 48 | """ 49 | def is_my_leader?(entity_id, leader_id) do 50 | my_leader = cond do 51 | Entity.has_attribute?(entity_id, Leader) -> entity_id 52 | Entity.has_attribute?(entity_id, Member) -> Entity.fetch_attribute!(entity_id, Member) |> Map.get(:leader) 53 | true -> nil 54 | end 55 | my_leader == leader_id 56 | end 57 | 58 | 59 | @doc """ 60 | Members cannot invite. 61 | Leaders will only invite other leaders. 62 | If you invite someone, that someone will get the event and not you. 63 | """ 64 | def invite(sender_id, target_id), 65 | do: Coordination.notify(target_id, {:group_invite, sender_id}) 66 | 67 | 68 | @doc """ 69 | Kick the target from your group. 70 | If target not in group, but in invites, will be removed from invites. 71 | Only usable by a leader. 72 | """ 73 | def kick(sender_id, target_id) do 74 | Coordination.notify(sender_id, {:group_kick, target_id}) 75 | Coordination.notify(target_id, {:group_kick, sender_id}) 76 | end 77 | 78 | 79 | # Internal API 80 | 81 | 82 | @doc """ 83 | Confirm that you got the invite, and that its valid (not that your taking it). 84 | """ 85 | def invite_ack(sender_id, target_id), 86 | do: Coordination.notify(target_id, {:group_invite_ack, sender_id}) 87 | 88 | 89 | @doc """ 90 | Enforce a new leader entity for the receiver. 91 | Members will simply reassign. 92 | Leader will propagate to their members and become a member. 93 | (Used internally if invite was successful) 94 | """ 95 | def new_leader(entity_id, leader_id, invs \\ []), 96 | do: Coordination.notify(entity_id, {:group_new_leader, leader_id, invs}) 97 | 98 | 99 | @doc """ 100 | Assigns the given entity to the receiver's party. 101 | Leaders will simply add, members will do nothing. 102 | (Used internally if invite was successful) 103 | """ 104 | def self_assign(sender_id, leader_id), 105 | do: Coordination.notify(leader_id, {:group_assign, sender_id}) 106 | 107 | 108 | @doc """ 109 | Leave a group, only works as a member. 110 | """ 111 | def leave(member_id, leader_id), 112 | do: Coordination.notify(leader_id, {:group_leave, member_id}) 113 | 114 | 115 | # Actual behaviour implementation 116 | 117 | 118 | defmodule LeaderBehaviour do 119 | use Entice.Entity.Behaviour 120 | alias Entice.Logic.Group 121 | 122 | 123 | def init(entity, %{invited: invs}), 124 | do: {:ok, entity |> put_attribute(%Leader{invited: invs})} 125 | 126 | def init(entity, _args), 127 | do: {:ok, entity |> put_attribute(%Leader{})} 128 | 129 | 130 | # merging... 131 | 132 | 133 | def handle_event({:group_invite, sender_id}, %Entity{id: id, attributes: %{Leader => %Leader{invited: invs}}} = entity) 134 | when sender_id != id do 135 | if sender_id in invs do 136 | sender_id |> Group.new_leader(id, invs) 137 | else 138 | id |> Group.invite_ack(sender_id) 139 | end 140 | {:ok, entity} 141 | end 142 | 143 | 144 | def handle_event({:group_invite_ack, sender_id}, %Entity{attributes: %{Leader => %Leader{invited: invs}}} = entity), 145 | do: {:ok, entity |> update_attribute(Leader, fn l -> %Leader{l | invited: [sender_id | invs]} end)} 146 | 147 | 148 | def handle_event({:group_new_leader, leader_id, _invs}, %Entity{attributes: %{Leader => %Leader{members: mems, invited: invs}}} = entity) do 149 | for m <- mems, do: m |> Group.new_leader(leader_id, invs) 150 | entity.id |> Group.self_assign(leader_id) 151 | {:become, MemberBehaviour, %{leader_id: leader_id}, entity |> put_attribute(%Leader{})} 152 | end 153 | 154 | 155 | def handle_event({:group_assign, sender_id}, %Entity{attributes: %{Leader => %Leader{members: mems, invited: invs}}} = entity), 156 | do: {:ok, entity |> put_attribute(%Leader{members: mems ++ [sender_id], invited: invs -- [sender_id]})} 157 | 158 | 159 | # kicking/leaving... 160 | 161 | 162 | def handle_event({:group_kick, id}, %Entity{id: id, attributes: %{Leader => %Leader{members: [hd | _] = mems, invited: invs}}} = entity) do 163 | for m <- mems, do: m |> Group.new_leader(hd, invs) 164 | {:ok, entity |> put_attribute(%Leader{})} 165 | end 166 | 167 | 168 | def handle_event({:group_kick, sender_id}, %Entity{attributes: %{Leader => %Leader{invited: invs}}} = entity), 169 | do: {:ok, entity |> update_attribute(Leader, fn l -> %Leader{l | invited: invs -- [sender_id]} end)} 170 | 171 | 172 | def handle_event({:group_leave, sender_id}, %Entity{attributes: %{Leader => %Leader{members: mems}}} = entity), 173 | do: {:ok, entity |> update_attribute(Leader, fn l -> %Leader{l | members: mems -- [sender_id]} end)} 174 | 175 | 176 | def terminate(_reason, %Entity{attributes: %{Leader => %Leader{members: [], invited: invs}}} = entity) do 177 | for i <- invs, do: entity.id |> Group.kick(i) 178 | {:ok, entity |> remove_attribute(Leader)} 179 | end 180 | 181 | 182 | def terminate(_reason, %Entity{attributes: %{Leader => %Leader{members: [hd | _] = mems, invited: invs}}} = entity) do 183 | for m <- mems, do: m |> Group.new_leader(hd, invs) 184 | {:ok, entity |> remove_attribute(Leader)} 185 | end 186 | end 187 | 188 | 189 | defmodule MemberBehaviour do 190 | use Entice.Entity.Behaviour 191 | alias Entice.Logic.Group 192 | 193 | 194 | def init(entity, %{leader_id: lead}), 195 | do: {:ok, entity |> put_attribute(%Member{leader: lead})} 196 | 197 | 198 | # merging... 199 | 200 | 201 | def handle_event({:group_invite, sender_id}, %Entity{attributes: %{Member => %Member{leader: leader_id}}} = entity) do 202 | Coordination.notify(leader_id, {:group_invite, sender_id}) # forward to actual group leader 203 | {:ok, entity} 204 | end 205 | 206 | 207 | # if leader id and my id are the same, make me leader 208 | def handle_event({:group_new_leader, id, invs}, %Entity{id: id} = entity), 209 | do: {:become, LeaderBehaviour, %{invited: invs}, entity} 210 | 211 | 212 | # if someone else is the leader, then just reassign to that entity 213 | def handle_event({:group_new_leader, leader_id, _invs}, entity) do 214 | entity.id |> Group.self_assign(leader_id) 215 | {:ok, entity |> put_attribute(%Member{leader: leader_id})} 216 | end 217 | 218 | 219 | # kicking... 220 | 221 | 222 | def handle_event({:group_kick, sender_id}, %Entity{id: id, attributes: %{Member => %Member{leader: leader_id}}} = entity) 223 | when sender_id == leader_id or sender_id == id do 224 | id |> Group.new_leader(id) 225 | {:ok, entity} 226 | end 227 | 228 | 229 | def terminate(_reason, %Entity{attributes: %{Member => %Member{leader: leader_id}}} = entity) do 230 | entity.id |> Group.leave(leader_id) 231 | {:ok, entity |> remove_attribute(Member)} 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /lib/entice/logic/map_instance.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MapInstance do 2 | use Entice.Logic.Attributes 3 | alias Entice.Entity 4 | alias Entice.Entity.Coordination 5 | alias Entice.Entity.Suicide 6 | alias Entice.Logic.MapInstance 7 | alias Entice.Logic.Npc 8 | alias Entice.Logic.MapRegistry 9 | 10 | 11 | defstruct(players: 0, map: nil) 12 | 13 | 14 | def register(entity, map) do 15 | Suicide.unregister(entity) # maps will kill themselfes on their own 16 | Entity.put_behaviour(entity, MapInstance.Behaviour, map) 17 | end 18 | 19 | 20 | def unregister(entity) do 21 | Suicide.register(entity) 22 | Entity.remove_behaviour(entity, MapInstance.Behaviour) 23 | end 24 | 25 | 26 | def add_player(entity, player_entity), 27 | do: Coordination.notify(entity, {:map_instance_player_add, player_entity}) 28 | 29 | 30 | def add_npc(entity, name, model, %Position{} = position) when is_binary(name) and is_atom(model), 31 | do: Coordination.notify(entity, {:map_instance_npc_add, %{name: name, model: model, position: position}}) 32 | 33 | 34 | defmodule Behaviour do 35 | use Entice.Entity.Behaviour 36 | alias Entice.Logic.Player.Appearance 37 | 38 | def init(entity, map) do 39 | Coordination.register_observer(self, map) # TODO change map to something else if we have multiple instances 40 | {:ok, entity |> put_attribute(%MapInstance{map: map})} 41 | end 42 | 43 | 44 | def handle_event( 45 | {:map_instance_player_add, player_entity}, 46 | %Entity{attributes: %{MapInstance => %MapInstance{map: map, players: players}}} = entity) do 47 | Coordination.register(player_entity, map) # TODO change map to something else if we have multiple instances 48 | {:ok, entity |> update_attribute(MapInstance, fn(m) -> %MapInstance{m | players: players+1} end)} 49 | end 50 | 51 | def handle_event( 52 | {:map_instance_npc_add, %{name: name, model: model, position: position}}, 53 | %Entity{attributes: %{MapInstance => %MapInstance{map: map}}} = entity) do 54 | {:ok, eid, _pid} = Npc.spawn(name, model, position) 55 | Coordination.register(eid, map) # TODO change map to something else if we have multiple instances 56 | {:ok, entity} 57 | end 58 | 59 | def handle_event( 60 | {:entity_leave, %{attributes: %{Appearance => _}}}, 61 | %Entity{attributes: %{MapInstance => %MapInstance{map: map, players: players}}} = entity) do 62 | new_entity = entity |> update_attribute(MapInstance, fn instance -> %MapInstance{instance | players: players-1} end) 63 | case players-1 do 64 | count when count <= 0 -> 65 | Coordination.notify_all(map, Suicide.poison_pill_message) # this is why we deactivate our suicide behaviour 66 | Coordination.stop_channel(map) 67 | {:stop_process, :normal, new_entity} 68 | _ -> {:ok, new_entity} 69 | end 70 | end 71 | 72 | 73 | def terminate(_reason, %Entity{attributes: %{MapInstance => %MapInstance{map: map}}} = entity) do 74 | MapRegistry.stop_instance(map) 75 | {:ok, entity |> remove_attribute(MapInstance)} 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/entice/logic/map_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MapRegistry do 2 | @doc """ 3 | Stores all the instances for each map. 4 | Its state is in the following format: %{map=>entity_id} 5 | """ 6 | alias Entice.Entity 7 | alias Entice.Entity.Suicide 8 | alias Entice.Logic.MapInstance 9 | 10 | 11 | def start_link, 12 | do: Agent.start_link(fn -> %{} end, name: __MODULE__) 13 | 14 | 15 | @doc "Get or create an instance entity for a specific map" 16 | def get_or_create_instance(map) when is_atom(map) do 17 | Agent.get_and_update(__MODULE__, fn state -> 18 | case fetch_active(map, state) do 19 | {:ok, entity_id} -> {entity_id, state} 20 | :error -> 21 | with {:ok, entity_id, _pid} <- Entity.start, 22 | :ok <- MapInstance.register(entity_id, map), 23 | new_state = Map.put(state, map, entity_id), 24 | do: {entity_id, new_state} 25 | end 26 | end) 27 | end 28 | 29 | 30 | @doc "Stops an instance if not already stopped, effectively killing the entity." 31 | def stop_instance(map) when is_atom(map) do 32 | Agent.cast(__MODULE__, fn state -> 33 | with {:ok, entity_id} <- Map.fetch(state, map), 34 | :ok <- MapInstance.unregister(entity_id), 35 | :ok <- Suicide.poison_pill(entity_id), 36 | do: :ok 37 | state |> Map.delete(map) 38 | end) 39 | end 40 | 41 | 42 | defp fetch_active(map, state) when is_atom(map) do 43 | case Map.fetch(state, map) do 44 | {:ok, entity_id} -> 45 | if Entity.exists?(entity_id), do: {:ok, entity_id}, 46 | else: :error 47 | _ -> :error 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/entice/logic/maps/map.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Map do 2 | @moduledoc """ 3 | Top-level map macros for convenient access to all defined maps. 4 | Is mainly used in area.ex where all the maps are defined. 5 | """ 6 | import Inflex 7 | alias Entice.Utils.Geom.Coord 8 | 9 | defmacro __using__(_) do 10 | quote do 11 | import Entice.Logic.Map 12 | 13 | @maps [] 14 | @before_compile Entice.Logic.Map 15 | end 16 | end 17 | 18 | 19 | defmacro defmap(mapname, opts \\ []) do 20 | spawn = Keyword.get(opts, :spawn, quote do %Coord{} end) 21 | outpost = Keyword.get(opts, :outpost, quote do true end) 22 | 23 | map_content = content(Macro.to_string(mapname)) 24 | quote do 25 | defmodule unquote(mapname) do 26 | alias Entice.Utils.Geom.Coord 27 | unquote(map_content) 28 | def spawn, do: unquote(spawn) 29 | def is_outpost?, do: unquote(outpost) 30 | end 31 | @maps [ unquote(mapname) | @maps ] 32 | end 33 | end 34 | 35 | 36 | defmacro __before_compile__(_) do 37 | quote do 38 | 39 | @doc """ 40 | Simplistic map getter, tries to convert a PascalCase map name to the module atom. 41 | """ 42 | def get_map(name) do 43 | try do 44 | {:ok, ((__MODULE__ |> Atom.to_string) <> "." <> name) |> String.to_existing_atom} 45 | rescue 46 | ArgumentError -> {:error, :map_not_found} 47 | end 48 | end 49 | 50 | def get_maps, do: @maps 51 | end 52 | end 53 | 54 | 55 | defp content(name) do 56 | uname = underscore(name) 57 | quote do 58 | def name, do: unquote(name) 59 | def underscore_name, do: unquote(uname) 60 | 61 | def spawn, do: %Coord{} 62 | def is_outpost?, do: true 63 | 64 | defoverridable [spawn: 0, is_outpost?: 0] 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/entice/logic/maps/maps.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Maps do 2 | use Entice.Logic.Map 3 | 4 | 5 | # Lobby is for special client entities that represent a logged in client. 6 | defmap Lobby 7 | 8 | # Outposts... 9 | defmap HeroesAscent, spawn: %Coord{x: 2017, y: -3241} 10 | defmap RandomArenas, spawn: %Coord{x: 3854, y: 3874} 11 | defmap TeamArenas, spawn: %Coord{x: -1873, y: 352} 12 | 13 | # Explorables... 14 | defmap GreatTempleOfBalthazar, spawn: %Coord{x: -6558, y: -6010}, outpost: false # faked for testing purpose 15 | defmap IsleOfTheNameless, spawn: %Coord{x: -6036, y: -2519}, outpost: false 16 | 17 | 18 | def default_map, do: HeroesAscent 19 | 20 | 21 | @doc """ 22 | Adds an alias for all defined maps when 'used'. 23 | """ 24 | defmacro __using__(_) do 25 | quote do 26 | alias Entice.Logic.Maps 27 | unquote(for map <- get_maps do 28 | quote do: alias unquote(map) 29 | end) 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/entice/logic/movement.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Movement do 2 | alias Entice.Entity 3 | alias Entice.Utils.Geom.Coord 4 | alias Entice.Logic.{Movement, Player.Position} 5 | 6 | 7 | @doc """ 8 | Note that velocity is actually a coefficient for the real velocity thats used inside 9 | the client, but for simplicities sake we used velocity as a name. 10 | """ 11 | defstruct goal: %Coord{}, plane: 1, move_type: 9, velocity: 1.0 12 | 13 | 14 | def register(entity), 15 | do: Entity.put_behaviour(entity, Movement.Behaviour, []) 16 | 17 | 18 | def unregister(entity), 19 | do: Entity.remove_behaviour(entity, Movement.Behaviour) 20 | 21 | 22 | def update(entity, 23 | %Position{} = new_pos, 24 | %Movement{} = new_movement) do 25 | entity |> Entity.attribute_transaction( 26 | fn attrs -> 27 | attrs 28 | |> Map.put(Position, new_pos) 29 | |> Map.put(Movement, new_movement) 30 | end) 31 | end 32 | 33 | 34 | defmodule Behaviour do 35 | use Entice.Entity.Behaviour 36 | 37 | def init(%Entity{attributes: %{Movement => _}} = entity, _args), 38 | do: {:ok, entity} 39 | 40 | def init(%Entity{attributes: %{Position => %Position{pos: pos, plane: plane}}} = entity, _args), 41 | do: {:ok, entity |> put_attribute(%Movement{goal: pos, plane: plane})} 42 | 43 | def init(entity, _args), 44 | do: {:ok, entity |> put_attribute(%Movement{})} 45 | 46 | 47 | def terminate(_reason, entity), 48 | do: {:ok, entity |> remove_attribute(Movement)} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/entice/logic/npc.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Npc do 2 | use Entice.Logic.Map 3 | alias Entice.Entity 4 | alias Entice.Logic.{Npc, Vitals} 5 | alias Entice.Logic.Player.{Name, Position, Level} 6 | 7 | 8 | defstruct(npc_model_id: :dhuum) 9 | 10 | 11 | def spawn(name, model, %Position{} = position) 12 | when is_binary(name) and is_atom(model) do 13 | {:ok, id, pid} = Entity.start() 14 | Npc.register(id, name, model, position) 15 | Vitals.register(id) 16 | {:ok, id, pid} 17 | end 18 | 19 | 20 | def register(entity, name, model, %Position{} = position) 21 | when is_binary(name) and is_atom(model) do 22 | entity |> Entity.attribute_transaction(fn (attrs) -> 23 | attrs 24 | |> Map.put(Name, %Name{name: name}) 25 | |> Map.put(Position, position) 26 | |> Map.put(Npc, %Npc{npc_model_id: model}) 27 | |> Map.put(Level, %Level{level: 20}) 28 | end) 29 | end 30 | 31 | 32 | def unregister(entity) do 33 | entity |> Entity.attribute_transaction(fn (attrs) -> 34 | attrs 35 | |> Map.delete(Name) 36 | |> Map.delete(Position) 37 | |> Map.delete(Npc) 38 | |> Map.delete(Level) 39 | end) 40 | end 41 | 42 | 43 | def attributes(entity), 44 | do: Entity.take_attributes(entity, [Name, Position, Level, Npc]) 45 | end 46 | -------------------------------------------------------------------------------- /lib/entice/logic/player.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Player do 2 | @moduledoc """ 3 | Responsible for the basic player stats. 4 | """ 5 | alias Entice.Entity 6 | alias Entice.Utils.Geom.Coord 7 | 8 | 9 | defmodule Name, do: defstruct( 10 | name: "Unknown Entity") 11 | 12 | defmodule Position, do: defstruct( 13 | pos: %Coord{}, 14 | plane: 1) 15 | 16 | defmodule Appearance, do: defstruct( 17 | profession: 1, 18 | campaign: 0, 19 | sex: 1, 20 | height: 0, 21 | skin_color: 3, 22 | hair_color: 0, 23 | hairstyle: 7, 24 | face: 30) 25 | 26 | defmodule Level, do: defstruct( 27 | level: 20) 28 | 29 | @doc "Prepares a single, simple player" 30 | def register(entity, map, name \\ "Unkown Entity", appearance \\ %Appearance{}) do 31 | entity |> Entity.attribute_transaction(fn (attrs) -> 32 | attrs 33 | |> Map.put(Name, %Name{name: name}) 34 | |> Map.put(Position, %Position{pos: map.spawn}) 35 | |> Map.put(Appearance, appearance) 36 | |> Map.put(Level, %Level{level: 20}) 37 | end) 38 | end 39 | 40 | 41 | @doc "Removes all player attributes from the entity" 42 | def unregister(entity) do 43 | entity |> Entity.attribute_transaction(fn (attrs) -> 44 | attrs 45 | |> Map.delete(Name) 46 | |> Map.delete(Position) 47 | |> Map.delete(Appearance) 48 | |> Map.delete(Level) 49 | end) 50 | end 51 | 52 | 53 | @doc "Returns all player related attributes as an attribute map" 54 | def attributes(entity), 55 | do: Entity.take_attributes(entity, [Name, Position, Appearance, Level]) 56 | 57 | 58 | def set_appearance(entity, %Appearance{} = new_appear), 59 | do: entity |> Entity.set_attribute(new_appear) 60 | end 61 | -------------------------------------------------------------------------------- /lib/entice/logic/skillbar.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.SkillBar do 2 | alias Entice.Entity 3 | alias Entice.Logic.{Skills, SkillBar} 4 | 5 | 6 | @skillbar_slots 8 7 | 8 | 9 | defstruct slots: List.duplicate(Skills.NoSkill, @skillbar_slots) 10 | 11 | 12 | def register(entity), do: register(entity, %SkillBar{}) 13 | 14 | def register(entity, skill_ids) when is_list(skill_ids), 15 | do: register(entity, from_skill_ids(skill_ids)) 16 | 17 | def register(entity, %SkillBar{} = skillbar), 18 | do: Entity.put_attribute(entity, skillbar) 19 | 20 | 21 | def unregister(entity), 22 | do: Entity.remove_attribute(entity, SkillBar) 23 | 24 | 25 | # External API 26 | 27 | 28 | def get_skill(entity, slot) do 29 | case Entity.fetch_attribute(entity, SkillBar) do 30 | {:ok, %SkillBar{slots: slots}} -> Enum.at(slots, slot, Skill.NoSkill) 31 | _ -> Skills.NoSkill 32 | end 33 | end 34 | 35 | 36 | def get_skills(entity) do 37 | case Entity.fetch_attribute(entity, SkillBar) do 38 | {:ok, %SkillBar{} = skillbar} -> to_skill_ids(skillbar) 39 | _ -> [] 40 | end 41 | end 42 | 43 | 44 | def change_skill(entity, slot, skill_id) when is_number(skill_id), 45 | do: change_skill(entity, slot, Skills.get_skill(skill_id)) 46 | 47 | def change_skill(entity, slot, skill) when not is_nil(skill) and is_atom(skill) do 48 | new_skillbar = Entity.get_and_update_attribute(entity, SkillBar, 49 | fn skillbar -> 50 | %SkillBar{slots: skillbar.slots |> List.replace_at(slot, skill)} 51 | end) 52 | to_skill_ids(new_skillbar) 53 | end 54 | 55 | 56 | # Internal 57 | 58 | 59 | defp to_skill_ids(%SkillBar{slots: skills}), 60 | do: skills |> Enum.map(fn skill -> skill.id end) 61 | 62 | 63 | defp from_skill_ids(skill_ids) when is_list(skill_ids) and length(skill_ids) <= @skillbar_slots do 64 | %SkillBar{slots: 65 | skill_ids 66 | |> skillbar_trunc_or_fill 67 | |> Enum.map(fn skill_id -> Skills.get_skill(skill_id) end) 68 | |> Enum.map( 69 | fn nil -> Skills.NoSkill 70 | skill -> skill 71 | end)} 72 | end 73 | 74 | 75 | defp skillbar_trunc_or_fill(skill_ids) when is_list(skill_ids) and length(skill_ids) < @skillbar_slots, 76 | do: skillbar_trunc_or_fill(skill_ids ++ [0]) 77 | 78 | defp skillbar_trunc_or_fill(skill_ids) when is_list(skill_ids) and length(skill_ids) > @skillbar_slots, 79 | do: skillbar_trunc_or_fill(skill_ids |> List.delete_at(-1)) 80 | 81 | defp skillbar_trunc_or_fill(skill_ids) when is_list(skill_ids), 82 | do: skill_ids 83 | end 84 | -------------------------------------------------------------------------------- /lib/entice/logic/skills/skill.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Skill do 2 | import Inflex 3 | 4 | defmacro __using__(_) do 5 | quote do 6 | import Entice.Logic.Skill 7 | import Entice.Logic.Skill.Prerequisite 8 | import Entice.Logic.Skill.Effect 9 | 10 | @skills %{} 11 | @before_compile Entice.Logic.Skill 12 | end 13 | end 14 | 15 | 16 | defmacro defskill(skillname, opts, do_block \\ []) do 17 | skillid = Keyword.get(opts, :id) 18 | name = skillname |> elem(2) |> hd |> to_string 19 | uname = underscore(name) 20 | 21 | quote do 22 | # add the module 23 | defmodule unquote(skillname) do 24 | import Entice.Logic.Skill.Effect 25 | import Entice.Logic.Skill.Prerequisite 26 | @behaviour Entice.Logic.Skill.Behaviour 27 | def id, do: unquote(skillid) 28 | def name, do: unquote(name) 29 | def underscore_name, do: unquote(uname) 30 | def apply_effect(target, caster), do: :ok 31 | def check_requirements(target, caster), do: :ok 32 | defoverridable [apply_effect: 2, check_requirements: 2] 33 | unquote(do_block) 34 | end 35 | # then update the stats 36 | @skills Map.put(@skills, unquote(skillid), unquote(skillname)) 37 | end 38 | end 39 | 40 | 41 | defmacro __before_compile__(_) do 42 | quote do 43 | 44 | @doc """ 45 | Simplistic skill getter. 46 | Either uses skill ID or tries to convert a skill name to the module atom. 47 | The skill should be a GW skill name in PascalCase. 48 | """ 49 | def get_skill(id) when is_integer(id), do: Map.get(@skills, id) 50 | def get_skill(name) do 51 | try do 52 | ((__MODULE__ |> Atom.to_string) <> "." <> name) |> String.to_existing_atom 53 | rescue 54 | ArgumentError -> nil 55 | end 56 | end 57 | 58 | @doc "Get all skills that are known" 59 | def get_skills, 60 | do: @skills |> Map.values 61 | 62 | def max_unlocked_skills, 63 | do: get_skills |> Enum.reduce(0, fn (skill, acc) -> Entice.Utils.BitOps.set_bit(acc, skill.id) end) 64 | end 65 | end 66 | end 67 | 68 | 69 | defmodule Entice.Logic.Skill.Behaviour do 70 | use Behaviour 71 | alias Entice.Entity 72 | 73 | @doc "Unique skill identitfier, resembles roughly GW" 74 | defcallback id() :: integer 75 | 76 | @doc "Unique skill name" 77 | defcallback name() :: String.t 78 | 79 | @doc "Unique skill name (snake case)" 80 | defcallback underscore_name() :: String.t 81 | 82 | @doc "General skill description" 83 | defcallback description() :: String.t 84 | 85 | @doc "Cast time of the skill in MS" 86 | defcallback cast_time() :: integer 87 | 88 | @doc "Recharge time of the skill in MS" 89 | defcallback recharge_time() :: integer 90 | 91 | @doc "Energy cost of the skill in mana" 92 | defcallback energy_cost() :: integer 93 | 94 | @doc "Is called after the casting finished." 95 | defcallback apply_effect(target_entity_id :: term, caster_entity :: %Entity{}) :: 96 | :ok | 97 | {:error, reason :: term} 98 | 99 | @doc "Is called before starting to cast." 100 | defcallback check_requirements(target_entity_id_or_entity :: term | %Entity{}, caster_entity :: %Entity{}) :: 101 | :ok | 102 | {:error, reason :: term} 103 | end 104 | 105 | defmodule Entice.Logic.Skill.Prerequisite do 106 | @moduledoc """ 107 | Helpers that can be used when implementing skill prerequisite scripts 108 | """ 109 | use Entice.Logic.Attributes 110 | alias Entice.Entity 111 | 112 | def require_dead(target_id) when is_binary(target_id) do 113 | {:ok, %Health{health: health, max_health: _, regeneration: _}} = Entity.fetch_attribute(target_id, Health) 114 | require_dead(health) 115 | end 116 | 117 | def require_dead(%Entity{attributes: %{Health => %Health{health: health}}}), 118 | do: require_dead(health) 119 | 120 | #TODO: Replace by check for death attribute instead of health 121 | def require_dead(0), do: :ok 122 | def require_dead(_health), do: {:error, :target_not_dead} 123 | end 124 | 125 | defmodule Entice.Logic.Skill.Effect do 126 | @moduledoc """ 127 | Helpers that can be used when implementing skill effect scripts 128 | """ 129 | use Entice.Logic.Attributes 130 | alias Entice.Logic.Vitals 131 | 132 | 133 | defdelegate damage(target, amount), to: Vitals 134 | defdelegate heal(target, amount), to: Vitals 135 | defdelegate resurrect(target, percent_health, percent_energy), to: Vitals 136 | end 137 | -------------------------------------------------------------------------------- /lib/entice/logic/skills/skills.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Skills do 2 | use Entice.Logic.Skill 3 | use Entice.Logic.Attributes 4 | 5 | defskill NoSkill, id: 0 do 6 | def description, do: "Non-existing skill as a placeholder for empty skillbar slots." 7 | def cast_time, do: 0 8 | def recharge_time, do: 0 9 | def energy_cost, do: 0 10 | end 11 | 12 | defskill HealingSignet, id: 1 do 13 | def description, do: "You gain 82...154...172 Health. You have -40 armor while using this skill." 14 | def cast_time, do: 2000 15 | def recharge_time, do: 4000 16 | def energy_cost, do: 0 17 | 18 | def apply_effect(_target, caster), 19 | do: heal(caster, 10) 20 | end 21 | 22 | defskill ResurrectionSignet, id: 2 do 23 | def description, do: "Resurrects target party member (100% Health, 25% Energy). This signet only recharges when you gain a morale boost." 24 | def cast_time, do: 3000 25 | def recharge_time, do: 0 26 | def energy_cost, do: 0 27 | 28 | def check_requirements(target, _caster), 29 | do: require_dead(target) 30 | 31 | def apply_effect(target, _caster), 32 | do: resurrect(target, 100, 25) 33 | end 34 | 35 | defskill SignetOfCapture, id: 3 do 36 | def description, do: "Choose one skill from a nearby dead Boss of your profession. Signet of Capture is permanently replaced by that skill. If that skill was elite, gain 250 XP for every level you have earned." 37 | def cast_time, do: 2000 38 | def recharge_time, do: 2000 39 | def energy_cost, do: 0 40 | end 41 | 42 | defskill Bamph, id: 4 do 43 | def description, do: "BAMPH!" 44 | def cast_time, do: 0 45 | def recharge_time, do: 0 46 | def energy_cost, do: 0 47 | 48 | def apply_effect(target, _caster), 49 | do: damage(target, 10) 50 | end 51 | 52 | defskill PowerBlock, id: 5 do 53 | def description, do: "If target foe is casting a spell or chant, that skill and all skills of the same attribute are disabled (1...10...12 seconds) and that skill is interrupted." 54 | def cast_time, do: 250 55 | def recharge_time, do: 20000 56 | def energy_cost, do: 15 57 | end 58 | 59 | defskill MantraOfEarth, id: 6 do 60 | def description, do: "(30...78...90 seconds.) Reduces earth damage you take by 26...45...50%. You gain 2 Energy when you take earth damage." 61 | def cast_time, do: 0 62 | def recharge_time, do: 20000 63 | def energy_cost, do: 10 64 | end 65 | 66 | defskill MantraOfFlame, id: 7 do 67 | def description, do: "(30...78...90 seconds.) Reduces fire damage you take by 26...45...50%. You gain 2 Energy when you take fire damage." 68 | def cast_time, do: 0 69 | def recharge_time, do: 20000 70 | def energy_cost, do: 10 71 | end 72 | 73 | defskill MantraOfFrost, id: 8 do 74 | def description, do: "(30...78...90 seconds.) Reduces cold damage you take by 26...45...50%. You gain 2 Energy when you take cold damage." 75 | def cast_time, do: 0 76 | def recharge_time, do: 20000 77 | def energy_cost, do: 10 78 | end 79 | 80 | defskill MantraOfLightning, id: 9 do 81 | def description, do: "(30...78...90 seconds.) Reduces lightning damage you take by 26...45...50%. You gain 2 Energy when you take lightning damage." 82 | def cast_time, do: 0 83 | def recharge_time, do: 20000 84 | def energy_cost, do: 10 85 | end 86 | 87 | defskill HexBreaker, id: 10 do 88 | def description, do: "(5...65...80 seconds.) The next hex against you fails and the caster takes 10...39...46 damage." 89 | def cast_time, do: 0 90 | def recharge_time, do: 15000 91 | def energy_cost, do: 5 92 | end 93 | 94 | defskill Distortion, id: 11 do 95 | def description, do: "(1...4...5 seconds.) You have 75% chance to block. Block cost: you lose 2 Energy or Distortion ends." 96 | def cast_time, do: 0 97 | def recharge_time, do: 8000 98 | def energy_cost, do: 5 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/entice/logic/vitals.ex: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Vitals do 2 | @moduledoc """ 3 | Responsible for the entities vital stats like (health, mana, regen, degen). 4 | If the entity has no explicit level, it is implicitly assumed to be 20. 5 | 6 | If and entity dies, a local broadcast will be send that looks like this: 7 | 8 | {:entity_dead, %{entity_id: entity_id, attributes: attribs}} 9 | 10 | If the entity then gets resurrected, a similar message will be broadcasted: 11 | 12 | {:entity_resurrected, %{entity_id: entity_id, attributes: attribs}} 13 | 14 | The regeneration of health and energy works in a value-per-second fashion. 15 | Usually, for health we have max/minimum of +-20 HP/s and for energy we have a 16 | max/minimum of +-3 mana/s (both are equal to +-10 pips on the health / energy bars) 17 | Note that the client assumes a standard mana regen of 0.71 mana/s (2 pips), 18 | which is 0.355 * 2, so we think 0.355 is the standard 1 pip regen. 19 | """ 20 | alias Entice.Entity 21 | alias Entice.Entity.Coordination 22 | alias Entice.Logic.Vitals 23 | 24 | 25 | defmodule Health, do: defstruct(health: 500, max_health: 620, regeneration: 0.0) 26 | 27 | 28 | defmodule Energy, do: defstruct(mana: 50, max_mana: 70, regeneration: 0.666) # hell yeah 29 | 30 | 31 | defmodule Morale, do: defstruct(morale: 0) 32 | 33 | 34 | def register(entity_id), 35 | do: Entity.put_behaviour(entity_id, Vitals.AliveBehaviour, []) 36 | 37 | 38 | def unregister(entity_id) do 39 | Entity.remove_behaviour(entity_id, Vitals.AliveBehaviour) 40 | Entity.remove_behaviour(entity_id, Vitals.DeadBehaviour) 41 | end 42 | 43 | 44 | @doc "Damage is dealt till death of the entity. (It then needs to be resurrected)" 45 | def damage(entity, amount), 46 | do: Coordination.notify(entity, {:vitals_entity_damage, amount}) 47 | 48 | 49 | @doc "Heals the entity until `max_health` is reached" 50 | def heal(entity, amount), 51 | do: Coordination.notify(entity, {:vitals_entity_heal, amount}) 52 | 53 | 54 | @doc "Kills an entity, reduces the lifepoints to 0." 55 | def kill(entity), 56 | do: Coordination.notify(entity, :vitals_entity_kill) 57 | 58 | 59 | @doc "Resurrect with percentage of health and energy. (Entity needs to be dead :P)" 60 | def resurrect(entity, percent_health, percent_energy), 61 | do: Coordination.notify(entity, {:vitals_entity_resurrect, percent_health, percent_energy}) 62 | 63 | 64 | def health_regeneration(entity, value), 65 | do: Coordination.notify(entity, {:vitals_health_regeneration, value}) 66 | 67 | 68 | def energy_regeneration(entity, value), 69 | do: Coordination.notify(entity, {:vitals_energy_regeneration, value}) 70 | 71 | 72 | defmodule AliveBehaviour do 73 | use Entice.Entity.Behaviour 74 | alias Entice.Logic.Vitals.Health 75 | alias Entice.Logic.Vitals.Energy 76 | alias Entice.Logic.Vitals.Morale 77 | alias Entice.Logic.Vitals.DeadBehaviour 78 | alias Entice.Logic.Player.Level 79 | 80 | 81 | @regeneration_interval 500 82 | @min_accumulated_health 5 # these values need to accumulate over time before 83 | @min_accumulated_energy 1 # the attribute is updated 84 | 85 | 86 | def init(entity, {:entity_resurrected, percent_health, percent_energy}) do 87 | entity.id |> Coordination.notify_locally({ 88 | :entity_resurrected, 89 | %{entity_id: entity.id, attributes: entity.attributes}}) 90 | 91 | %Health{max_health: max_health} = get_max_health(entity.attributes) 92 | resurrected_health = round(max_health / 100 * percent_health) 93 | 94 | %Energy{max_mana: max_mana} = get_max_energy(entity.attributes) 95 | resurrected_mana = round(max_mana / 100 * percent_energy) 96 | 97 | self |> Process.send_after({:vitals_regeneration_update, %{ 98 | interval: @regeneration_interval, 99 | health_accumulator: 0, 100 | energy_accumulator: 0}}, @regeneration_interval) 101 | {:ok, 102 | entity 103 | |> put_attribute(%Health{health: resurrected_health, max_health: max_health}) 104 | |> put_attribute(%Energy{mana: resurrected_mana, max_mana: max_mana})} 105 | end 106 | 107 | def init(entity, _args) do 108 | self |> Process.send_after({:vitals_regeneration_update, %{ 109 | interval: @regeneration_interval, 110 | health_accumulator: 0, 111 | energy_accumulator: 0}}, @regeneration_interval) 112 | {:ok, 113 | entity 114 | |> put_attribute(%Morale{morale: 0}) 115 | |> attribute_transaction( 116 | fn attrs -> 117 | attrs |> Map.merge(%{ 118 | Health => get_max_health(attrs), 119 | Energy => get_max_energy(attrs)}) 120 | end)} 121 | end 122 | 123 | 124 | def handle_event( 125 | {:vitals_entity_damage, amount}, 126 | %Entity{attributes: %{Health => %Health{health: health}}} = entity) do 127 | 128 | new_health = health - amount 129 | cond do 130 | new_health <= 0 -> {:become, DeadBehaviour, :entity_died, entity} 131 | new_health > 0 -> 132 | {:ok, entity |> update_attribute(Health, fn health -> %Health{health | health: new_health} end)} 133 | end 134 | end 135 | 136 | def handle_event( 137 | {:vitals_entity_heal, amount}, 138 | %Entity{attributes: %{Health => %Health{health: health, max_health: max_health}}} = entity) do 139 | 140 | new_health = health + amount 141 | if new_health > max_health, do: new_health = max_health 142 | 143 | {:ok, entity |> update_attribute(Health, fn health -> %Health{health | health: new_health} end)} 144 | end 145 | 146 | def handle_event(:vitals_entity_kill, entity), do: {:become, DeadBehaviour, :entity_died, entity} 147 | 148 | def handle_event({:vitals_health_regeneration, value}, entity) when (-10 <= value) and (value <= 10), 149 | do: {:ok, entity |> update_attribute(Health, fn health -> %Health{health | regeneration: value} end)} 150 | 151 | def handle_event({:vitals_energy_regeneration, value}, entity) when (-3 <= value) and (value <= 3), 152 | do: {:ok, entity |> update_attribute(Energy, fn energy -> %Energy{energy | regeneration: value} end)} 153 | 154 | def handle_event( 155 | {:vitals_regeneration_update, %{interval: interval, health_accumulator: health_acc, energy_accumulator: energy_acc}}, 156 | %Entity{attributes: %{ 157 | Health => %Health{regeneration: health_regen} = health, 158 | Energy => %Energy{regeneration: energy_regen} = energy}} = entity) do 159 | health_acc = health_acc + (health_regen * interval/1000) 160 | energy_acc = energy_acc + (energy_regen * interval/1000) 161 | 162 | {new_health, new_health_acc} = regenerate_health(health, health_acc) 163 | {new_energy, new_energy_acc} = regenerate_energy(energy, energy_acc) 164 | 165 | self |> Process.send_after({:vitals_regeneration_update, %{ 166 | interval: @regeneration_interval, 167 | health_accumulator: new_health_acc, 168 | energy_accumulator: new_energy_acc}}, @regeneration_interval) 169 | {:ok, entity |> update_attribute(Health, fn _ -> new_health end) 170 | |> update_attribute(Energy, fn _ -> new_energy end)} 171 | end 172 | 173 | 174 | def terminate({:become_handler, DeadBehaviour, _}, entity), 175 | do: {:ok, entity} 176 | 177 | def terminate(_reason, entity) do 178 | {:ok, 179 | entity 180 | |> remove_attribute(Morale) 181 | |> remove_attribute(Health) 182 | |> remove_attribute(Energy)} 183 | end 184 | 185 | 186 | # Internal 187 | 188 | 189 | defp get_max_health(%{Level => %Level{level: level}, Morale => %Morale{morale: morale}}), do: get_max_health(level, morale) 190 | defp get_max_health(%{Morale => %Morale{morale: morale}}), do: get_max_health(20, morale) 191 | defp get_max_health(%{}), do: get_max_health(20, 0) 192 | 193 | def get_max_health(level, morale) do 194 | health = calc_life_points_for_level(level) 195 | max_health_with_morale = round(health / 100 * (100 + morale)) 196 | %Health{health: max_health_with_morale, max_health: max_health_with_morale} 197 | end 198 | 199 | #TODO: Take care of Armor, Runes, Weapons... 200 | defp calc_life_points_for_level(level), 201 | do: 100 + ((level - 1) * 20) # Dont add 20 lifePoints for level1 202 | 203 | 204 | #TODO: Take care of Armor, Runes, Weapons... 205 | defp get_max_energy(%{Morale => %Morale{morale: morale}}), do: get_max_energy(morale) 206 | defp get_max_energy(%{}), do: get_max_energy(0) 207 | 208 | defp get_max_energy(morale) do 209 | inital_mana = 70 210 | mana_with_morale = round(inital_mana / 100 * (100 + morale)) 211 | %Energy{mana: mana_with_morale, max_mana: mana_with_morale} 212 | end 213 | 214 | 215 | defp regenerate_health(health, amount) when amount >= @min_accumulated_health do 216 | health_addition = trunc(amount) 217 | leftover = amount - health_addition 218 | {%Health{health | health: ((health.health + health_addition) |> min(health.max_health))}, leftover} 219 | end 220 | 221 | defp regenerate_health(health, amount), do: {health, amount} 222 | 223 | 224 | defp regenerate_energy(energy, amount) when amount >= @min_accumulated_energy do 225 | energy_addition = trunc(amount) 226 | leftover = amount - energy_addition 227 | {%Energy{energy | mana: ((energy.mana + energy_addition) |> min(energy.max_mana))}, leftover} 228 | end 229 | 230 | defp regenerate_energy(energy, amount), do: {energy, amount} 231 | end 232 | 233 | 234 | defmodule DeadBehaviour do 235 | use Entice.Entity.Behaviour 236 | alias Entice.Logic.Vitals.Morale 237 | alias Entice.Logic.Vitals.AliveBehaviour 238 | 239 | def init(%Entity{attributes: %{Morale => %Morale{morale: morale}}} = entity, :entity_died) do 240 | entity.id |> Coordination.notify_locally({ 241 | :entity_dead, 242 | %{entity_id: entity.id, attributes: entity.attributes}}) 243 | 244 | new_morale = morale - 15 245 | if new_morale < -60, #-60 is max negative morale 246 | do: new_morale = -60 247 | 248 | {:ok, 249 | entity 250 | |> put_attribute(%Morale{morale: new_morale}) 251 | |> update_attribute(Health, fn health -> %Health{health | health: 0} end)} 252 | end 253 | 254 | 255 | def handle_event({:vitals_entity_resurrect, percent_health, percent_energy}, entity), 256 | do: {:become, AliveBehaviour, {:entity_resurrected, percent_health, percent_energy}, entity} 257 | 258 | 259 | def terminate({:become_handler, AliveBehaviour, _}, entity), 260 | do: {:ok, entity} 261 | 262 | def terminate(_reason, entity) do 263 | {:ok, 264 | entity 265 | |> remove_attribute(Morale) 266 | |> remove_attribute(Health) 267 | |> remove_attribute(Energy)} 268 | end 269 | end 270 | end 271 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :entice_logic, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | deps: deps] 9 | end 10 | 11 | def application do 12 | [applications: [:logger, :entice_entity]] 13 | end 14 | 15 | defp deps do 16 | [{:entice_entity, github: "entice/entity", ref: "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a"}, 17 | {:uuid, "~> 1.1"}, 18 | {:inflex, "~> 1.5"}, 19 | {:pipe, "~> 0.0.2"}] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"entice_entity": {:git, "https://github.com/entice/entity.git", "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a", [ref: "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a"]}, 2 | "entice_utils": {:git, "https://github.com/entice/utils.git", "79ead4dca77324b4c24f584468edbaff2029eeab", [ref: "79ead4dca77324b4c24f584468edbaff2029eeab"]}, 3 | "inflex": {:hex, :inflex, "1.5.0"}, 4 | "pipe": {:hex, :pipe, "0.0.2"}, 5 | "uuid": {:hex, :uuid, "1.1.3"}} 6 | -------------------------------------------------------------------------------- /test/entice/logic/casting_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.CastingTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Attributes 4 | alias Entice.Entity 5 | alias Entice.Entity.Attribute 6 | alias Entice.Entity.Test.Spy 7 | alias Entice.Logic.Skills 8 | alias Entice.Logic.Casting 9 | alias Entice.Logic.Vitals 10 | @moduletag :casting 11 | 12 | 13 | # setup all tags programmatically 14 | setup do 15 | {:ok, entity_id, _pid} = Entity.start_plain 16 | Attribute.register(entity_id) 17 | Casting.register(entity_id) 18 | Vitals.register(entity_id) 19 | Spy.register(entity_id, self) 20 | Entity.put_attribute(entity_id, %Energy{mana: 50}) 21 | {:ok, [entity_id: entity_id]} 22 | end 23 | 24 | 25 | test "won't cast when not enough energy", %{entity_id: eid} do 26 | Entity.put_attribute(eid, %Energy{mana: 0}) 27 | assert {:error, :not_enough_energy} = Casting.cast_skill(eid, Skills.MantraOfEarth, nil, self) 28 | end 29 | 30 | 31 | test "won't cast recharging skill", %{entity_id: eid} do 32 | Entity.put_attribute(eid, %Energy{mana: 100}) 33 | 34 | recharge_timers = Map.put(%{}, Skills.MantraOfEarth, 10) 35 | Entity.update_attribute(eid, Casting, fn c -> %Casting{c | recharge_timers: recharge_timers} end) 36 | assert {:error, :still_recharging} = Casting.cast_skill(eid, Skills.MantraOfEarth, 0, nil, self) 37 | Entity.update_attribute(eid, Casting, fn _c -> %Casting{} end) 38 | end 39 | 40 | 41 | test "won't cast with casting timer != nil", %{entity_id: eid} do 42 | Entity.update_attribute(eid, Casting, fn c -> %Casting{c | cast_timer: 10} end) 43 | assert {:error, :still_casting} = Casting.cast_skill(eid, Skills.HealingSignet, 0, nil, self) 44 | end 45 | 46 | 47 | test "won't cast with after_cast_timer != nil", %{entity_id: eid} do 48 | Entity.update_attribute(eid, Casting, fn c -> %Casting{c | after_cast_timer: 10} end) 49 | assert {:error, :still_casting} = Casting.cast_skill(eid, Skills.HealingSignet, 0, nil, self) 50 | end 51 | 52 | 53 | test "won't cast with both cast_timer and after_cast_timer != nil", %{entity_id: eid} do 54 | Entity.update_attribute(eid, Casting, fn c -> %Casting{c | cast_timer: 10, after_cast_timer: 10} end) 55 | assert {:error, :still_casting} = Casting.cast_skill(eid, Skills.HealingSignet, 0, nil, self) 56 | end 57 | 58 | test "won't cast prerequisites not fulfilled", %{entity_id: eid} do 59 | assert {:error, :target_not_dead} = Casting.cast_skill(eid, Skills.ResurrectionSignet, 0, eid, self) 60 | end 61 | 62 | 63 | test "check correct cast time", %{entity_id: eid} do 64 | cast_time = Skills.SignetOfCapture.cast_time 65 | assert {:ok, Skills.SignetOfCapture, ^cast_time} = Casting.cast_skill(eid, Skills.SignetOfCapture, 0, nil, self) 66 | # the timers are only set after casting is done 67 | assert nil == Entity.get_attribute(eid, Casting).recharge_timers[Skills.SignetOfCapture] 68 | assert nil == Entity.get_attribute(eid, Casting).after_cast_timer 69 | 70 | assert_receive %{sender: ^eid, event: {:casting_cast_end, Skills.SignetOfCapture, 0, nil, _pid}}, (Skills.SignetOfCapture.cast_time + 100) 71 | assert_receive {:skill_casted, %{entity_id: ^eid, skill: Skills.SignetOfCapture, slot: 0, target_entity_id: nil}} 72 | assert nil != Entity.get_attribute(eid, Casting).recharge_timers[Skills.SignetOfCapture] 73 | assert nil != Entity.get_attribute(eid, Casting).after_cast_timer 74 | end 75 | 76 | 77 | test "check correct after_cast time", %{entity_id: eid} do 78 | cast_time = Skills.SignetOfCapture.cast_time 79 | assert {:ok, Skills.SignetOfCapture, ^cast_time} = Casting.cast_skill(eid, Skills.SignetOfCapture, 0, nil, self) 80 | assert nil == Entity.get_attribute(eid, Casting).after_cast_timer 81 | 82 | assert_receive %{sender: ^eid, event: {:casting_after_cast_end, _pid}}, ( 83 | Skills.SignetOfCapture.cast_time + Entice.Logic.Casting.after_cast_delay + 100) 84 | assert_receive {:after_cast_delay_ended, %{entity_id: ^eid}} 85 | assert nil == Entity.get_attribute(eid, Casting).after_cast_timer 86 | end 87 | 88 | 89 | test "check correct recharge time", %{entity_id: eid} do 90 | cast_time = Skills.SignetOfCapture.cast_time 91 | recharge_time = Skills.SignetOfCapture.recharge_time 92 | assert {:ok, Skills.SignetOfCapture, ^cast_time} = Casting.cast_skill(eid, Skills.SignetOfCapture, 0, nil, self) 93 | assert nil == Entity.get_attribute(eid, Casting).recharge_timers[Skills.SignetOfCapture] 94 | 95 | assert_receive %{sender: ^eid, event: {:casting_recharge_end, Skills.SignetOfCapture, 0, ^recharge_time, _pid}}, ( 96 | Skills.SignetOfCapture.cast_time + Skills.SignetOfCapture.recharge_time + 100) 97 | assert_receive {:skill_recharged, %{entity_id: ^eid, skill: Skills.SignetOfCapture, slot: 0}} 98 | assert nil == Entity.get_attribute(eid, Casting).recharge_timers[Skills.SignetOfCapture] 99 | assert nil == Entity.get_attribute(eid, Casting).cast_timer 100 | assert nil == Entity.get_attribute(eid, Casting).after_cast_timer 101 | end 102 | 103 | 104 | test "check correct effect application", %{entity_id: eid} do 105 | # prepare a target 106 | {:ok, e1, _p1} = Entity.start_plain 107 | Attribute.register(e1) 108 | Vitals.register(e1) 109 | %Health{health: health} = Entity.get_attribute(e1, Health) 110 | 111 | cast_time = Skills.Bamph.cast_time 112 | assert {:ok, Skills.Bamph, ^cast_time} = Casting.cast_skill(eid, Skills.Bamph, 0, e1, self) 113 | assert_receive %{sender: ^eid, event: {:casting_cast_end, Skills.Bamph, 0, ^e1, _pid}}, (Skills.Bamph.cast_time + 100) 114 | assert_receive {:skill_casted, %{entity_id: ^eid, skill: Skills.Bamph, slot: 0, target_entity_id: ^e1}} 115 | 116 | %Health{health: health_after_damage} = Entity.get_attribute(e1, Health) 117 | assert health_after_damage < health 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/entice/logic/group_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.GroupTest do 2 | use ExUnit.Case, async: true 3 | alias Entice.Entity 4 | alias Entice.Entity.Attribute 5 | alias Entice.Entity.Test.Spy 6 | alias Entice.Logic.Group 7 | alias Entice.Logic.Group.Leader 8 | alias Entice.Logic.Group.Member 9 | 10 | 11 | setup do 12 | {:ok, e1, _pid} = Entity.start_plain 13 | {:ok, e2, _pid} = Entity.start_plain 14 | {:ok, e3, _pid} = Entity.start_plain 15 | {:ok, e4, _pid} = Entity.start_plain 16 | 17 | Attribute.register(e1) 18 | Group.register(e1) 19 | Spy.register(e1, self) 20 | 21 | Attribute.register(e2) 22 | Group.register(e2) 23 | Spy.register(e2, self) 24 | 25 | Attribute.register(e3) 26 | Group.register(e3) 27 | Group.new_leader(e3, e1) 28 | Spy.register(e3, self) 29 | assert_receive %{sender: ^e1, event: {:group_assign, ^e3}} 30 | 31 | Attribute.register(e4) 32 | Group.register(e4) 33 | Group.new_leader(e4, e2) 34 | Spy.register(e4, self) 35 | assert_receive %{sender: ^e2, event: {:group_assign, ^e4}} 36 | 37 | {:ok, [e1: e1, e2: e2, e3: e3, e4: e4]} 38 | end 39 | 40 | 41 | test "setup", %{e1: e1, e2: e2, e3: e3, e4: e4} do 42 | assert {:ok, %Leader{members: [^e3], invited: []}} = Entity.fetch_attribute(e1, Leader) 43 | assert {:ok, %Leader{members: [^e4], invited: []}} = Entity.fetch_attribute(e2, Leader) 44 | assert {:ok, %Member{leader: ^e1}} = Entity.fetch_attribute(e3, Member) 45 | assert {:ok, %Member{leader: ^e2}} = Entity.fetch_attribute(e4, Member) 46 | end 47 | 48 | 49 | test "leader check", %{e1: e1, e2: e2, e3: e3} do 50 | assert Group.is_my_leader?(e3, e1) == true 51 | assert Group.is_my_leader?(e3, e2) == false 52 | assert Group.is_my_leader?(e1, e1) == true 53 | assert Group.is_my_leader?(e1, e2) == false 54 | assert Group.is_my_leader?(e1, e3) == false 55 | end 56 | 57 | 58 | test "inviting", %{e1: e1, e2: e2} do 59 | e1 |> Group.invite(e2) 60 | 61 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 62 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 63 | 64 | assert {:ok, %Leader{invited: [^e2]}} = Entity.fetch_attribute(e1, Leader) 65 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 66 | end 67 | 68 | 69 | test "inviting a member of another group", %{e1: e1, e2: e2, e4: e4} do 70 | e1 |> Group.invite(e4) 71 | 72 | assert_receive %{sender: ^e4, event: {:group_invite, ^e1}} 73 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 74 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 75 | 76 | assert {:ok, %Leader{invited: [^e2]}} = Entity.fetch_attribute(e1, Leader) 77 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 78 | end 79 | 80 | 81 | test "merging", %{e1: e1, e2: e2, e3: e3, e4: e4} do 82 | e1 |> Group.invite(e2) 83 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 84 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 85 | 86 | e2 |> Group.invite(e1) 87 | assert_receive %{sender: ^e1, event: {:group_invite, ^e2}} 88 | 89 | # leader should receive new members... 90 | assert_receive %{sender: ^e1, event: {:group_assign, ^e2}} 91 | assert_receive %{sender: ^e1, event: {:group_assign, ^e4}} 92 | 93 | assert {:ok, %Leader{members: [^e3 | new_mems], invited: []}} = Entity.fetch_attribute(e1, Leader) 94 | assert {:ok, %Member{leader: ^e1}} = Entity.fetch_attribute(e2, Member) 95 | assert e2 in new_mems and e4 in new_mems 96 | assert Entity.has_attribute?(e2, Leader) == false 97 | end 98 | 99 | 100 | test "kicking a member", %{e1: e1, e3: e3} do 101 | e1 |> Group.kick(e3) 102 | 103 | assert_receive %{sender: ^e3, event: {:group_kick, ^e1}} 104 | assert_receive %{sender: ^e1, event: {:group_leave, ^e3}} 105 | 106 | assert {:ok, %Leader{members: [], invited: []}} = Entity.fetch_attribute(e1, Leader) 107 | assert {:ok, %Leader{members: [], invited: []}} = Entity.fetch_attribute(e3, Leader) 108 | assert Entity.has_attribute?(e3, Member) == false 109 | end 110 | 111 | 112 | test "kicking myself as a leader", %{e1: e1, e2: e2, e3: e3} do 113 | e1 |> Group.invite(e2) 114 | 115 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 116 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 117 | 118 | assert {:ok, %Leader{invited: [e2]}} = Entity.fetch_attribute(e1, Leader) 119 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 120 | 121 | e1 |> Group.kick(e1) 122 | 123 | assert_receive %{sender: ^e1, event: {:group_kick, ^e1}} 124 | assert_receive %{sender: ^e3, event: {:group_new_leader, ^e3, [^e2]}} 125 | 126 | assert {:ok, %Leader{members: [], invited: []}} = Entity.fetch_attribute(e1, Leader) 127 | assert {:ok, %Leader{members: [], invited: [^e2]}} = Entity.fetch_attribute(e3, Leader) 128 | assert Entity.has_attribute?(e3, Member) == false 129 | end 130 | 131 | 132 | test "kicking an invite - another", %{e1: e1, e2: e2} do 133 | e1 |> Group.invite(e2) 134 | 135 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 136 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 137 | 138 | assert {:ok, %Leader{invited: [e2]}} = Entity.fetch_attribute(e1, Leader) 139 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 140 | 141 | e2 |> Group.kick(e1) 142 | 143 | assert_receive %{sender: ^e2, event: {:group_kick, ^e1}} 144 | assert_receive %{sender: ^e1, event: {:group_kick, ^e2}} 145 | 146 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e1, Leader) 147 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 148 | end 149 | 150 | 151 | test "kicking an invite - my own", %{e1: e1, e2: e2} do 152 | e1 |> Group.invite(e2) 153 | 154 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 155 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 156 | 157 | assert {:ok, %Leader{invited: [e2]}} = Entity.fetch_attribute(e1, Leader) 158 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 159 | 160 | e1 |> Group.kick(e2) 161 | 162 | assert_receive %{sender: ^e1, event: {:group_kick, ^e2}} 163 | assert_receive %{sender: ^e2, event: {:group_kick, ^e1}} 164 | 165 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e1, Leader) 166 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 167 | end 168 | 169 | 170 | test "remove behaviour as leader", %{e1: e1, e2: e2, e3: e3} do 171 | e1 |> Group.invite(e2) 172 | 173 | assert_receive %{sender: ^e2, event: {:group_invite, ^e1}} 174 | assert_receive %{sender: ^e1, event: {:group_invite_ack, ^e2}} 175 | 176 | assert {:ok, %Leader{invited: [e2]}} = Entity.fetch_attribute(e1, Leader) 177 | assert {:ok, %Leader{invited: []}} = Entity.fetch_attribute(e2, Leader) 178 | 179 | e1 |> Group.unregister() 180 | 181 | assert_receive %{sender: ^e3, event: {:group_new_leader, ^e3, [^e2]}} 182 | 183 | assert {:ok, %Leader{members: [], invited: [^e2]}} = Entity.fetch_attribute(e3, Leader) 184 | assert :error = Entity.fetch_attribute(e1, Leader) 185 | end 186 | 187 | 188 | test "remove behaviour as member", %{e1: e1, e3: e3} do 189 | e3 |> Group.unregister() 190 | 191 | assert_receive %{sender: ^e1, event: {:group_leave, ^e3}} 192 | 193 | assert {:ok, %Leader{members: [], invited: []}} = Entity.fetch_attribute(e1, Leader) 194 | assert :error = Entity.fetch_attribute(e3, Member) 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /test/entice/logic/map_instance_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MapInstanceTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Attributes 4 | use Entice.Logic.Map 5 | alias Entice.Entity 6 | alias Entice.Entity.Coordination 7 | alias Entice.Entity.Suicide 8 | alias Entice.Entity.Test.Spy 9 | alias Entice.Logic.MapInstance 10 | alias Entice.Logic.MapRegistry 11 | alias Entice.Logic.Player 12 | @moduletag :map_instance 13 | 14 | 15 | defmap TestMap1 16 | defmap TestMap2 17 | defmap TestMap3 18 | defmap TestMap4 19 | defmap TestMap5 20 | defmap TestMap6 21 | 22 | 23 | setup do 24 | MapRegistry.start_link() 25 | {:ok, entity_id, entity_pid} = Entity.start 26 | {:ok, %{entity_id: entity_id, entity_pid: entity_pid}} 27 | end 28 | 29 | 30 | test "register", %{entity_id: entity_id} do 31 | MapInstance.register(entity_id, TestMap1) 32 | m = %MapInstance{map: TestMap1, players: 0} 33 | assert {:ok, ^m} = Entity.fetch_attribute(entity_id, MapInstance) 34 | end 35 | 36 | 37 | test "player joins", %{entity_id: entity_id} do 38 | MapInstance.register(entity_id, TestMap2) 39 | {:ok, player_id, _pid} = Entity.start 40 | Player.register(player_id, TestMap2) 41 | 42 | {:ok, e1, _pid} = Entity.start 43 | Coordination.register(e1, TestMap2) 44 | Spy.register(e1, self) 45 | 46 | MapInstance.add_player(entity_id, player_id) 47 | 48 | assert 1 = Entity.get_attribute(entity_id, MapInstance).players 49 | assert_receive %{sender: ^e1, event: {:entity_join, %{entity_id: ^player_id, attributes: _}}} 50 | end 51 | 52 | 53 | test "npc joins", %{entity_id: entity_id} do 54 | MapInstance.register(entity_id, TestMap3) 55 | {:ok, e1, _pid} = Entity.start 56 | Coordination.register(e1, TestMap3) 57 | Spy.register(e1, self) 58 | 59 | MapInstance.add_npc(entity_id, "Gwen", :gwen, %Position{}) 60 | 61 | assert_receive %{sender: ^e1, event: {:entity_join, %{entity_id: _, attributes: %{Npc => %Npc{npc_model_id: :gwen}}}}}, 300 62 | end 63 | 64 | 65 | test "player leaves", %{entity_id: entity_id} do 66 | MapInstance.register(entity_id, TestMap4) 67 | {:ok, player_id_1, _pid} = Entity.start 68 | Player.register(player_id_1, TestMap4) 69 | {:ok, player_id_2, _pid} = Entity.start 70 | Player.register(player_id_2, TestMap4) 71 | 72 | Spy.register(entity_id, self) 73 | 74 | MapInstance.add_player(entity_id, player_id_1) 75 | MapInstance.add_player(entity_id, player_id_2) 76 | 77 | assert 2 = Entity.get_attribute(entity_id, MapInstance).players 78 | 79 | Entity.stop(player_id_2) 80 | 81 | assert_receive %{sender: _, event: {:entity_leave, %{entity_id: ^player_id_2}}} 82 | assert 1 = Entity.get_attribute(entity_id, MapInstance).players 83 | end 84 | 85 | 86 | test "last player leaves", %{entity_id: entity_id, entity_pid: entity_pid} do 87 | MapInstance.register(entity_id, TestMap5) 88 | {:ok, player_id, _pid} = Entity.start 89 | Player.register(player_id, TestMap5) 90 | 91 | Process.monitor(entity_pid) 92 | Coordination.register_observer(self, TestMap5) 93 | 94 | MapInstance.add_player(entity_id, player_id) 95 | 96 | m = %MapInstance{map: TestMap5, players: 1} 97 | assert {:ok, ^m} = Entity.fetch_attribute(entity_id, MapInstance) 98 | 99 | Entity.stop(player_id) 100 | 101 | poison_pill = Suicide.poison_pill_message 102 | assert_receive ^poison_pill 103 | 104 | assert_receive {:coordination_stop_channel, TestMap5} 105 | assert_receive {:DOWN, _, _, _, :normal} 106 | end 107 | 108 | 109 | test "unregister", %{entity_id: entity_id} do 110 | MapInstance.register(entity_id, TestMap6) 111 | MapInstance.unregister(entity_id) 112 | :timer.sleep(100) 113 | assert :error = Entity.fetch_attribute(entity_id, MapInstance) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/entice/logic/map_registry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MapRegistryTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Map 4 | alias Entice.Entity 5 | alias Entice.Logic.{MapInstance, MapRegistry} 6 | @moduletag :map_registry 7 | 8 | defmap TestMap1 9 | defmap TestMap2 10 | defmap TestMap3 11 | defmap TestMap4 12 | 13 | 14 | setup do 15 | MapRegistry.start_link() 16 | :ok 17 | end 18 | 19 | 20 | test "start instance" do 21 | entity = MapRegistry.get_or_create_instance(TestMap1) 22 | assert Entity.exists?(entity) 23 | assert Entity.has_behaviour?(entity, MapInstance.Behaviour) 24 | assert Process.alive?(Entity.fetch!(entity)) 25 | end 26 | 27 | test "get instance that already exists" do 28 | entity = MapRegistry.get_or_create_instance(TestMap2) 29 | assert ^entity = MapRegistry.get_or_create_instance(TestMap2) 30 | end 31 | 32 | test "stop instance" do 33 | entity = MapRegistry.get_or_create_instance(TestMap4) 34 | Process.monitor(Entity.fetch!(entity)) 35 | MapRegistry.stop_instance(TestMap4) 36 | 37 | assert_receive {:DOWN, _, _, _, :normal} 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/entice/logic/maps/maps_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MapTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Maps 4 | 5 | test "map api" do 6 | assert {:ok, TeamArenas} = Maps.get_map("TeamArenas") 7 | end 8 | 9 | test "outposts & non-outposts" do 10 | assert TeamArenas.is_outpost? == true 11 | assert IsleOfTheNameless.is_outpost? == false 12 | end 13 | 14 | test "default map" do 15 | assert HeroesAscent = Maps.default_map 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/entice/logic/movement_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.MovementTest do 2 | use ExUnit.Case, async: true 3 | alias Entice.Entity 4 | alias Entice.Utils.Geom.Coord 5 | alias Entice.Logic.Movement 6 | alias Entice.Logic.Player.Position 7 | 8 | 9 | setup do 10 | {:ok, _id, pid} = Entity.start 11 | Movement.register(pid) 12 | {:ok, [entity: pid]} 13 | end 14 | 15 | 16 | test "register plain", %{entity: pid} do 17 | m = %Movement{} 18 | assert {:ok, ^m} = Entity.fetch_attribute(pid, Movement) 19 | end 20 | 21 | 22 | test "register with position", %{entity: pid} do 23 | Movement.unregister(pid) # remove again, so we can add a new one 24 | Entity.put_attribute(pid, %Position{pos: %Coord{x: 42, y: 1337}, plane: 7}) 25 | Movement.register(pid) 26 | m = %Movement{goal: %Coord{x: 42, y: 1337}, plane: 7} 27 | assert {:ok, ^m} = Entity.fetch_attribute(pid, Movement) 28 | end 29 | 30 | 31 | test "register with movement", %{entity: pid} do 32 | Movement.unregister(pid) # remove again, so we can add a new one 33 | Entity.put_attribute(pid, %Movement{goal: %Coord{x: 42, y: 1337}}) 34 | Movement.register(pid) 35 | m = %Movement{goal: %Coord{x: 42, y: 1337}} 36 | assert {:ok, ^m} = Entity.fetch_attribute(pid, Movement) 37 | end 38 | 39 | 40 | test "update", %{entity: pid} do 41 | Movement.update(pid, 42 | %Position{pos: %Coord{x: 42, y: 1337}, plane: 7}, 43 | %Movement{goal: %Coord{x: 1337, y: 42}, plane: 13, move_type: 5, velocity: 0.5}) 44 | assert {:ok, %Position{plane: 7}} = Entity.fetch_attribute(pid, Position) 45 | assert {:ok, %Movement{move_type: 5}} = Entity.fetch_attribute(pid, Movement) 46 | end 47 | 48 | 49 | test "terminate", %{entity: pid} do 50 | Movement.unregister(pid) 51 | assert :error = Entity.fetch_attribute(pid, Movement) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/entice/logic/npc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.NpcTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Maps 4 | use Entice.Logic.Attributes 5 | alias Entice.Entity 6 | alias Entice.Logic.Npc 7 | alias Entice.Logic.Player.{Name, Position, Level} 8 | 9 | 10 | setup do 11 | {:ok, _id, pid} = Npc.spawn("Dhuum", :dhuum, %Position{pos: %Coord{x: 1, y: 2}, plane: 3}) 12 | {:ok, [entity: pid]} 13 | end 14 | 15 | test "correct spawn", %{entity: pid} do 16 | assert {:ok, %Name{name: "Dhuum"}} = Entity.fetch_attribute(pid, Name) 17 | assert {:ok, %Npc{npc_model_id: :dhuum}} = Entity.fetch_attribute(pid, Npc) 18 | assert {:ok, %Level{level: 20}} = Entity.fetch_attribute(pid, Level) 19 | assert {:ok, %Position{pos: %Coord{x: 1, y: 2}, plane: 3}} = Entity.fetch_attribute(pid, Position) 20 | end 21 | 22 | test "correct unregister", %{entity: pid} do 23 | Npc.unregister(pid) 24 | assert Entity.has_attribute?(pid, Name) == false 25 | assert Entity.has_attribute?(pid, Position) == false 26 | assert Entity.has_attribute?(pid, Npc) == false 27 | assert Entity.has_attribute?(pid, Level) == false 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/entice/logic/player_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.PlayerTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Maps 4 | use Entice.Logic.Attributes 5 | alias Entice.Entity 6 | alias Entice.Logic.Player 7 | 8 | 9 | setup do 10 | {:ok, _id, pid} = Entity.start 11 | Player.register(pid, HeroesAscent) 12 | {:ok, [entity: pid]} 13 | end 14 | 15 | 16 | test "correct register", %{entity: pid} do 17 | assert Entity.has_attribute?(pid, Name) == true 18 | assert Entity.has_attribute?(pid, Position) == true 19 | assert Entity.has_attribute?(pid, Appearance) == true 20 | assert Entity.has_attribute?(pid, Level) == true 21 | end 22 | 23 | 24 | test "correct unregister", %{entity: pid} do 25 | Player.unregister(pid) 26 | assert Entity.has_attribute?(pid, Name) == false 27 | assert Entity.has_attribute?(pid, Position) == false 28 | assert Entity.has_attribute?(pid, Appearance) == false 29 | assert Entity.has_attribute?(pid, Level) == false 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/entice/logic/skillbar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.SkillBarTest do 2 | use ExUnit.Case, async: true 3 | alias Entice.Entity 4 | alias Entice.Logic.Skills 5 | alias Entice.Logic.SkillBar 6 | 7 | setup do 8 | {:ok, entity_id, _pid} = Entity.start 9 | {:ok, [entity_id: entity_id]} 10 | end 11 | 12 | 13 | test "change skill", %{entity_id: eid} do 14 | SkillBar.register(eid) 15 | empty_skills = %SkillBar{} 16 | 17 | assert [0,0,0,0,0,0,0,0] = SkillBar.get_skills(eid) 18 | assert {:ok, ^empty_skills} = Entity.fetch_attribute(eid, SkillBar) 19 | 20 | new_skill = Skills.get_skill(1) 21 | new_skills = %SkillBar{slots: [new_skill | empty_skills.slots |> tl]} 22 | 23 | assert [1,0,0,0,0,0,0,0] = SkillBar.change_skill(eid, 0, new_skill.id) 24 | assert {:ok, ^new_skills} = Entity.fetch_attribute(eid, SkillBar) 25 | end 26 | 27 | 28 | test "get skill", %{entity_id: eid} do 29 | SkillBar.register(eid, [1,2,3,4,0,0,0,313373]) 30 | 31 | assert Skills.HealingSignet = SkillBar.get_skill(eid, 0) 32 | assert Skills.Bamph = SkillBar.get_skill(eid, 3) 33 | assert Skills.NoSkill = SkillBar.get_skill(eid, 4) 34 | assert Skills.NoSkill = SkillBar.get_skill(eid, 7) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/entice/logic/skills/skills_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.SkillsTest do 2 | use Entice.Logic.Skill 3 | use Entice.Logic.Attributes 4 | use ExUnit.Case, async: true 5 | alias Entice.Entity 6 | alias Entice.Entity.Attribute 7 | alias Entice.Logic.Vitals 8 | 9 | 10 | defmodule TestAttr, do: defstruct test_pid: nil 11 | 12 | 13 | defskill SomeSkill, id: 1 do 14 | def description, do: "Is some skill." 15 | def cast_time, do: 5000 16 | def recharge_time, do: 10000 17 | def energy_cost, do: 10 18 | end 19 | 20 | defskill SomeOtherSkill, id: 2 do 21 | def description, do: "Is some other skill." 22 | def cast_time, do: 5000 23 | def recharge_time, do: 10000 24 | def energy_cost, do: 10 25 | 26 | def apply_effect(pid, pid) do 27 | send pid, :gotcha 28 | :ok 29 | end 30 | end 31 | 32 | 33 | test "the skill's id" do 34 | assert SomeSkill.id == 1 35 | assert SomeOtherSkill.id == 2 36 | end 37 | 38 | test "the skill's name" do 39 | assert SomeSkill.name == "SomeSkill" 40 | end 41 | 42 | test "the skill's underscore name" do 43 | assert SomeSkill.underscore_name == "some_skill" 44 | end 45 | 46 | test "retrieveing skills by id" do 47 | assert get_skill(1) == SomeSkill 48 | assert get_skill(2) == SomeOtherSkill 49 | end 50 | 51 | test "retrieveing skills by name" do 52 | assert get_skill("SomeSkill") == SomeSkill 53 | assert get_skill("SomeOtherSkill") == SomeOtherSkill 54 | end 55 | 56 | test "retrieve all skills" do 57 | assert SomeSkill in get_skills 58 | end 59 | 60 | test "bit-array (as int) that contains all skill-ids as set bits" do 61 | assert 3 == max_unlocked_skills 62 | end 63 | 64 | test "skill after-cast-time effects" do 65 | SomeOtherSkill.apply_effect(self, self) 66 | assert_receive :gotcha 67 | end 68 | 69 | # prerequisites 70 | 71 | test "target is dead prerequisite" do 72 | {:ok, eid, _pid} = Entity.start_plain() 73 | Attribute.register(eid) 74 | Vitals.register(eid) 75 | 76 | assert {:error, :target_not_dead} == require_dead(eid) 77 | 78 | Vitals.kill(eid) 79 | assert Entity.has_behaviour?(eid, Vitals.DeadBehaviour) 80 | 81 | assert :ok == require_dead(eid) 82 | end 83 | 84 | 85 | # effects 86 | 87 | 88 | test "damage effect" do 89 | {:ok, eid, _pid} = Entity.start_plain() 90 | Attribute.register(eid) 91 | Vitals.register(eid) 92 | 93 | %Health{health: health} = Entity.get_attribute(eid, Health) 94 | 95 | damage(eid, 10) 96 | 97 | %Health{health: health_after_damage} = Entity.get_attribute(eid, Health) 98 | assert health_after_damage == (health - 10) 99 | end 100 | 101 | 102 | test "healing effect" do 103 | {:ok, eid, _pid} = Entity.start_plain() 104 | Attribute.register(eid) 105 | Vitals.register(eid) 106 | 107 | %Health{health: health} = Entity.get_and_update_attribute(eid, Health, fn health -> %Health{health | health: health.health - 20} end) 108 | 109 | heal(eid, 10) 110 | 111 | %Health{health: health_after_heal} = Entity.get_attribute(eid, Health) 112 | assert health_after_heal == (health + 10) 113 | end 114 | 115 | 116 | test "resurrection effect" do 117 | {:ok, eid, _pid} = Entity.start_plain() 118 | Attribute.register(eid) 119 | Vitals.register(eid) 120 | 121 | Vitals.kill(eid) 122 | :timer.sleep(100) 123 | assert Entity.has_behaviour?(eid, Vitals.DeadBehaviour) 124 | 125 | resurrect(eid, 50, 50) 126 | :timer.sleep(100) 127 | assert Entity.has_behaviour?(eid, Vitals.AliveBehaviour) 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/entice/logic/vitals_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Entice.Logic.VitalsTest do 2 | use ExUnit.Case, async: true 3 | use Entice.Logic.Attributes 4 | alias Entice.Entity 5 | alias Entice.Entity.Coordination 6 | alias Entice.Logic.Vitals 7 | alias Entice.Logic.Vitals.AliveBehaviour 8 | alias Entice.Logic.Vitals.DeadBehaviour 9 | 10 | setup do 11 | {:ok, e1, _pid} = Entity.start 12 | {:ok, e2, _pid} = Entity.start 13 | 14 | Entity.put_attribute(e1, %Level{level: 20}) 15 | Entity.put_attribute(e2, %Level{level: 3}) 16 | 17 | Vitals.register(e1) 18 | Vitals.register(e2) 19 | 20 | {:ok, [e1: e1, e2: e2]} 21 | end 22 | 23 | test "entity has AliveBehaviour", %{e1: e1} do 24 | assert Entity.has_behaviour?(e1, AliveBehaviour) 25 | end 26 | 27 | test "entity has health", %{e1: e1} do 28 | assert Entity.has_attribute?(e1, Health) 29 | end 30 | 31 | test "entity has health level 20", %{e1: e1} do 32 | assert {:ok, %Health{health: 480, max_health: 480}} = Entity.fetch_attribute(e1, Health) 33 | end 34 | 35 | test "entity has health level 3", %{e2: e2} do 36 | assert {:ok, %Health{health: 140, max_health: 140}} = Entity.fetch_attribute(e2, Health) 37 | end 38 | 39 | test "entity has mana", %{e1: e1} do 40 | assert Entity.has_attribute?(e1, Energy) 41 | end 42 | 43 | test "entity has morale", %{e1: e1} do 44 | assert Entity.has_attribute?(e1, Morale) 45 | end 46 | 47 | test "health & energy & morale are removed on termination", %{e1: e1} do 48 | Vitals.unregister(e1) 49 | assert not Entity.has_attribute?(e1, Health) 50 | assert not Entity.has_attribute?(e1, Energy) 51 | assert not Entity.has_attribute?(e1, Morale) 52 | end 53 | 54 | test "do damage on entity", %{e1: e1} do 55 | Vitals.damage(e1, 140) 56 | assert {:ok, %Health{health: 340, max_health: 480}} = Entity.fetch_attribute(e1, Health) 57 | end 58 | 59 | test "do damage on entity and heal the entity", %{e1: e1} do 60 | Vitals.damage(e1, 100) 61 | assert {:ok, %Health{health: 380, max_health: 480}} = Entity.fetch_attribute(e1, Health) 62 | Vitals.heal(e1, 100) 63 | assert {:ok, %Health{health: 480, max_health: 480}} = Entity.fetch_attribute(e1, Health) 64 | end 65 | 66 | test "heal entity and check if health <= max_health", %{e1: e1} do 67 | Vitals.heal(e1, 200) 68 | assert {:ok, %Health{health: 480, max_health: 480}} = Entity.fetch_attribute(e1, Health) 69 | end 70 | 71 | test "entity dies and has -15 morale", %{e1: e1} do 72 | Vitals.damage(e1, 1000) 73 | assert Entity.has_behaviour?(e1, DeadBehaviour) 74 | assert {:ok, %Morale{morale: -15}} = Entity.fetch_attribute(e1, Morale) 75 | end 76 | 77 | test "resurrect entity with -15 morale", %{e1: e1} do 78 | Coordination.register_observer(self, __MODULE__) 79 | Coordination.register(e1, __MODULE__) 80 | Vitals.damage(e1, 1000) 81 | assert_receive {:entity_dead, %{entity_id: ^e1, attributes: %{}}} 82 | assert Entity.has_behaviour?(e1, DeadBehaviour) 83 | 84 | Vitals.resurrect(e1, 50, 50) 85 | assert_receive {:entity_resurrected, %{entity_id: ^e1, attributes: %{}}} 86 | assert Entity.has_behaviour?(e1, AliveBehaviour) 87 | assert {:ok, %Morale{morale: -15}} = Entity.fetch_attribute(e1, Morale) 88 | end 89 | 90 | test "resurrect entity with -15 morale and 50 percent of health and mana", %{e1: e1} do 91 | Vitals.damage(e1, 1000) 92 | assert Entity.has_behaviour?(e1, DeadBehaviour) 93 | Vitals.resurrect(e1, 50, 50) 94 | assert Entity.has_behaviour?(e1, AliveBehaviour) 95 | assert {:ok, %Morale{morale: -15}} = Entity.fetch_attribute(e1, Morale) 96 | assert {:ok, %Health{health: 204, max_health: 408}} = Entity.fetch_attribute(e1, Health) 97 | assert {:ok, %Energy{mana: 30, max_mana: 59}} = Entity.fetch_attribute(e1, Energy) 98 | end 99 | 100 | 101 | test "killing an entity", %{e1: e1} do 102 | Vitals.kill(e1) 103 | assert Entity.has_behaviour?(e1, DeadBehaviour) 104 | end 105 | 106 | 107 | test "recharging health", %{e1: e1} do 108 | assert {:ok, %Health{health: 480}} = Entity.fetch_attribute(e1, Health) 109 | 110 | Vitals.health_regeneration(e1, 10) 111 | Vitals.damage(e1, 10) 112 | 113 | assert {:ok, %Health{health: health}} = Entity.fetch_attribute(e1, Health) 114 | assert health <= 480 115 | 116 | :timer.sleep(1100) 117 | 118 | assert {:ok, %Health{health: 480}} = Entity.fetch_attribute(e1, Health) 119 | end 120 | 121 | 122 | test "recharging energy", %{e1: e1} do 123 | assert {:ok, %Energy{mana: 70}} = Entity.fetch_attribute(e1, Energy) 124 | 125 | Vitals.energy_regeneration(e1, 2) 126 | Entity.update_attribute(e1, Energy, fn ene -> %Energy{ene | mana: (ene.mana - 2)} end) 127 | 128 | assert {:ok, %Energy{mana: energy}} = Entity.fetch_attribute(e1, Energy) 129 | assert energy <= 70 130 | 131 | :timer.sleep(1100) 132 | 133 | assert {:ok, %Energy{mana: 70}} = Entity.fetch_attribute(e1, Energy) 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------