~technomancy/tremendous-quest-iv

tremendous-quest-iv/ai.fnl -rw-r--r-- 10.1 KiB
a3c24536Phil Hagelberg Bump to Fennel 0.9.1; fix some bugs. 5 months ago
                                                                                
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
(local lume (require :polywell.lib.lume))
(local util (require :util))
(local pathfinding (require :pathfinding))
(local fennelview (require :polywell.lib.fennelview))
(local personalities (require :personalities))
(local moods (require :moods))
(local barks (require :barks))

(fn make [character options]
  (let [char (or character {})
        opts (or options {})
        ai {:mood (or opts.mood (love.math.randomNormal 4 10))
            :personality (or opts.personality (lume.randomchoice personalities))
            :task {:name :pause
                   :timer (lume.random 5)}
            :lifetime 0}]
    (tset char :ai ai)
    char))

(fn in-zone? [point zone]
  (and (<= zone.x point.x (+ zone.x zone.width))
       (<= zone.y point.y (+ zone.y zone.height))))

(fn choose-poi [state]
  (-> (. state.map.layers "points of interest" :objects)
      (lume.filter #(not $.properties.claimed?))
      (lume.randomchoice)))

(fn choose-poi-in-zone [zone state limit]
  (assert (< 0 (or limit 1)) "couldn't find target but that's ok")
  (let [target (choose-poi state)]
    (if (and target (in-zone? target zone))
        target
        (choose-poi-in-zone zone state (if limit (- limit 1) 128)))))

(fn choose-player-target [zone state]
  (-> state.characters
      ;; maybe filter also by bad mood here? and prefer nearby?
      (lume.filter #(and (not $.team?) (in-zone? $ zone)))
      (lume.randomchoice)))

(fn choose-path [state pathfinder char choose-target]
  (var (tries path target) 0)
  (while (and (not path) (< tries 8))
    (set tries (+ tries 1))
    ;; TODO: find a way to bail gracefully here if target isn't found?
    (set target (assert (choose-target state) "no target found but that's ok"))
    (let [(sx sy) (pathfinding.absolute-points->path-points char.x char.y)
          (x y) (pathfinding.absolute-points->path-points target.x target.y)]
      (if (and char.ai.zone (not (in-zone? target char.ai.zone))) nil
          (do (set path (pathfinder:getPath sx sy x y))
              (when (not path)
                (dbg "bad point of interest: " char.name
                     (or target.id target.name) sx sy x y))))))
  (when path
    (tset (getmetatable path) :__fennelview #"#<path>"))
  (values path target))

(fn make-walk-to-task [state char choose-target]
  (let [choose-target (or choose-target (if char.team?
                                            (partial choose-poi-in-zone
                                                     char.ai.zone)
                                            choose-poi))
        old-target (and char.ai.task char.ai.task.target)
        (path target) (choose-path state state.pathfinder char choose-target)]
    (dbg char.name :walking-to target.x target.y)
    (assert path (string.format "no path %s - %s but that's ok"
                                char.name target.name))
    (when target.properties
      (assert (not target.properties.claimed?) "claimed!")
      (dbg char.name :claimed target.id)
      (set target.properties.claimed? true))
    (when (and old-target old-target.properties)
      (dbg char.name :unclaimed old-target.id)
      (set old-target.properties.claimed? false))
    {:name :walk-to
     :path path
     :target target
     :path-points (when path (pathfinding.path->points path))
     :next-point 2}))

;; seek-player is just like walk-to, except it has to target a player and
;; periodically re-pathfind
(fn make-seek-task [state char target]
  (let [choose-target (if target
                          #target
                          (partial choose-player-target char.ai.zone))
        task (make-walk-to-task state char choose-target)
        dist (lume.distance task.target.x task.target.y char.x char.y)]
    (set task.name :seek-player)
    (set task.timer (math.log dist))
    (dbg char.name :seeking task.target.name)
    task))

(local rage-threshold 0)

;; todo: don't take the entire state as an argument
(fn choose-next-task [last-task state char]
  (match (lume.weightedchoice
          (if char.team?
              {:pause (if (= last-task :pause) 1 25)
               ;; :seek-player (if (= last-task :seek-player) 2 10)
               :walk-to 5}
              (= last-task :stuck)
              {:stuck 1
               :rage (if (< char.ai.mood rage-threshold) 99 0)}
              {:level-up (if (= last-task :combat) 5 0)
               :pause (if (or (= last-task :pause)
                              (= last-task :combat)) 1 10)
               :combat (if (= last-task :level-up) 0 2)
               :walk-to (if (= last-task :walk-to) 3 5)
               :stuck 1
               :rage (if (< char.ai.mood rage-threshold) 99 0)}))
    :pause {:name :pause :timer (lume.random 5 25)}
    :combat {:name :combat :timer (love.math.randomNormal 5 12)}
    :walk-to (make-walk-to-task state char)
    :seek-player (make-seek-task state char)
    :level-up {:name :level-up :timer 4}
    :stuck {:name :stuck :timer (math.max 10 (love.math.randomNormal 5 25))}
    :rage (do (dbg :raging char.name)
              {:name :rage :timer (math.max 15 (love.math.randomNormal
                                                5 (+ 40 (* char.ai.mood 2))))})))

(fn push [tab val ...]
  (when val
    (tset tab (+ (# tab) 1) val)
    (push tab ...)))

(fn walk-to [state char dt task-name]
  (let [pyi (* char.ai.task.next-point 2) pxi (- pyi 1)
        px (. char.ai.task.path-points pxi) py (. char.ai.task.path-points pyi)
        (apx apy) (pathfinding.path-points->absolute-points px py)]
    (if (or (not apx) (not apy))
        (set char.ai.task (choose-next-task task-name state char))

        (> (lume.distance char.x char.y apx apy) (* char.speed dt))
        (let [(dx dy) (lume.vector (lume.angle char.x char.y apx apy)
                                   (* char.speed dt))
              (nx ny colls) (state.world:move char (+ char.x dx)
                                              (+ char.y dy) #nil)]
          (set char.x nx) (set char.y ny))

        (and char.ai.task.target.properties
             char.ai.task.target.properties.spawn char.logout
             (< (love.math.random) 0.5))
        (do (when (and char.ai.task.target char.ai.task.target.properties)
              (set char.ai.task.target.properties.claimed? false))
            (char.logout))

        (do (set char.x apx) (set char.y apy)
            (set char.ai.task.next-point (+ char.ai.task.next-point 1))
            (when (and (> (* char.ai.task.next-point 2)
                          (# char.ai.task.path-points)))
              (set char.ai.task (choose-next-task task-name state char)))))))

(fn set-follow [char target offset]
  (set char.ai.task {:name :follow : target : offset}))

(fn seek-player [state char task dt] ; like walk-to but the target moves
  (let [dist (lume.distance task.target.x task.target.y char.x char.y)]
    (when (<= task.timer 0)
      (set task.timer (math.log dist))
      (let [path (choose-path state state.pathfinder char #task.target)]
        (if path
            (set (task.path task.path-points task.next-point)
                 (values path (pathfinding.path->points path) 2))
            ;; assume the target logged out or became unreachable
            (set char.ai.task (choose-next-task :seek-player state char)))))
    (when char.ai.task.path
      (walk-to state char dt :seek-player))))

(fn rage [state char task dt]
  (if (<= task.timer 0)
      (do (dbg :RAGEQUIT char.name)
          (set state.ragequits (+ state.ragequits 1))
          (char.logout))
      (do (when (< rage-threshold char.ai.mood)
            (set char.ai.task (choose-next-task :rage state char)))
          (set task.timer (- task.timer dt)))))

(fn run-task [dt state char task]
  (match task.name
    :pause
    (when (<= task.timer 0)
      (set char.ai.task (choose-next-task :pause state char)))
    :walk-to (walk-to state char dt :walk-to)
    :seek-player (seek-player state char task dt)
    :combat
    (when (<= task.timer 0)
      (if (lume.weightedchoice {true 3 false 1})
          ;; win combat
          (let [new-gold (math.floor (math.max 10 (love.math.randomNormal 100 25)))]
            (set char.gold (+ char.gold new-gold))
            (barks.bark char (.. "+" new-gold "gp"))
            (set char.ai.task (choose-next-task :combat state char)))
          ;; lose combat
          (do (set char.ai.mood (- char.ai.mood 1))
              (barks.bark char "i lost!")
              (set char.ai.task (choose-next-task :combat state char)))
          ))
    :level-up
    (when (<= task.timer 0)
      (if (< char.ai.mood moods.max-mood) (set char.ai.mood (+ char.ai.mood 1)))
      (set char.ai.task (choose-next-task :level-up state char)))
    :stuck
    (when (<= task.timer 0)
      (set char.ai.mood (- char.ai.mood 2))
      (set char.ai.task (choose-next-task :stuck state char)))
    :rage (rage state char task dt)
    :follow (do (state.world:move char
                                  (+ task.target.x task.offset)
                                  (+ task.target.y 32) #nil)
                (let [(x y) (: state.world :getRect char)]
                  (set char.x x)
                  (set char.y y)))))

(fn run-ai [dt state char]
  (set char.ai.lifetime (+ char.ai.lifetime dt))
  (barks.run dt char)
  (when (not char.team?)
    (set char.ui-color (if (< char.ai.mood rage-threshold)
                           [1 0.2 0.2]
                           char.closest?
                           [0.2 0.2 1]
                           [1 1 1])))
  (when (and char.bugged-item (< (lume.random) 0.0001))
    (barks.bark char "argh! this item's bugged!")
    (set char.ai.mood (- char.ai.mood 1)))
  (when (not char.ai.task)
    (set char.ai.task (choose-next-task :reset state char)))
  (when char.ai.task.timer
    (set char.ai.task.timer (- char.ai.task.timer dt)))
  (run-task dt state char char.ai.task))

(fn run [dt state char]
  "We shouldn't let bugs in the AI crash the whole game."
  (let [(ok? msg) (xpcall #(run-ai dt state char)
                          #(when (not (: $ :match "that's ok"))
                             (dbg $ (debug.traceback))))]
    (when (not ok?)
      (dbg "AI error" char.name msg)
      (if char.team?
          (set char.ai.task nil)
          char.logout
          (char.logout)
          (lume.remove state.characters char)))))

{: make : run : set-follow}