๐Ÿ“ฆ sleepyfran / duets

๐Ÿ“„ Simulation.fs ยท 113 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113[<RequireQualifiedAccess>]
module Duets.Simulation.Simulation

open Aether
open Duets.Common
open Duets.Entities
open Duets.Simulation.Events
open Duets.Simulation.Time.AdvanceTime
open Duets.Simulation.Time.InteractionMinutes

type private TickState =
    { AppliedEffects: Effect list
      State: State }

let rec private tick' tickState (nextEffectFns: EffectFn list) : TickState =
    match nextEffectFns with
    | effectFn :: rest -> effectFn tickState.State |> tickEffect tickState rest
    | [] -> tickState

and private tickEffect tickState nextEffectFns effects =
    match effects with
    | [] -> tick' tickState nextEffectFns
    | effect :: restOfEffects ->
        (*
        Before applying the effect and gathering its associated effects, check if
        there's any current modifier that needs to be applied to the effect. For
        example, if the character is not inspired, song related effects have
        less effect.
        *)
        let effect =
            EffectModifiers.EffectModifiers.modify tickState.State effect

        let updatedState = State.Root.applyEffect tickState.State effect

        let associatedEffectFns =
            [ yield! Events.associatedEffects effect
              yield! applyTime effect updatedState ]

        (* Tick all the associated effects first, and pass the rest of the
           effects that come after the current one that was applied plus all
           the other effect functions that are left to be applied.
           tickAssociatedEffects will then decide what to apply and what to
           discard. *)
        tickAssociatedEffects
            { AppliedEffects = tickState.AppliedEffects @ [ effect ]
              State = updatedState }
            associatedEffectFns
            (* Prepend the rest of the effects so that they'll be processed
               before the next effects on the chain. *)
            ((fun _ -> restOfEffects) :: nextEffectFns)

and private tickAssociatedEffects tickState associatedEffects nextEffectFns =
    match associatedEffects with
    | BreakChain effectFns :: _ ->
        (* Breaking the chain means discarding the tail of associated effects
           and also the rest of the effect fns that were left to be applied. *)
        tick' tickState effectFns
    | ContinueChain effectFns :: restOfAssociatedEffects ->
        (* When continuing a chain, we pre-pend all the effect functions that
           were generated in this associated effect to the actual tail of effect
           functions that are left to be applied. *)
        effectFns @ nextEffectFns
        |> tickAssociatedEffects tickState restOfAssociatedEffects
    | [] -> tick' tickState nextEffectFns

and private applyTime effect state =
    let totalTurnTime = effectMinutes effect

    if totalTurnTime > 0<minute> then
        applyTime' state totalTurnTime
    else
        // The effect didn't consume any time, so no need to do anything.
        []

and private applyTime' state totalTurnTime =
    let currentTurnMinutes = Optic.get Lenses.State.turnMinutes_ state

    let total = currentTurnMinutes + totalTurnTime

    if total >= Config.Time.minutesPerDayMoment then
        // Enough time has passed to trigger a new day moment, advance the
        // time by the number of day moments that have passed and apply those
        // to the current chain.
        let totalDayMoments =
            total / Config.Time.minutesPerDayMoment |> (*) 1<dayMoments>

        [ [ fun state -> advanceDayMoment' state totalDayMoments ]
          |> ContinueChain ]
    else if total > 0<minute> then
        // Not enough time has passed to trigger a new day moment, so just
        // update the turn time.
        [ [ (Func.toConst [ TurnTimeUpdated total ]) ] |> ContinueChain ]
    else
        []

//// Ticks the simulation by applying multiple effects, gathering its associated
/// effects and applying them as well.
/// Returns a tuple with the list of all the effects that were applied in the
/// order in which they were applied and the updated state.
let tickMultiple currentState effects =
    let effectFns = fun _ -> effects

    let tickResult =
        tick'
            { AppliedEffects = []
              State = currentState }
            (effectFns :: Events.endOfChainEffects)

    tickResult.AppliedEffects, tickResult.State

/// Same as `tickMultiple` but with one effect.
let tickOne currentState effect = tickMultiple currentState [ effect ]