-
-
Notifications
You must be signed in to change notification settings - Fork 30
/
spawn.lisp
188 lines (163 loc) · 8.01 KB
/
spawn.lisp
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
(in-package #:kandria)
(define-global +random-draw+ (make-hash-table :test 'eq))
(define-global +spawn-cache+ (make-hash-table :test 'eq))
(define-global +spawn-tracker+ (make-hash-table :test 'eq))
(defun weighted-random-elt (segments &optional (random-fun #'random))
(let* ((total (loop for segment in segments
sum (second segment)))
(index (funcall random-fun total)))
;; Could try to binsearch, but eh. Probably fine.
(loop for prev = 0.0 then (+ prev weight)
for (part weight) in segments
do (when (< index (+ prev weight))
(return part)))))
(defun random-drawer (name)
(gethash name +random-draw+))
(defun (setf random-drawer) (value name)
(setf (gethash name +random-draw+) value))
(defmethod draw-item ((item symbol))
(funcall (gethash item +random-draw+)))
(defmacro define-random-draw (&environment env name &body items)
(let ((total (float (loop for (item weight) in items
do (unless (or (null item) (find-class item NIL env))
(alexandria:simple-style-warning "Unknown item type: ~s" item))
sum weight))))
`(setf (random-drawer ',name)
(lambda (&optional (f #'random))
(let ((r (funcall f ,total)))
(cond ,@(nreverse (loop for prev = 0.0 then (+ prev weight)
for (item weight) in items
collect `((< ,prev r) ',item)))))))))
(defmethod spawned-p (entity)
(gethash entity +spawn-tracker+))
(defun mark-as-spawned (entity &optional clear)
(if clear
(remhash entity +spawn-tracker+)
(setf (gethash entity +spawn-tracker+) T)))
(defclass spawner (listener sized-entity ephemeral resizable creatable)
((name :initform (generate-name 'spawner))
(spawn-type :initarg :spawn-type :initform NIL :accessor spawn-type :type alloy::any
:documentation "The thing to spawn
Can be either a name of an object
The name of a random distribution
Or a list of names of objects to spawn")
(spawn-count :initarg :spawn-count :initform 2 :accessor spawn-count :type integer
:documentation "How many times to spawn the spawn type")
(spawn-args :initarg :spawn-args :initform NIL :accessor spawn-args :type alloy::any
:documentation "Initialization arguments to pass to the spawned entities")
(reflist :initform () :accessor reflist)
(adjacent :initform () :accessor adjacent)
(auto-deactivate :initarg :auto-deactivate :initform NIL :accessor auto-deactivate :type boolean
:documentation "Whether to deactivate the spawner when all spawned entities have been removed")
(active-p :initarg :active-p :initform T :accessor active-p :type boolean
:documentation "Whether the spawner is currently active or not")
(jitter-y-p :initarg :jitter-y-p :initform T :accessor jitter-y-p :type boolean
:documentation "Whether to randomise the height of the spawned entities")
(rng :initarg :rng :initform (random-state:make-generator :squirrel (random (1- (ash 1 32)))) :accessor rng)))
(defmethod initargs append ((spawner spawner))
'(:spawn-type :spawn-count :spawn-args :auto-deactivate :active-p :jitter-y-p))
(defmethod (setf location) :after (location (spawner spawner))
(let ((chunk (find-chunk spawner))
(adjacent ()))
(when chunk
(do-fitting (entity (bvh (region +world+))
(vec (- (vx (location chunk)) (vx (bsize chunk)) 8)
(- (vy (location chunk)) (vy (bsize chunk)) 8)
(+ (vx (location chunk)) (vx (bsize chunk)) 8)
(+ (vy (location chunk)) (vy (bsize chunk)) 8)))
(when (typep entity 'chunk)
(push entity adjacent))))
(setf (adjacent spawner) adjacent)))
(defun handle-spawn (spawner chunk)
(when (null (adjacent spawner))
(setf (location spawner) (location spawner)))
(cond ((null (reflist spawner))
(when (find chunk (adjacent spawner))
(setf (reflist spawner)
(apply #'spawn (location spawner) (spawn-type spawner)
:count (spawn-count spawner)
:collect T
:jitter (vec (* 2.0 (vx (bsize spawner)))
(if (jitter-y-p spawner)
(* 2.0 (vy (bsize spawner)))
0))
:rng (rng spawner)
(spawn-args spawner)))
(dolist (entity (reflist spawner))
(mark-as-spawned entity))))
((not (find chunk (adjacent spawner)))
(when (and (done-p spawner) (auto-deactivate spawner))
(v:info :kandria.spawn "Deactivating ~a" spawner)
(setf (active-p spawner) NIL))
(dolist (entity (reflist spawner))
(remhash entity +spawn-tracker+)
(leave entity T))
(setf (reflist spawner) ()))))
(defmethod done-p ((spawner spawner))
(loop for entity in (reflist spawner)
never (container entity)))
(defmethod quest:status ((spawner spawner))
(if (or (not (active-p spawner))
;; We only have a reflist if we got the spawns done...
(and (reflist spawner) (done-p spawner)))
:complete
:unresolved))
(define-unit-resolver-methods done-p (unit))
(defmethod (setf active-p) :after (state (spawner spawner))
(when (and state (node 'player +world+))
(handle-spawn spawner (chunk (node 'player +world+)))))
(defmethod quest:activate ((spawner spawner))
(setf (active-p spawner) T))
(defmethod quest:deactivate ((spawner spawner))
(setf (active-p spawner) NIL)
(dolist (entity (reflist spawner))
(remhash entity +spawn-tracker+)
(leave entity T)))
(defmethod handle ((ev switch-chunk) (spawner spawner))
(when (active-p spawner)
(handle-spawn spawner (chunk ev))))
(defmethod spawn ((location vec2) (types cons) &rest initargs)
(loop for type in types
nconc (apply #'spawn location type initargs)))
(defmethod spawn ((location vec2) type &rest initargs &key (count 1) (jitter +tile-size+) collect (rng *random-state*) &allow-other-keys)
(let ((initargs (remf* initargs :count :collect :jitter :rng))
(region (region +world+))
(spawner (random-drawer type))
(rng (lambda (max) (random-state:random max rng))))
(labels ((draw ()
(if spawner
(funcall spawner rng)
type))
(create ()
(apply #'make-instance (draw)
:location (v+ location
(etypecase jitter
(real (vrandr 0 jitter PI rng))
(vec2 (vec (- (funcall rng (vx jitter)) (* 0.5 (vx jitter)))
(- (funcall rng (vy jitter)) (* 0.5 (vy jitter)))))))
initargs)))
(handler-case
(with-error-logging (:kandria.spawn "Failed to spawn ~a" type)
(if collect
(loop repeat count
collect (spawn region (create)))
(loop repeat count
do (spawn region (create)))))
#+kandria-release (error ())))))
(defmethod spawn ((container container) (entity entity) &key)
(cond ((gethash (class-of entity) +spawn-cache+)
(enter entity container))
(T
(enter-and-load entity container +main+)
(setf (gethash (class-of entity) +spawn-cache+) T)))
entity)
(defmethod spawn ((marker located-entity) type &rest initargs)
(apply #'spawn (location marker) type initargs))
(defmethod spawn ((name symbol) type &rest initargs &key &allow-other-keys)
(apply #'spawn (location (node name +world+)) type initargs))
(defun clear-spawns ()
(loop for entity being the hash-keys of +spawn-tracker+
when entity
do (leave entity T))
(clrhash +spawn-cache+)
(clrhash +spawn-tracker+))