remove room: and npc: and thing: prefixes
[mudsync.git] / mudsync / gameobj.scm
1 ;;; Mudsync --- Live hackable MUD
2 ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
3 ;;;
4 ;;; This file is part of Mudsync.
5 ;;;
6 ;;; Mudsync is free software; you can redistribute it and/or modify it
7 ;;; under the terms of the GNU General Public License as published by
8 ;;; the Free Software Foundation; either version 3 of the License, or
9 ;;; (at your option) any later version.
10 ;;;
11 ;;; Mudsync is distributed in the hope that it will be useful, but
12 ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 ;;; General Public License for more details.
15 ;;;
16 ;;; You should have received a copy of the GNU General Public License
17 ;;; along with Mudsync.  If not, see <http://www.gnu.org/licenses/>.
18
19 ;;; Game actor
20 ;;; ==========
21
22 (define-module (mudsync gameobj)
23   #:use-module (mudsync command)
24   #:use-module (8sync actors)
25   #:use-module (8sync agenda)
26   #:use-module (8sync rmeta-slot)
27   #:use-module (srfi srfi-1)
28   #:use-module (ice-9 control)
29   #:use-module (ice-9 format)
30   #:use-module (ice-9 match)
31   #:use-module (oop goops)
32   #:export (<gameobj>
33
34             gameobj-loc
35             gameobj-gm
36
37             gameobj-act-init
38             gameobj-set-loc!
39             gameobj-occupants
40             gameobj-self-destruct
41
42             slot-ref-maybe-runcheck
43             val-or-run
44
45             dyn-ref
46
47             ;; Some of the more common commands
48             cmd-take cmd-drop
49             cmd-take-from cmd-put-in))
50
51 ;;; Gameobj
52 ;;; =======
53
54
55 ;;; *all* game components that talk to players should somehow
56 ;;; derive from this class.
57 ;;; And all of them need a GM!
58
59 (define-class <gameobj> (<actor>)
60   ;; location id
61   (loc #:init-value #f
62        #:getter gameobj-loc)
63   
64   ;; Uses a hash table like a set (values ignored)
65   (occupants #:init-thunk make-hash-table)
66
67   ;; game master id
68   (gm #:init-keyword #:gm
69       #:getter gameobj-gm)
70
71   ;; a name to be known by
72   (name #:init-keyword #:name
73         #:init-value #f)
74   (goes-by #:init-keyword #:goes-by
75            #:init-value #f)
76
77   (desc #:init-value #f
78         #:init-keyword #:desc)
79
80   ;; Commands we can handle
81   (commands #:allocation #:each-subclass
82             #:init-thunk (build-commands
83                           ("take" ((direct-command cmd-take)
84                                    (prep-indir-command cmd-take-from
85                                                        '("from" "out of"))))
86                           ("put" ((prep-indir-command cmd-put-in
87                                                       '("in" "inside" "on"))))))
88
89   ;; Commands we can handle by being something's container
90   (container-commands #:allocation #:each-subclass
91                       #:init-thunk (build-commands))
92
93   ;; Commands we can handle by being contained by something else
94   (contained-commands #:allocation #:each-subclass
95                       #:init-thunk 
96                       (build-commands
97                        ("drop" ((direct-command cmd-drop #:obvious? #f)))))
98
99   ;; Most objects are generally visible by default
100   (invisible? #:init-value #f
101               #:init-keyword #:invisible?)
102   ;; TODO: Fold this into a procedure in invisible? similar
103   ;;   to take-me? and etc
104   (visible-to-player?
105    #:init-value (wrap-apply gameobj-visible-to-player?))
106
107   ;; Can be a boolean or a procedure accepting
108   ;; (gameobj whos-acting #:key from)
109   (take-me? #:init-value #f
110             #:init-keyword #:take-me?)
111   ;; Can be a boolean or a procedure accepting
112   ;; (gameobj whos-acting where)
113   (drop-me? #:init-value #t
114             #:init-keyword #:drop-me?)
115   ;; Can be a boolean or a procedure accepting
116   ;; (gameobj whos-acting take-what)
117   (take-from-me? #:init-value #f
118                  #:init-keyword #:take-from-me?)
119   ;; Can be a boolean or a procedure accepting
120   ;; (gameobj whos-acting put-what)
121   (put-in-me? #:init-value #f
122               #:init-keyword #:put-in-me?)
123
124   ;; TODO: Remove this and use actor-alive? instead.
125   ;; Set this on self-destruct
126   ;; (checked by some "long running" game routines)
127   (destructed #:init-value #f)
128
129   (actions #:allocation #:each-subclass
130            ;;; Actions supported by all gameobj
131            #:init-thunk
132            (build-actions
133             (init gameobj-act-init)
134             ;; Commands for co-occupants
135             (get-commands gameobj-get-commands)
136             ;; Commands for participants in a room
137             (get-container-commands gameobj-get-container-commands)
138             ;; Commands for inventory items, etc (occupants of the gameobj commanding)
139             (get-contained-commands gameobj-get-contained-commands)
140
141             (get-occupants gameobj-get-occupants)
142             (add-occupant! gameobj-add-occupant!)
143             (remove-occupant! gameobj-remove-occupant!)
144             (get-loc gameobj-act-get-loc)
145             (set-loc! gameobj-act-set-loc!)
146             (get-name gameobj-get-name)
147             (set-name! gameobj-act-set-name!)
148             (get-desc gameobj-get-desc)
149             (goes-by gameobj-act-goes-by)
150             (visible-name gameobj-visible-name)
151             (self-destruct gameobj-act-self-destruct)
152             (tell gameobj-tell-no-op)
153             (assist-replace gameobj-act-assist-replace)
154             (ok-to-drop-here? (lambda (gameobj message . _)
155                                 (<-reply message #t))) ; ok to drop by default
156             (ok-to-be-taken-from? gameobj-ok-to-be-taken-from)
157             (ok-to-be-put-in? gameobj-ok-to-be-put-in)
158
159             ;; Common commands
160             (cmd-take cmd-take)
161             (cmd-take-from cmd-take-from)
162             (cmd-put-in cmd-put-in)
163             (cmd-drop cmd-drop))))
164
165
166 ;;; gameobj message handlers
167 ;;; ========================
168
169 ;; Kind of a useful utility, maybe?
170 (define (simple-slot-getter slot)
171   (lambda (actor message)
172     (<-reply message (slot-ref actor slot))))
173
174 (define (gameobj-replace-step-occupants actor occupants)
175   ;; Snarf all the occupants!
176   (display "replacing occupant\n")
177   (when occupants
178     (for-each
179      (lambda (occupant)
180        (<-wait occupant 'set-loc!
181                #:loc (actor-id actor)))
182      occupants)))
183
184 (define gameobj-replace-steps*
185   (list gameobj-replace-step-occupants))
186
187 (define (run-replacement actor replaces replace-steps)
188   (when replaces
189     (mbody-receive (_ #:key occupants)
190         (<-wait replaces 'assist-replace)
191       (for-each
192        (lambda (replace-step)
193          (replace-step actor occupants))
194        replace-steps))))
195
196 ;; @@: This could be kind of a messy way of doing gameobj-act-init
197 ;;   stuff.  If only we had generic methods :(
198 (define* (gameobj-act-init actor message #:key replace)
199   "Your most basic game object init procedure.
200 Assists in its replacement of occupants if necessary and nothing else."
201   (run-replacement actor replace gameobj-replace-steps*))
202
203 (define (gameobj-goes-by gameobj)
204   "Find the name we go by.  Defaults to #:name if nothing else provided."
205   (cond ((slot-ref gameobj 'goes-by) =>
206          identity)
207         ((slot-ref gameobj 'name) =>
208          (lambda (name)
209            (list name)))
210         (else '())))
211
212 (define (gameobj-act-goes-by actor message)
213   "Reply to a message requesting what we go by."
214   (<-reply message #:goes-by (gameobj-goes-by actor)))
215
216 (define (val-or-run val-or-proc)
217   "Evaluate if a procedure, or just return otherwise"
218   (if (procedure? val-or-proc)
219       (val-or-proc)
220       val-or-proc))
221
222 (define (get-candidate-commands actor rmeta-sym verb)
223   (class-rmeta-ref (class-of actor) rmeta-sym verb
224                    #:dflt '()))
225
226 (define* (gameobj-get-commands actor message #:key verb)
227   "Get commands a co-occupant of the room might execute for VERB"
228   (define candidate-commands
229     (get-candidate-commands actor 'commands verb))
230   (<-reply message
231            #:commands candidate-commands
232            #:goes-by (gameobj-goes-by actor)))
233
234 (define* (gameobj-get-container-commands actor message #:key verb)
235   "Get commands as the container / room of message's sender"
236   (define candidate-commands
237     (get-candidate-commands actor 'container-commands verb))
238   (<-reply message #:commands candidate-commands))
239
240 (define* (gameobj-get-contained-commands actor message #:key verb)
241   "Get commands as being contained (eg inventory) of commanding gameobj"
242   (define candidate-commands
243     (get-candidate-commands actor 'contained-commands verb))
244   (<-reply message
245            #:commands candidate-commands
246            #:goes-by (gameobj-goes-by actor)))
247
248 (define* (gameobj-add-occupant! actor message #:key who)
249   "Add an actor to our list of present occupants"
250   (hash-set! (slot-ref actor 'occupants)
251              who #t))
252
253 (define* (gameobj-remove-occupant! actor message #:key who)
254   "Remove an occupant from the room."
255   (hash-remove! (slot-ref actor 'occupants) who))
256
257 (define* (gameobj-occupants gameobj #:key exclude)
258   (hash-fold
259    (lambda (occupant _ prev)
260      (define exclude-it?
261        (match exclude
262          ;; Empty list and #f are non-exclusion
263          (() #f)
264          (#f #f)
265          ;; A list of addresses... since our address object is (annoyingly)
266          ;; currently a simple cons cell...
267          ((exclude-1 ... exclude-rest)
268           (member occupant exclude))
269          ;; Must be an individual address!
270          (_ (equal? occupant exclude))))
271      (if exclude-it?
272          prev
273          (cons occupant prev)))
274    '()
275    (slot-ref gameobj 'occupants)))
276
277 (define* (gameobj-get-occupants actor message #:key exclude)
278   "Get all present occupants of the room."
279   (define occupants
280     (gameobj-occupants actor #:exclude exclude))
281   (<-reply message occupants))
282
283 (define (gameobj-act-get-loc actor message)
284   (<-reply message (slot-ref actor 'loc)))
285
286 (define (gameobj-set-loc! gameobj loc)
287   "Set the location of this object."
288   (define old-loc (gameobj-loc gameobj))
289   (format #t "DEBUG: Location set to ~s for ~s\n"
290           loc (actor-id-actor gameobj))
291
292   (when (not (equal? old-loc loc))
293     (slot-set! gameobj 'loc loc)
294     ;; Change registation of where we currently are
295     (if old-loc
296         (<-wait old-loc 'remove-occupant! #:who (actor-id gameobj)))
297     (if loc
298         (<-wait loc 'add-occupant! #:who (actor-id gameobj)))))
299
300 ;; @@: Should it really be #:id ?  Maybe #:loc-id or #:loc?
301 (define* (gameobj-act-set-loc! actor message #:key loc)
302   "Action routine to set the location."
303   (gameobj-set-loc! actor loc))
304
305 (define (slot-ref-maybe-runcheck gameobj slot whos-asking . other-args)
306   "Do a slot-ref on gameobj, evaluating it including ourselves
307 and whos-asking, and see if we should just return it or run it."
308   (match (slot-ref gameobj slot)
309     ((? procedure? slot-val-proc)
310      (apply slot-val-proc gameobj whos-asking other-args))
311     (anything-else anything-else)))
312
313 (define gameobj-get-name (simple-slot-getter 'name))
314
315 (define* (gameobj-act-set-name! actor message val)
316   (slot-set! actor 'name val))
317
318 (define* (gameobj-get-desc actor message #:key whos-looking)
319   (define desc-text
320     (match (slot-ref actor 'desc)
321       ((? procedure? desc-proc)
322        (desc-proc actor whos-looking))
323       (desc desc)))
324   (<-reply message desc-text))
325
326 (define (gameobj-visible-to-player? gameobj whos-looking)
327   "Check to see whether we're visible to the player or not.
328 By default, this is whether or not the generally-visible flag is set."
329   (not (slot-ref gameobj 'invisible?)))
330
331 (define* (gameobj-visible-name actor message #:key whos-looking)
332   ;; Are we visible?
333   (define we-are-visible
334     ((slot-ref actor 'visible-to-player?) actor whos-looking))
335
336   (define name-to-return
337     (if we-are-visible
338         ;; Return our name
339         (match (slot-ref actor 'name)
340           ((? procedure? name-proc)
341            (name-proc actor whos-looking))
342           ((? string? name)
343            name)
344           (#f #f))
345         #f))
346   (<-reply message #:text name-to-return))
347
348 (define (gameobj-self-destruct gameobj)
349   "General gameobj self destruction routine"
350   ;; Unregister from being in any particular room
351   (gameobj-set-loc! gameobj #f)
352   (slot-set! gameobj 'destructed #t)
353   ;; Boom!
354   (self-destruct gameobj))
355
356 (define* (gameobj-act-self-destruct gameobj message #:key why)
357   "Action routine for self destruction"
358   (gameobj-self-destruct gameobj))
359
360 ;; Unless an actor has a tell message, we just ignore it
361 (define gameobj-tell-no-op
362   (const 'no-op))
363
364 (define (gameobj-replace-data-occupants gameobj)
365   "The general purpose list of replacement data"
366   (list #:occupants (hash-map->list (lambda (occupant _) occupant)
367                                     (slot-ref gameobj 'occupants))))
368
369 (define (gameobj-replace-data* gameobj)
370   ;; For now, just call gameobj-replace-data-occupants.
371   ;; But there may be more in the future!
372   (gameobj-replace-data-occupants gameobj))
373
374 ;; So sad that objects must assist in their replacement ;_;
375 ;; But that's life in a live hacked game!
376 (define (gameobj-act-assist-replace gameobj message)
377   "Vanilla method for assisting in self-replacement for live hacking"
378   (apply <-reply message
379          (gameobj-replace-data* gameobj)))
380
381 (define (gameobj-ok-to-be-taken-from gameobj message whos-acting)
382   (<-reply message (slot-ref-maybe-runcheck gameobj 'take-me?
383                                             whos-acting #:from #t)))
384
385 (define (gameobj-ok-to-be-put-in gameobj message whos-acting where)
386   (<-reply message (slot-ref-maybe-runcheck gameobj 'drop-me?
387                                             whos-acting where)))
388
389 \f
390 ;;; Utilities every gameobj has
391 ;;; ---------------------------
392
393 (define (dyn-ref gameobj special-symbol)
394   "Dynamically look up a special object from the gm"
395   (match special-symbol
396     ;; if it's a symbol, look it up dynamically
397     ((? symbol? _)
398      (mbody-val (<-wait (slot-ref gameobj 'gm) 'lookup-special
399                         #:symbol special-symbol)))
400     ;; if it's false, return nothing
401     (#f #f)
402     ;; otherwise it's probably an address, return it as-is
403     (_ special-symbol)))
404
405
406 \f
407 ;;; Basic actions
408 ;;; -------------
409
410 (define* (cmd-take gameobj message #:key direct-obj)
411   (define player (message-from message))
412   (define player-name
413     (mbody-val (<-wait player 'get-name)))
414   (define player-loc
415     (mbody-val (<-wait player 'get-loc)))
416   (define our-name (slot-ref gameobj 'name))
417   (define self-should-take
418     (slot-ref-maybe-runcheck gameobj 'take-me? player))
419   ;; @@: Is there any reason to allow the room to object in the way
420   ;;   that there is for dropping?  It doesn't seem like it.
421   ;; TODO: Allow gameobj to customize
422   (if self-should-take
423       ;; Set the location to whoever's picking us up
424       (begin
425         (gameobj-set-loc! gameobj player)
426         (<- player 'tell
427             #:text (format #f "You pick up ~a.\n"
428                            our-name))
429         (<- player-loc 'tell-room
430             #:text (format #f "~a picks up ~a.\n"
431                            player-name
432                            our-name)
433             #:exclude player))
434       (<- player 'tell
435           #:text (format #f "It doesn't seem like you can take ~a.\n"
436                          our-name))))
437
438 (define* (cmd-drop gameobj message #:key direct-obj)
439   (define player (message-from message))
440   (define player-name
441     (mbody-val (<-wait player 'get-name)))
442   (define player-loc
443     (mbody-val (<-wait player 'get-loc)))
444   (define our-name (slot-ref gameobj 'name))
445   (define should-drop
446     (slot-ref-maybe-runcheck gameobj 'drop-me? player))
447   (define (room-objection-to-drop)
448     (mbody-receive (_ drop-ok? #:key why-not) ; does the room object to dropping?
449         (<-wait player-loc 'ok-to-drop-here? player (actor-id gameobj))
450       (and (not drop-ok?)
451            ;; Either give the specified reason, or give a boilerplate one
452            (or why-not
453                `("You'd love to drop " ,our-name
454                  " but for some reason it doesn't seem like you can"
455                  " do that here.")))))
456   (cond
457    ((not player-loc)
458     (<- player 'tell
459         #:text `("It doesn't seem like you can drop " ,our-name 
460                 " here, because you don't seem to be anywhere?!?")))
461    ;; TODO: Let ourselves supply a reason why not.
462    ((not should-drop)
463     (<- player 'tell
464         #:text (format #f "It doesn't seem like you can drop ~a.\n"
465                        our-name)))
466    ((room-objection-to-drop)
467     (<- player 'tell
468         #:text room-objection-to-drop))
469    (else
470     (gameobj-set-loc! gameobj player-loc)
471     ;; TODO: Allow more flavortext here.
472     (<- player 'tell
473         #:text (format #f "You drop ~a.\n"
474                        our-name))
475     (<- player-loc 'tell-room
476         #:text (format #f "~a drops ~a.\n"
477                        player-name
478                        our-name)
479         #:exclude player))))
480
481 ;; @@: Moving this to a container subclass/mixin could allow a lot more
482 ;;   customization of take out / put in phrases
483 (define* (cmd-take-from gameobj message
484                         #:key direct-obj indir-obj preposition)
485   (define player (message-from message))
486   (define player-name
487     (mbody-val (<-wait player 'get-name)))
488   (define player-loc
489     (mbody-val (<-wait player 'get-loc)))
490   (define our-name (slot-ref gameobj 'name))
491   ;; We need to check if we even have such a thing
492   (define this-thing
493     (call/ec
494      (lambda (return)
495        (for-each (lambda (occupant)
496                    (mbody-receive (_ #:key goes-by)
497                        (<-wait occupant 'goes-by)
498                      (when (member direct-obj goes-by)
499                        (return occupant))))
500                  (gameobj-occupants gameobj))
501        ;; nothing found
502        #f)))
503   (define (this-thing-name)
504     (mbody-val (<-wait this-thing 'get-name)))
505   (define (should-take-from-me)
506     (and this-thing
507          (slot-ref-maybe-runcheck gameobj 'take-from-me? player this-thing)))
508   (define (default-objection)
509     `("Unfortunately, it doesn't seem like you can take "
510       (this-thing-name) " " preposition " " our-name "."))
511
512   (define (this-thing-objection)
513     (mbody-receive (_ taken-ok? #:key why-not) ; does the object object to being removed?
514         (<-wait this-thing 'ok-to-be-taken-from? player) ; @@ no need to supply from where
515       (and (not taken-ok?)
516            ;; Either give the specified reason, or give a boilerplate one
517            (or why-not
518                (default-objection)))))
519   (cond
520    ;; Wait, aren't we going to check (should-take-from-me) later?
521    ;; Well yes, but this checks if there's a #f as the value, which
522    ;; is a much clearer indication that this doesn't take *anything*.
523    ((not (slot-ref gameobj 'take-from-me?))
524     (<- player 'tell
525         #:text `("It's not really clear how to take something " ,preposition
526                  " " ,our-name ".")))
527
528    ;; Unfortunately this does leak information about what is contained
529    ;; by us.  Maybe not what's wanted in all circumstances.
530    ((not this-thing)
531     (<- player 'tell
532         #:text `("You don't see any such " ,direct-obj " to take from "
533                  ,our-name ".")))
534    ;; A particular objection to taking this thing.
535    ;; We should allow customizing the reason here, which could be
536    ;; provided by the 'ok-to-be-taken-from? slot.
537    ((not (should-take-from-me))
538     (<- player 'tell
539         #:text (default-objection)))
540    ;; the thing we wsant to take itself has objected...
541    ((this-thing-objection) =>
542     (lambda (objection)
543       (<- player 'tell
544           #:text objection)))
545    ;; looks like we can take it
546    (else
547     ;; Wait to announce to the player just in case settting the location
548     ;; errors out or something.  Maybe it's overthinking things, I dunno.
549     (<-wait this-thing 'set-loc! #:loc player)
550     (<- player 'tell
551         #:text `("You take " ,(this-thing-name) " from "
552                  ,our-name "."))
553     (<- player-loc 'tell-room
554         #:text `(,player-name " takes " ,(this-thing-name) " from "
555                               ,our-name ".")
556         #:exclude player))))
557
558 (define* (cmd-put-in gameobj message
559                      #:key direct-obj indir-obj preposition)
560   (define player (message-from message))
561   (define player-name
562     (mbody-val (<-wait player 'get-name)))
563   (define player-loc
564     (mbody-val (<-wait player 'get-loc)))
565   (define our-name (slot-ref gameobj 'name))
566   ;; We need to check if we even have such a thing
567   (define this-thing
568     (call/ec
569      (lambda (return)
570        (for-each (lambda (occupant)
571                    (mbody-receive (_ #:key goes-by)
572                        (<-wait occupant 'goes-by)
573                      (when (member direct-obj goes-by)
574                        (return occupant))))
575                  (mbody-val (<-wait player 'get-occupants)))
576        ;; nothing found
577        #f)))
578   (define (this-thing-name)
579     (mbody-val (<-wait this-thing 'get-name)))
580   (define (should-put-in-me)
581     (and this-thing
582          (slot-ref-maybe-runcheck gameobj 'put-in-me? player this-thing)))
583   (define (default-objection)
584     `("As much as you'd like to, it doesn't seem like you can put "
585       ,(this-thing-name) " " ,preposition " " ,our-name "."))
586   (define (this-thing-objection)
587     (mbody-receive (_ put-in-ok? #:key why-not) ; does the object object to being moved?
588         (<-wait this-thing 'ok-to-be-put-in? player (actor-id gameobj))
589       (and (not put-in-ok?)
590            ;; Either give the specified reason, or give a boilerplate one
591            (or why-not (default-objection)))))
592   (cond
593    ;; Is it not there, or maybe we won't allow it to be taken?
594    ((not this-thing)
595     (<- (message-from message) 'tell
596         #:text `("You don't seem to have any such " ,direct-obj " to put "
597                  ,preposition " " ,our-name ".")))
598
599    ((or (not (should-put-in-me)))
600     (<- (message-from message) 'tell
601         #:text (default-objection)))
602    ;; the thing we wsant to take itself has objected...
603    ((this-thing-objection) =>
604     (lambda (objection)
605       (<- (message-from message) 'tell
606           #:text objection)))
607    ;; looks like we can take it
608    (else
609     ;; Wait to announce to the player just in case settting the location
610     ;; errors out or something.  Maybe it's overthinking things, I dunno.
611     (<-wait this-thing 'set-loc! #:loc (actor-id gameobj))
612     (<- player 'tell
613         #:text `("You put " ,(this-thing-name) " " ,preposition " "
614                  ,our-name "."))
615     (<- player-loc 'tell-room
616         #:text `(,player-name " puts " ,(this-thing-name) " " ,preposition " "
617                               ,our-name ".")
618         #:exclude player))))