Skip to content
This repository has been archived by the owner on Aug 26, 2024. It is now read-only.

Commit

Permalink
NPCs will respawn after a spawn interval
Browse files Browse the repository at this point in the history
The genserver for them will detect a death and set a timestamp for
respawn. After this respawn time has passed it will give max health back
to the NPC and enter the room.
  • Loading branch information
oestrich committed Aug 24, 2017
1 parent 5397035 commit d7735e6
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 12 deletions.
4 changes: 3 additions & 1 deletion .projections.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"priv/repo/migrations":{ "type": "migration", }
"priv/repo/migrations": { "type": "migration" },
"lib/*.ex": { "alternate": "test/{}_test.exs" },
"test/*_test.exs": { "alternate": "lib/{}.ex" }
}
5 changes: 3 additions & 2 deletions lib/data/npc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Data.NPC do
field :name, :string
field :hostile, :boolean
field :stats, Data.Stats
field :spawn_interval, :integer

belongs_to :room, Data.Room

Expand All @@ -17,7 +18,7 @@ defmodule Data.NPC do

def changeset(struct, params) do
struct
|> cast(params, [:name, :room_id, :hostile, :stats])
|> validate_required([:name, :room_id, :hostile, :stats])
|> cast(params, [:name, :room_id, :hostile, :stats, :spawn_interval])
|> validate_required([:name, :room_id, :hostile, :stats, :spawn_interval])
end
end
4 changes: 2 additions & 2 deletions lib/data/room_item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ defmodule Data.RoomItem do
belongs_to :room, Data.Room
belongs_to :item, Data.Item

field :interval, :integer
field :spawn_interval, :integer

timestamps()
end

def changeset(struct, params) do
struct
|> cast(params, [:room_id, :item_id, :interval])
|> cast(params, [:room_id, :item_id, :spawn_interval])
|> validate_required([:room_id, :item_id])
end
end
21 changes: 21 additions & 0 deletions lib/game/npc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Game.NPC do
alias Game.Character
alias Game.Effect
alias Game.Message
alias Game.NPC.Actions

@doc """
Starts a new NPC server
Expand Down Expand Up @@ -48,6 +49,14 @@ defmodule Game.NPC do
GenServer.cast(pid(id), {:heard, message})
end

@doc """
Send a tick message
"""
@spec tick(pid :: pid, time :: DateTime.t) :: :ok
def tick(pid, time) do
GenServer.cast(pid, {:tick, time})
end

def init(npc) do
GenServer.cast(self(), :enter)
{:ok, %{npc: npc, is_targeting: MapSet.new()}}
Expand All @@ -57,6 +66,7 @@ defmodule Game.NPC do
@room.enter(npc.room_id, {:npc, npc})
{:noreply, state}
end

def handle_cast({:heard, message}, state = %{npc: npc}) do
case message.message do
"Hello" <> _ ->
Expand All @@ -65,6 +75,17 @@ defmodule Game.NPC do
end
{:noreply, state}
end

def handle_cast({:tick, time}, state) do
case Actions.tick(state, time) do
:ok -> {:noreply, state}
{:update, state} -> {:noreply, state}
end
end

#
# Character callbacks
#
def handle_cast({:targeted, {_, player}}, state = %{npc: npc}) do
npc.room_id |> @room.say(npc, Message.npc(npc, "Why are you targeting me, #{player.name}?"))
state = Map.put(state, :is_targeting, MapSet.put(state.is_targeting, {:user, player.id}))
Expand Down
29 changes: 29 additions & 0 deletions lib/game/npc/actions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Game.NPC.Actions do
@moduledoc """
"""

use Game.Room

@doc """
Respawn the NPC as a tick happens
"""
@spec tick(state :: map, time :: DateTime.t) :: :ok | {:update, map}
def tick(state = %{npc: %{stats: %{health: health}}}, time) when health < 1 do
state = state |> handle_respawn(time)
{:update, state}
end
def tick(_state, _time), do: :ok

defp handle_respawn(state = %{respawn_at: respawn_at, npc: npc}, time) when respawn_at != nil do
case Timex.after?(time, respawn_at) do
true ->
npc = %{npc | stats: %{npc.stats | health: npc.stats.max_health}}
npc.room_id |> @room.enter({:npc, npc})
%{state | npc: npc, respawn_at: nil}
false -> state
end
end
defp handle_respawn(state, time) do
Map.put(state, :respawn_at, time |> Timex.shift(seconds: state.npc.spawn_interval))
end
end
10 changes: 10 additions & 0 deletions lib/game/npc/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ defmodule Game.NPC.Supervisor do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end

@doc """
Return all npcs that are currently online
"""
@spec npcs() :: [pid]
def npcs() do
__MODULE__
|> Supervisor.which_children()
|> Enum.map(&(elem(&1, 1)))
end

def init(_) do
children = NPC.all |> Enum.map(fn (npc) ->
worker(NPC, [npc], id: npc.id, restart: :permanent)
Expand Down
4 changes: 3 additions & 1 deletion lib/game/room.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Game.Room do

alias Game.Room.Actions
alias Game.Room.Repo
alias Game.Format
alias Game.Message
alias Game.NPC
alias Game.Session
Expand Down Expand Up @@ -126,7 +127,8 @@ defmodule Game.Room do
players |> echo_to_players("{blue}#{user.name}{/blue} enters")
{:noreply, Map.put(state, :players, [player | players])}
end
def handle_cast({:enter, {:npc, npc}}, state = %{npcs: npcs}) do
def handle_cast({:enter, character = {:npc, npc}}, state = %{npcs: npcs, players: players}) do
players |> echo_to_players("#{Format.target_name(character)} enters")
{:noreply, Map.put(state, :npcs, [npc | npcs])}
end

Expand Down
2 changes: 1 addition & 1 deletion lib/game/room/actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ defmodule Game.Room.Actions do
end

defp respawn_if_after_interval(room_item, time, state) do
case past_interval?(time, room_item.interval) do
case past_interval?(time, room_item.spawn_interval) do
true -> respawn_item(room_item, state)
false -> state
end
Expand Down
2 changes: 2 additions & 0 deletions lib/game/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Game.Server do

use GenServer

alias Game.NPC
alias Game.Session
alias Game.Zone

Expand Down Expand Up @@ -36,6 +37,7 @@ defmodule Game.Server do
end)

Zone.Supervisor.zones |> Enum.each(&Zone.tick/1)
NPC.Supervisor.npcs |> Enum.each(&(NPC.tick(&1, time)))

{:noreply, state}
end
Expand Down
11 changes: 11 additions & 0 deletions priv/repo/migrations/20170823232426_add_npc_spawn_time.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Data.Repo.Migrations.AddNpcSpawnTime do
use Ecto.Migration

def change do
alter table(:npcs) do
add :spawn_interval, :integer, default: 30, null: false
end

rename table(:room_items), :interval, to: :spawn_interval
end
end
6 changes: 3 additions & 3 deletions priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ defmodule Seeds do
effects: [],
keywords: ["sword"],
})
entrance = entrance |> add_item_to_room(sword, %{interval: 15})
entrance = entrance |> add_item_to_room(sword, %{spawn_interval: 15})

leather_armor = create_item(%{
name: "Leather Armor",
Expand All @@ -172,7 +172,7 @@ defmodule Seeds do
effects: [],
keywords: ["leather"],
})
entrance = entrance |> add_item_to_room(leather_armor, %{interval: 15})
entrance = entrance |> add_item_to_room(leather_armor, %{spawn_interval: 15})

elven_armor = create_item(%{
name: "Elven armor",
Expand All @@ -182,7 +182,7 @@ defmodule Seeds do
effects: [%{kind: "stats", field: :dexterity, amount: 5}, %{kind: "stats", field: :strength, amount: 5}],
keywords: ["elven"],
})
entrance = entrance |> add_item_to_room(elven_armor, %{interval: 15})
entrance = entrance |> add_item_to_room(elven_armor, %{spawn_interval: 15})

save = %{
room_id: entrance.id,
Expand Down
37 changes: 37 additions & 0 deletions test/game/npc/actions_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule Game.NPC.ActionsTest do
use ExUnit.Case

alias Game.NPC.Actions

@room Test.Game.Room

describe "tick - respawning the npc" do
setup do
%{time: Timex.now(), npc: %{room_id: 1, stats: %{health: 10, max_health: 15}, spawn_interval: 10}}
end

test "does nothing if the npc is alive", %{time: time, npc: npc} do
:ok = Actions.tick(%{npc: npc}, time)
end

test "detecting death", %{time: time, npc: npc} do
{:update, state} = Actions.tick(%{npc: put_in(npc, [:stats, :health], 0)}, time)
assert state.respawn_at == time |> Timex.shift(seconds: 10)
end

test "doesn't spawn until time", %{time: time, npc: npc} do
respawn_at = time |> Timex.shift(seconds: 2)
{:update, state} = Actions.tick(%{npc: put_in(npc, [:stats, :health], 0), respawn_at: respawn_at}, time)
assert state.respawn_at == respawn_at
assert state.npc.stats.health == 0
end

test "respawns the npc", %{time: time, npc: npc} do
respawn_at = time |> Timex.shift(seconds: -31)
{:update, state} = Actions.tick(%{npc: put_in(npc, [:stats, :health], 0), respawn_at: respawn_at}, time)
assert is_nil(state.respawn_at)
assert state.npc.stats.health == 15
assert [{1, {:npc, _}}] = @room.get_enters()
end
end
end
2 changes: 1 addition & 1 deletion test/game/room/actions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ defmodule Game.Room.ActionsTest do

describe "tick - respawning items" do
setup %{room: room, item: item} do
create_room_item(room, item, %{spawn: true, interval: 30})
create_room_item(room, item, %{spawn: true, spawn_interval: 30})
room = Repo.preload(room, [:room_items])
{:ok, %{room: room, item: item}}
end
Expand Down
12 changes: 11 additions & 1 deletion test/support/room.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ defmodule Test.Game.Room do
Agent.get(__MODULE__, &(&1.room))
end

def enter(_id, {:user, _session, _user}) do
def enter(id, who) do
start_link()
Agent.update(__MODULE__, fn (state) ->
enters = Map.get(state, :enter, [])
Map.put(state, :enter, enters ++ [{id, who}])
end)
end

def get_enters() do
start_link()
Agent.get(__MODULE__, fn (state) -> Map.get(state, :enter, []) end)
end

def leave(id, user) do
Expand Down

0 comments on commit d7735e6

Please sign in to comment.