From 489bf84761a1ef29a9a260216381b2862e06a820 Mon Sep 17 00:00:00 2001 From: Martin Janiczek Date: Mon, 28 Oct 2024 16:03:23 +0100 Subject: [PATCH] Check and pay Quest.playerRequirements when starting a quest --- src/Backend.elm | 169 +++++++++++++++++++++++------------- src/Data/Player/SPlayer.elm | 123 ++++++++++---------------- src/Data/Quest.elm | 18 +--- src/Data/World.elm | 13 ++- src/Logic.elm | 41 ++++----- 5 files changed, 187 insertions(+), 177 deletions(-) diff --git a/src/Backend.elm b/src/Backend.elm index 56d7f06..04af0a8 100644 --- a/src/Backend.elm +++ b/src/Backend.elm @@ -258,22 +258,14 @@ getPlayerData_ worldName world player = |> SeqDict.map (\quest ticksGivenPerPlayer -> let - engagements : List Quest.Engagement - engagements = + engagedPlayersCount : Int + engagedPlayersCount = players - |> List.map (\player_ -> SPlayer.questEngagement player_ quest) - - playersActive : Int - playersActive = - engagements - |> List.filter (\e -> e /= Quest.NotProgressing) - |> List.length + |> List.count (\player_ -> SeqSet.member quest player_.questsActive) ticksPerHour : Int ticksPerHour = - engagements - |> List.map Logic.ticksGivenPerQuestEngagement - |> List.sum + engagedPlayersCount * Logic.questTicksPerHour ticksGiven : Int ticksGiven = @@ -290,7 +282,7 @@ getPlayerData_ worldName world player = in { ticksGiven = ticksGiven , ticksPerHour = ticksPerHour - , playersActive = playersActive + , playersActive = engagedPlayersCount , ticksGivenByPlayer = ticksGivenByPlayer } ) @@ -498,9 +490,11 @@ processGameTickForQuests worldName model = player |> Maybe.map (\player_ -> - quest - |> SPlayer.questEngagement player_ - |> Logic.ticksGivenPerQuestEngagement + if SeqSet.member quest player_.questsActive then + Logic.questTicksPerHour + + else + 0 ) |> Maybe.withDefault 0 in @@ -2160,51 +2154,110 @@ stopProgressing quest clientId _ worldName player model = startProgressing : Quest.Name -> ClientId -> World -> World.Name -> SPlayer -> Model -> ( Model, Cmd BackendMsg ) startProgressing quest clientId world worldName player model = + -- TODO: there are requirements other than Quest.playerRequirements, check them as well if we don't elsewhere already let - ensurePlayerIsInQuestProgressDict : World -> World - ensurePlayerIsInQuestProgressDict world_ = - if SPlayer.canStartProgressing quest world.tickPerIntervalCurve player then - { world_ - | questsProgress = - world_.questsProgress - |> SeqDict.update quest - (\maybePlayersProgress -> - case maybePlayersProgress of - Nothing -> - Just (Dict.singleton player.name 0) - - Just playersProgress -> - playersProgress - |> Dict.update player.name - (\maybePlayerProgress -> - case maybePlayerProgress of - Nothing -> - Just 0 - - Just n -> - Just n - ) - |> Just - ) - } + playerRequirements : List Quest.PlayerRequirement + playerRequirements = + Quest.playerRequirements quest + + playerAlreadyPaidRequirements : Bool + playerAlreadyPaidRequirements = + world.questRequirementsPaid + |> SeqDict.get quest + |> Maybe.withDefault Set.empty + |> Set.member player.name + + playerCanPayRequirements : Bool + playerCanPayRequirements = + playerRequirements + |> List.all + (\req -> + case req of + Quest.SkillRequirement r -> + case r.skill of + Quest.Combat -> + let + maxCombatSkill : Int + maxCombatSkill = + Logic.questRequirementCombatSkills + |> List.map (Skill.get player.special player.addedSkillPercentages) + |> List.maximum + |> Maybe.withDefault 0 + in + maxCombatSkill >= r.percentage - else - world_ + Quest.Specific skill -> + Skill.get player.special player.addedSkillPercentages skill >= r.percentage - newModel = - if World.isQuestDone quest world then - model + Quest.ItemRequirementOneOf itemsNeeded -> + itemsNeeded + |> List.any + (\item -> + player.items + |> Dict.any (\_ { kind, count } -> kind == item && count >= 1) + ) - else - model - |> updatePlayer worldName player.name (SPlayer.startProgressing quest world.tickPerIntervalCurve) - |> updateWorld worldName ensurePlayerIsInQuestProgressDict + Quest.CapsRequirement capsNeeded -> + player.caps >= capsNeeded + ) in - getPlayerData worldName player.name newModel - |> Maybe.map - (\data -> - ( newModel - , Lamdera.sendToFrontend clientId <| CurrentPlayer data + if playerAlreadyPaidRequirements || playerCanPayRequirements then + let + ensurePlayerPresent : World -> World + ensurePlayerPresent world_ = + if SPlayer.canStartProgressing world.tickPerIntervalCurve player then + { world_ + | questsProgress = + world_.questsProgress + |> SeqDict.update quest + (\maybePlayersProgress -> + case maybePlayersProgress of + Nothing -> + Just (Dict.singleton player.name 0) + + Just playersProgress -> + playersProgress + |> Dict.update player.name (Maybe.withDefault 0 >> Just) + |> Just + ) + } + + else + world_ + + newModel = + if World.isQuestDone quest world then + model + + else + model + |> updatePlayer worldName player.name (SPlayer.startProgressing quest world.tickPerIntervalCurve) + |> updateWorld worldName ensurePlayerPresent + |> (if playerAlreadyPaidRequirements then + identity + + else + updatePlayer worldName player.name (SPlayer.payQuestRequirements playerRequirements) + >> updateWorld worldName (notePlayerPaidRequirements quest player.name) + ) + in + getPlayerData worldName player.name newModel + |> Maybe.map + (\data -> + ( newModel + , Lamdera.sendToFrontend clientId <| CurrentPlayer data + ) ) - ) - |> Maybe.withDefault ( model, Cmd.none ) + |> Maybe.withDefault ( model, Cmd.none ) + + else + ( model, Cmd.none ) + + +notePlayerPaidRequirements : Quest.Name -> PlayerName -> World -> World +notePlayerPaidRequirements quest player world = + { world + | questRequirementsPaid = + world.questRequirementsPaid + |> SeqDict.update quest (Maybe.withDefault Set.empty >> Set.insert player >> Just) + } diff --git a/src/Data/Player/SPlayer.elm b/src/Data/Player/SPlayer.elm index a88c87b..02c92c5 100644 --- a/src/Data/Player/SPlayer.elm +++ b/src/Data/Player/SPlayer.elm @@ -17,8 +17,8 @@ module Data.Player.SPlayer exposing , incSpecial , incWins , levelUpHereAndNow + , payQuestRequirements , preferAmmo - , questEngagement , readMessage , recalculateHp , removeAllMessages @@ -51,20 +51,16 @@ import Data.Message as Message exposing (Content(..), Message) import Data.Perk as Perk exposing (Perk) import Data.Player exposing (SPlayer) import Data.Quest as Quest - exposing - ( Engagement(..) - , PlayerRequirement(..) - , SkillRequirement(..) - ) import Data.Skill as Skill exposing (Skill) import Data.Special as Special import Data.Tick as Tick exposing (TickPerIntervalCurve) import Data.Trait as Trait import Data.Xp as Xp import Dict exposing (Dict) +import Dict.Extra import Logic import SeqDict exposing (SeqDict) -import SeqSet exposing (SeqSet) +import SeqSet import Time exposing (Posix) @@ -290,7 +286,7 @@ tick : Posix -> TickPerIntervalCurve -> SPlayer -> SPlayer tick currentTime worldTickCurve player = player |> addTicks (ticksPerHourAvailableAfterQuests worldTickCurve player) - |> addQuestProgressXp currentTime + |> addTickQuestProgressXp currentTime |> (if player.hp < player.maxHp then addHp (Logic.healOverTimePerTick @@ -305,8 +301,8 @@ tick currentTime worldTickCurve player = ) -addQuestProgressXp : Posix -> SPlayer -> SPlayer -addQuestProgressXp currentTime player = +addTickQuestProgressXp : Posix -> SPlayer -> SPlayer +addTickQuestProgressXp currentTime player = let xpPerQuest : SeqDict Quest.Name Int xpPerQuest = @@ -315,10 +311,7 @@ addQuestProgressXp currentTime player = |> List.map (\quest -> ( quest - , quest - |> questEngagement player - |> Logic.ticksGivenPerQuestEngagement - |> (*) (Quest.xpPerTickGiven quest) + , Quest.xpPerTickGiven quest * Logic.questTicksPerHour ) ) |> SeqDict.fromList @@ -713,76 +706,19 @@ setPreferredAmmo preferredAmmo player = { player | preferredAmmo = preferredAmmo } -questEngagement : SPlayer -> Quest.Name -> Quest.Engagement -questEngagement player quest = - let - reqs : List PlayerRequirement - reqs = - Quest.playerRequirements quest - - meetsRequirement : PlayerRequirement -> Bool - meetsRequirement req = - let - oneSkill : Int -> Skill -> Bool - oneSkill percentage skill = - Skill.get player.special player.addedSkillPercentages skill >= percentage - in - case req of - SkillRequirement { skill, percentage } -> - case skill of - Combat -> - List.any (oneSkill percentage) Skill.combatSkills - - Specific skill_ -> - oneSkill percentage skill_ - - ItemRequirementOneOf items -> - -- TODO consume the items once adding the quest? Only in some of these cases (chimpanzee brain) and not in others (lockpick)? - let - playerItemKinds : SeqSet ItemKind.Kind - playerItemKinds = - player.items - |> Dict.values - |> List.map .kind - |> SeqSet.fromList - in - List.all (\kind -> SeqSet.member kind playerItemKinds) items - - CapsRequirement amount -> - -- TODO remove the caps once adding the quest? - player.caps >= amount - in - if SeqSet.member quest player.questsActive then - if List.isEmpty reqs then - Progressing - - else if List.all meetsRequirement reqs then - Progressing - - else - {- TODO only allow this if the reqs not met are the skill ones! - We can't allow the user to progress if they don't meet the caps / items reqs! - -} - ProgressingSlowly - - else - NotProgressing - - stopProgressing : Quest.Name -> SPlayer -> SPlayer stopProgressing quest player = { player | questsActive = SeqSet.remove quest player.questsActive } -canStartProgressing : Quest.Name -> TickPerIntervalCurve -> SPlayer -> Bool -canStartProgressing quest worldTickCurve player = - -- TODO is it expected that `quest` is not used? - ticksPerHourAvailableAfterQuests worldTickCurve player >= Logic.minTicksPerHourNeededForQuest +canStartProgressing : TickPerIntervalCurve -> SPlayer -> Bool +canStartProgressing worldTickCurve player = + ticksPerHourAvailableAfterQuests worldTickCurve player >= Logic.questTicksPerHour startProgressing : Quest.Name -> TickPerIntervalCurve -> SPlayer -> SPlayer startProgressing quest worldTickCurve player = - if canStartProgressing quest worldTickCurve player then + if canStartProgressing worldTickCurve player then { player | questsActive = SeqSet.insert quest player.questsActive } else @@ -792,11 +728,42 @@ startProgressing quest worldTickCurve player = ticksPerHourUsedOnQuests : SPlayer -> Int ticksPerHourUsedOnQuests player = player.questsActive - |> SeqSet.toList - |> List.map (questEngagement player >> Logic.ticksGivenPerQuestEngagement) - |> List.sum + |> SeqSet.size + |> (*) Logic.questTicksPerHour ticksPerHourAvailableAfterQuests : TickPerIntervalCurve -> SPlayer -> Int ticksPerHourAvailableAfterQuests worldTickCurve player = Tick.ticksAddedPerInterval worldTickCurve player.ticks - ticksPerHourUsedOnQuests player + + +payQuestRequirements : List Quest.PlayerRequirement -> SPlayer -> SPlayer +payQuestRequirements reqs player = + List.foldl + (\req accPlayer -> + case req of + Quest.SkillRequirement _ -> + accPlayer + + Quest.ItemRequirementOneOf requiredItems -> + -- Only pay the first item you find + case Dict.Extra.find (\_ item -> List.member item.kind requiredItems && item.count >= 1) accPlayer.items of + Nothing -> + accPlayer + + Just ( id, item ) -> + { accPlayer + | items = + if item.count == 1 then + Dict.remove id accPlayer.items + + else + Dict.insert id { item | count = max 0 (item.count - 1) } accPlayer.items + } + + Quest.CapsRequirement capsRequired -> + -- It should have been checked outside this function that we _do_ have enough caps to pay + { accPlayer | caps = max 0 (player.caps - capsRequired) } + ) + player + reqs diff --git a/src/Data/Quest.elm b/src/Data/Quest.elm index b495329..fcb5e29 100644 --- a/src/Data/Quest.elm +++ b/src/Data/Quest.elm @@ -1,13 +1,11 @@ module Data.Quest exposing - ( Engagement(..) - , GlobalReward(..) + ( GlobalReward(..) , Name(..) , PlayerRequirement(..) , PlayerReward(..) , Progress , SkillRequirement(..) , all - , allEngagement , allForLocation , decoder , encode @@ -207,20 +205,6 @@ type alias Progress = } -type Engagement - = NotProgressing - | ProgressingSlowly - | Progressing - - -allEngagement : List Engagement -allEngagement = - [ NotProgressing - , ProgressingSlowly - , Progressing - ] - - title : Name -> String title name = case name of diff --git a/src/Data/World.elm b/src/Data/World.elm index fca7f25..240ca32 100644 --- a/src/Data/World.elm +++ b/src/Data/World.elm @@ -25,6 +25,7 @@ import SeqDict exposing (SeqDict) import SeqDict.Extra as SeqDict import SeqSet exposing (SeqSet) import SeqSet.Extra as SeqSet +import Set exposing (Set) import Time exposing (Posix) import Time.Extra as Time import Time.ExtraExtra as Time @@ -47,6 +48,9 @@ type alias World = , vendorRestockFrequency : Time.Interval , questsProgress : SeqDict Quest.Name (Dict PlayerName Int) , questRewardShops : SeqSet Shop + , -- Which players have paid the item / caps requirement for a quest? + -- (They'll be able to pause/unpause their progress on the quest for free) + questRequirementsPaid : SeqDict Quest.Name (Set PlayerName) } @@ -81,6 +85,10 @@ init { fast } = |> List.map (\q -> ( q, Dict.empty )) |> SeqDict.fromList , questRewardShops = SeqSet.empty + , questRequirementsPaid = + Quest.all + |> List.map (\q -> ( q, Set.empty )) + |> SeqDict.fromList } @@ -102,6 +110,7 @@ encode world = , ( "vendorRestockFrequency", Time.encodeInterval world.vendorRestockFrequency ) , ( "questsProgress", SeqDict.encode Quest.encode (JE.dict identity JE.int) world.questsProgress ) , ( "questRewardShops", SeqSet.encode Shop.encode world.questRewardShops ) + , ( "questRequirementsPaid", SeqDict.encode Quest.encode (JE.set JE.string) world.questRequirementsPaid ) ] @@ -131,7 +140,7 @@ lastItemId players vendors = decoder : Decoder World decoder = JD.succeed - (\players nextWantedTick nextVendorRestockTick vendors description startedAt tickFrequency tickPerIntervalCurve vendorRestockFrequency questsProgress questRewardShops -> + (\players nextWantedTick nextVendorRestockTick vendors description startedAt tickFrequency tickPerIntervalCurve vendorRestockFrequency questsProgress questRewardShops questRequirementsPaid -> { players = players , nextWantedTick = nextWantedTick , nextVendorRestockTick = nextVendorRestockTick @@ -144,6 +153,7 @@ decoder = , vendorRestockFrequency = vendorRestockFrequency , questsProgress = questsProgress , questRewardShops = questRewardShops + , questRequirementsPaid = questRequirementsPaid } ) |> JD.andMap @@ -165,6 +175,7 @@ decoder = |> JD.andMap (JD.field "vendorRestockFrequency" Time.intervalDecoder) |> JD.andMap (JD.field "questsProgress" (SeqDict.decoder Quest.decoder (Dict.decoder JD.string JD.int))) |> JD.andMap (JD.field "questRewardShops" (SeqSet.decoder Shop.decoder)) + |> JD.andMap (JD.field "questRequirementsPaid" (SeqDict.decoder Quest.decoder (JD.set JD.string))) isQuestDone : Quest.Name -> World -> Bool diff --git a/src/Logic.elm b/src/Logic.elm index dc776fb..00b7051 100644 --- a/src/Logic.elm +++ b/src/Logic.elm @@ -25,7 +25,6 @@ module Logic exposing , mainWorldName , maxPossibleMove , maxTraits - , minTicksPerHourNeededForQuest , naturalArmorClass , newCharAvailableSpecialPoints , newCharMaxTaggedSkills @@ -34,13 +33,14 @@ module Logic exposing , playerCombatCapsGained , playerCombatXpGained , price + , questRequirementCombatSkills + , questTicksPerHour , regainConciousnessApCost , sequence , skillPointCost , skillPointsPerLevel , standUpApCost , tickHealPercentage - , ticksGivenPerQuestEngagement , totalTags , unaimedAttackStyle , unarmedApCost @@ -61,7 +61,6 @@ import Data.Item.Effect as ItemEffect import Data.Item.Kind as ItemKind import Data.Item.Type as ItemType import Data.Perk as Perk exposing (Perk) -import Data.Quest as Quest exposing (Engagement(..)) import Data.Skill as Skill exposing (Skill) import Data.Special as Special exposing (Special) import Data.Trait as Trait exposing (Trait) @@ -1780,26 +1779,9 @@ mainWorldName = "main" -minTicksPerHourNeededForQuest : Int -minTicksPerHourNeededForQuest = - Quest.allEngagement - |> List.map ticksGivenPerQuestEngagement - |> List.filter (\tph -> tph /= 0) - |> List.minimum - |> Maybe.withDefault 1 - - -ticksGivenPerQuestEngagement : Quest.Engagement -> Int -ticksGivenPerQuestEngagement engagement = - case engagement of - NotProgressing -> - 0 - - ProgressingSlowly -> - 1 - - Progressing -> - 2 +questTicksPerHour : Int +questTicksPerHour = + 2 burstShotChanceToHitPenalty : Int @@ -2259,3 +2241,16 @@ maxPossibleMove r = else 0 + + +questRequirementCombatSkills : List Skill +questRequirementCombatSkills = + [ Skill.SmallGuns + , Skill.BigGuns + , Skill.EnergyWeapons + , Skill.MeleeWeapons + , Skill.Throwing + , Skill.Unarmed + + -- TODO traps? + ]