summaryrefslogtreecommitdiff |
diff options
Diffstat (limited to 'src/battle/mechanic/action/btl_action_attack.erl')
-rw-r--r-- | src/battle/mechanic/action/btl_action_attack.erl | 655 |
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}. |