summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/battlemap')
-rw-r--r--src/battlemap/Makefile72
-rw-r--r--src/battlemap/src/battle/battle_turn.erl161
-rw-r--r--src/battlemap/src/battle/movement.erl58
-rw-r--r--src/battlemap/src/battle/roll.erl32
-rw-r--r--src/battlemap/src/handler.erl23
-rw-r--r--src/battlemap/src/io/security.erl33
-rw-r--r--src/battlemap/src/io/timed_cache.erl130
-rw-r--r--src/battlemap/src/io/timed_caches_manager.erl152
-rw-r--r--src/battlemap/src/query/character_turn.erl255
-rw-r--r--src/battlemap/src/query/load_state.erl110
-rw-r--r--src/battlemap/src/reply/add_char.erl74
-rw-r--r--src/battlemap/src/reply/set_map.erl28
-rw-r--r--src/battlemap/src/reply/set_timeline.erl27
-rw-r--r--src/battlemap/src/reply/turn_results.erl27
-rw-r--r--src/battlemap/src/shim/database_shim.erl137
-rw-r--r--src/battlemap/src/struct/attack.erl299
-rw-r--r--src/battlemap/src/struct/attributes.erl108
-rw-r--r--src/battlemap/src/struct/battle.erl250
-rw-r--r--src/battlemap/src/struct/battle_action.erl300
-rw-r--r--src/battlemap/src/struct/battlemap.erl121
-rw-r--r--src/battlemap/src/struct/character.erl133
-rw-r--r--src/battlemap/src/struct/character_instance.erl160
-rw-r--r--src/battlemap/src/struct/direction.erl37
-rw-r--r--src/battlemap/src/struct/location.erl71
-rw-r--r--src/battlemap/src/struct/player.erl72
-rw-r--r--src/battlemap/src/struct/player_turn.erl58
-rw-r--r--src/battlemap/src/struct/statistics.erl193
-rw-r--r--src/battlemap/src/struct/tile.erl47
-rw-r--r--src/battlemap/src/struct/turn_result.erl142
-rw-r--r--src/battlemap/src/struct/weapon.erl361
30 files changed, 3671 insertions, 0 deletions
diff --git a/src/battlemap/Makefile b/src/battlemap/Makefile
new file mode 100644
index 0000000..c5bb150
--- /dev/null
+++ b/src/battlemap/Makefile
@@ -0,0 +1,72 @@
+## Directories
+SRC_DIR ?= src
+BIN_DIR ?= ebin
+CONF_DIR ?= conf
+INCLUDE_DIR ?= include
+UNUSED_WWW_DIR ?= www
+
+YAWS_CONF ?= $(CONF_DIR)/yaws.conf
+YAWS_API_HEADER ?= /my/src/yaws/include/yaws_api.hrl
+
+DIALYZER_PLT_FILE ?= tacticians-server.plt
+
+## Binaries
+YAWS ?= yaws
+ERLC ?= erlc
+ERLC_OPTS ?=
+DIALYZER ?= dialyzer
+
+################################################################################
+REQUIRED_HEADERS = $(INCLUDE_DIR)/yaws_api.hrl
+
+SRC_FILES = $(wildcard $(SRC_DIR)/*.erl)
+MODULES = $(patsubst %.erl,%,$(SRC_FILES))
+SUB_DIRS = $(filter-out $(MODULES),$(sort $(dir $(wildcard $(SRC_DIR)/*/))))
+BIN_FILES = $(patsubst $(SRC_DIR)/%.erl,$(BIN_DIR)/%.beam,$(SRC_FILES))
+
+export
+################################################################################
+all:
+ for subdir in $(SUB_DIRS) ; do \
+ echo "Building dir $$subdir" ; \
+ $(MAKE) build SRC_DIR=$$subdir || exit 1;\
+ done
+
+debug: $(DIALYZER_PLT_FILE)
+ $(MAKE) build_debug
+ $(DIALYZER) --check_plt --plt $(DIALYZER_PLT_FILE)
+ $(DIALYZER) --get_warnings $(SRC_DIR)/*.erl $(SRC_DIR)/*/*.erl \
+ --src --plt $(DIALYZER_PLT_FILE)
+
+build_debug:
+ $(MAKE) clean
+ $(MAKE) ERLC_OPTS=+debug_info
+
+build: $(BIN_DIR) $(REQUIRED_HEADERS) $(BIN_FILES)
+
+run: all $(UNUSED_WWW_DIR)
+ $(YAWS) --conf $(YAWS_CONF)
+
+clean:
+ rm -rf $(BIN_DIR)/*
+
+$(DIALYZER_PLT_FILE):
+ $(DIALYZER) --build_plt --apps erts kernel stdlib jiffy --output_plt $@
+ $(MAKE) build_debug
+ $(DIALYZER) --add_to_plt --plt $@ -r $(BIN_DIR)
+
+$(INCLUDE_DIR)/yaws_api.hrl: $(YAWS_API_HEADER) $(INCLUDE_DIR)
+ cp $< $@
+
+$(BIN_DIR):
+ mkdir -p $@
+
+$(UNUSED_WWW_DIR):
+ mkdir -p $@
+
+$(INCLUDE_DIR):
+ mkdir -p $@
+
+.SECONDEXPANSION:
+$(BIN_FILES): $(BIN_DIR)/%.beam : $(SRC_DIR)/%.erl $$(wildcard $$(SRC_DIR)/%/.)
+ $(ERLC) $(ERLC_OPTS) -o $(BIN_DIR) $<
diff --git a/src/battlemap/src/battle/battle_turn.erl b/src/battlemap/src/battle/battle_turn.erl
new file mode 100644
index 0000000..638e8f9
--- /dev/null
+++ b/src/battlemap/src/battle/battle_turn.erl
@@ -0,0 +1,161 @@
+-module(battle_turn).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ handle_post_play/1,
+ store_timeline/2
+ ]
+).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec activate_relevant_character_instances
+ (
+ list(non_neg_integer()),
+ array:array(character_instance:struct()),
+ player:id(),
+ (-1 | non_neg_integer())
+ )
+ -> {list(non_neg_integer()), array:array(character_instance:struct())}.
+activate_relevant_character_instances (IXs, CharacterInstances, _Owner, -1) ->
+ {IXs, CharacterInstances};
+activate_relevant_character_instances (IXs, CharacterInstances, Owner, IX) ->
+ CharacterInstance = array:get(IX, CharacterInstances),
+ Character = character_instance:get_character(CharacterInstance),
+ case character:get_owner_id(Character) of
+ OwnerID when (OwnerID == Owner) ->
+ activate_relevant_character_instances
+ (
+ [IX|IXs],
+ array:set
+ (
+ IX,
+ character_instance:set_is_active(true, CharacterInstance),
+ CharacterInstances
+ ),
+ Owner,
+ (IX - 1)
+ );
+
+ _ ->
+ activate_relevant_character_instances
+ (
+ IXs,
+ CharacterInstances,
+ Owner,
+ (IX - 1)
+ )
+ end.
+
+-spec start_next_players_turn (battle:struct()) ->
+ {list(non_neg_integer()), battle:struct()}.
+start_next_players_turn (Battle) ->
+ Players = battle:get_players(Battle),
+ PlayerTurn = battle:get_current_player_turn(Battle),
+ CurrentPlayerIX = player_turn:get_player_ix(PlayerTurn),
+ CurrentTurnNumber = player_turn:get_number(PlayerTurn),
+ CharacterInstances = battle:get_character_instances(Battle),
+
+ NextPlayerIX = ((CurrentPlayerIX + 1) rem (array:size(Players))),
+ NextPlayerTurn =
+ player_turn:new
+ (
+ case NextPlayerIX of
+ 0 -> (CurrentTurnNumber + 1);
+ _ -> CurrentTurnNumber
+ end,
+ NextPlayerIX
+ ),
+
+ NextPlayer = array:get(NextPlayerIX, Players),
+ UpdatedNextPlayer = player:reset_timeline(NextPlayer),
+
+ {ActivatedCharacterInstanceIXs, UpdatedCharacterInstances} =
+ activate_relevant_character_instances
+ (
+ [],
+ CharacterInstances,
+ player:get_id(NextPlayer),
+ (array:size(CharacterInstances) - 1)
+ ),
+ UpdatedBattle =
+ battle:set_player
+ (
+ NextPlayerIX,
+ UpdatedNextPlayer,
+ battle:set_character_instances
+ (
+ UpdatedCharacterInstances,
+ battle:set_current_player_turn
+ (
+ NextPlayerTurn,
+ Battle
+ )
+ )
+ ),
+ % TODO: have a diff operation for the player's timeline being reset.
+ {ActivatedCharacterInstanceIXs, UpdatedBattle}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-spec store_timeline
+ (
+ list(any()),
+ battle:struct()
+ )
+ -> battle:struct().
+store_timeline (TurnEffects, Battle) ->
+ PlayerTurn = battle:get_current_player_turn(Battle),
+ PlayerIX = player_turn:get_player_ix(PlayerTurn),
+ Player = battle:get_player(PlayerIX, Battle),
+
+ UpdatedPlayer = player:add_to_timeline(TurnEffects, Player),
+
+ battle:set_player(PlayerIX, UpdatedPlayer, Battle).
+
+
+-spec handle_post_play (battle:struct())
+ -> {database_diff:struct(), battle:struct()}.
+handle_post_play (Battle) ->
+ CharacterInstances = battle:get_character_instances(Battle),
+
+ AnActiveCharacterInstanceRemains =
+ array:foldl
+ (
+ fun (_IX, CharacterInstance, Prev) ->
+ (Prev or character_instance:get_is_active(CharacterInstance))
+ end,
+ false,
+ CharacterInstances
+ ),
+
+ case AnActiveCharacterInstanceRemains of
+ true ->
+ io:format("~nThere are still active characters.~n"),
+ {[], Battle};
+
+ false ->
+ io:format("~nThere are no more active characters.~n"),
+ {UpdatedCharacterInstanceIXs, UpdatedBattle} =
+ start_next_players_turn(Battle),
+ {
+ lists:map
+ (
+ fun (IX) ->
+ {set, character_instance, IX, is_active, true}
+ end,
+ UpdatedCharacterInstanceIXs
+ ),
+ UpdatedBattle
+ }
+ end.
diff --git a/src/battlemap/src/battle/movement.erl b/src/battlemap/src/battle/movement.erl
new file mode 100644
index 0000000..588fad9
--- /dev/null
+++ b/src/battlemap/src/battle/movement.erl
@@ -0,0 +1,58 @@
+-module(movement).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([cross/4]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec cross
+ (
+ battlemap:struct(),
+ list(location:type()),
+ list(direction:enum()),
+ non_neg_integer(),
+ location:type()
+ )
+ -> {location:type(), non_neg_integer()}.
+cross (_Battlemap, _ForbiddenLocations, [], Cost, Location) ->
+ {Location, Cost};
+cross (Battlemap, ForbiddenLocations, [Step|NextSteps], Cost, Location) ->
+ NextLocation = location:apply_direction(Step, Location),
+ NextTile = battlemap:get_tile_id(NextLocation, Battlemap),
+ NextCost = (Cost + tile:get_cost(NextTile)),
+ IsForbidden =
+ lists:foldl
+ (
+ fun (ForbiddenLocation, Prev) ->
+ (Prev or (NextLocation == ForbiddenLocation))
+ end,
+ false,
+ ForbiddenLocations
+ ),
+
+ IsForbidden = false,
+
+ cross(Battlemap, ForbiddenLocations, NextSteps, NextCost, NextLocation).
+
+-spec cross
+ (
+ battlemap:struct(),
+ list(location:type()),
+ list(direction:enum()),
+ location:type()
+ )
+ -> {location:type(), non_neg_integer()}.
+cross (Battlemap, ForbiddenLocations, Path, Location) ->
+ cross(Battlemap, ForbiddenLocations, Path, 0, Location).
diff --git a/src/battlemap/src/battle/roll.erl b/src/battlemap/src/battle/roll.erl
new file mode 100644
index 0000000..074054b
--- /dev/null
+++ b/src/battlemap/src/battle/roll.erl
@@ -0,0 +1,32 @@
+-module(roll).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ percentage/0,
+ between/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec between (non_neg_integer(), non_neg_integer()) -> non_neg_integer().
+between (Min, Max) ->
+ Diff = (Max - Min),
+ (Min + (rand:uniform(Diff + 1) - 1)).
+
+-spec percentage () -> 0..100.
+percentage () ->
+ between(0, 100).
diff --git a/src/battlemap/src/handler.erl b/src/battlemap/src/handler.erl
new file mode 100644
index 0000000..0ecc8be
--- /dev/null
+++ b/src/battlemap/src/handler.erl
@@ -0,0 +1,23 @@
+-module(handler).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([start/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+start (_YawsParams) ->
+ {ok, Pid} = timed_caches_manager:start(),
+ database_shim:generate_db(Pid),
+ timed_caches_manager:new_cache(Pid, battle_db, none),
+ ok.
diff --git a/src/battlemap/src/io/security.erl b/src/battlemap/src/io/security.erl
new file mode 100644
index 0000000..60f6661
--- /dev/null
+++ b/src/battlemap/src/io/security.erl
@@ -0,0 +1,33 @@
+-module(security).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ assert_identity/2,
+ lock_queries/1,
+ unlock_queries/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec assert_identity (any(), any()) -> 'unimplemented'.
+assert_identity (_PlayerID, _SessionToken) -> unimplemented.
+
+-spec lock_queries (any()) -> 'unimplemented'.
+lock_queries (_PlayerID) -> unimplemented.
+
+-spec unlock_queries (any()) -> 'unimplemented'.
+unlock_queries (_PlayerID) -> unimplemented.
diff --git a/src/battlemap/src/io/timed_cache.erl b/src/battlemap/src/io/timed_cache.erl
new file mode 100644
index 0000000..52b98d6
--- /dev/null
+++ b/src/battlemap/src/io/timed_cache.erl
@@ -0,0 +1,130 @@
+-module(timed_cache).
+-behavior(gen_server).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% 'gen_server' Exports
+-export
+(
+ [
+ init/1,
+ handle_cast/2,
+ handle_call/3, %% No reply will ever be given.
+ terminate/2,
+ code_change/3,
+ format_status/2,
+ handle_info/2
+ ]
+).
+
+%%%% Actual Interface
+-export
+(
+ [
+ fetch/3,
+ update/4,
+ invalidate/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec add_to_cache (atom(), any(), any()) -> any().
+add_to_cache (DB, Owner, ObjectID) ->
+ {ok, TimerPID} = gen_server:start(?MODULE, {DB, {Owner, ObjectID}}, []),
+ {ok, Data} = database_shim:fetch(DB, ObjectID),
+ ets:insert(DB, {{Owner, ObjectID}, TimerPID, Data}),
+ Data.
+
+-spec add_update_to_cache (atom(), any(), any(), any()) -> 'ok'.
+add_update_to_cache (DB, Owner, ObjectID, Data) ->
+ {ok, TimerPID} = gen_server:start(?MODULE, {DB, {Owner, ObjectID}}, []),
+ ets:insert(DB, {{Owner, ObjectID}, TimerPID, Data}),
+ ok.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% 'gen_server' functions
+init ({DB, ObjectID}) ->
+ io:format("~nCache entry added: ~p.~n", [{DB, ObjectID}]),
+ {ok, {DB, ObjectID}, timed_caches_manager:get_timeout()}.
+
+handle_call (invalidate, _, State) ->
+ {stop, normal, State};
+handle_call (ping, _, State) ->
+ {noreply, State, timed_caches_manager:get_timeout()}.
+
+handle_cast (invalidate, State) ->
+ {stop, normal, State};
+handle_cast (ping, State) ->
+ {noreply, State, timed_caches_manager:get_timeout()}.
+
+terminate (_, {DB, ObjectID}) ->
+ io:format
+ (
+ "~nCache entry timed out or was invalidated: ~p.~n",
+ [{DB, ObjectID}]
+ ),
+ ets:delete(DB, ObjectID).
+
+code_change (_, State, _) ->
+ {ok, State}.
+
+format_status (_, [_, State]) ->
+ [{data, [{"State", State}]}].
+
+handle_info(timeout, State) ->
+ {stop, normal, State};
+handle_info(_, {DB, ObjectID}) ->
+ {noreply, {DB, ObjectID}, timed_caches_manager:get_timeout()}.
+
+%%%% Interface Functions
+-spec fetch (atom(), any(), any()) -> any().
+fetch (DB, Owner, ObjectID) ->
+ io:format("~nfetch from cache: ~p.~n", [{DB, {Owner, ObjectID}}]),
+ case ets:lookup(DB, {Owner, ObjectID}) of
+ [] -> add_to_cache(DB, Owner, ObjectID);
+
+ [{_, TimerPID, Data}] ->
+ gen_server:cast(TimerPID, ping),
+ Data
+ end.
+
+-spec update (atom(), any(), any(), any()) -> 'ok'.
+update (DB, Owner, ObjectID, Data) ->
+ io:format("~nUpdating cache: ~p.~n", [{DB, {Owner, ObjectID}}]),
+ case ets:lookup(DB, {Owner, ObjectID}) of
+ [] -> ok;
+
+ [{_OwnerID, TimerPID, _Data}] ->
+ gen_server:stop(TimerPID)
+ end,
+ add_update_to_cache(DB, Owner, ObjectID, Data).
+
+-spec invalidate (atom(), any(), any()) -> 'ok'.
+invalidate (DB, Owner, ObjectID) ->
+ case ets:lookup(DB, {Owner, ObjectID}) of
+ [] ->
+ io:format
+ (
+ "~nInvalidation request on non-stored entry: ~p.~n",
+ [{DB, Owner, ObjectID}]
+ ),
+ ok;
+
+ [{_, TimerPID, _}] ->
+ io:format
+ (
+ "~nInvalidation request on stored entry: ~p.~n",
+ [{DB, Owner, ObjectID}]
+ ),
+ gen_server:stop(TimerPID),
+ ok
+ end.
diff --git a/src/battlemap/src/io/timed_caches_manager.erl b/src/battlemap/src/io/timed_caches_manager.erl
new file mode 100644
index 0000000..5901964
--- /dev/null
+++ b/src/battlemap/src/io/timed_caches_manager.erl
@@ -0,0 +1,152 @@
+-module(timed_caches_manager).
+-behavior(gen_server).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% 'gen_server' Exports
+-export(
+ [
+ init/1,
+ handle_cast/2,
+ handle_call/3,
+ terminate/2,
+ code_change/3,
+ format_status/2,
+ handle_info/2
+ ]
+).
+
+%%%% Actual Interface
+-export(
+ [
+ start/0,
+ new_cache/3,
+ delete_cache/2,
+ get_timeout/0
+ ]
+)
+.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+remove_cache (DB) ->
+ ets:delete(DB).
+
+add_cache (DB, none) ->
+ io:format("~nTimed Caches Manager added a new cache. ~n"),
+ ets:new(
+ DB,
+ [
+ set,
+ public,
+ named_table,
+ {keypos, 1},
+ {read_concurrency, true},
+ {heir, none}
+ ]
+ );
+add_cache (DB, Heir) ->
+ io:format("~nTimed Caches Manager added a new cache. ~n"),
+ ets:new(
+ DB,
+ [
+ set,
+ public,
+ named_table,
+ {keypos, 1},
+ {read_concurrency, true},
+ {heir, Heir, DB}
+ ]
+ ).
+
+inherit_cache (CacheList, DB, Heir) ->
+ case lists:member(DB, CacheList) of
+ true ->
+ ets:setopts(DB, {heir, Heir, DB}),
+ CacheList;
+
+ false ->
+ [DB|CacheList]
+ end.
+
+remove_cache (CacheList, DB) ->
+ case lists:member(DB, CacheList) of
+ true ->
+ remove_cache(DB),
+ lists:delete(DB, CacheList);
+ false ->
+ CacheList
+ end.
+
+add_cache (CacheList, DB, Heir) ->
+ case lists:member(DB, CacheList) of
+ true when (Heir =:= none) ->
+ CacheList;
+
+ true ->
+ ets:setopts(DB, {heir, Heir, DB}),
+ CacheList;
+
+ false ->
+ add_cache(DB, Heir),
+ [DB|CacheList]
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% 'gen_server' functions
+init (CacheList) ->
+ io:format("~nStarting Timed Caches Manager..."),
+ {ok, CacheList}.
+
+handle_call ({remove, CacheName}, _Caller, State) ->
+ {noreply, remove_cache(State, CacheName)};
+handle_call ({add, CacheName, Heir}, _Caller, State)->
+ {noreply, add_cache(State, CacheName, Heir)};
+handle_call ({inherit, CacheName, Heir}, _Caller, State)->
+ {noreply, inherit_cache(State, CacheName, Heir)};
+handle_call (terminate, _, State) ->
+ {stop, normal, State}.
+
+handle_cast ({remove, CacheName}, State) ->
+ {noreply, remove_cache(State, CacheName)};
+handle_cast ({add, CacheName, Heir}, State)->
+ {noreply, add_cache(State, CacheName, Heir)};
+handle_cast ({inherit, CacheName, Heir}, State)->
+ {noreply, inherit_cache(State, CacheName, Heir)};
+handle_cast (terminate, State) ->
+ {stop, normal, State}.
+
+terminate (_Reason, []) ->
+ ok;
+terminate (Reason, [CacheName|OtherCaches]) ->
+ remove_cache(CacheName),
+ terminate(Reason, OtherCaches).
+
+code_change (_, State, _) ->
+ {ok, State}.
+
+format_status (_, [_, State]) ->
+ [{data, [{"State", State}]}].
+
+handle_info(_, State) ->
+ {noreply, State}.
+
+%%%% Interface Functions
+start () ->
+ gen_server:start(timed_caches_manager, [], []).
+
+new_cache (ManagerPid, DB, Heir) ->
+ gen_server:cast(ManagerPid, {add, DB, Heir}).
+
+delete_cache (ManagerPid, DB) ->
+ gen_server:cast(ManagerPid, {remove, DB}).
+
+get_timeout () ->
+ 120000. % 2min.
diff --git a/src/battlemap/src/query/character_turn.erl b/src/battlemap/src/query/character_turn.erl
new file mode 100644
index 0000000..85c5db8
--- /dev/null
+++ b/src/battlemap/src/query/character_turn.erl
@@ -0,0 +1,255 @@
+-module(character_turn).
+% FIXME: There's still too much of a mess in this module.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("../../include/yaws_api.hrl").
+
+-record
+(
+ input,
+ {
+ player_id :: player:id(),
+ session_token :: binary(),
+ battle_id :: binary(),
+ character_instance_ix :: non_neg_integer(),
+ actions :: list(battle_action:struct())
+ }
+).
+
+-record
+(
+ relevant_data,
+ {
+ battle :: battle:struct(),
+ played_character_instance :: character_instance:struct()
+ }
+).
+
+-type input() :: #input{}.
+-type relevant_data() :: #relevant_data{}.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([out/1]).
+
+-export_type([relevant_data/0]).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-spec parse_input (binary()) -> input().
+parse_input (Req) ->
+ JSONReqMap = jiffy:decode(Req, [return_maps]),
+ CharacterInstanceIX = binary_to_integer(maps:get(<<"cix">>, JSONReqMap)),
+ EncodedActions = maps:get(<<"act">>, JSONReqMap),
+ Actions = lists:map(fun battle_action:decode/1, EncodedActions),
+
+ #input
+ {
+ player_id = maps:get(<<"pid">>, JSONReqMap),
+ session_token = maps:get(<<"stk">>, JSONReqMap),
+ battle_id = maps:get(<<"bid">>, JSONReqMap),
+ character_instance_ix = CharacterInstanceIX,
+ actions = Actions
+ }.
+
+-spec fetch_relevant_data (input()) -> relevant_data().
+fetch_relevant_data (Input) ->
+ PlayerID = Input#input.player_id,
+ BattleID = Input#input.battle_id,
+ CharacterInstanceIX = Input#input.character_instance_ix,
+
+ Battle = timed_cache:fetch(battle_db, PlayerID, BattleID),
+ CharacterInstance =
+ battle:get_character_instance(CharacterInstanceIX, Battle),
+
+ #relevant_data
+ {
+ battle = Battle,
+ played_character_instance = CharacterInstance
+ }.
+
+-spec assert_character_instance_can_be_played
+ (
+ relevant_data(),
+ input()
+ )
+ -> true.
+assert_character_instance_can_be_played (RData, Input) ->
+ PlayerID = Input#input.player_id,
+ CharacterInstance = RData#relevant_data.played_character_instance,
+ Battle = RData#relevant_data.battle,
+ Character = character_instance:get_character(CharacterInstance),
+ CurrentPlayerIX =
+ player_turn:get_player_ix
+ (
+ battle:get_current_player_turn(Battle)
+ ),
+ CurrentPlayer = battle:get_player(CurrentPlayerIX, Battle),
+ CharacterOwnerID = character:get_owner_id(Character),
+
+ PlayerID = player:get_id(CurrentPlayer),
+ PlayerID = CharacterOwnerID,
+
+ true = character_instance:get_is_active(CharacterInstance).
+
+-spec finalize_and_fuse_relevant_data
+ (
+ relevant_data(),
+ input()
+ )
+ -> battle:struct().
+finalize_and_fuse_relevant_data (RData, Input) ->
+ Battle = RData#relevant_data.battle,
+ CharacterInstance = RData#relevant_data.played_character_instance,
+
+ io:format("~nNot a character instance? ~p~n", [CharacterInstance]),
+
+ FinalizedCharacterInstance =
+ character_instance:set_is_active(false, CharacterInstance),
+
+ battle:set_character_instance
+ (
+ Input#input.character_instance_ix,
+ FinalizedCharacterInstance,
+ Battle
+ ).
+
+%-spec send_to_database (list(database_diff:struct()), input()) -> 'ok'.
+-spec send_to_database (battle:struct(), input()) -> 'ok'.
+send_to_database (FinalizedBattle, Input) ->
+ PlayerID = Input#input.player_id,
+ BattleID = Input#input.battle_id,
+
+ %% TODO: differential commit
+ database_shim:commit
+ (
+ battle_db,
+ PlayerID,
+ BattleID,
+ FinalizedBattle
+ ).
+
+-spec update_cache (battle:struct(), input()) -> 'ok'.
+update_cache (Battle, Input) ->
+ PlayerID = Input#input.player_id,
+ BattleID = Input#input.battle_id,
+
+ timed_cache:update
+ (
+ battle_db,
+ PlayerID,
+ BattleID,
+ Battle
+ ).
+
+-spec generate_reply ( list(any())) -> binary().
+generate_reply (EncodedClientUpdate) ->
+ jiffy:encode([turn_results:generate(EncodedClientUpdate)]).
+
+handle_actions (RData, Input) ->
+ Battle = RData#relevant_data.battle,
+ CharacterInstance = RData#relevant_data.played_character_instance,
+ CharacterInstanceIX = Input#input.character_instance_ix,
+ Actions = Input#input.actions,
+
+ {
+ ActionsDiffUpdates,
+ ClientUpdates,
+ PostActionBattle,
+ PostActionCharacterInstance
+ } =
+ lists:foldl
+ (
+ fun
+ (
+ Action,
+ {
+ CurrActionsDiffUpdates,
+ CurrClientUpdates,
+ CurrBattle,
+ CurrCharacterInstance
+ }
+ ) ->
+ {
+ NewActionsDiffUpdates,
+ NewClientUpdates,
+ NewBattle,
+ NewCharacterInstance
+ } =
+ battle_action:handle
+ (
+ CurrBattle,
+ CurrCharacterInstance,
+ CharacterInstanceIX,
+ Action
+ ),
+ {
+ (NewActionsDiffUpdates ++ CurrActionsDiffUpdates),
+ (NewClientUpdates ++ CurrClientUpdates),
+ NewBattle,
+ NewCharacterInstance
+ }
+ end,
+ {[], [], Battle, CharacterInstance},
+ Actions
+ ),
+ {
+ ActionsDiffUpdates,
+ ClientUpdates,
+ RData#relevant_data
+ {
+ battle = PostActionBattle,
+ played_character_instance = PostActionCharacterInstance
+ }
+ }.
+
+-spec handle (binary()) -> binary().
+handle (Req) ->
+ Input = parse_input(Req),
+ PlayerID = Input#input.player_id,
+ PlayerSessionToken = Input#input.session_token,
+
+ security:assert_identity(PlayerID, PlayerSessionToken),
+ security:lock_queries(PlayerID),
+
+ RData = fetch_relevant_data(Input),
+
+ assert_character_instance_can_be_played(RData, Input),
+
+ {ActionsDiffUpdate, ClientUpdate, UpdatedRData} =
+ handle_actions(RData, Input),
+
+ EncodedClientUpdate = lists:map(fun turn_result:encode/1, ClientUpdate),
+
+ UpdatedBattle = finalize_and_fuse_relevant_data(UpdatedRData, Input),
+
+ UpdatedBattle2 =
+ battle_turn:store_timeline(EncodedClientUpdate, UpdatedBattle),
+
+ {TurnDiffUpdate, FinalizedBattle} =
+ battle_turn:handle_post_play(UpdatedBattle2),
+
+ DiffUpdate = (TurnDiffUpdate ++ ActionsDiffUpdate),
+
+ %send_to_database(DiffUpdate, Input),
+ send_to_database(FinalizedBattle, Input),
+ update_cache(FinalizedBattle, Input),
+
+ io:format("~nCharacter turn result:~n~p~n", [DiffUpdate]),
+
+ security:unlock_queries(PlayerID),
+
+ generate_reply(EncodedClientUpdate).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+out(A) ->
+ {
+ content,
+ "application/json; charset=UTF-8",
+ handle(A#arg.clidata)
+ }.
diff --git a/src/battlemap/src/query/load_state.erl b/src/battlemap/src/query/load_state.erl
new file mode 100644
index 0000000..a03a20f
--- /dev/null
+++ b/src/battlemap/src/query/load_state.erl
@@ -0,0 +1,110 @@
+-module(load_state).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("../../include/yaws_api.hrl").
+
+-record
+(
+ input,
+ {
+ player_id :: player:id(),
+ session_token :: binary(),
+ battle_id :: binary()
+ }
+).
+
+-record
+(
+ query_state,
+ {
+ battle :: battle:struct()
+ }
+).
+
+-type input() :: #input{}.
+-type query_state() :: #query_state{}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([out/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec parse_input (binary()) -> input().
+parse_input (Req) ->
+ JSONReqMap = jiffy:decode(Req, [return_maps]),
+ PlayerID = maps:get(<<"pid">>, JSONReqMap),
+ SessionToken = maps:get(<<"stk">>, JSONReqMap),
+ BattleID = maps:get(<<"bmi">>, JSONReqMap),
+
+ #input
+ {
+ player_id = PlayerID,
+ session_token = SessionToken,
+ battle_id = BattleID
+ }.
+
+-spec fetch_data (input()) -> query_state().
+fetch_data (Input) ->
+ PlayerID = Input#input.player_id,
+ BattleID = Input#input.battle_id,
+
+ Battle =
+ timed_cache:fetch
+ (
+ battle_db,
+ PlayerID,
+ BattleID
+ ),
+
+ #query_state
+ {
+ battle = Battle
+ }.
+
+-spec generate_reply(query_state(), input()) -> binary().
+generate_reply (QueryState, Input) ->
+ PlayerID = Input#input.player_id,
+ Battle = QueryState#query_state.battle,
+
+ jiffy:encode
+ (
+ [
+ set_timeline:generate(battle:get_encoded_last_turns_effects(Battle)),
+ set_map:generate(battle:get_battlemap(Battle))
+ |
+ array:sparse_to_list
+ (
+ array:map
+ (
+ fun (IX, CharacterInstance) ->
+ add_char:generate(IX, CharacterInstance, PlayerID)
+ end,
+ battle:get_character_instances(Battle)
+ )
+ )
+ ]
+ ).
+
+-spec handle (binary()) -> binary().
+handle (Req) ->
+ Input = parse_input(Req),
+ security:assert_identity(Input#input.player_id, Input#input.session_token),
+ security:lock_queries(Input#input.player_id),
+ QueryState = fetch_data(Input),
+ security:unlock_queries(Input#input.player_id),
+ generate_reply(QueryState, Input).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+out(A) ->
+ {
+ content,
+ "application/json; charset=UTF-8",
+ handle(A#arg.clidata)
+ }.
diff --git a/src/battlemap/src/reply/add_char.erl b/src/battlemap/src/reply/add_char.erl
new file mode 100644
index 0000000..b3ef128
--- /dev/null
+++ b/src/battlemap/src/reply/add_char.erl
@@ -0,0 +1,74 @@
+-module(add_char).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([generate/3]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec attributes_as_json
+ (
+ attributes:struct()
+ ) ->
+ {list({binary(), non_neg_integer()})}.
+attributes_as_json (Attributes) ->
+ {
+ [
+ {<<"con">>, attributes:get_constitution(Attributes)},
+ {<<"dex">>, attributes:get_dexterity(Attributes)},
+ {<<"int">>, attributes:get_intelligence(Attributes)},
+ {<<"min">>, attributes:get_mind(Attributes)},
+ {<<"spe">>, attributes:get_speed(Attributes)},
+ {<<"str">>, attributes:get_strength(Attributes)}
+ ]
+ }.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate
+ (
+ non_neg_integer(),
+ character_instance:struct(),
+ player:id()
+ )
+ -> {list(any())}.
+generate (IX, CharacterInstance, PlayerID) ->
+ Character = character_instance:get_character(CharacterInstance),
+ Location = character_instance:get_location(CharacterInstance),
+ Attributes = character:get_attributes(Character),
+ {ActiveWeapon, SecondaryWeapon} = character:get_weapon_ids(Character),
+ OwnerID = character:get_owner_id(Character),
+
+ {
+ [
+ {<<"msg">>, <<"add_char">>},
+ {<<"ix">>, IX},
+ {<<"nam">>, character:get_name(Character)},
+ {<<"ico">>, character:get_icon(Character)},
+ {<<"prt">>, character:get_portrait(Character)},
+ {
+ <<"hea">>,
+ character_instance:get_current_health(CharacterInstance)
+ },
+ {<<"lc">>, location:encode(Location)},
+ {<<"pla">>, OwnerID},
+ {
+ <<"ena">>,
+ (
+ character_instance:get_is_active(CharacterInstance)
+ and
+ (OwnerID == PlayerID)
+ )
+ },
+ {<<"att">>, attributes_as_json(Attributes)},
+ {<<"awp">>, ActiveWeapon},
+ {<<"swp">>, SecondaryWeapon}
+ ]
+ }.
diff --git a/src/battlemap/src/reply/set_map.erl b/src/battlemap/src/reply/set_map.erl
new file mode 100644
index 0000000..6a7cd39
--- /dev/null
+++ b/src/battlemap/src/reply/set_map.erl
@@ -0,0 +1,28 @@
+-module(set_map).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([generate/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate (battlemap:struct()) -> {list(any())}.
+generate (Battlemap) ->
+ {
+ [
+ {<<"msg">>, <<"set_map">>},
+ {<<"w">>, battlemap:get_width(Battlemap)},
+ {<<"h">>, battlemap:get_height(Battlemap)},
+ {<<"t">>, array:sparse_to_list(battlemap:get_tile_ids(Battlemap))}
+ ]
+ }.
diff --git a/src/battlemap/src/reply/set_timeline.erl b/src/battlemap/src/reply/set_timeline.erl
new file mode 100644
index 0000000..bfe621a
--- /dev/null
+++ b/src/battlemap/src/reply/set_timeline.erl
@@ -0,0 +1,27 @@
+-module(set_timeline).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([generate/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate (list(any())) -> {list(any())}.
+generate (EncodedClientUpdate) ->
+ io:format("~nSending timeline:~n~p~n", [EncodedClientUpdate]),
+ {
+ [
+ {<<"msg">>, <<"set_timeline">>},
+ {<<"cnt">>, EncodedClientUpdate}
+ ]
+ }.
diff --git a/src/battlemap/src/reply/turn_results.erl b/src/battlemap/src/reply/turn_results.erl
new file mode 100644
index 0000000..0f3ff25
--- /dev/null
+++ b/src/battlemap/src/reply/turn_results.erl
@@ -0,0 +1,27 @@
+-module(turn_results).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export([generate/1]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate (list(any())) -> {list(any())}.
+generate (EncodedClientUpdate) ->
+ io:format("~nSending turn results:~n~p~n", [EncodedClientUpdate]),
+ {
+ [
+ {<<"msg">>, <<"turn_results">>},
+ {<<"cnt">>, EncodedClientUpdate}
+ ]
+ }.
diff --git a/src/battlemap/src/shim/database_shim.erl b/src/battlemap/src/shim/database_shim.erl
new file mode 100644
index 0000000..a26087d
--- /dev/null
+++ b/src/battlemap/src/shim/database_shim.erl
@@ -0,0 +1,137 @@
+-module(database_shim).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ generate_db/1,
+ fetch/2,
+ commit/4
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec create_db (pid()) -> 'ok'.
+create_db (_Heir) ->
+ ets:new
+ (
+ db_shim,
+ [
+ set,
+ public,
+ named_table,
+ {keypos, 1},
+ {read_concurrency, true}
+ ]
+ ),
+ io:format("~ndb_shim ets created.~n"),
+ ok.
+
+-spec add_to_db (any(), any()) -> 'ok'.
+add_to_db (ID, Val) ->
+ io:format("~nadd to db_shim: ~p.~n", [{ID, Val}]),
+ ets:insert(db_shim, {ID, Val}),
+ ok.
+
+-spec generate_random_characters
+ (
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer(),
+ list(character:struct())
+ )
+ -> list(character:struct()).
+generate_random_characters
+(
+ 0,
+ 0,
+ _CharactersPerPlayer,
+ _TotalCharacterCount,
+ Result
+) ->
+ Result;
+generate_random_characters
+(
+ MaxPlayerID,
+ 0,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Result
+) ->
+ generate_random_characters
+ (
+ (MaxPlayerID - 1),
+ CharactersPerPlayer,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Result
+ );
+generate_random_characters
+(
+ MaxPlayerID,
+ PlayerCharacterCount,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Result
+) ->
+ NewCharacter =
+ character:random
+ (
+ TotalCharacterCount,
+ list_to_binary(integer_to_list(MaxPlayerID))
+ ),
+ generate_random_characters
+ (
+ MaxPlayerID,
+ (PlayerCharacterCount - 1),
+ CharactersPerPlayer,
+ (TotalCharacterCount + 1),
+ [NewCharacter|Result]
+ ).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate_db (pid()) -> 'ok'.
+generate_db (Heir) ->
+ Pid = self(),
+ spawn(fun () -> create_db(Heir), Pid ! ok, receive ok -> ok end end),
+ receive
+ ok -> ok
+ end,
+ BattlemapWidth = roll:between(16, 64),
+ BattlemapHeight = roll:between(16, 64),
+ Battlemap = battlemap:random(0, BattlemapWidth, BattlemapHeight),
+ Characters = generate_random_characters(1, 8, 8, 0, []),
+ PlayersAsList = [player:new(<<"0">>), player:new(<<"1">>)],
+ Battle =
+ battle:random
+ (
+ <<"0">>,
+ PlayersAsList,
+ Battlemap,
+ Characters
+ ),
+
+ add_to_db({battle_db, <<"0">>}, Battle).
+
+-spec fetch (atom(), any()) -> ({'ok', any()} | 'nothing').
+fetch (DB, ObjectID) ->
+ io:format("~ndb_shim lookup: ~p.~n", [{DB, ObjectID}]),
+ case ets:lookup(db_shim, {DB, ObjectID}) of
+ [{_Key, Value}] -> {ok, Value};
+ [] -> nothing
+ end.
+
+-spec commit (atom(), any(), any(), any()) -> 'ok'.
+commit (DB, _Owner, ObjectID, Value) ->
+ add_to_db({DB, ObjectID}, Value).
diff --git a/src/battlemap/src/struct/attack.erl b/src/battlemap/src/struct/attack.erl
new file mode 100644
index 0000000..71bc2bb
--- /dev/null
+++ b/src/battlemap/src/struct/attack.erl
@@ -0,0 +1,299 @@
+-module(attack).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type order() :: ('first' | 'second' | 'counter').
+-type precision() :: ('misses' | 'grazes' | 'hits').
+
+-record
+(
+ attack,
+ {
+ order :: order(),
+ precision :: precision(),
+ is_critical :: boolean(),
+ is_parry :: boolean(),
+ damage :: non_neg_integer()
+ }
+).
+
+-opaque struct() :: #attack{}.
+-type maybe_struct() :: ('nothing' | struct()).
+-opaque step() :: {order(), boolean()}.
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export_type([struct/0, maybe_struct/0, step/0]).
+
+-export
+(
+ [
+ get_sequence/3,
+ get_description_of/3,
+ apply_to_healths/3
+ ]
+).
+
+-export
+(
+ [
+ encode/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec roll_precision
+ (
+ statistics:struct(),
+ statistics:struct()
+ )
+ -> precision().
+roll_precision (AttackerStatistics, DefenderStatistics) ->
+ DefenderDodges = statistics:get_dodges(DefenderStatistics),
+ AttackerAccuracy = statistics:get_accuracy(AttackerStatistics),
+ MissChance = max(0, (DefenderDodges - AttackerAccuracy)),
+ case roll:percentage() of
+ X when (X =< MissChance) -> misses;
+ X when (X =< (MissChance * 2)) -> grazes;
+ _ -> hits
+ end.
+
+-spec roll_damage
+ (
+ statistics:struct(),
+ statistics:struct()
+ )
+ -> {non_neg_integer(), boolean()}.
+roll_damage (AttackerStatistics, _DefenderStatistics) ->
+ {MinimumDamage, MaximumDamage} = statistics:get_damages(AttackerStatistics),
+ MaximumRoll = max(1, MaximumDamage - MinimumDamage),
+ BaseDamage = MinimumDamage + (rand:uniform(MaximumRoll) - 1),
+ CriticalHitChance = statistics:get_critical_hits(AttackerStatistics),
+ case roll:percentage() of
+ X when (X =< CriticalHitChance) -> {(BaseDamage * 2), true};
+ _ -> {BaseDamage, false}
+ end.
+
+-spec roll_parry (statistics:struct()) -> boolean().
+roll_parry (DefenderStatistics) ->
+ DefenderParryChance = statistics:get_parries(DefenderStatistics),
+ (roll:percentage() =< DefenderParryChance).
+
+-spec effect_of_attack
+ (
+ order(),
+ statistics:struct(),
+ statistics:struct(),
+ boolean()
+ )
+ -> struct().
+effect_of_attack (Order, AttackerStatistics, DefenderStatistics, CanParry) ->
+ ParryIsSuccessful = (CanParry and roll_parry(DefenderStatistics)),
+ {ActualAtkStatistics, ActualDefStatistics} =
+ case ParryIsSuccessful of
+ true -> {DefenderStatistics, AttackerStatistics};
+ false -> {AttackerStatistics, DefenderStatistics}
+ end,
+
+ Precision = roll_precision(ActualAtkStatistics, ActualDefStatistics),
+ {Damage, IsCritical} = roll_damage(ActualAtkStatistics, ActualDefStatistics),
+ ActualDamage =
+ case Precision of
+ misses -> 0;
+ grazes -> trunc(Damage / 2);
+ hits -> Damage
+ end,
+
+ #attack
+ {
+ order = Order,
+ precision = Precision,
+ is_critical = IsCritical,
+ is_parry = ParryIsSuccessful,
+ damage = ActualDamage
+ }.
+
+-spec encode_order (order()) -> binary().
+encode_order (first) -> <<"f">>;
+encode_order (counter) -> <<"c">>;
+encode_order (second) -> <<"s">>.
+
+-spec encode_precision (precision()) -> binary().
+encode_precision (hits) -> <<"h">>;
+encode_precision (grazes) -> <<"g">>;
+encode_precision (misses) -> <<"m">>.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+-spec get_description_of
+ (
+ step(),
+ statistics:struct(),
+ statistics:struct()
+ )
+ -> maybe_struct().
+get_description_of
+(
+ {first, CanParry},
+ AttackerStatistics,
+ DefenderStatistics
+) ->
+ effect_of_attack(first, AttackerStatistics, DefenderStatistics, CanParry);
+get_description_of
+(
+ {second, CanParry},
+ AttackerStatistics,
+ DefenderStatistics
+) ->
+ AttackerDoubleAttackChange = statistics:get_double_hits(AttackerStatistics),
+
+ case roll:percentage() of
+ X when (X =< AttackerDoubleAttackChange) ->
+ effect_of_attack
+ (
+ second,
+ AttackerStatistics,
+ DefenderStatistics,
+ CanParry
+ );
+
+ _ ->
+ nothing
+ end;
+get_description_of
+(
+ {counter, CanParry},
+ AttackerStatistics,
+ DefenderStatistics
+) ->
+ effect_of_attack(counter, DefenderStatistics, AttackerStatistics, CanParry).
+
+-spec apply_to_healths
+ (
+ maybe_struct(),
+ non_neg_integer(),
+ non_neg_integer()
+ )
+ -> {maybe_struct(), non_neg_integer(), non_neg_integer()}.
+apply_to_healths
+(
+ nothing,
+ AttackerHealth,
+ DefenderHealth
+) ->
+ {nothing, AttackerHealth, DefenderHealth};
+apply_to_healths
+(
+ Attack,
+ AttackerHealth,
+ DefenderHealth
+)
+when
+(
+ (Attack#attack.order == first)
+ or (Attack#attack.order == second)
+ or ((Attack#attack.order == counter) and Attack#attack.is_parry)
+) ->
+ Damage = Attack#attack.damage,
+
+ case AttackerHealth of
+ 0 ->
+ {nothing, AttackerHealth, DefenderHealth};
+
+ _ ->
+ {
+ Attack,
+ AttackerHealth,
+ max(0, (DefenderHealth - Damage))
+ }
+ end;
+apply_to_healths
+(
+ Attack,
+ AttackerHealth,
+ DefenderHealth
+)
+when
+(
+ (Attack#attack.order == counter)
+ or
+ (
+ (Attack#attack.is_parry)
+ and ((Attack#attack.order == first) or (Attack#attack.order == second))
+ )
+) ->
+ Damage = Attack#attack.damage,
+
+ case DefenderHealth of
+ 0 ->
+ {nothing, AttackerHealth, DefenderHealth};
+
+ _ ->
+ {
+ Attack,
+ max(0, (AttackerHealth - Damage)),
+ DefenderHealth
+ }
+ end.
+
+-spec get_sequence
+ (
+ non_neg_integer(),
+ weapon:struct(),
+ weapon:struct()
+ )
+ -> list(step()).
+get_sequence (AttackRange, AttackerWeapon, DefenderWeapon) ->
+ {AttackerDefenseRange, AttackerAttackRange} =
+ weapon:get_ranges(AttackerWeapon),
+ {DefenderDefenseRange, DefenderAttackRange} =
+ weapon:get_ranges(DefenderWeapon),
+
+ AttackerCanAttack = (AttackRange =< AttackerAttackRange),
+ AttackerCanAttack = true,
+ AttackerCanDefend =
+ (AttackerCanAttack and (AttackRange > AttackerDefenseRange)),
+ AttackerCanParry =
+ (AttackerCanDefend and weapon:can_parry(AttackerWeapon)),
+
+ DefenderCanAttack = (AttackRange =< DefenderAttackRange),
+ DefenderCanDefend =
+ (DefenderCanAttack and (AttackRange > DefenderDefenseRange)),
+ DefenderCanParry =
+ (DefenderCanDefend and weapon:can_parry(DefenderWeapon)),
+
+ First = {first, DefenderCanParry},
+ Second = {second, DefenderCanParry},
+ Counter = {counter, AttackerCanParry},
+
+ if
+ (not DefenderCanDefend) ->
+ [First, Second];
+
+ true ->
+ [First, Counter, Second]
+ end.
+
+-spec encode (struct()) -> {list(any())}.
+% This shouldn't be a possibility. Types in this module are a mess...
+encode (Attack) ->
+ Order = Attack#attack.order,
+ Precision = Attack#attack.precision,
+ IsCritical = Attack#attack.is_critical,
+ IsParry = Attack#attack.is_parry,
+ Damage = Attack#attack.damage,
+
+ {
+ [
+ {<<"ord">>, encode_order(Order)},
+ {<<"pre">>, encode_precision(Precision)},
+ {<<"cri">>, IsCritical},
+ {<<"par">>, IsParry},
+ {<<"dmg">>, Damage}
+ ]
+ }.
diff --git a/src/battlemap/src/struct/attributes.erl b/src/battlemap/src/struct/attributes.erl
new file mode 100644
index 0000000..6728831
--- /dev/null
+++ b/src/battlemap/src/struct/attributes.erl
@@ -0,0 +1,108 @@
+-module(attributes).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ attributes,
+ {
+ constitution :: integer(),
+ dexterity :: integer(),
+ intelligence :: integer(),
+ mind :: integer(),
+ speed :: integer(),
+ strength :: integer()
+ }
+).
+
+-opaque struct() :: #attributes{}.
+
+-export_type([struct/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_constitution/1,
+ get_dexterity/1,
+ get_intelligence/1,
+ get_mind/1,
+ get_speed/1,
+ get_strength/1,
+
+ set_constitution/2,
+ set_dexterity/2,
+ set_intelligence/2,
+ set_mind/2,
+ set_speed/2,
+ set_strength/2
+ ]
+).
+
+%%%% Accessors
+-export
+(
+ [
+ random/0
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_constitution (struct()) -> integer().
+get_constitution (Att) -> Att#attributes.constitution.
+
+-spec get_dexterity (struct()) -> integer().
+get_dexterity (Att) -> Att#attributes.dexterity.
+
+-spec get_intelligence (struct()) -> integer().
+get_intelligence (Att) -> Att#attributes.intelligence.
+
+-spec get_mind (struct()) -> integer().
+get_mind (Att) -> Att#attributes.mind.
+
+-spec get_speed (struct()) -> integer().
+get_speed (Att) -> Att#attributes.speed.
+
+-spec get_strength (struct()) -> integer().
+get_strength (Att) -> Att#attributes.strength.
+
+-spec set_constitution (integer(), struct()) -> struct().
+set_constitution (Val, Att) -> Att#attributes{ constitution = Val }.
+
+-spec set_dexterity (integer(), struct()) -> struct().
+set_dexterity (Val, Att) -> Att#attributes{ dexterity = Val }.
+
+-spec set_intelligence (integer(), struct()) -> struct().
+set_intelligence (Val, Att) -> Att#attributes{ intelligence = Val }.
+
+-spec set_mind (integer(), struct()) -> struct().
+set_mind (Val, Att) -> Att#attributes{ mind = Val }.
+
+-spec set_speed (integer(), struct()) -> struct().
+set_speed (Val, Att) -> Att#attributes{ speed = Val }.
+
+-spec set_strength (integer(), struct()) -> struct().
+set_strength (Val, Att) -> Att#attributes{ strength = Val }.
+
+-spec random () -> struct().
+random () ->
+ #attributes
+ {
+ constitution = roll:percentage(),
+ dexterity = roll:percentage(),
+ intelligence = roll:percentage(),
+ mind = roll:percentage(),
+ speed = roll:percentage(),
+ strength = roll:percentage()
+ }.
diff --git a/src/battlemap/src/struct/battle.erl b/src/battlemap/src/struct/battle.erl
new file mode 100644
index 0000000..5ac12e4
--- /dev/null
+++ b/src/battlemap/src/struct/battle.erl
@@ -0,0 +1,250 @@
+-module(battle).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type id() :: binary().
+
+-record
+(
+ battle,
+ {
+ id :: id(),
+ battlemap :: battlemap:struct(),
+ character_instances :: array:array(character_instance:struct()),
+ players :: array:array(player:struct()),
+ current_player_turn :: player_turn:struct()
+ }
+).
+
+-opaque struct() :: #battle{}.
+
+-export_type([struct/0, id/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_id/1,
+ get_battlemap/1,
+ get_character_instances/1,
+ get_character_instance/2,
+ get_players/1,
+ get_player/2,
+ get_current_player_turn/1,
+ get_encoded_last_turns_effects/1,
+
+ set_battlemap/2,
+ set_character_instances/2,
+ set_character_instance/3,
+ set_players/2,
+ set_player/3,
+ set_current_player_turn/2
+ ]
+).
+
+-export
+(
+ [
+ random/4
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+get_all_timelines (Result, CurrentIndex, EndPoint, ArraySize, Players) ->
+ Player = array:get(CurrentIndex, Players),
+ Timeline = player:get_timeline(Player),
+ NextIndex = ((CurrentIndex + 1) rem ArraySize),
+ NextResult = (Timeline ++ Result),
+ case CurrentIndex of
+ EndPoint ->
+ NextResult;
+
+ _ ->
+ get_all_timelines(NextResult, NextIndex, EndPoint, ArraySize, Players)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_id (struct()) -> id().
+get_id (Battle) -> Battle#battle.id.
+
+-spec get_battlemap (struct()) -> battlemap:struct().
+get_battlemap (Battle) ->
+ Battle#battle.battlemap.
+
+-spec get_character_instances (struct()) ->
+ array:array(character_instance:struct()).
+get_character_instances (Battle) ->
+ Battle#battle.character_instances.
+
+-spec get_character_instance (non_neg_integer(), struct()) ->
+ character_instance:struct().
+get_character_instance (IX, Battle) ->
+ array:get(IX, Battle#battle.character_instances).
+
+-spec get_players (struct()) -> array:array(player:struct()).
+get_players (Battle) ->
+ Battle#battle.players.
+
+-spec get_player (non_neg_integer(), struct()) -> player:struct().
+get_player (IX, Battle) ->
+ array:get(IX, Battle#battle.players).
+
+-spec get_current_player_turn (struct()) -> player_turn:struct().
+get_current_player_turn (Battle) ->
+ Battle#battle.current_player_turn.
+
+-spec get_encoded_last_turns_effects (struct()) -> list(any()).
+get_encoded_last_turns_effects (Battle) ->
+ CurrentPlayerTurn = Battle#battle.current_player_turn,
+ Players = Battle#battle.players,
+ CurrentPlayerIX = player_turn:get_player_ix(CurrentPlayerTurn),
+
+ PlayersCount = array:size(Players),
+ StartingPoint = ((CurrentPlayerIX + 1) rem PlayersCount),
+ get_all_timelines([], StartingPoint, CurrentPlayerIX, PlayersCount, Players).
+
+-spec set_battlemap (battlemap:struct(), struct()) -> struct().
+set_battlemap (Battlemap, Battle) ->
+ Battle#battle
+ {
+ battlemap = Battlemap
+ }.
+
+-spec set_character_instances
+ (
+ array:array(character_instance:struct()),
+ struct()
+ )
+ -> struct().
+set_character_instances (CharacterInstances, Battle) ->
+ Battle#battle
+ {
+ character_instances = CharacterInstances
+ }.
+
+-spec set_character_instance
+ (
+ non_neg_integer(),
+ character_instance:struct(),
+ struct()
+ )
+ -> struct().
+set_character_instance (IX, CharacterInstance, Battle) ->
+ Battle#battle
+ {
+ character_instances =
+ array:set
+ (
+ IX,
+ CharacterInstance,
+ Battle#battle.character_instances
+ )
+ }.
+
+-spec set_players
+ (
+ array:array(player:struct()),
+ struct()
+ )
+ -> struct().
+set_players (Players, Battle) ->
+ Battle#battle
+ {
+ players = Players
+ }.
+
+-spec set_player
+ (
+ non_neg_integer(),
+ player:struct(),
+ struct()
+ )
+ -> struct().
+set_player (IX, Player, Battle) ->
+ Battle#battle
+ {
+ players =
+ array:set
+ (
+ IX,
+ Player,
+ Battle#battle.players
+ )
+ }.
+
+-spec set_current_player_turn
+ (
+ player_turn:struct(),
+ struct()
+ )
+ -> struct().
+set_current_player_turn (PlayerTurn, Battle) ->
+ Battle#battle
+ {
+ current_player_turn = PlayerTurn
+ }.
+
+-spec random
+ (
+ id(),
+ list(player:struct()),
+ battlemap:struct(),
+ list(character:struct())
+ )
+ -> struct().
+random (ID, PlayersAsList, Battlemap, Characters) ->
+ BattlemapWidth = battlemap:get_width(Battlemap),
+ BattlemapHeight = battlemap:get_height(Battlemap),
+ {CharacterInstancesAsList, _ForbiddenLocations} =
+ lists:mapfoldl
+ (
+ fun (Character, ForbiddenLocations) ->
+ CharacterOwner = character:get_owner_id(Character),
+ NewCharacterInstance =
+ character_instance:random
+ (
+ Character,
+ BattlemapWidth,
+ BattlemapHeight,
+ ForbiddenLocations
+ ),
+ NewCharacterInstanceActive =
+ case CharacterOwner of
+ <<"0">> ->
+ character_instance:set_is_active
+ (
+ true,
+ NewCharacterInstance
+ );
+
+ _ ->
+ NewCharacterInstance
+ end,
+ NewCharacterInstanceLocation =
+ character_instance:get_location(NewCharacterInstanceActive),
+ {
+ NewCharacterInstanceActive,
+ [NewCharacterInstanceLocation|ForbiddenLocations]
+ }
+ end,
+ [],
+ Characters
+ ),
+
+ #battle
+ {
+ id = ID,
+ battlemap = Battlemap,
+ character_instances = array:from_list(CharacterInstancesAsList),
+ players = array:from_list(PlayersAsList),
+ current_player_turn = player_turn:new(0, 0)
+ }.
diff --git a/src/battlemap/src/struct/battle_action.erl b/src/battlemap/src/struct/battle_action.erl
new file mode 100644
index 0000000..8aaaef9
--- /dev/null
+++ b/src/battlemap/src/struct/battle_action.erl
@@ -0,0 +1,300 @@
+-module(battle_action).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ move,
+ {
+ path :: list(direction:enum())
+ }
+).
+
+-record
+(
+ switch_weapon,
+ {
+ }
+).
+
+-record
+(
+ attack,
+ {
+ target_ix :: non_neg_integer()
+ }
+).
+
+-type category() :: ('move' | 'switch_weapon' | 'attack' | 'nothing').
+-opaque struct() :: (#move{} | #switch_weapon{} | #attack{}).
+
+-export_type([category/0, struct/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ decode/1,
+ handle/4,
+ can_follow/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec decode_mov_action (map()) -> struct().
+decode_mov_action (JSONMap) ->
+ PathInBinary = maps:get(<<"p">>, JSONMap),
+ Path = lists:map(fun direction:decode/1, PathInBinary),
+
+ #move { path = Path }.
+
+-spec decode_atk_action (map()) -> struct().
+decode_atk_action (JSONMap) ->
+ TargetIX = binary_to_integer(maps:get(<<"tix">>, JSONMap)),
+
+ #attack { target_ix = TargetIX }.
+
+-spec decode_swp_action (map()) -> struct().
+decode_swp_action (_JSONMap) ->
+ #switch_weapon{}.
+
+-spec handle_attack_sequence
+ (
+ character_instance:struct(),
+ character_instance:struct(),
+ list(attack:step())
+ )
+ -> {list(attack:struct()), non_neg_integer(), non_neg_integer()}.
+handle_attack_sequence
+(
+ CharacterInstance,
+ TargetCharacterInstance,
+ AttackSequence
+) ->
+ Character = character_instance:get_character(CharacterInstance),
+ TargetCharacter = character_instance:get_character(TargetCharacterInstance),
+ CharacterStatistics = character:get_statistics(Character),
+ TargetCharacterStatistics = character:get_statistics(TargetCharacter),
+
+ AttackPlannedEffects =
+ lists:map
+ (
+ fun (AttackStep) ->
+ attack:get_description_of
+ (
+ AttackStep,
+ CharacterStatistics,
+ TargetCharacterStatistics
+ )
+ end,
+ AttackSequence
+ ),
+
+ lists:foldl
+ (
+ fun
+ (
+ AttackEffectCandidate,
+ {AttackValidEffects, AttackerHealth, DefenderHealth}
+ ) ->
+ {AttackResult, NewAttackerHealth, NewDefenderHealth} =
+ attack:apply_to_healths
+ (
+ AttackEffectCandidate,
+ AttackerHealth,
+ DefenderHealth
+ ),
+ case AttackResult of
+ nothing -> {AttackValidEffects, AttackerHealth, DefenderHealth};
+ _ ->
+ {
+ (AttackValidEffects ++ [AttackResult]),
+ NewAttackerHealth,
+ NewDefenderHealth
+ }
+ end
+ end,
+ {
+ [],
+ character_instance:get_current_health(CharacterInstance),
+ character_instance:get_current_health(TargetCharacterInstance)
+ },
+ AttackPlannedEffects
+ ).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec decode (map()) -> struct().
+decode (EncodedAction) ->
+ JSONActionMap = EncodedAction, %jiffy:decode(EncodedAction, [return_maps]),
+ ActionType = maps:get(<<"t">>, JSONActionMap),
+ case ActionType of
+ <<"mov">> -> decode_mov_action(JSONActionMap);
+ <<"atk">> -> decode_atk_action(JSONActionMap);
+ <<"swp">> -> decode_swp_action(JSONActionMap)
+ end.
+
+-spec can_follow (category(), category()) -> boolean().
+can_follow (nothing, attack) -> true;
+can_follow (nothing, switch_weapon) -> true;
+can_follow (nothing, move) -> true;
+can_follow (switch_weapon, attack) -> true;
+can_follow (move, attack) -> true;
+can_follow (_, _) -> false.
+
+-spec handle
+(
+ battle:struct(),
+ character_instance:struct(),
+ non_neg_integer(),
+ struct()
+)
+->
+{
+ list(database_diff:struct()),
+ list(turn_result:struct()),
+ battle:struct(),
+ character_instance:struct()
+}.
+handle (Battle, CharacterInstance, CharacterInstanceIX, BattleAction)
+when is_record(BattleAction, switch_weapon) ->
+ Character = character_instance:get_character(CharacterInstance),
+ CharacterAttributes = character:get_attributes(Character),
+ {PrimaryWeaponID, SecondaryWeaponID} = character:get_weapon_ids(Character),
+
+ UpdatedWeaponIDs = {SecondaryWeaponID, PrimaryWeaponID},
+ UpdatedCharacterStatistics =
+ statistics:new(CharacterAttributes, UpdatedWeaponIDs),
+ UpdatedCharacter =
+ character:set_statistics
+ (
+ UpdatedCharacterStatistics,
+ character:set_weapon_ids(UpdatedWeaponIDs, Character)
+ ),
+ UpdatedCharacterInstance =
+ character_instance:set_character(UpdatedCharacter, CharacterInstance),
+
+ {
+ % TODO: hide that into database_diff structs.
+ [
+ {character_instance, CharacterInstanceIX, wp0, SecondaryWeaponID},
+ {character_instance, CharacterInstanceIX, wp1, PrimaryWeaponID}
+ % ... statistics as well.
+ ],
+ [turn_result:new_character_switched_weapons(CharacterInstanceIX)],
+ Battle,
+ UpdatedCharacterInstance
+ };
+handle (Battle, CharacterInstance, CharacterInstanceIX, BattleAction)
+when is_record(BattleAction, move) ->
+ Character = character_instance:get_character(CharacterInstance),
+ CharacterStatistics = character:get_statistics(Character),
+ Battlemap = battle:get_battlemap(Battle),
+ Path = BattleAction#move.path,
+ CharacterMovementPoints =
+ statistics:get_movement_points(CharacterStatistics),
+
+ ForbiddenLocations =
+ array:foldl
+ (
+ fun (IX, CharInst, Prev) ->
+ case IX of
+ CharacterInstanceIX -> Prev;
+ _ -> [character_instance:get_location(CharInst)|Prev]
+ end
+ end,
+ [],
+ battle:get_character_instances(Battle)
+ ),
+
+ {NewLocation, Cost} =
+ movement:cross
+ (
+ Battlemap,
+ ForbiddenLocations,
+ Path,
+ character_instance:get_location(CharacterInstance)
+ ),
+
+ true = (Cost =< CharacterMovementPoints),
+
+ UpdatedCharacterInstance =
+ character_instance:set_location(NewLocation, CharacterInstance),
+
+ {
+ % TODO: hide that into database_diff structs.
+ [{character_instance, CharacterInstanceIX, loc, NewLocation}],
+ % TODO: hide that into turn_result structs.
+ [turn_result:new_character_moved(CharacterInstanceIX, Path, NewLocation)],
+ Battle,
+ UpdatedCharacterInstance
+ };
+handle (Battle, CharacterInstance, CharacterInstanceIX, BattleAction)
+when is_record(BattleAction, attack) ->
+ Character = character_instance:get_character(CharacterInstance),
+ TargetIX = BattleAction#attack.target_ix,
+ TargetCharacterInstance = battle:get_character_instance(TargetIX, Battle),
+ TargetCharacter = character_instance:get_character(TargetCharacterInstance),
+
+ Range =
+ location:dist
+ (
+ character_instance:get_location(CharacterInstance),
+ character_instance:get_location(TargetCharacterInstance)
+ ),
+
+ {AttackingWeaponID, _} = character:get_weapon_ids(Character),
+ {DefendingWeaponID, _} = character:get_weapon_ids(TargetCharacter),
+
+ AttackingWeapon = weapon:from_id(AttackingWeaponID),
+ DefendingWeapon = weapon:from_id(DefendingWeaponID),
+
+ AttackSequence =
+ attack:get_sequence(Range, AttackingWeapon, DefendingWeapon),
+
+ {AttackEffects, RemainingAttackerHealth, RemainingDefenderHealth} =
+ handle_attack_sequence
+ (
+ CharacterInstance,
+ TargetCharacterInstance,
+ AttackSequence
+ ),
+
+ UpdatedCharacterInstance =
+ character_instance:set_current_health
+ (
+ RemainingAttackerHealth,
+ CharacterInstance
+ ),
+
+ UpdatedBattle =
+ battle:set_character_instance
+ (
+ TargetIX,
+ character_instance:set_current_health
+ (
+ RemainingDefenderHealth,
+ TargetCharacterInstance
+ ),
+ Battle
+ ),
+ {
+ % TODO: hide that into database_diff structs.
+ [], % TODO
+ [
+ turn_result:new_character_attacked
+ (
+ CharacterInstanceIX,
+ TargetIX,
+ AttackEffects
+ )
+ ],
+ UpdatedBattle,
+ UpdatedCharacterInstance
+ }.
diff --git a/src/battlemap/src/struct/battlemap.erl b/src/battlemap/src/struct/battlemap.erl
new file mode 100644
index 0000000..59e0639
--- /dev/null
+++ b/src/battlemap/src/struct/battlemap.erl
@@ -0,0 +1,121 @@
+-module(battlemap).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type id() :: binary().
+
+-record
+(
+ battlemap,
+ {
+ id :: id(),
+ width :: integer(),
+ height :: integer(),
+ tile_ids :: array:array(tile:id())
+ }
+).
+
+-opaque struct() :: #battlemap{}.
+
+-export_type([struct/0, id/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_id/1,
+ get_width/1,
+ get_height/1,
+ get_tile_ids/1,
+ get_tile_id/2
+ ]
+).
+
+-export
+(
+ [
+ random/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate_random_tile_ids
+ (
+ tile:id(),
+ list(tile:id()),
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer()
+ )
+ -> list(tile:id()).
+generate_random_tile_ids (_PreviousTileID, Result, _X, 0, _Width) ->
+ Result;
+generate_random_tile_ids (PreviousTileID, Result, 0, Y, Width) ->
+ generate_random_tile_ids(PreviousTileID, Result, Width, (Y - 1), Width);
+generate_random_tile_ids (PreviousTileID, Result, X, Y, Width) ->
+ NewTile =
+ case roll:percentage() of
+ N when (N >= 10) -> PreviousTileID;
+ _ -> tile:random_id()
+ end,
+ generate_random_tile_ids(NewTile, [NewTile|Result], (X - 1), Y, Width).
+
+-spec location_to_array_index
+ (
+ non_neg_integer(),
+ location:type()
+ )
+ -> ('error' | non_neg_integer()).
+location_to_array_index (ArrayWidth, {X, Y}) ->
+ if
+ (X < 0) -> error;
+ (Y < 0) -> error;
+ (X >= ArrayWidth) -> error;
+ true -> ((Y * ArrayWidth) + X)
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_id (struct()) -> id().
+get_id (Battlemap) -> Battlemap#battlemap.id.
+
+-spec get_width (struct()) -> integer().
+get_width (Battlemap) -> Battlemap#battlemap.width.
+
+-spec get_height (struct()) -> integer().
+get_height (Battlemap) -> Battlemap#battlemap.height.
+
+-spec get_tile_ids (struct()) -> array:array(tile:id()).
+get_tile_ids (Battlemap) -> Battlemap#battlemap.tile_ids.
+
+-spec get_tile_id (location:type(), struct()) -> tile:id().
+get_tile_id (Location, Battlemap) ->
+ TileIX = location_to_array_index(Battlemap#battlemap.width, Location),
+ array:get(TileIX, Battlemap#battlemap.tile_ids).
+
+-spec random
+ (
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer()
+ )
+ -> struct().
+random (ID, Width, Height) ->
+ InitialTile = tile:random_id(),
+ TileIDs = generate_random_tile_ids(InitialTile, [], Width, Height, Width),
+
+ #battlemap
+ {
+ id = list_to_binary(integer_to_list(ID)),
+ width = Width,
+ height = Height,
+ tile_ids = array:from_list(TileIDs)
+ }.
diff --git a/src/battlemap/src/struct/character.erl b/src/battlemap/src/struct/character.erl
new file mode 100644
index 0000000..8e1099e
--- /dev/null
+++ b/src/battlemap/src/struct/character.erl
@@ -0,0 +1,133 @@
+-module(character).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type id() :: non_neg_integer().
+
+-record
+(
+ character,
+ {
+ id :: id(),
+ owner_id :: player:id(),
+ name :: binary(),
+ icon :: binary(),
+ portrait :: binary(),
+ attributes :: attributes:struct(),
+ statistics :: statistics:struct(),
+ weapon_ids :: {weapon:id(), weapon:id()}
+ }
+).
+
+-opaque struct() :: #character{}.
+
+-export_type([struct/0, id/0]).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_id/1,
+ get_owner_id/1,
+ get_name/1,
+ get_icon/1,
+ get_portrait/1,
+ get_attributes/1,
+ get_statistics/1,
+ get_weapon_ids/1,
+
+ set_weapon_ids/2,
+ set_statistics/2
+ ]
+).
+
+-export
+(
+ [
+ random/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_id (struct()) -> id().
+get_id (Char) -> Char#character.id.
+
+-spec get_owner_id (struct()) -> player:id().
+get_owner_id (Char) -> Char#character.owner_id.
+
+-spec get_name (struct()) -> binary().
+get_name (Char) -> Char#character.name.
+
+-spec get_icon (struct()) -> binary().
+get_icon (Char) -> Char#character.icon.
+
+-spec get_portrait (struct()) -> binary().
+get_portrait (Char) -> Char#character.portrait.
+
+-spec get_attributes (struct()) -> attributes:struct().
+get_attributes (Char) -> Char#character.attributes.
+
+-spec get_weapon_ids (struct()) -> {weapon:id(), weapon:id()}.
+get_weapon_ids (Char) -> Char#character.weapon_ids.
+
+-spec get_statistics (struct()) -> statistics:struct().
+get_statistics (Char) -> Char#character.statistics.
+
+-spec set_weapon_ids
+ (
+ {weapon:id(), weapon:id()},
+ struct()
+ )
+ -> struct().
+set_weapon_ids (WeaponIDs, Char) ->
+ Char#character
+ {
+ weapon_ids = WeaponIDs
+ }.
+
+-spec set_statistics
+ (
+ statistics:struct(),
+ struct()
+ )
+ -> struct().
+set_statistics (Stats, Char) ->
+ Char#character
+ {
+ statistics = Stats
+ }.
+
+-spec random
+ (
+ non_neg_integer(),
+ player:id()
+ )
+ -> struct().
+random (ID, OwnerID) ->
+ WeaponIDs = {weapon:random_id(), weapon:random_id()},
+ Attributes = attributes:random(),
+ Statistics = statistics:new(Attributes, WeaponIDs),
+ IDAsListString = integer_to_list(ID),
+ IDAsBinaryString = list_to_binary(IDAsListString),
+
+ #character
+ {
+ id = ID,
+ owner_id = OwnerID,
+ name = list_to_binary("Char" ++ IDAsListString),
+ icon = IDAsBinaryString,
+ portrait = IDAsBinaryString,
+ attributes = Attributes,
+ weapon_ids = WeaponIDs,
+ statistics = Statistics
+ }.
diff --git a/src/battlemap/src/struct/character_instance.erl b/src/battlemap/src/struct/character_instance.erl
new file mode 100644
index 0000000..9b64f9a
--- /dev/null
+++ b/src/battlemap/src/struct/character_instance.erl
@@ -0,0 +1,160 @@
+-module(character_instance).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ character_instance,
+ {
+ character :: character:struct(),
+ location :: {non_neg_integer(), non_neg_integer()},
+ current_health :: non_neg_integer(),
+ active :: boolean()
+ }
+).
+
+-opaque struct() :: #character_instance{}.
+
+-export_type([struct/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ new/2,
+ random/4
+ ]
+).
+
+%%%% Accessors
+-export
+(
+ [
+ get_character/1,
+ get_location/1,
+ get_current_health/1,
+ get_is_active/1,
+
+ set_character/2,
+ set_location/2,
+ set_current_health/2,
+ set_is_active/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec find_random_location
+ (
+ non_neg_integer(),
+ non_neg_integer(),
+ list({non_neg_integer(), non_neg_integer()})
+ )
+ -> {non_neg_integer(), non_neg_integer()}.
+find_random_location (BattlemapWidth, BattlemapHeight, ForbiddenLocations) ->
+ X = roll:between(0, (BattlemapWidth - 1)),
+ Y = roll:between(0, (BattlemapHeight - 1)),
+
+ IsForbidden = lists:member({X, Y}, ForbiddenLocations),
+
+ case IsForbidden of
+ true ->
+ find_random_location
+ (
+ BattlemapWidth,
+ BattlemapHeight,
+ ForbiddenLocations
+ );
+
+ _ -> {X, Y}
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_character (struct()) -> character:struct().
+get_character (CharInst) -> CharInst#character_instance.character.
+
+-spec get_location (struct()) -> {non_neg_integer(), non_neg_integer()}.
+get_location (CharInst) -> CharInst#character_instance.location.
+
+-spec get_current_health (struct()) -> non_neg_integer().
+get_current_health (CharInst) -> CharInst#character_instance.current_health.
+
+-spec get_is_active (struct()) -> boolean().
+get_is_active (CharInst) ->
+ (
+ CharInst#character_instance.active
+ and
+ (CharInst#character_instance.current_health > 0)
+ ).
+
+-spec set_character (character:struct(), struct()) -> struct().
+set_character (Char, CharInst) ->
+ CharInst#character_instance
+ {
+ character = Char
+ }.
+
+-spec set_location
+ (
+ {non_neg_integer(), non_neg_integer()},
+ struct()
+ )
+ -> struct().
+set_location (Location, CharInst) ->
+ CharInst#character_instance
+ {
+ location = Location
+ }.
+
+-spec set_current_health (non_neg_integer(), struct()) -> struct().
+set_current_health (Health, CharInst) ->
+ CharInst#character_instance
+ {
+ current_health = Health
+ }.
+
+-spec set_is_active (boolean(), struct()) -> struct().
+set_is_active (Active, CharInst) ->
+ CharInst#character_instance
+ {
+ active = Active
+ }.
+
+%%%% Utils
+-spec new
+ (
+ character:struct(),
+ {non_neg_integer(), non_neg_integer()}
+ )
+ -> struct().
+new (Character, Location) ->
+ CharacterStatistics = character:get_statistics(Character),
+ #character_instance
+ {
+ character = Character,
+ location = Location,
+ current_health = statistics:get_health(CharacterStatistics),
+ active = false
+ }.
+
+-spec random
+ (
+ character:struct(),
+ non_neg_integer(),
+ non_neg_integer(),
+ list({non_neg_integer(), non_neg_integer()})
+ )
+ -> struct().
+random (Character, BattlemapWidth, BattlemapHeight, ForbiddenLocations) ->
+ new
+ (
+ Character,
+ find_random_location(BattlemapWidth, BattlemapHeight, ForbiddenLocations)
+ ).
diff --git a/src/battlemap/src/struct/direction.erl b/src/battlemap/src/struct/direction.erl
new file mode 100644
index 0000000..84ae272
--- /dev/null
+++ b/src/battlemap/src/struct/direction.erl
@@ -0,0 +1,37 @@
+-module(direction).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type enum() :: ('up' | 'down' | 'left' | 'right').
+
+-export_type([enum/0]).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ decode/1,
+ encode/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec decode (binary()) -> enum().
+decode (<<"U">>) -> up;
+decode (<<"D">>) -> down;
+decode (<<"L">>) -> left;
+decode (<<"R">>) -> right.
+
+-spec encode (enum()) -> binary().
+encode (up) -> <<"U">>;
+encode (down) -> <<"D">>;
+encode (left) -> <<"L">>;
+encode (right) -> <<"R">>.
diff --git a/src/battlemap/src/struct/location.erl b/src/battlemap/src/struct/location.erl
new file mode 100644
index 0000000..b8e2bf3
--- /dev/null
+++ b/src/battlemap/src/struct/location.erl
@@ -0,0 +1,71 @@
+-module(location).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type type() :: {non_neg_integer(), non_neg_integer()}.
+
+-export_type([type/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ decode/1,
+ encode/1
+ ]
+).
+
+-export
+(
+ [
+ apply_direction/2,
+ dist/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec validate ({integer(), integer()}) -> type().
+validate ({X, Y}) ->
+ true = (X >= 0),
+ true = (Y >= 0),
+ {X, Y}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec apply_direction (direction:enum(), type()) -> type().
+apply_direction (left, {X, Y}) ->
+ validate({(X - 1), Y});
+apply_direction (right, {X, Y}) ->
+ validate({(X + 1), Y});
+apply_direction (up, {X, Y}) ->
+ validate({X, (Y - 1)});
+apply_direction (down, {X, Y}) ->
+ validate({X, (Y + 1)}).
+
+-spec dist(type(), type()) -> non_neg_integer().
+dist ({OX, OY}, {DX, DY}) ->
+ (abs(DY - OY) + abs(DX - OX)).
+
+-spec encode (type()) -> {list(any())}.
+encode ({X, Y}) ->
+ {
+ [
+ {<<"x">>, X},
+ {<<"y">>, Y}
+ ]
+ }.
+
+-spec decode (map()) -> type().
+decode (Map) ->
+ X = maps:get(<<"x">>, Map),
+ Y = maps:get(<<"y">>, Map),
+
+ true = (is_integer(X) and is_integer(Y)),
+
+ validate({X, Y}).
diff --git a/src/battlemap/src/struct/player.erl b/src/battlemap/src/struct/player.erl
new file mode 100644
index 0000000..c4aefd1
--- /dev/null
+++ b/src/battlemap/src/struct/player.erl
@@ -0,0 +1,72 @@
+-module(player).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type id() :: binary().
+
+-record
+(
+ player,
+ {
+ id :: id(),
+ timeline :: list(any())
+ }
+).
+
+-opaque struct() :: #player{}.
+
+-export_type([struct/0, id/0]).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ get_id/1,
+ get_timeline/1,
+ add_to_timeline/2,
+ reset_timeline/1
+ ]
+).
+
+-export
+(
+ [
+ new/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec get_id (struct()) -> id().
+get_id (Player) -> Player#player.id.
+
+-spec get_timeline (struct()) -> list(any()).
+get_timeline (Player) -> Player#player.timeline.
+
+-spec add_to_timeline (list(any()), struct()) -> struct().
+add_to_timeline (NewEvents, Player) ->
+ OldTimeline = Player#player.timeline,
+
+ Player#player
+ {
+ timeline = (NewEvents ++ OldTimeline)
+ }.
+
+-spec reset_timeline (struct()) -> struct().
+reset_timeline (Player) -> Player#player{ timeline = [] }.
+
+-spec new (id()) -> struct().
+new (ID) ->
+ #player
+ {
+ id = ID,
+ timeline = []
+ }.
+
diff --git a/src/battlemap/src/struct/player_turn.erl b/src/battlemap/src/struct/player_turn.erl
new file mode 100644
index 0000000..7795f35
--- /dev/null
+++ b/src/battlemap/src/struct/player_turn.erl
@@ -0,0 +1,58 @@
+-module(player_turn).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ player_turn,
+ {
+ number :: non_neg_integer(),
+ player_ix :: non_neg_integer()
+ }
+).
+
+-opaque struct() :: #player_turn{}.
+
+-export_type([struct/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ new/2
+ ]
+).
+
+%%%% Accessors
+-export
+(
+ [
+ get_number/1,
+ get_player_ix/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec new (non_neg_integer(), non_neg_integer()) -> struct().
+new (Number, PlayerIX) ->
+ #player_turn
+ {
+ number = Number,
+ player_ix = PlayerIX
+ }.
+
+-spec get_number (struct()) -> non_neg_integer().
+get_number (PlayerTurn) -> PlayerTurn#player_turn.number.
+
+-spec get_player_ix (struct()) -> non_neg_integer().
+get_player_ix (PlayerTurn) -> PlayerTurn#player_turn.player_ix.
diff --git a/src/battlemap/src/struct/statistics.erl b/src/battlemap/src/struct/statistics.erl
new file mode 100644
index 0000000..6e29ea4
--- /dev/null
+++ b/src/battlemap/src/struct/statistics.erl
@@ -0,0 +1,193 @@
+-module(statistics).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ statistics,
+ {
+ movement_points :: non_neg_integer(),
+ health :: non_neg_integer(),
+ dodges :: non_neg_integer(),
+ parries :: non_neg_integer(),
+ damage_min :: non_neg_integer(),
+ damage_max :: non_neg_integer(),
+ accuracy :: non_neg_integer(),
+ double_hits :: non_neg_integer(),
+ critical_hits :: non_neg_integer()
+ }
+).
+
+-opaque struct() :: #statistics{}.
+
+-export_type([struct/0]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_movement_points/1,
+ get_health/1,
+ get_dodges/1,
+ get_parries/1,
+ get_damage_min/1,
+ get_damage_max/1,
+ get_accuracy/1,
+ get_double_hits/1,
+ get_critical_hits/1,
+
+ get_damages/1
+ ]
+).
+
+-export
+(
+ [
+ new/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec ceil (float()) -> integer().
+ceil (F) ->
+ I = trunc(F),
+ case (F > I) of
+ true -> (I + 1);
+ _ -> I
+ end.
+
+-spec float_to_int (float()) -> integer().
+float_to_int (F) -> ceil(F).
+
+-spec min_max (number(), number(), number()) -> number().
+min_max (Min, Max, V) -> min(Max, max(Min, V)).
+
+-spec average (list(number())) -> number().
+%average ([]) -> 0;
+average (L) -> lists:sum(L) / length(L).
+
+% V | 010 | 030 | 050 | 070 | 100 |
+% F | 004 | 023 | 058 | 104 | 200 |
+-spec gentle_squared_growth (number()) -> non_neg_integer().
+gentle_squared_growth (V) -> float_to_int(math:pow(V, 1.8) / 20).
+
+% V | 010 | 030 | 050 | 070 | 100 |
+% F | 001 | 005 | 018 | 041 | 100 |
+-spec sudden_squared_growth (number()) -> non_neg_integer().
+sudden_squared_growth (V) -> float_to_int(math:pow(V, 2.5) / 1000).
+
+% V | 010 | 030 | 050 | 070 | 100 |
+% F | 002 | 006 | 016 | 049 | 256 |
+-spec sudden_exp_growth (number()) -> non_neg_integer().
+sudden_exp_growth (V) -> float_to_int(math:pow(4, V / 25)).
+
+% V | 010 | 030 | 050 | 070 | 100 |
+% F | 040 | 066 | 079 | 088 | 099 |
+% Seems too generous, values for attributes below 50 should dip faster and
+% lower.
+%-spec already_high_slow_growth (non_neg_integer()) -> non_neg_integer().
+%already_high_slow_growth (V) -> float_to_int(30 * math:log((V + 5)/4)).
+
+-spec damage_base_modifier (non_neg_integer()) -> float().
+damage_base_modifier (Strength) -> ((math:pow(Strength, 1.8) / 2000.0) - 0.75).
+
+-spec apply_damage_base_modifier
+ (
+ float(),
+ non_neg_integer()
+ )
+ -> non_neg_integer().
+apply_damage_base_modifier (Modifier, BaseValue) ->
+ max(0, float_to_int(BaseValue + (BaseValue * Modifier))).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_movement_points (struct()) -> non_neg_integer().
+get_movement_points (Stats) -> Stats#statistics.movement_points.
+
+-spec get_health (struct()) -> non_neg_integer().
+get_health (Stats) -> Stats#statistics.health.
+
+-spec get_dodges (struct()) -> non_neg_integer().
+get_dodges (Stats) -> Stats#statistics.dodges.
+
+-spec get_parries (struct()) -> non_neg_integer().
+get_parries (Stats) -> Stats#statistics.parries.
+
+-spec get_damage_min (struct()) -> non_neg_integer().
+get_damage_min (Stats) -> Stats#statistics.damage_min.
+
+-spec get_damage_max (struct()) -> non_neg_integer().
+get_damage_max (Stats) -> Stats#statistics.damage_max.
+
+-spec get_accuracy (struct()) -> non_neg_integer().
+get_accuracy (Stats) -> Stats#statistics.accuracy.
+
+-spec get_double_hits (struct()) -> non_neg_integer().
+get_double_hits (Stats) -> Stats#statistics.double_hits.
+
+-spec get_critical_hits (struct()) -> non_neg_integer().
+get_critical_hits (Stats) -> Stats#statistics.critical_hits.
+
+-spec get_damages (struct()) -> {non_neg_integer(), non_neg_integer()}.
+get_damages (Stats) ->
+ {
+ Stats#statistics.damage_min,
+ Stats#statistics.damage_max
+ }.
+
+-spec new
+ (
+ attributes:struct(),
+ {weapon:id(), weapon:id()}
+ )
+ -> struct().
+new (BaseAttributes, WeaponIDs) ->
+ {ActiveWeaponID, _} = WeaponIDs,
+ ActiveWeapon = weapon:from_id(ActiveWeaponID),
+ {MinDamage, MaxDamage} = weapon:get_damages(ActiveWeapon),
+ Attributes = weapon:apply_to_attributes(BaseAttributes, ActiveWeapon),
+ Constitution = attributes:get_constitution(Attributes),
+ Dexterity = attributes:get_dexterity(Attributes),
+ Intelligence = attributes:get_intelligence(Attributes),
+ Mind = attributes:get_mind(Attributes),
+ Speed = attributes:get_speed(Attributes),
+ Strength = attributes:get_strength(Attributes),
+ DamageBaseModifier = damage_base_modifier(Strength),
+
+ #statistics
+ {
+ movement_points =
+ gentle_squared_growth
+ (
+ average([Mind, Constitution, Constitution, Speed, Speed, Speed])
+ ),
+ health =
+ gentle_squared_growth(average([Mind, Constitution, Constitution])),
+ dodges =
+ min_max(0, 100, sudden_exp_growth(average([Dexterity, Mind, Speed]))),
+ parries =
+ min_max
+ (
+ 0,
+ 75,
+ sudden_exp_growth
+ (
+ average([Dexterity, Intelligence, Speed, Strength])
+ )
+ ),
+ damage_min = apply_damage_base_modifier(DamageBaseModifier, MinDamage),
+ damage_max = apply_damage_base_modifier(DamageBaseModifier, MaxDamage),
+ accuracy = min_max(0, 100, sudden_squared_growth(Dexterity)),
+ double_hits =
+ min_max(0, 100, sudden_squared_growth(average([Mind, Speed]))),
+ critical_hits = min_max(0, 100, sudden_squared_growth(Intelligence))
+ }.
diff --git a/src/battlemap/src/struct/tile.erl b/src/battlemap/src/struct/tile.erl
new file mode 100644
index 0000000..e86da56
--- /dev/null
+++ b/src/battlemap/src/struct/tile.erl
@@ -0,0 +1,47 @@
+-module(tile).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-opaque id() :: non_neg_integer().
+-opaque struct() :: id().
+
+-export_type([struct/0, id/0]).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ get_cost/1,
+ cost_when_oob/0
+ ]
+).
+
+-export
+(
+ [
+ random_id/0
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec cost_when_oob () -> non_neg_integer().
+cost_when_oob () -> 255.
+
+-spec get_cost (id()) -> non_neg_integer().
+get_cost (N) ->
+ if
+ (N =< 200) -> (N + 8);
+ true -> cost_when_oob()
+ end.
+
+-spec random_id () -> id().
+random_id () ->
+ roll:between(0, 15).
diff --git a/src/battlemap/src/struct/turn_result.erl b/src/battlemap/src/struct/turn_result.erl
new file mode 100644
index 0000000..5f796ca
--- /dev/null
+++ b/src/battlemap/src/struct/turn_result.erl
@@ -0,0 +1,142 @@
+-module(turn_result).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%
+-record
+(
+ switched_weapon,
+ {
+ character_instance_ix :: character_instance:id()
+ }
+).
+
+-record
+(
+ moved,
+ {
+ character_instance_ix :: character_instance:id(),
+ path :: list(direction:enum()),
+ new_location :: location:type()
+ }
+).
+
+-record
+(
+ attacked,
+ {
+ attacker_ix :: character_instance:id(),
+ defender_ix :: character_instance:id(),
+ sequence :: list(attack:struct())
+ }
+).
+
+-opaque struct() :: (#switched_weapon{} | #moved{} | #attacked{}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export_type([struct/0]).
+
+-export
+(
+ [
+ new_character_switched_weapons/1,
+ new_character_moved/3,
+ new_character_attacked/3
+ ]
+).
+
+-export
+(
+ [
+ encode/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec new_character_switched_weapons (character_instance:id()) -> struct().
+new_character_switched_weapons (CharacterInstanceIX) ->
+ #switched_weapon { character_instance_ix = CharacterInstanceIX }.
+
+-spec new_character_moved
+ (
+ character_instance:id(),
+ list(direction:enum()),
+ location:type()
+ )
+ -> struct().
+new_character_moved (CharacterInstanceIX, Path, NewLocation) ->
+ #moved
+ {
+ character_instance_ix = CharacterInstanceIX,
+ path = Path,
+ new_location = NewLocation
+ }.
+
+-spec new_character_attacked
+ (
+ character_instance:id(),
+ character_instance:id(),
+ list(attack:struct())
+ )
+ -> struct().
+new_character_attacked (AttackerIX, DefenderIX, AttackSequence) ->
+ #attacked
+ {
+ attacker_ix = AttackerIX,
+ defender_ix = DefenderIX,
+ sequence = AttackSequence
+ }.
+
+-spec encode (struct()) -> {list(any())}.
+encode (TurnResult) when is_record(TurnResult, switched_weapon) ->
+ CharacterInstanceIX = TurnResult#switched_weapon.character_instance_ix,
+
+ {
+ [
+ {<<"t">>, <<"swp">>},
+ {<<"ix">>, CharacterInstanceIX}
+ ]
+ };
+encode (TurnResult) when is_record(TurnResult, moved) ->
+ CharacterInstanceIX = TurnResult#moved.character_instance_ix,
+ Path = TurnResult#moved.path,
+ NewLocation = TurnResult#moved.new_location,
+
+ EncodedPath = lists:map(fun direction:encode/1, Path),
+ EncodedNewLocation = location:encode(NewLocation),
+
+ {
+ [
+ {<<"t">>, <<"mv">>},
+ {<<"ix">>, CharacterInstanceIX},
+ {<<"p">>, EncodedPath},
+ {<<"nlc">>, EncodedNewLocation}
+ ]
+ };
+encode (TurnResult) when is_record(TurnResult, attacked) ->
+ AttackerIX = TurnResult#attacked.attacker_ix,
+ DefenderIX = TurnResult#attacked.defender_ix,
+ Sequence = TurnResult#attacked.sequence,
+
+ EncodedSequence = lists:map(fun attack:encode/1, Sequence),
+
+ {
+ [
+ {<<"t">>, <<"atk">>},
+ {<<"aix">>, AttackerIX},
+ {<<"dix">>, DefenderIX},
+ {<<"seq">>, EncodedSequence}
+ ]
+ };
+encode (Other) ->
+ io:format("~n invalid encode param\"~p\"~n", [Other]),
+ true = Other.
diff --git a/src/battlemap/src/struct/weapon.erl b/src/battlemap/src/struct/weapon.erl
new file mode 100644
index 0000000..80cb925
--- /dev/null
+++ b/src/battlemap/src/struct/weapon.erl
@@ -0,0 +1,361 @@
+-module(weapon).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-opaque id() :: non_neg_integer().
+
+-type range_type() :: 'ranged' | 'melee'.
+-type range_mod() :: 'long' | 'short'.
+-type damage_type() :: 'slash' | 'pierce' | 'blunt'.
+-type damage_mod() :: 'heavy' | 'light'.
+
+-record
+(
+ weapon,
+ {
+ id :: id(),
+ name :: binary(),
+ range_type :: range_type(),
+ range_mod :: range_mod(),
+ damage_type :: damage_type(),
+ damage_mod :: damage_mod()
+ }
+).
+
+-opaque struct() :: #weapon{}.
+
+-export_type([struct/0, id/0]).
+-export_type
+(
+ [
+ range_type/0,
+ range_mod/0,
+ damage_type/0,
+ damage_mod/0
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-export
+(
+ [
+ get_id/1,
+ get_range_type/1,
+ get_ranges/1,
+ get_damages/1
+ ]
+).
+
+-export
+(
+ [
+ random_id/0,
+ from_id/1,
+ can_parry/1,
+ apply_to_attributes/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec ranges_of_type
+ (
+ range_type(),
+ range_mod()
+ )
+ -> {non_neg_integer(), non_neg_integer()}.
+ranges_of_type (ranged, long) -> {2, 6};
+ranges_of_type (ranged, short) -> {1, 4};
+ranges_of_type (melee, long) -> {0, 2};
+ranges_of_type (melee, short) -> {0, 1}.
+
+-spec damages_of_type
+ (
+ range_type(),
+ damage_mod()
+ )
+ -> {non_neg_integer(), non_neg_integer()}.
+damages_of_type (ranged, heavy) -> {10, 25};
+damages_of_type (ranged, light) -> {5, 20};
+damages_of_type (melee, heavy) -> {20, 35};
+damages_of_type (melee, light) -> {15, 30}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%% Accessors
+-spec get_id (struct()) -> id().
+get_id (Wp) -> Wp#weapon.id.
+
+-spec get_range_type (struct()) -> range_type().
+get_range_type (Wp) -> Wp#weapon.range_type.
+
+-spec get_ranges (struct()) -> {non_neg_integer(), non_neg_integer()}.
+get_ranges (Wp) ->
+ ranges_of_type(Wp#weapon.range_type, Wp#weapon.range_mod).
+
+-spec get_damages (struct()) -> {non_neg_integer(), non_neg_integer()}.
+get_damages (Wp) ->
+ damages_of_type(Wp#weapon.range_type, Wp#weapon.damage_mod).
+
+-spec can_parry (struct()) -> boolean().
+can_parry (Wp) -> (Wp#weapon.range_type == melee).
+
+-spec from_id (id()) -> struct().
+from_id (0) ->
+ #weapon{
+ id = 0,
+ name = <<"None">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = blunt,
+ damage_mod = light
+ };
+from_id (1) ->
+ #weapon{
+ id = 1,
+ name = <<"Dagger">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = slash,
+ damage_mod = light
+ };
+from_id (2) ->
+ #weapon{
+ id = 2,
+ name = <<"Sword">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = slash,
+ damage_mod = heavy
+ };
+from_id (3) ->
+ #weapon{
+ id = 3,
+ name = <<"Claymore">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = slash,
+ damage_mod = light
+ };
+from_id (4) ->
+ #weapon{
+ id = 4,
+ name = <<"Bardiche">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = slash,
+ damage_mod = heavy
+ };
+from_id (5) ->
+ #weapon{
+ id = 5,
+ name = <<"Stiletto">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = pierce,
+ damage_mod = light
+ };
+from_id (6) ->
+ #weapon{
+ id = 6,
+ name = <<"Pickaxe">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = pierce,
+ damage_mod = heavy
+ };
+from_id (7) ->
+ #weapon{
+ id = 7,
+ name = <<"Rapier">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = pierce,
+ damage_mod = light
+ };
+from_id (8) ->
+ #weapon{
+ id = 8,
+ name = <<"Pike">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = pierce,
+ damage_mod = heavy
+ };
+from_id (9) ->
+ #weapon{
+ id = 9,
+ name = <<"Club">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = blunt,
+ damage_mod = light
+ };
+from_id (10) ->
+ #weapon{
+ id = 10,
+ name = <<"Mace">>,
+ range_type = melee,
+ range_mod = short,
+ damage_type = blunt,
+ damage_mod = heavy
+ };
+from_id (11) ->
+ #weapon{
+ id = 11,
+ name = <<"Staff">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = blunt,
+ damage_mod = light
+ };
+from_id (12) ->
+ #weapon{
+ id = 12,
+ name = <<"War Hammer">>,
+ range_type = melee,
+ range_mod = long,
+ damage_type = blunt,
+ damage_mod = heavy
+ };
+from_id (13) ->
+ #weapon{
+ id = 13,
+ name = <<"Short Bow (Broadhead)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = slash,
+ damage_mod = light
+ };
+from_id (14) ->
+ #weapon{
+ id = 14,
+ name = <<"Short Bow (Blunt)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = blunt,
+ damage_mod = light
+ };
+from_id (15) ->
+ #weapon{
+ id = 15,
+ name = <<"Short Bow (Bodkin Point)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = pierce,
+ damage_mod = light
+ };
+from_id (16) ->
+ #weapon{
+ id = 16,
+ name = <<"Long Bow (Broadhead)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = slash,
+ damage_mod = light
+ };
+from_id (17) ->
+ #weapon{
+ id = 17,
+ name = <<"Long Bow (Blunt)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = blunt,
+ damage_mod = light
+ };
+from_id (18) ->
+ #weapon{
+ id = 18,
+ name = <<"Long Bow (Bodkin Point)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = pierce,
+ damage_mod = light
+ };
+from_id (19) ->
+ #weapon{
+ id = 19,
+ name = <<"Crossbow (Broadhead)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = slash,
+ damage_mod = heavy
+ };
+from_id (20) ->
+ #weapon{
+ id = 20,
+ name = <<"Crossbow (Blunt)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = blunt,
+ damage_mod = heavy
+ };
+from_id (21) ->
+ #weapon{
+ id = 21,
+ name = <<"Crossbow (Bodkin Point)">>,
+ range_type = ranged,
+ range_mod = short,
+ damage_type = pierce,
+ damage_mod = heavy
+ };
+from_id (22) ->
+ #weapon{
+ id = 22,
+ name = <<"Arbalest (Broadhead)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = slash,
+ damage_mod = heavy
+ };
+from_id (23) ->
+ #weapon{
+ id = 23,
+ name = <<"Arbalest (Blunt)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = blunt,
+ damage_mod = heavy
+ };
+from_id (24) ->
+ #weapon{
+ id = 24,
+ name = <<"Arbalest (Bodkin Point)">>,
+ range_type = ranged,
+ range_mod = long,
+ damage_type = pierce,
+ damage_mod = heavy
+ }.
+
+-spec random_id () -> id().
+random_id () -> roll:between(0, 24).
+
+-spec apply_to_attributes
+ (
+ attributes:struct(),
+ weapon:struct()
+ )
+ -> attributes:struct().
+apply_to_attributes (Attributes, Weapon) ->
+ Dexterity = attributes:get_dexterity(Attributes),
+ Speed = attributes:get_speed(Attributes),
+ RangeModifier = Weapon#weapon.range_mod,
+ DamageModifier = Weapon#weapon.damage_mod,
+ WithRangeModifier =
+ case RangeModifier of
+ long ->
+ attributes:set_dexterity(max(0, (Dexterity - 20)), Attributes);
+ _ -> Attributes
+ end,
+ case DamageModifier of
+ heavy ->
+ attributes:set_speed(max(0, (Speed - 20)), WithRangeModifier);
+ _ -> WithRangeModifier
+ end.
+