summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornsensfel <SpamShield0@noot-noot.org>2019-06-05 17:52:58 +0200
committernsensfel <SpamShield0@noot-noot.org>2019-06-05 17:52:58 +0200
commit9e7dcabc164eacb7024387e060623993b48c60b4 (patch)
tree50df2a6143eb2eca18b2588383630c81bb11c1b6 /src/battle/mechanic/action
parent1afb69a11b0e291c7bfd6c24bdd8e55742e61889 (diff)
[Broken] ...
Diffstat (limited to 'src/battle/mechanic/action')
-rw-r--r--src/battle/mechanic/action/btl_action_attack.erl655
-rw-r--r--src/battle/mechanic/action/btl_action_move.erl291
-rw-r--r--src/battle/mechanic/action/btl_action_switch_weapon.erl66
-rw-r--r--src/battle/mechanic/action/btl_turn_actions_opportunity_attack.erl178
4 files changed, 1190 insertions, 0 deletions
diff --git a/src/battle/mechanic/action/btl_action_attack.erl b/src/battle/mechanic/action/btl_action_attack.erl
new file mode 100644
index 0000000..1241735
--- /dev/null
+++ b/src/battle/mechanic/action/btl_action_attack.erl
@@ -0,0 +1,655 @@
+-module(btl_turn_actions_attack).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ handle/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec roll_precision_modifier
+ (
+ shr_statistics:type(),
+ shr_statistics:type(),
+ integer()
+ )
+ -> {float(), integer(), integer()}.
+roll_precision_modifier (Statistics, TargetStatistics, TargetLuck) ->
+ TargetDodges = shr_statistics:get_dodges(TargetStatistics),
+ Accuracy = shr_statistics:get_accuracy(Statistics),
+ MissChance = max(0, (TargetDodges - Accuracy)),
+
+ {Roll, _IsSuccess, PositiveModifier, NegativeModifier} =
+ shr_roll:percentage_with_luck(MissChance, TargetLuck),
+
+ {
+ case Roll of
+ X when (X =< MissChance) -> 0.0;
+ X when (X =< (MissChance * 2)) -> 0.5;
+ _ -> 1.0
+ end,
+ PositiveModifier,
+ NegativeModifier
+ }.
+
+-spec roll_critical_hit_modifier
+ (
+ shr_statistics:type(),
+ integer()
+ )
+ -> {boolean(), integer(), integer()}.
+roll_critical_hit_modifier (Statistics, Luck) ->
+ CriticalHitChance = shr_statistics:get_critical_hits(Statistics),
+ {_Roll, IsSuccess, PositiveModifier, NegativeModifier} =
+ shr_roll:percentage_with_luck(CriticalHitChance, Luck),
+
+ {
+ case IsSuccess of
+ true -> 2.0; % [TODO][FUTURE]: variable critical multiplier?
+ false -> 1.0
+ end,
+ PositiveModifier,
+ NegativeModifier
+ }.
+
+-spec roll_parry
+ (
+ shr_statistics:type(),
+ integer()
+ )
+ -> {boolean(), integer(), integer()}.
+roll_parry (DefenderStatistics, DefenderLuck) ->
+ DefenderParryChance = shr_statistics:get_parries(DefenderStatistics),
+ {_Roll, IsSuccess, PositiveModifier, NegativeModifier} =
+ shr_roll:percentage_with_luck(DefenderParryChance, DefenderLuck),
+
+ {IsSuccess, PositiveModifier, NegativeModifier}.
+
+-spec get_damage
+ (
+ precision(),
+ boolean(),
+ float(),
+ shr_omnimods:type(),
+ shr_omnimods:type()
+ )
+ -> non_neg_integer().
+get_damage
+(
+ Precision,
+ IsCritical,
+ StartingDamageMultiplier,
+ AttackerOmnimods,
+ DefenderOmnimods
+) ->
+ ActualDamageMultiplier =
+ (
+ StartingDamageMultiplier
+ *
+ (
+ case Precision of
+ misses -> 0;
+ grazes -> 0.5;
+ hits -> 1
+ end
+ )
+ *
+ (
+ case IsCritical of
+ true -> 2;
+ _ -> 1
+ end
+ )
+ ),
+
+ ActualDamage =
+ shr_omnimods:get_attack_damage
+ (
+ ActualDamageMultiplier,
+ AttackerOmnimods,
+ DefenderOmnimods
+ ),
+
+ ActualDamage.
+
+-spec get_character_abilities
+ (
+ btl_action:type(),
+ btl_character:type(),
+ btl_character:type()
+ )
+ -> {boolean(), boolean(), boolean()}.
+get_character_abilities (Action, Character, TargetCharacter) ->
+ CharacterWeapon =
+ shr_character:get_active_weapon
+ (
+ btl_character:get_base_character(Character)
+ ),
+
+ TargetCharacterWeapon =
+ shr_character:get_active_weapon
+ (
+ btl_character:get_base_character(TargetCharacter)
+ ),
+
+ DefenseRange = shr_weapon:get_minimum_range(CharacterWeapon),
+ AttackRange = shr_weapon:get_maximum_range(CharacterWeapon),
+ TargetDefenseRange = shr_weapon:get_minimum_range(TargetCharacterWeapon),
+ TargetAttackRange = shr_weapon:get_maximum_range(TargetCharacterWeapon),
+
+ IsNotOpportunistic = btl_action:get_is_opportunistic(Action),
+
+ AttackRange =
+ shr_location:dist
+ (
+ btl_character:get_location(Character),
+ btl_character:get_location(TargetCharacter)
+ ),
+
+ {
+ (DefenseRange == 0),
+ (
+ IsNotOpportunistic
+ and (TargetDefenseRange == 0)
+ and (TargetAttackRange =< AttackRange)
+ ),
+ (
+ IsNotOpportunistic
+ and (TargetAttackRange =< AttackRange)
+ )
+ }.
+
+-spec effect_of_attack
+ (
+ btl_attack:category(),
+ non_neg_integer(),
+ non_neg_integer(),
+ btl_character:type(),
+ btl_character:type(),
+ integer(),
+ integer(),
+ boolean(),
+ btl_character_turn_update:type()
+ )
+ ->
+ {
+ btl_character:type(),
+ btl_character:type(),
+ integer(),
+ integer(),
+ btl_character_turn_update:type(),
+ btl_attack:type()
+ }.
+effect_of_attack
+(
+ Category,
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S0Luck,
+ S0TargetLuck,
+ TargetCanParry,
+ S0Update
+) ->
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ %%%% Roll parry to see if the roles have to be swapped. %%%%%%%%%%%%%%%%%%%%%
+
+ {ParryIsSuccessful, ParryPositiveLuckMod, ParryNegativeLuckMod} =
+ case TargetCanParry of
+ true ->
+ TargetStatistics =
+ shr_character:get_statistics
+ (
+ btl_character:get_base_character(TargetCharacter)
+ ),
+ roll_parry(TargetStatistics, S0TargetLuck);
+
+ false -> {false, 0, 0}
+ end,
+
+ { AttackerIX, DefenderIX, Attacker, Defender, AttackerLuck, DefenderLuck } =
+ case ParryIsSuccessful of
+ true ->
+ {
+ TargetCharacterIX,
+ CharacterIX,
+ TargetCharacter,
+ Character,
+ TargetLuck,
+ Luck
+ };
+
+ false ->
+ {
+ CharacterIX,
+ TargetCharacterIX,
+ Character,
+ TargetCharacter,
+ Luck,
+ TargetLuck
+ }
+ end,
+
+ AttackerStatistics =
+
+-spec handle_attack_sequence
+ (
+ list({btl_attack:category(), boolean()}),
+ non_neg_integer(),
+ non_neg_integer(),
+ btl_character:type(),
+ btl_character:type(),
+ integer(),
+ integer(),
+ list(btl_attack:type()),
+ btl_character_turn_update:type()
+ )
+ ->
+ {
+ btl_character:type(),
+ btl_character:type(),
+ integer(),
+ integer(),
+ list(btl_attack:type()),
+ btl_character_turn_update:type()
+ }.
+handle_attack_sequence
+(
+ [],
+ _CharacterIX,
+ _TargetCharacterIX,
+ Character,
+ TargetCharacter,
+ PlayerLuck,
+ TargetPlayerLuck,
+ Results,
+ Update
+)
+->
+ {
+ Character,
+ TargetCharacter,
+ PlayerLuck,
+ TargetPlayerLuck,
+ lists:reverse(Results),
+ Update
+ };
+handle_attack_sequence
+(
+ [{first, TargetCanParry}|NextAttacks],
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S0PlayerLuck,
+ S0TargetPlayerLuck,
+ Results,
+ S0Update
+)
+->
+ {
+ S1Character,
+ S1TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ S1Update,
+ Result
+ } =
+ effect_of_attack
+ (
+ first,
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S0PlayerLuck,
+ S0TargetPlayerLuck,
+ TargetCanParry,
+ S0Update
+ ),
+
+ handle_attack_sequence
+ (
+ NextAttacks,
+ CharacterIX,
+ TargetCharacterIX,
+ S1Character,
+ S1TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ [Result|Results],
+ S1Update
+ );
+handle_attack_sequence
+(
+ [{counter, CanParry}|NextAttacks],
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S0PlayerLuck,
+ S0TargetPlayerLuck,
+ Results,
+ S0Update
+)
+->
+ {
+ S1TargetCharacter,
+ S1Character,
+ S2TargetPlayerLuck,
+ S2PlayerLuck,
+ S1Update,
+ Result
+ } =
+ effect_of_attack
+ (
+ counter,
+ TargetCharacterIX,
+ CharacterIX,
+ S0TargetCharacter,
+ S0Character,
+ S1TargetPlayerLuck,
+ S1PlayerLuck,
+ CanParry,
+ S0Update
+ ),
+
+ handle_attack_sequence
+ (
+ NextAttacks,
+ CharacterIX,
+ TargetCharacterIX,
+ S1Character,
+ S1TargetCharacter,
+ S2PlayerLuck,
+ S2TargetPlayerLuck,
+ [Result|Results],
+ S1Update
+ );
+handle_attack_sequence
+(
+ [{second, TargetCanParry}|NextAttacks],
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S0PlayerLuck,
+ S0TargetPlayerLuck,
+ Results,
+ S0Update
+)
+->
+ Statistics = shr_character:get_statistics(S0Character),
+ DoubleAttackChance = shr_statistics:get_double_hits(Statistics),
+ {_Roll, IsSuccessful, PositiveModifier, NegativeModifier} =
+ shr_roll:percentage_with_luck(DoubleAttackChance, S0PlayerLuck),
+
+ S1PlayerLuck = (S0PlayerLuck + PositiveModifier),
+ S1TargetPlayerLuck = (S0TargetPlayerLuck + NegativeModifier),
+
+ case IsSuccessful of
+ false ->
+ handle_attack_sequence
+ (
+ NextAttacks,
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ Results,
+ S0Update
+ );
+
+ true ->
+ {
+ S1Character,
+ S1TargetCharacter,
+ S2PlayerLuck,
+ S2TargetPlayerLuck,
+ S1Update,
+ Result
+ } =
+ effect_of_attack
+ (
+ second,
+ CharacterIX,
+ TargetCharacterIX,
+ S0Character,
+ S0TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ TargetCanParry,
+ S0Update
+ ),
+
+ handle_attack_sequence
+ (
+ CharacterIX,
+ TargetCharacterIX,
+ NextAttacks,
+ S1Character,
+ S1TargetCharacter,
+ S2PlayerLuck,
+ S2TargetPlayerLuck,
+ [Result|Results],
+ S1Update
+ )
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec handle
+ (
+ btl_action:type(),
+ btl_character:type(),
+ btl_character_turn_update:type()
+ )
+ -> {ok, btl_character_turn_update:type()}.
+handle (Action, S0Character, S0Update) ->
+ S0Battle = btl_character_turn_update:get_battle(S0Update),
+ CharacterIX = btl_action:get_actor_index(Action),
+
+ PlayerIX = btl_character:get_player_index(S0Character),
+ Player = btl_battle:get_player(PlayerIX, S0Battle),
+ S0PlayerLuck = btl_player:get_luck(Player),
+
+ TargetCharacterIX = btl_action:get_target_index(Action),
+ {S0TargetCharacter, S1Battle} =
+ btl_battle:get_resolved_character(TargetCharacterIX, S0Battle),
+
+ TargetPlayerIX = btl_character:get_player_index(TargetCharacter),
+ TargetPlayer = btl_battle:get_player(TargetPlayerIX, S1Battle),
+ TargetPlayerLuck = btl_player:get_luck(TargetPlayer),
+
+ {CanParry, TargetCanParry, TargetCanCounter} =
+ get_character_abilities(Action, S0Character, S0TargetCharacter),
+
+ {
+ S1Character,
+ S1TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ Results,
+ S1Update
+ } =
+ handle_attack_sequence
+ (
+ case TargetCanCounter of
+ true ->
+ [
+ {first, TargetCanParry},
+ {counter, CanParry},
+ {second, TargetCanParry}
+ ];
+
+ false ->
+ [
+ {first, TargetCanParry},
+ {second, TargetCanParry}
+ ]
+ end,
+ S1Character,
+ S1TargetCharacter,
+ S1PlayerLuck,
+ S1TargetPlayerLuck,
+ Results,
+ S1Update
+ ),
+
+ {
+ AttackEffects,
+ RemainingAttackerHealth,
+ RemainingDefenderHealth,
+ NewAttackerLuck,
+ NewDefenderLuck
+ } =
+ handle_attack_sequence
+ (
+ Character,
+ btl_character:get_current_health(Character),
+ TargetCharacter,
+ btl_character:get_current_health(TargetCharacter),
+ PlayerLuck,
+ TargetPlayerLuck,
+ AttackSequence,
+ []
+ ),
+
+ S0NewAttackerLuck =
+ case {(NewAttackerLuck =< -2), (NewAttackerLuck >= 2)} of
+ {true, _} -> (NewAttackerLuck + 2);
+ {_, true} -> (NewAttackerLuck - 2);
+ _ -> 0
+ end,
+
+ S0NewDefenderLuck =
+ case {(NewDefenderLuck =< -2), (NewDefenderLuck >= 2)} of
+ {true, _} -> (NewDefenderLuck + 2);
+ {_, true} -> (NewDefenderLuck - 2);
+ _ -> 0
+ end,
+
+ {UpdatedAttackingPlayer, AttackingPlayerAtaxiaUpdate} =
+ btl_player:ataxia_set_luck(S0NewAttackerLuck, AttackingPlayer),
+
+ {UpdatedDefendingPlayer, DefendingPlayerAtaxiaUpdate} =
+ btl_player:ataxia_set_luck(S0NewDefenderLuck, DefendingPlayer),
+
+ {UpdatedCharacter, CharacterAtaxiaUpdate} =
+ btl_character:ataxia_set_current_health
+ (
+ RemainingAttackerHealth,
+ Character
+ ),
+
+ {UpdatedTargetCharacterRef, TargetCharacterRefAtaxiaUpdate} =
+ btl_character:ataxia_set_current_health
+ (
+ RemainingDefenderHealth,
+ TargetCharacterRef
+ ),
+
+ {S0Battle, BattleAtaxiaUpdate0} =
+ btl_battle:ataxia_set_player
+ (
+ AttackingPlayerIX,
+ UpdatedAttackingPlayer,
+ AttackingPlayerAtaxiaUpdate,
+ Battle
+ ),
+
+ {S1Battle, BattleAtaxiaUpdate1} =
+ btl_battle:ataxia_set_player
+ (
+ DefendingPlayerIX,
+ UpdatedDefendingPlayer,
+ DefendingPlayerAtaxiaUpdate,
+ S0Battle
+ ),
+
+ {S2Battle, BattleAtaxiaUpdate2} =
+ btl_battle:ataxia_set_character
+ (
+ TargetIX,
+ UpdatedTargetCharacterRef,
+ TargetCharacterRefAtaxiaUpdate,
+ S1Battle
+ ),
+
+ % Potential danger ahead: we're going to update both the 'character' and
+ % 'battle' members of a btl_character_turn_update.
+ % 'S1Update' is sure to have both up to date (as it's the result of 'get'
+ % requests for both) and there is no risk of the 'battle' update influencing
+ % 'character', making what follows safe.
+
+ S2Update =
+ btl_character_turn_update:ataxia_set_battle
+ (
+ S2Battle,
+ false,
+ ataxic:optimize
+ (
+ ataxic:sequence
+ (
+ [
+ BattleAtaxiaUpdate0,
+ BattleAtaxiaUpdate1,
+ BattleAtaxiaUpdate2
+ ]
+ )
+ ),
+ S1Update
+ ),
+
+ S3Update =
+ btl_character_turn_update:ataxia_set_character
+ (
+ UpdatedCharacter,
+ CharacterAtaxiaUpdate,
+ S2Update
+ ),
+
+ TimelineItem =
+ btl_turn_result:new_character_attacked
+ (
+ btl_character_turn_update:get_character_ix(S3Update),
+ TargetIX,
+ AttackEffects,
+ S0NewAttackerLuck,
+ S0NewDefenderLuck
+ ),
+
+ S4Update = btl_character_turn_update:add_to_timeline(TimelineItem, S3Update),
+
+ S5Update =
+ case (RemainingAttackerHealth > 0) of
+ true -> S4Update;
+ false ->
+ btl_victory_progression:handle_character_loss(Character, S4Update)
+ end,
+
+ S6Update =
+ case (RemainingDefenderHealth > 0) of
+ true -> S5Update;
+ false ->
+ btl_victory_progression:handle_character_loss
+ (
+ TargetCharacterRef,
+ S5Update
+ )
+ end,
+
+ {ok, S6Update}.
diff --git a/src/battle/mechanic/action/btl_action_move.erl b/src/battle/mechanic/action/btl_action_move.erl
new file mode 100644
index 0000000..a32a40f
--- /dev/null
+++ b/src/battle/mechanic/action/btl_action_move.erl
@@ -0,0 +1,291 @@
+-module(btl_action_move).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ handle/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec cross
+ (
+ non_neg_integer(),
+ shr_map:type(),
+ list(shr_location:type()),
+ list(shr_direction:enum()),
+ non_neg_integer(),
+ shr_location:type()
+ )
+ ->
+ {
+ shr_location:type(),
+ list(shr_direction:type()),
+ non_neg_integer(),
+ list(shr_map_marker:type())
+ }.
+cross (_PlayerIX, _Map, _ForbiddenLocations, [], Cost, Location) ->
+ {Location, [], Cost, []};
+cross (PlayerIX, Map, ForbiddenLocations, [Step|NextSteps], Cost, Location) ->
+ NextLocation = shr_location:apply_direction(Step, Location),
+ NextTileInstance = shr_map:get_tile_instance(NextLocation, Map),
+ NextTileClassID = shr_tile_instance:get_tile_id(NextTileInstance),
+ NextTile = shr_tile:from_id(NextTileClassID),
+ NextCost = (Cost + shr_tile:get_cost(NextTile)),
+ IsForbidden =
+ lists:foldl
+ (
+ fun (ForbiddenLocation, Prev) ->
+ (Prev or (NextLocation == ForbiddenLocation))
+ end,
+ false,
+ ForbiddenLocations
+ ),
+
+ false = IsForbidden,
+
+ Interruptions =
+ list:foldl
+ (
+ fun (MarkerName, CurrentInterruptions) ->
+ case shr_map:get_marker(MarkerName, Map) of
+ {ok, Marker} ->
+ case shr_map_marker:interrupts_movement(PlayerIX, Marker) of
+ true -> [Marker|CurrentInterruptions];
+ _ -> CurrentInterruptions
+ end;
+
+ error ->
+ %% TODO: Error.
+ CurrentInterruptions
+ end
+ end,
+ [],
+ shr_tile_instance:get_triggers(NextTileInstance)
+ ),
+
+ case Interruptions of
+ [] ->
+ cross
+ (
+ PlayerIX,
+ Map,
+ ForbiddenLocations,
+ NextSteps,
+ NextCost,
+ NextLocation
+ );
+
+ _ -> {NextLocation, NextSteps, NextCost, Interruptions}
+ end.
+
+-spec cross
+ (
+ non_neg_integer(),
+ shr_map:type(),
+ list(shr_location:type()),
+ list(shr_direction:enum()),
+ shr_location:type()
+ )
+ ->
+ {
+ shr_location:type(),
+ list(shr_direction:type()),
+ non_neg_integer(),
+ list(shr_map_marker:type())
+ }.
+cross (PlayerIX, Map, ForbiddenLocations, Path, Location) ->
+ cross(PlayerIX, Map, ForbiddenLocations, Path, 0, Location).
+
+-spec get_path_cost_and_destination
+ (
+ non_neg_integer(),
+ btl_character:type(),
+ btl_character_turn_update:type(),
+ list(shr_direction:type())
+ )
+ ->
+ {
+ non_neg_integer(),
+ shr_location:type(),
+ list(shr_direction:type()),
+ list(shr_map_marker:type())
+ }.
+get_path_cost_and_destination (CharacterIX, Character, Update, Path) ->
+ Battle = btl_character_turn_update:get_battle(Update),
+ Map = btl_battle:get_map(Battle),
+
+ % [TODO][OPTIMIZATION] Redundant calculations.
+ % This is recalculated at every move action, despite there be no need
+ % to: The client will not allow the character to go somewhere that would
+ % only be freed because of an event.
+ ForbiddenLocations =
+ orddict:fold
+ (
+ fun (IX, Char, Prev) ->
+ IsAlive = btl_character:get_is_alive(Char),
+ if
+ (IX == CharacterIX) -> Prev;
+ (not IsAlive) -> Prev;
+ true ->
+ ordsets:add_element(btl_character:get_location(Char), Prev)
+ end
+ end,
+ ordsets:new(),
+ btl_battle:get_characters(Battle)
+ ),
+
+ {NewLocation, RemainingPath, Cost, Interruptions} =
+ cross
+ (
+ btl_character:get_player_index(Character),
+ Map,
+ ForbiddenLocations,
+ Path,
+ btl_character:get_location(Character)
+ ),
+
+ {Cost, NewLocation, RemainingPath, Interruptions}.
+
+-spec get_movement_points
+ (
+ btl_action:type(),
+ btl_character:type()
+ )
+ -> non_neg_integer().
+get_movement_points (Action, Character) ->
+ case btl_action:get_movement_points(Action) of
+ -1 ->
+ shr_statistics:get_movement_points
+ (
+ shr_character:get_statistics
+ (
+ btl_character:get_base_character(Character)
+ )
+ );
+
+ Other -> Other
+ end.
+
+-spec commit_move
+ (
+ non_neg_integer(),
+ btl_character:type(),
+ btl_character_turn_update:type(),
+ list(shr_direction:type()),
+ shr_location:type()
+ )
+ -> btl_character_turn_update:type().
+commit_move (CharacterIX, Character, S0Update, Path, NewLocation) ->
+ S0Battle = btl_character_turn_update:get_battle(S0Update),
+
+ Map = btl_battle:get_map(S0Battle),
+
+ TileOmnimods =
+ shr_tile:get_omnimods
+ (
+ shr_tile:from_id
+ (
+ shr_tile_instance:get_tile_id
+ (
+ shr_map:get_tile_instance(NewLocation, Map)
+ )
+ )
+ ),
+
+ {UpdatedCharacter, CharacterAtaxiaUpdate} =
+ btl_character:ataxia_set_location(NewLocation, TileOmnimods, Character),
+
+ {UpdatedBattle, BattleAtaxiaUpdate} =
+ btl_battle:ataxia_set_character
+ (
+ CharacterIX,
+ UpdatedCharacter,
+ CharacterAtaxiaUpdate,
+ S0Battle
+ ),
+
+ TimelineItem =
+ btl_turn_result:new_character_moved(CharacterIX, Path, NewLocation),
+
+ S1Update = btl_character_turn_update:add_to_timeline(TimelineItem, S0Update),
+ S2Update =
+ btl_character_turn_update:ataxia_set_battle
+ (
+ UpdatedBattle,
+ BattleAtaxiaUpdate,
+ S1Update
+ ),
+
+ S2Update.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec handle
+ (
+ btl_action:type(),
+ btl_character:type(),
+ btl_character_turn_update:type()
+ )
+ ->
+ (
+ {'ok', btl_character_turn_update:type()}
+ | {'events', list(btl_action:type()), btl_character_turn_update:type()}
+ ).
+handle (Action, Character, S0Update) ->
+ Path = btl_action:get_path(Action),
+ CharacterIX = btl_action:get_actor_index(Action),
+
+ {PathCost, NewLocation, RemainingPath, Interruptions} =
+ get_path_cost_and_destination(CharacterIX, Character, S0Update, Path),
+
+ MovementPoints = get_movement_points(Action, Character),
+
+ true = (MovementPoints >= PathCost),
+
+ S1Update = commit_move(CharacterIX, Character, S0Update, Path, NewLocation),
+
+ case RemainingPath of
+ [] -> {ok, S1Update};
+ _ ->
+ {events,
+ (
+ lists:foldl
+ (
+ fun (Marker, CurrentActions) ->
+ (
+ btl_action:from_map_marker
+ (
+ CharacterIX,
+ Character,
+ Marker
+ )
+ ++
+ CurrentActions
+ )
+ end,
+ [],
+ Interruptions
+ )
+ ++
+ [
+ btl_action:new_move
+ (
+ CharacterIX,
+ RemainingPath,
+ (MovementPoints - PathCost)
+ )
+ ]
+ ),
+ S1Update
+ }
+ end.
diff --git a/src/battle/mechanic/action/btl_action_switch_weapon.erl b/src/battle/mechanic/action/btl_action_switch_weapon.erl
new file mode 100644
index 0000000..cf1a31a
--- /dev/null
+++ b/src/battle/mechanic/action/btl_action_switch_weapon.erl
@@ -0,0 +1,66 @@
+-module(btl_turn_actions_switch_weapon).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ handle/3
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec handle
+ (
+ btl_action:type(),
+ btl_character:type(),
+ btl_character_turn_update:type()
+ )
+ -> {'ok', btl_character_turn_update:type()}.
+handle (Action, Character, S0Update) ->
+ CharacterIX = btl_action:get_actor_index(Action),
+
+ BaseCharacter = btl_character:get_base_character(Character),
+
+ {UpdatedBaseCharacter, BaseCharacterAtaxiaUpdate} =
+ shr_character:ataxia_switch_weapons(BaseCharacter),
+
+ {UpdatedCharacter, CharacterAtaxiaUpdate} =
+ btl_character:ataxia_set_base_character
+ (
+ UpdatedBaseCharacter,
+ BaseCharacterAtaxiaUpdate,
+ Character
+ ),
+
+ {UpdatedBattle, BattleAtaxiaUpdate} =
+ btl_battle:ataxia_set_character
+ (
+ CharacterIX,
+ UpdatedCharacter,
+ CharacterAtaxiaUpdate,
+ btl_character_turn_update:get_battle(S0Update)
+ ),
+
+ TimelineItem = btl_turn_result:new_character_switched_weapons(CharacterIX),
+
+ S1Update = btl_character_turn_update:add_to_timeline(TimelineItem, S0Update),
+ S2Update =
+ btl_character_turn_update:ataxia_set_battle
+ (
+ UpdatedBattle,
+ BattleAtaxiaUpdate,
+ S1Update
+ ),
+
+ {ok, S2Update}.
diff --git a/src/battle/mechanic/action/btl_turn_actions_opportunity_attack.erl b/src/battle/mechanic/action/btl_turn_actions_opportunity_attack.erl
new file mode 100644
index 0000000..c1dbbdd
--- /dev/null
+++ b/src/battle/mechanic/action/btl_turn_actions_opportunity_attack.erl
@@ -0,0 +1,178 @@
+-module(btl_turn_actions_opportunity_attack).
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% TYPES %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-export
+(
+ [
+ handle/2
+ ]
+).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% LOCAL FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% EXPORTED FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-spec handle
+ (
+ btl_action:type(),
+ btl_character_turn_update:type()
+ )
+ -> {ok, btl_character_turn_update:type()}.
+handle (BattleAction, Update) ->
+ {S0Update, Battle} = btl_character_turn_update:get_battle(Update),
+ {S1Update, Character} = btl_character_turn_update:get_character(S0Update),
+
+ DefendingPlayerIX = btl_character:get_player_index(Character),
+ DefendingPlayer = btl_battle:get_player(DefendingPlayerIX, Battle),
+ DefendingPlayerLuck = btl_player:get_luck(DefendingPlayer),
+
+ AttackerIX = btl_action:get_target_ix(BattleAction),
+ AttackerRef = btl_battle:get_character(AttackerIX, Battle),
+ Attacker = btl_battle:resolve_character(AttackerRef, Battle),
+
+ AttackingPlayerIX = btl_character:get_player_index(Attacker),
+ AttackingPlayer = btl_battle:get_player(AttackingPlayerIX, Battle),
+ AttackingPlayerLuck = btl_player:get_luck(AttackingPlayer),
+
+ Attack = btl_attack:attack_of_opportunity(),
+
+ AttackEffect =
+ btl_attack:get_description_of
+ (
+ Attack,
+ btl_character:get_base_character(Character),
+ btl_character:get_base_character(Attacker),
+ AttackingPlayerLuck,
+ DefendingPlayerLuck
+ ),
+
+ {
+ AttackResult,
+ NewAttackerHealth,
+ S0NewAttackerLuck,
+ NewDefenderHealth,
+ S0NewDefenderLuck
+ } =
+ btl_attack:apply_to_healths_and_lucks
+ (
+ AttackEffect,
+ btl_character:get_current_health(Attacker),
+ AttackingPlayerLuck,
+ btl_character:get_current_health(Character),
+ DefendingPlayerLuck
+ ),
+
+ S1NewAttackerLuck =
+ case {(S0NewAttackerLuck =< -2), (S0NewAttackerLuck >= 2)} of
+ {true, _} -> (S0NewAttackerLuck + 2);
+ {_, true} -> (S0NewAttackerLuck - 2);
+ _ -> 0
+ end,
+
+ S1NewDefenderLuck =
+ case {(S0NewDefenderLuck =< -2), (S0NewDefenderLuck >= 2)} of
+ {true, _} -> (S0NewDefenderLuck + 2);
+ {_, true} -> (S0NewDefenderLuck - 2);
+ _ -> 0
+ end,
+
+ {UpdatedAttackingPlayer, AttackingPlayerAtaxiaUpdate} =
+ btl_player:ataxia_set_luck(S1NewAttackerLuck, AttackingPlayer),
+
+ {UpdatedDefendingPlayer, DefendingPlayerAtaxiaUpdate} =
+ btl_player:ataxia_set_luck(S1NewDefenderLuck, DefendingPlayer),
+
+ {UpdatedCharacter, CharacterAtaxiaUpdate} =
+ btl_character:ataxia_set_current_health(NewDefenderHealth, Character),
+
+ {UpdatedAttackerRef, AttackerRefAtaxiaUpdate} =
+ btl_character:ataxia_set_current_health(NewAttackerHealth, AttackerRef),
+
+ {S0Battle, BattleAtaxiaUpdate0} =
+ btl_battle:ataxia_set_player
+ (
+ AttackingPlayerIX,
+ UpdatedAttackingPlayer,
+ AttackingPlayerAtaxiaUpdate,
+ Battle
+ ),
+
+ {S1Battle, BattleAtaxiaUpdate1} =
+ btl_battle:ataxia_set_player
+ (
+ DefendingPlayerIX,
+ UpdatedDefendingPlayer,
+ DefendingPlayerAtaxiaUpdate,
+ S0Battle
+ ),
+
+ {S2Battle, BattleAtaxiaUpdate2} =
+ btl_battle:ataxia_set_character
+ (
+ AttackerIX,
+ UpdatedAttackerRef,
+ AttackerRefAtaxiaUpdate,
+ S1Battle
+ ),
+
+ % Potential danger ahead: we're going to update both the 'character' and
+ % 'battle' members of a btl_character_turn_update.
+ % 'S1Update' is sure to have both up to date (as it's the result of 'get'
+ % requests for both) and there is no risk of the 'battle' update influencing
+ % 'character', making what follows safe.
+
+ S2Update =
+ btl_character_turn_update:ataxia_set_battle
+ (
+ S2Battle,
+ false,
+ ataxic:optimize
+ (
+ ataxic:sequence
+ (
+ [
+ BattleAtaxiaUpdate0,
+ BattleAtaxiaUpdate1,
+ BattleAtaxiaUpdate2
+ ]
+ )
+ ),
+ S1Update
+ ),
+
+ S3Update =
+ btl_character_turn_update:ataxia_set_character
+ (
+ UpdatedCharacter,
+ CharacterAtaxiaUpdate,
+ S2Update
+ ),
+
+ TimelineItem =
+ btl_turn_result:new_character_attacked
+ (
+ AttackerIX,
+ btl_character_turn_update:get_character_ix(S3Update),
+ AttackResult,
+ S0NewAttackerLuck,
+ S0NewDefenderLuck
+ ),
+
+ S4Update = btl_character_turn_update:add_to_timeline(TimelineItem, S3Update),
+
+ S5Update =
+ case (NewDefenderHealth > 0) of
+ true -> S4Update;
+ false ->
+ btl_victory_progression:handle_character_loss(Character, S4Update)
+ end,
+
+ {ok, S5Update}.