summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/SampleScript.fs
blob: 0f3519ef3a25cce2909d018e8c0a1c5ed3d34bfd (plain)
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
namespace LivingVillage.Desktop

open System
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop.VillagePresentation

module SampleScript =

    type SampleStage =
        | ApproachDoor
        | InspectInterior
        | LeaveDoorstep
        | ApproachNpc
        | ReadDialogueOptions
        | CloseDialogue
        | WalkAway
        | SampleComplete

    type SampleAction =
        | NoAction
        | MoveTo of Vec2
        | PushM5 of M5Command

    type SampleState =
        { Stage: SampleStage
          StageFrames: int
          TotalFrames: int
          DialogueTarget: NpcId option
          TimedOut: bool }

    let initial : SampleState =
        { Stage = ApproachDoor
          StageFrames = 0
          TotalFrames = 0
          DialogueTarget = None
          TimedOut = false }

    let private tileCenter (tile: TilePosition) : Vec2 =
        { X = float32 (tile.X * Sim.tilePixels + Sim.tilePixels / 2)
          Y = float32 (tile.Y * Sim.tilePixels + Sim.tilePixels / 2) }

    let doorWorldCenter : Vec2 =
        let home = (createScene ()).Homes |> List.head
        tileCenter (worldTileForSceneTile home.Door)

    let private leaveDoorstepCenter : Vec2 =
        tileCenter (worldTileForSceneTile { X = 3; Y = 5 })

    let private walkAwayCenter : Vec2 =
        tileCenter (worldTileForSceneTile { X = 6; Y = 7 })

    let private arriveRadius = 12.0f
    let private inspectFrames = 150
    let private promptHoldFrames = 60
    let private menuHoldFrames = 60
    let private dialogueHoldFrames = 120
    let private totalFrameLimit = 20000

    let private distanceSquared (a: Vec2) (b: Vec2) : float32 =
        let dx = b.X - a.X
        let dy = b.Y - a.Y
        dx * dx + dy * dy

    let private timeoutFor (stage: SampleStage) : int =
        match stage with
        | ApproachDoor -> 1800
        | InspectInterior -> 600
        | LeaveDoorstep -> 1800
        | ApproachNpc -> 5400
        | ReadDialogueOptions -> 600
        | CloseDialogue -> 300
        | WalkAway -> 3600
        | SampleComplete -> Int32.MaxValue

    let private advance (stage: SampleStage) (state: SampleState) : SampleState =
        { state with Stage = stage; StageFrames = 0 }

    let private nearestChatableNpc (world: World) : Npc option =
        world.Npcs
        |> Array.filter chatableForChat
        |> Array.sortBy (fun npc -> distanceSquared world.Avatar.Pos npc.Pos, npc.Id)
        |> Array.tryHead

    let private timedOut (state: SampleState) : SampleState * SampleAction =
        { state with Stage = SampleComplete; TimedOut = true }, NoAction

    let step (world: World) (view: M5View) (state: SampleState) : SampleState * SampleAction =
        let state =
            { state with StageFrames = state.StageFrames + 1; TotalFrames = state.TotalFrames + 1 }
        if state.TotalFrames > totalFrameLimit then
            timedOut state
        elif state.StageFrames >= timeoutFor state.Stage then
            timedOut state
        else
            match state.Stage with
            | SampleComplete -> state, NoAction
            | ApproachDoor ->
                match view.HomeMode with
                | Inside _ -> advance InspectInterior state, NoAction
                | Outside ->
                    if distanceSquared world.Avatar.Pos doorWorldCenter <= arriveRadius * arriveRadius then
                        state, PushM5 Interact
                    else
                        state, MoveTo doorWorldCenter
            | InspectInterior ->
                match view.HomeMode with
                | Outside -> advance LeaveDoorstep state, NoAction
                | Inside _ ->
                    if state.StageFrames >= inspectFrames then
                        state, PushM5 Interact
                    else
                        state, NoAction
            | LeaveDoorstep ->
                match view.HomeMode with
                | Inside _ -> state, PushM5 Interact
                | Outside ->
                    if distanceSquared world.Avatar.Pos leaveDoorstepCenter <= arriveRadius * arriveRadius then
                        advance ApproachNpc state, NoAction
                    else
                        state, MoveTo leaveDoorstepCenter
            | ApproachNpc ->
                match M5Interaction.resolveAction world Outside with
                | Some { Action = TalkTo target } ->
                    if state.StageFrames >= promptHoldFrames then
                        { advance ReadDialogueOptions state with DialogueTarget = Some target }, PushM5 Interact
                    else
                        state, NoAction
                | _ ->
                    match nearestChatableNpc world with
                    | Some npc -> state, MoveTo npc.Pos
                    | None -> state, NoAction
            | ReadDialogueOptions ->
                if view.Panel = DialoguePanel && view.Menu.IsSome then
                    if state.StageFrames >= menuHoldFrames then
                        state, PushM5 Intent1
                    else
                        state, NoAction
                else
                    advance CloseDialogue state, NoAction
            | CloseDialogue ->
                if state.StageFrames >= dialogueHoldFrames then
                    advance WalkAway state, PushM5 ClosePanel
                else
                    state, NoAction
            | WalkAway ->
                if distanceSquared world.Avatar.Pos walkAwayCenter <= arriveRadius * arriveRadius then
                    advance SampleComplete state, NoAction
                else
                    state, MoveTo walkAwayCenter

    let runFrame (world: World) (view: M5View) (state: SampleState) : World * M5View * SampleState =
        let nextState, action = step world view state
        let nextWorld, nextView =
            match action with
            | NoAction -> world, view
            | PushM5 command -> M5Interaction.apply command world view
            | MoveTo target ->
                let dx = target.X - world.Avatar.Pos.X
                let dy = target.Y - world.Avatar.Pos.Y
                let length = sqrt (dx * dx + dy * dy)
                let input =
                    if length > 0.001f then
                        { MoveX = dx / length; MoveY = dy / length }
                    else
                        { MoveX = 0.0f; MoveY = 0.0f }
                let movedWorld = Sim.step { Input = input } world
                movedWorld, view
        nextWorld, M5Interaction.refreshPrompt nextWorld nextView, nextState