From fe518f6f0b2ba62a460c091e4b1049415ce63cd1 Mon Sep 17 00:00:00 2001 From: Martin Janiczek Date: Wed, 16 Oct 2024 21:24:38 +0200 Subject: [PATCH] Revamp critical chance --- src/Data/Fight/Generator.elm | 8 ++- src/Data/Perk.elm | 21 ++++++- src/Frontend/News.elm | 2 - src/Logic.elm | 106 +++++++++++++++++++++++++++-------- tests/LogicTest.elm | 72 ++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 30 deletions(-) diff --git a/src/Data/Fight/Generator.elm b/src/Data/Fight/Generator.elm index 26a47b1..71be633 100644 --- a/src/Data/Fight/Generator.elm +++ b/src/Data/Fight/Generator.elm @@ -1170,9 +1170,11 @@ attack_ who ongoing attackStyle baseApCost = baseCriticalChance = Logic.baseCriticalChance { special = opponent.special - , hasFinesseTrait = Trait.isSelected Trait.Finesse opponent.traits - , moreCriticalPerkRanks = Perk.rank Perk.MoreCriticals opponent.perks - , hasSlayerPerk = Perk.rank Perk.Slayer opponent.perks > 0 + , perks = opponent.perks + , traits = opponent.traits + , attackStyle = attackStyle + , chanceToHit = chance + , hitOrMissRoll = roll } attackStatsCriticalChanceBonus : Int diff --git a/src/Data/Perk.elm b/src/Data/Perk.elm index 77b2b12..ed41e53 100644 --- a/src/Data/Perk.elm +++ b/src/Data/Perk.elm @@ -101,7 +101,7 @@ type Perk -- lvl 18 -- TODO SilentDeath -- needs sneaking in combat -- lvl 24 - -- TODO Sniper -- needs ranged weapons + | Sniper | Slayer -- special | GeckoSkinning @@ -146,6 +146,7 @@ all = , Pathfinder , Ranger , Salesman + , Sniper , Slayer , Speaker , Survivalist @@ -214,6 +215,9 @@ name perk = GainLuck -> "Gain Luck" + Sniper -> + "Sniper" + Slayer -> "Slayer" @@ -427,6 +431,9 @@ maxRank perk = BonusRateOfFire -> 1 + Sniper -> + 1 + Slayer -> 1 @@ -498,6 +505,9 @@ encode perk = GainLuck -> "gain-luck" + Sniper -> + "sniper" + Slayer -> "slayer" @@ -643,6 +653,9 @@ decoder = "gain-luck" -> JD.succeed GainLuck + "sniper" -> + JD.succeed Sniper + "slayer" -> JD.succeed Slayer @@ -817,6 +830,9 @@ isApplicableForLevelup r perk = GainLuck -> r.level >= 12 && s.luck < 10 + Sniper -> + r.level >= 24 && s.agility >= 8 && s.perception >= 8 && skill Skill.SmallGuns >= 80 + Slayer -> r.level >= 24 && s.agility >= 8 && s.strength >= 8 && skill Skill.Unarmed >= 80 @@ -1009,6 +1025,9 @@ description perk = Salesman -> "You are an adept salesperson. With this Perk you gain +20% towards your Barter skill." + Sniper -> + "You have mastered the firearm as a source of pain. This perk gives you an increased chance to score a critical hit with ranged weapons." + Slayer -> "The Slayer walks the earth! In hand-to-hand combat all of your hits are upgraded to critical hits, causing destruction and mayhem." diff --git a/src/Frontend/News.elm b/src/Frontend/News.elm index 5c6af33..711bcc5 100644 --- a/src/Frontend/News.elm +++ b/src/Frontend/News.elm @@ -30,10 +30,8 @@ items = - Weapon/ammo part of item loot of enemies? - Fight Strategy: number of available ammo - Fight Strategy: walk away -- Fight Strategy: warning for distance <=0: min distance is 1 - refactoring: fight Opponents shouldn't hold attackStats and naturalArmorClass? - make movement on the map challenging (random encounters, fights you can't skip, have to heal...) -- logic: Are we adding bonus to Critical Chance based on AimedShot? - regain conciousness in fight (cost 1/2 of max AP) ~janiczek diff --git a/src/Logic.elm b/src/Logic.elm index 8e1b77a..8d33ab3 100644 --- a/src/Logic.elm +++ b/src/Logic.elm @@ -32,6 +32,7 @@ module Logic exposing , playerCombatCapsGained , playerCombatXpGained , price + , regainConciousnessApCost , sequence , skillPointCost , skillPointsPerLevel @@ -252,6 +253,11 @@ aimedShotApCostPenalty = 1 +aimedShotCriticalChanceBonus : AimedShot -> Int +aimedShotCriticalChanceBonus aimedShot = + aimedShotChanceToHitPenalty aimedShot + + aimedShotChanceToHitPenalty : AimedShot -> Int aimedShotChanceToHitPenalty aimedShot = case aimedShot of @@ -962,51 +968,103 @@ meleeAttackStats r = baseCriticalChance : { special : Special - , hasFinesseTrait : Bool - , moreCriticalPerkRanks : Int - , hasSlayerPerk : Bool + , traits : SeqSet Trait + , perks : SeqDict Perk Int + , attackStyle : AttackStyle + , chanceToHit : Int + , hitOrMissRoll : Int } -> Int baseCriticalChance r = - -- TODO sniper perk and non-unarmed combat - unarmedBaseCriticalChance r - - -unarmedBaseCriticalChance : - { special : Special - , hasFinesseTrait : Bool - , moreCriticalPerkRanks : Int - , hasSlayerPerk : Bool - } - -> Int -unarmedBaseCriticalChance r = let + fromChanceToHit : Int + fromChanceToHit = + max 0 ((r.chanceToHit - r.hitOrMissRoll) // 10) + + fromSpecial : Int fromSpecial = r.special.luck + fromFinesse : Int fromFinesse = - if r.hasFinesseTrait then + if SeqSet.member Trait.Finesse r.traits then 10 else 0 + fromMoreCriticals : Int fromMoreCriticals = - r.moreCriticalPerkRanks * 5 + 5 * Perk.rank Perk.MoreCriticals r.perks + fromSlayer : Int fromSlayer = - if r.hasSlayerPerk then + if Perk.rank Perk.Slayer r.perks > 0 then 100 else 0 + + fromSniper : Int + fromSniper = + if Perk.rank Perk.Sniper r.perks > 0 then + -- instead of fromSpecial 1% per Luck point, we get 10% per Luck point. + r.special.luck * 9 + + else + 0 + + fromAimedShot : AimedShot -> Int + fromAimedShot aim = + aimedShotCriticalChanceBonus aim + + unaimed : Int -> Int + unaimed fromEndgamePerk = + (fromChanceToHit + + fromSpecial + + fromFinesse + + fromMoreCriticals + + fromEndgamePerk + ) + |> min 95 + + aimed : AimedShot -> Int -> Int + aimed aim fromEndgamePerk = + (fromChanceToHit + + fromSpecial + + fromFinesse + + fromMoreCriticals + + fromAimedShot aim + + fromEndgamePerk + ) + |> min 95 in - (fromSpecial - + fromFinesse - + fromMoreCriticals - + fromSlayer - ) - |> min 100 + case r.attackStyle of + -- Slayer's 100% wins over the rest + UnarmedUnaimed -> + max fromSlayer (unaimed 0) + + UnarmedAimed aim -> + max fromSlayer (aimed aim 0) + + MeleeUnaimed -> + max fromSlayer (unaimed 0) + + MeleeAimed aim -> + max fromSlayer (aimed aim 0) + + -- Sniper is capped to 95% + Throw -> + unaimed fromSniper + + ShootSingleUnaimed -> + unaimed fromSniper + + ShootSingleAimed aim -> + aimed aim fromSniper + + ShootBurst -> + unaimed fromSniper price : diff --git a/tests/LogicTest.elm b/tests/LogicTest.elm index 7668dce..91945e3 100644 --- a/tests/LogicTest.elm +++ b/tests/LogicTest.elm @@ -1,5 +1,6 @@ module LogicTest exposing (test) +import Data.Fight.AimedShot as AimedShot import Data.Fight.AttackStyle as AttackStyle exposing (AttackStyle(..)) import Data.Item as Item exposing (Item) import Data.Item.Kind as ItemKind @@ -21,10 +22,53 @@ test : Test test = Test.describe "Logic" [ chanceToHitSuite + , baseCriticalChanceSuite , attackStatsSuite ] +baseCriticalChanceSuite : Test +baseCriticalChanceSuite = + Test.describe "baseCriticalChance" + [ Test.fuzz2 baseCriticalChanceArgsFuzzer + (Fuzz.oneOfValues + [ AttackStyle.UnarmedUnaimed + , AttackStyle.UnarmedAimed AimedShot.Eyes + , AttackStyle.MeleeUnaimed + , AttackStyle.MeleeAimed AimedShot.Torso + ] + ) + "Slayer always gets 100% if unarmed/melee" + <| + \args attackStyle -> + Logic.baseCriticalChance + { args + | perks = args.perks |> SeqDict.insert Perk.Slayer 1 + , attackStyle = attackStyle + } + |> Expect.equal 100 + , Test.fuzz2 baseCriticalChanceArgsFuzzer + (Fuzz.oneOfValues + [ AttackStyle.Throw + , AttackStyle.ShootSingleUnaimed + , AttackStyle.ShootSingleAimed AimedShot.Eyes + , AttackStyle.ShootSingleAimed AimedShot.Torso + , AttackStyle.ShootBurst + ] + ) + "Sniper always gets 95% if Luck is 10" + <| + \args attackStyle -> + Logic.baseCriticalChance + { args + | perks = args.perks |> SeqDict.insert Perk.Sniper 1 + , attackStyle = attackStyle + , special = args.special |> Special.set Special.Luck 10 + } + |> Expect.equal 95 + ] + + attackStatsSuite : Test attackStatsSuite = Test.describe "attackStats" @@ -282,3 +326,31 @@ chanceToHitArgsFuzzer = |> Fuzz.andMap TestHelpers.preferredAmmoKindFuzzer |> Fuzz.andMap TestHelpers.armorClassFuzzer |> Fuzz.andMap TestHelpers.attackStyleFuzzer + + +baseCriticalChanceArgsFuzzer : + Fuzzer + { special : Special + , traits : SeqSet Trait + , perks : SeqDict Perk Int + , attackStyle : AttackStyle + , chanceToHit : Int + , hitOrMissRoll : Int + } +baseCriticalChanceArgsFuzzer = + Fuzz.constant + (\special traits perks attackStyle chanceToHit hitOrMissRoll -> + { special = special + , traits = traits + , perks = perks + , attackStyle = attackStyle + , chanceToHit = chanceToHit + , hitOrMissRoll = hitOrMissRoll + } + ) + |> Fuzz.andMap TestHelpers.specialFuzzer + |> Fuzz.andMap TestHelpers.traitsFuzzer + |> Fuzz.andMap TestHelpers.perksFuzzer + |> Fuzz.andMap TestHelpers.attackStyleFuzzer + |> Fuzz.andMap (Fuzz.intRange 0 95) + |> Fuzz.andMap (Fuzz.intRange 0 100)