summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornsensfel <SpamShield0@noot-noot.org>2018-06-06 15:54:18 +0200
committernsensfel <SpamShield0@noot-noot.org>2018-06-06 15:54:18 +0200
commitee9c2ac044cc77b80f30420c8f0788cad4281084 (patch)
tree6ce9c45b6c0f0c556839b6f462f84eab06e26594 /src/shared
parent97f7511e61cebae3676a83aa9c0dc2efb15d8d8c (diff)
Figuring out how to organize the src folder(s)...
Diffstat (limited to 'src/shared')
-rw-r--r--src/shared/include/db_item.hrl15
-rw-r--r--src/shared/include/db_query.hrl63
-rw-r--r--src/shared/include/db_user.hrl7
-rw-r--r--src/shared/io/sh_database.erl137
-rw-r--r--src/shared/io/sh_timed_cache.erl130
-rw-r--r--src/shared/io/sh_timed_caches_manager.erl152
-rw-r--r--src/shared/struct/sh_attributes.erl108
-rw-r--r--src/shared/struct/sh_db_item.erl70
-rw-r--r--src/shared/struct/sh_db_query.erl59
-rw-r--r--src/shared/struct/sh_db_user.erl28
-rw-r--r--src/shared/struct/sh_statistics.erl193
11 files changed, 962 insertions, 0 deletions
diff --git a/src/shared/include/db_item.hrl b/src/shared/include/db_item.hrl
new file mode 100644
index 0000000..86d5863
--- /dev/null
+++ b/src/shared/include/db_item.hrl
@@ -0,0 +1,15 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ db_item,
+ {
+ id :: any(),
+ perm :: db_user:permission(),
+ val :: any()
+ }
+).
+
+-type db_item() :: #db_item{}.
+
diff --git a/src/shared/include/db_query.hrl b/src/shared/include/db_query.hrl
new file mode 100644
index 0000000..7e5a5b0
--- /dev/null
+++ b/src/shared/include/db_query.hrl
@@ -0,0 +1,63 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ set_field,
+ {
+ field :: non_neg_integer(),
+ value :: any()
+ }
+).
+
+-record
+(
+ add_to_field,
+ {
+ field :: non_neg_integer(),
+ values :: list(any()),
+ head :: boolean()
+ }
+).
+
+-record
+(
+ update_indexed,
+ {
+ field :: non_neg_integer(),
+ ix :: non_neg_integer(),
+ ops :: list(db_query_op())
+ }
+).
+
+-record
+(
+ set_perm,
+ {
+ perm :: db_user:permission()
+ }
+).
+
+-record
+(
+ set_val,
+ {
+ val :: any()
+ }
+).
+
+-record
+(
+ db_query,
+ {
+ db :: atom(),
+ id :: any(),
+ user :: db_user:user(),
+ ops :: list(db_query_master_op())
+ }
+).
+
+-type db_query_op() :: (#set_field{} | #add_to_field{} | #update_indexed{}).
+-type db_query_master_op() :: (db_query_op() | #set_perm{} | #set_val{}).
+-type db_query() :: #db_query{}.
+
diff --git a/src/shared/include/db_user.hrl b/src/shared/include/db_user.hrl
new file mode 100644
index 0000000..c5b033e
--- /dev/null
+++ b/src/shared/include/db_user.hrl
@@ -0,0 +1,7 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-type db_named_user() :: {'user', any()}.
+-type db_user() :: (db_named_user() | 'admin' | 'any').
+-type db_permission() :: (list(db_named_user()) | 'any').
+
diff --git a/src/shared/io/sh_database.erl b/src/shared/io/sh_database.erl
new file mode 100644
index 0000000..a65b784
--- /dev/null
+++ b/src/shared/io/sh_database.erl
@@ -0,0 +1,137 @@
+-module(sh_database).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ generate_db/0,
+ fetch/2,
+ commit/1
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+get_db_node () ->
+ list_to_atom("db_node@" ++ net_adm:localhost()).
+
+-spec generate_random_characters
+ (
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer(),
+ non_neg_integer(),
+ bm_battlemap:type(),
+ list(bm_location:type()),
+ list(bm_character:type())
+ )
+ -> list(bm_character:type()).
+generate_random_characters
+(
+ 0,
+ 0,
+ _CharactersPerPlayer,
+ _TotalCharacterCount,
+ _Battlemap,
+ _ForbiddenLocations,
+ Result
+) ->
+ Result;
+generate_random_characters
+(
+ MaxPlayerID,
+ 0,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Battlemap,
+ ForbiddenLocations,
+ Result
+) ->
+ generate_random_characters
+ (
+ (MaxPlayerID - 1),
+ CharactersPerPlayer,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Battlemap,
+ ForbiddenLocations,
+ Result
+ );
+generate_random_characters
+(
+ MaxPlayerID,
+ PlayerCharacterCount,
+ CharactersPerPlayer,
+ TotalCharacterCount,
+ Battlemap,
+ ForbiddenLocations,
+ Result
+) ->
+ NewCharacter =
+ bm_character:random
+ (
+ TotalCharacterCount,
+ list_to_binary(integer_to_list(MaxPlayerID)),
+ bm_battlemap:get_width(Battlemap),
+ bm_battlemap:get_height(Battlemap),
+ ForbiddenLocations
+ ),
+ Character =
+ case MaxPlayerID of
+ 0 -> bm_character:set_is_active(true, NewCharacter);
+ _ -> NewCharacter
+ end,
+
+ generate_random_characters
+ (
+ MaxPlayerID,
+ (PlayerCharacterCount - 1),
+ CharactersPerPlayer,
+ (TotalCharacterCount + 1),
+ Battlemap,
+ [bm_character:get_location(Character)|ForbiddenLocations],
+ [Character|Result]
+ ).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec generate_db () -> 'ok'.
+generate_db () ->
+ BattlemapWidth = sh_roll:between(16, 64),
+ BattlemapHeight = sh_roll:between(16, 64),
+ Battlemap = bm_battlemap:random(0, BattlemapWidth, BattlemapHeight),
+ Characters = generate_random_characters(1, 8, 8, 0, Battlemap, [], []),
+ PlayersAsList = [bm_player:new(<<"0">>), bm_player:new(<<"1">>)],
+ Battle = bm_battle:new(<<"0">>, PlayersAsList, Battlemap, Characters),
+
+ {atomic, ok} =
+ rpc:call
+ (
+ get_db_node(),
+ storage_access,
+ insert,
+ [battle_db, <<"0">>, any, Battle]
+ ),
+
+ ok.
+
+-spec fetch (atom(), any()) -> ({'ok', any()} | 'not_found').
+fetch (DB, ObjectID) ->
+ {atomic, Reply} =
+ rpc:call(get_db_node(), storage_access, read, [DB, ObjectID]),
+ io:format("~ndb_shim:fetch(~p) -> ~p.~n", [{DB, ObjectID}, Reply]),
+ Reply.
+
+-spec commit (db_query:type()) -> 'ok'.
+commit (Query) ->
+ {atomic, ok} = rpc:call(get_db_node(), storage_access, query, [Query]),
+ io:format("~ndb_shim:commit(~p) -> ok.~n", [Query]),
+ ok.
diff --git a/src/shared/io/sh_timed_cache.erl b/src/shared/io/sh_timed_cache.erl
new file mode 100644
index 0000000..1839992
--- /dev/null
+++ b/src/shared/io/sh_timed_cache.erl
@@ -0,0 +1,130 @@
+-module(sh_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} = sh_gen_server:start(?MODULE, {DB, {Owner, ObjectID}}, []),
+ {ok, Data} = sh_database: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}, sh_timed_caches_manager:get_timeout()}.
+
+handle_call (invalidate, _, State) ->
+ {stop, normal, State};
+handle_call (ping, _, State) ->
+ {noreply, State, sh_timed_caches_manager:get_timeout()}.
+
+handle_cast (invalidate, State) ->
+ {stop, normal, State};
+handle_cast (ping, State) ->
+ {noreply, State, sh_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/shared/io/sh_timed_caches_manager.erl b/src/shared/io/sh_timed_caches_manager.erl
new file mode 100644
index 0000000..7921552
--- /dev/null
+++ b/src/shared/io/sh_timed_caches_manager.erl
@@ -0,0 +1,152 @@
+-module(sh_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/shared/struct/sh_attributes.erl b/src/shared/struct/sh_attributes.erl
new file mode 100644
index 0000000..93dcfc6
--- /dev/null
+++ b/src/shared/struct/sh_attributes.erl
@@ -0,0 +1,108 @@
+-module(sh_attributes).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-record
+(
+ attributes,
+ {
+ constitution :: integer(),
+ dexterity :: integer(),
+ intelligence :: integer(),
+ mind :: integer(),
+ speed :: integer(),
+ strength :: integer()
+ }
+).
+
+-opaque type() :: #attributes{}.
+
+-export_type([type/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 (type()) -> integer().
+get_constitution (Att) -> Att#attributes.constitution.
+
+-spec get_dexterity (type()) -> integer().
+get_dexterity (Att) -> Att#attributes.dexterity.
+
+-spec get_intelligence (type()) -> integer().
+get_intelligence (Att) -> Att#attributes.intelligence.
+
+-spec get_mind (type()) -> integer().
+get_mind (Att) -> Att#attributes.mind.
+
+-spec get_speed (type()) -> integer().
+get_speed (Att) -> Att#attributes.speed.
+
+-spec get_strength (type()) -> integer().
+get_strength (Att) -> Att#attributes.strength.
+
+-spec set_constitution (integer(), type()) -> type().
+set_constitution (Val, Att) -> Att#attributes{ constitution = Val }.
+
+-spec set_dexterity (integer(), type()) -> type().
+set_dexterity (Val, Att) -> Att#attributes{ dexterity = Val }.
+
+-spec set_intelligence (integer(), type()) -> type().
+set_intelligence (Val, Att) -> Att#attributes{ intelligence = Val }.
+
+-spec set_mind (integer(), type()) -> type().
+set_mind (Val, Att) -> Att#attributes{ mind = Val }.
+
+-spec set_speed (integer(), type()) -> type().
+set_speed (Val, Att) -> Att#attributes{ speed = Val }.
+
+-spec set_strength (integer(), type()) -> type().
+set_strength (Val, Att) -> Att#attributes{ strength = Val }.
+
+-spec random () -> type().
+random () ->
+ #attributes
+ {
+ constitution = sh_roll:percentage(),
+ dexterity = sh_roll:percentage(),
+ intelligence = sh_roll:percentage(),
+ mind = sh_roll:percentage(),
+ speed = sh_roll:percentage(),
+ strength = sh_roll:percentage()
+ }.
diff --git a/src/shared/struct/sh_db_item.erl b/src/shared/struct/sh_db_item.erl
new file mode 100644
index 0000000..e499634
--- /dev/null
+++ b/src/shared/struct/sh_db_item.erl
@@ -0,0 +1,70 @@
+-module(sh_db_item).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("../include/db_item.hrl").
+
+-type type() :: db_item().
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export_type([type/0]).
+
+-export
+(
+ [
+ new/3,
+
+ get_id/1,
+ get_permission/1,
+ get_value/1,
+
+ set_permission/2,
+ set_value/2,
+
+ get_id_field/0,
+ get_record_info/0,
+ get_record_name/0
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec new (any(), db_user:permission(), any()) -> type().
+new (ID, Permission, Value) ->
+ #db_item
+ {
+ id = ID,
+ perm = Permission,
+ val = Value
+ }.
+
+-spec get_id (type()) -> any().
+get_id (#db_item { id = Result }) -> Result.
+
+-spec get_permission (type()) -> db_user:permission().
+get_permission (#db_item { perm = Result }) -> Result.
+
+-spec get_value (type()) -> any().
+get_value (#db_item { val = Result }) -> Result.
+
+-spec set_permission (db_user:permission(), type()) -> type().
+set_permission (Perm, Item) -> Item#db_item{ perm = Perm }.
+
+-spec set_value (any(), type()) -> type().
+set_value (Value, Item) -> Item#db_item{ val = Value }.
+
+-spec get_id_field () -> non_neg_integer().
+get_id_field () -> #db_item.id.
+
+get_record_info () -> record_info(fields, db_item).
+
+get_record_name () -> db_item.
+
diff --git a/src/shared/struct/sh_db_query.erl b/src/shared/struct/sh_db_query.erl
new file mode 100644
index 0000000..998568e
--- /dev/null
+++ b/src/shared/struct/sh_db_query.erl
@@ -0,0 +1,59 @@
+-module(sh_db_query).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("../include/db_query.hrl").
+
+-opaque op() :: db_query_op().
+-opaque type() :: db_query().
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export_type([type/0, op/0]).
+
+-export
+(
+ [
+ new/4,
+ set_field/2,
+ add_to_field/2,
+ update_indexed/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec new (atom(), any(), sh_db_user:user(), list(op())) -> type().
+new (DBName, ObjectID, User, Ops) ->
+ #db_query
+ {
+ db = DBName,
+ id = ObjectID,
+ user = User,
+ ops = Ops
+ }.
+
+-spec set_field (non_neg_integer(), any()) -> op().
+set_field (Field, Value) ->
+ #set_field { field = Field, value = Value }.
+
+-spec add_to_field (non_neg_integer(), list(any())) -> op().
+add_to_field (Field, Values) ->
+ #add_to_field { field = Field, values = Values }.
+
+-spec update_indexed
+ (
+ non_neg_integer(),
+ non_neg_integer(),
+ list(op())
+ )
+ -> op().
+update_indexed (Field, IX, Updates) ->
+ #update_indexed { field = Field, ix = IX, ops = Updates}.
diff --git a/src/shared/struct/sh_db_user.erl b/src/shared/struct/sh_db_user.erl
new file mode 100644
index 0000000..40a46d3
--- /dev/null
+++ b/src/shared/struct/sh_db_user.erl
@@ -0,0 +1,28 @@
+-module(sh_db_user).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-include("../include/db_user.hrl").
+
+-type user() :: db_user().
+-type permission() :: db_permission().
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export_type([user/0, permission/0]).
+
+-export([can_access/2]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec can_access (permission(), user()) -> boolean().
+can_access (_, admin) -> true;
+can_access (any, _) -> true;
+can_access (List, {'user', User}) ->
+ lists:member(User, List).
diff --git a/src/shared/struct/sh_statistics.erl b/src/shared/struct/sh_statistics.erl
new file mode 100644
index 0000000..7cf2b06
--- /dev/null
+++ b/src/shared/struct/sh_statistics.erl
@@ -0,0 +1,193 @@
+-module(sh_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 type() :: #statistics{}.
+
+-export_type([type/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 (type()) -> non_neg_integer().
+get_movement_points (Stats) -> Stats#statistics.movement_points.
+
+-spec get_health (type()) -> non_neg_integer().
+get_health (Stats) -> Stats#statistics.health.
+
+-spec get_dodges (type()) -> non_neg_integer().
+get_dodges (Stats) -> Stats#statistics.dodges.
+
+-spec get_parries (type()) -> non_neg_integer().
+get_parries (Stats) -> Stats#statistics.parries.
+
+-spec get_damage_min (type()) -> non_neg_integer().
+get_damage_min (Stats) -> Stats#statistics.damage_min.
+
+-spec get_damage_max (type()) -> non_neg_integer().
+get_damage_max (Stats) -> Stats#statistics.damage_max.
+
+-spec get_accuracy (type()) -> non_neg_integer().
+get_accuracy (Stats) -> Stats#statistics.accuracy.
+
+-spec get_double_hits (type()) -> non_neg_integer().
+get_double_hits (Stats) -> Stats#statistics.double_hits.
+
+-spec get_critical_hits (type()) -> non_neg_integer().
+get_critical_hits (Stats) -> Stats#statistics.critical_hits.
+
+-spec get_damages (type()) -> {non_neg_integer(), non_neg_integer()}.
+get_damages (Stats) ->
+ {
+ Stats#statistics.damage_min,
+ Stats#statistics.damage_max
+ }.
+
+-spec new
+ (
+ attributes:type(),
+ {weapon:id(), weapon:id()}
+ )
+ -> type().
+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))
+ }.