summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/battle/mechanic/action/btl_action_attack.erl')
-rw-r--r--src/battle/mechanic/action/btl_action_attack.erl655
1 files changed, 655 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}.