-
Notifications
You must be signed in to change notification settings - Fork 3
/
tutorial-part06.lisp
485 lines (415 loc) · 17.2 KB
/
tutorial-part06.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
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
;;;; tutorial-part06.lisp
;;;; "Going Berserk!"
;;;; February 2017
;;;;
;;;; Commentary to be posted on 9bladed.com
;;;;
;;;; Original python+libtcod tutorial, part 6:
;;;; http://www.roguebasin.com/index.php?title=Complete_Roguelike_Tutorial,_using_python%2Blibtcod,_part_6
(ql:quickload :tcod)
(defparameter *screen-width* 80)
(defparameter *screen-height* 50)
(defparameter *limit-fps* 20)
(defvar *map*)
(defparameter *map-width* 80)
(defparameter *map-height* 45)
(defparameter *room-max-size* 10)
(defparameter *room-min-size* 6)
(defparameter *max-rooms* 30)
(defparameter *max-room-monsters* 3)
(defvar *fov-map* (tcod:map-new *map-width* *map-height*))
(defparameter *fov-recompute* t)
(defparameter *fov-algo* :FOV-BASIC)
(defparameter *fov-light-walls* t)
(defparameter *torch-radius* 10)
(tcod:make-color :dark-wall 0 0 100)
(tcod:make-color :light-wall 130 110 50)
(tcod:make-color :dark-ground 50 50 150)
(tcod:make-color :light-ground 200 180 50)
(tcod:console-set-custom-font "arial10x10.png" '(:FONT-TYPE-GREYSCALE :FONT-LAYOUT-TCOD))
(tcod:console-init-root *screen-width* *screen-height*
:title "lisp/libtcod tutorial" :fullscreen? nil :renderer :renderer-sdl)
(defparameter *con* (tcod:console-new *screen-width* *screen-height*))
(tcod:sys-set-fps *limit-fps*)
(defgeneric move (obj dx dy)
(:documentation "move by the given amount"))
(defgeneric move-towards (obj target-x target-y)
(:documentation "move towards target"))
(defgeneric distance-to (obj other)
(:documentation "return the distance to another object"))
(defgeneric draw (obj)
(:documentation "set the color and then draw the character that represents this object at its position"))
(defgeneric clear (obj)
(:documentation "erase the character that represents this object"))
(defgeneric send-to-back (obj)
(:documentation "send object to beginning of list, to be drawn first and overwritten"))
(defclass tile ()
((blocked
:initarg :blocked
:initform (error "Must specify blocked")
:accessor blocked)
(block-sight
:initarg :block-sight
:initform nil
:accessor block-sight)
(explored
:initform nil
:accessor explored))
(:documentation "a tile of the map and its properties"))
(defmethod initialize-instance :after ((tt tile) &key)
"by default, if a tile is blocked, it also blocks sight"
(if (not (block-sight tt))
(setf (block-sight tt) (blocked tt))))
(defgeneric intersect (this other)
(:documentation "returns true if this rectangle intersects with another one"))
(defclass rect ()
((x1
:initarg :x1
:initform (error "Must have an x value")
:accessor x1)
(y1
:initarg :y1
:initform (error "Must have a y value")
:accessor y1)
(x2
:accessor x2)
(y2
:accessor y2)
(center
:accessor center))
(:documentation "a rectangle on the map. used to characterize a room."))
(defmethod initialize-instance :after ((r rect) &key width height)
(setf (x2 r) (+ (x1 r) width))
(setf (y2 r) (+ (y1 r) height))
(setf (center r) (cons (floor (/ (+ (x1 r) (x2 r)) 2)) (floor (/ (+ (y1 r) (y2 r)) 2)))))
(defmethod intersect ((this rect) (other rect))
(and (<= (x1 this) (x2 other)) (>= (x2 this) (x1 other))
(<= (y1 this) (y2 other)) (>= (y2 this) (y1 other))))
(defun create-room (room)
;; go through the tiles in the rectangle and make them passable
(loop for x from (1+ (x1 room)) below (x2 room)
do (loop for y from (1+ (y1 room)) below (y2 room)
do (setf (blocked (aref *map* y x)) nil)
do (setf (block-sight (aref *map* y x)) nil))))
(defun create-h-tunnel (x1 x2 y)
(loop for x from (min x1 x2) upto (max x1 x2)
do (setf (blocked (aref *map* y x)) nil)
do (setf (block-sight (aref *map* y x)) nil)))
(defun create-v-tunnel (y1 y2 x)
;; vertical tunnel
(loop for y from (min y1 y2) upto (max y1 y2)
do (setf (blocked (aref *map* y x)) nil)
(setf (block-sight (aref *map* y x)) nil)))
(defun is-blocked (x y)
;; first test the map tile
(if (blocked (aref *map* y x))
t
;; now check for any blocking objects
(dolist (obj *objects*)
(if (and (blocks obj) (eql (x obj) x) (eql (y obj) y))
(return t)))))
(defclass object ()
((x
:initarg :x
:initform (error "Must have an x value")
:accessor x)
(y
:initarg :y
:initform (error "Must have a y value")
:accessor y)
(cha
:initarg :cha
:initform (error "Must have a character")
:accessor cha)
(color
:initarg :color
:initform (error "Must have a color")
:accessor color)
(name
:initarg :name
:initform (error "Must have a name")
:accessor name)
(blocks
:initarg :blocks
:initform nil
:accessor blocks)
(ai
:initarg :ai
:initform nil
:accessor ai))
(:documentation "this is a generic object: the player, a monster, an item, the stairs...
it's always represented by a character on the screen."))
(defmethod move ((obj object) dx dy)
(if (not (is-blocked (+ (x obj) dx) (+ (y obj) dy)))
(progn
(incf (x obj) dx)
(incf (y obj) dy))))
(defmethod move-towards ((obj object) target-x target-y)
;; vector from this object to the target, and distance
(let* ((dx (- target-x (x obj))) (dy (- target-y (y obj)))
(distance (sqrt (+ (expt dx 2) (expt dy 2)))))
;; normalize it to length 1 (preserving direction), then round it and
;; convert to integer so the movement is restricted to the map grid
(setf dx (round (/ dx distance))
dy (round (/ dy distance)))
(move obj dx dy)))
(defmethod distance-to ((obj object) (other object))
(let ((dx (- (x other) (x obj))) (dy (- (y other) (y obj))))
(sqrt (+ (expt dx 2) (expt dy 2)))))
(defmethod move-or-attack ((obj object) dx dy)
;;the coordinates the player is moving to/attacking
(let ((x (+ (x obj) dx)) (y (+ (y obj) dy)) (target nil))
;;try to find an attackable object there
(dolist (object *objects*)
(when (and (ai object) (eql (x object) x) (eql (y object) y))
(setf target object)
(return)))
;; attack if target found, move otherwise
(if target
(attack *player* target)
(progn (move obj dx dy)
(setf *fov-recompute* t)))))
(defmethod draw ((obj object))
(when (tcod:map-is-in-fov? *fov-map* (x obj) (y obj))
(tcod:console-set-default-foreground *con* (tcod:color (color obj)))
(tcod:console-put-char *con* (x obj) (y obj) (char-code (cha obj)) :NONE)))
(defmethod clear ((obj object))
(tcod:console-put-char *con* (x obj) (y obj) (char-code #\SPACE) :NONE))
(defmethod send-to-back ((obj object))
;; make this object be drawn first, so all others appear above it if they're in the same tile
(setf *objects* (remove obj *objects*))
(push obj *objects*))
(defclass fighter (object)
((max-hp
:initarg :max-hp
:initform (error "Must provide max-hp")
:accessor max-hp)
(hp
:accessor hp)
(defense
:initarg :defense
:initform (error "Must provide defense")
:accessor defense)
(power
:initarg :power
:initform (error "Must provide power")
:accessor power)
(death-fun
:initarg :death-fun
:initform nil
:accessor death-fun))
(:documentation "fighter class for players, npcs, monsters"))
(defmethod initialize-instance :after ((f fighter) &key)
(setf (hp f) (max-hp f)))
(defgeneric take-damage (self damage)
(:documentation "take damage"))
(defgeneric attack (self target)
(:documentation "attack target"))
(defmethod attack ((self fighter) target)
;; a simple formula for attack damage
(let ((damage (- (power self) (defense target))))
(if (> damage 0)
;; make the target take some damage
(progn (format t "~&~@(~a~) attacks ~a for ~a hit points." (name self) (name target) damage)
(take-damage target damage))
(format t "~&~@(~a~) attacks ~a but it has no effect!" (name self) (name target)))))
(defmethod take-damage ((self fighter) damage)
;; apply damage if possible
(if (> damage 0)
(decf (hp self) damage))
;; check for death. if there's a death function, call it
(if (<= (hp self) 0)
(if (death-fun self)
(funcall (death-fun self) self))))
(defgeneric take-turn (self)
(:documentation "take turn"))
(defclass basic-monster (object)
((ai
:initform #'take-turn))
(:documentation "AI for a basic monster"))
(defmethod take-turn ((monster basic-monster))
"a basic monster takes its turn. If you can see it, it can see you"
(if (tcod:map-is-in-fov? *fov-map* (x monster) (y monster))
(cond
;; move towards player if far away
((>= (distance-to monster *player*) 2)
(move-towards monster (x *player*) (y *player*)))
;; close enough, attack! (if the player s still alive.)
((> (hp *player*) 0)
(attack monster *player*)))))
(defclass basic-fighter-monster (fighter basic-monster)
())
(defun player-death (player)
;; the game ended!
(format t "~&You died!~%")
(setf *game-state* :dead)
;; for added effect, transform the player into a corpse!
(setf (cha player) #\%
(color player) :dark-red))
(defun monster-death (monster)
;; transform it into a nasty corpse! it doesn't block, can't be
;; attacked and doesn't move
(format t "~&~@(~a~) is dead!" (name monster))
(setf (cha monster) #\%
(color monster) :dark-red
(blocks monster) nil
(ai monster) nil
(name monster) (format nil "remains of ~a" (name monster)))
(send-to-back monster))
(defun place-objects (room)
;; choose random number of monsters
(let ((num-monsters (tcod:random-get-int tcod:+null+ 0 *max-room-monsters*))
monster x y)
(dotimes (i num-monsters)
;; choose random spot for this monster
(setf x (tcod:random-get-int tcod:+null+ (x1 room) (x2 room))
y (tcod:random-get-int tcod:+null+ (y1 room) (y2 room)))
(unless (is-blocked x y)
(if (< (tcod:random-get-int tcod:+null+ 0 100) 80) ; 80% chance of getting an orc
;; create an orc
(setf monster (make-instance 'basic-fighter-monster
:x x :y y :cha #\o :name "orc" :color :pale-green
:blocks t :max-hp 10 :defense 0 :power 3
:death-fun #'monster-death))
;; create a troll
(setf monster (make-instance 'basic-fighter-monster
:x x :y y :cha #\T :name "troll" :color :dark-sea-green
:blocks t :max-hp 10 :defense 1 :power 4
:death-fun #'monster-death)))
(setf *objects* (append *objects* (list monster)))))))
(defun handle-keys ()
(let ((event (tcod:sys-wait-events :event-key-press t)))
(when (eql (car event) :event-key-press)
(cond ((and (eql (tcod:key-vk (cdr event)) :ENTER) (tcod:key-lalt (cdr event)))
(tcod:console-set-fullscreen (not (tcod:console-is-fullscreen?))))
((eql (tcod:key-vk (cdr event)) :ESCAPE) (return-from handle-keys :exit)))
(if (eql *game-state* :playing)
(cond
;; movement keys
((eql (tcod:key-vk (cdr event)) :UP)
(move-or-attack *player* 0 -1))
((eql (tcod:key-vk (cdr event)) :DOWN)
(move-or-attack *player* 0 1))
((eql (tcod:key-vk (cdr event)) :LEFT)
(move-or-attack *player* -1 0))
((eql (tcod:key-vk (cdr event)) :RIGHT)
(move-or-attack *player* 1 0))
(t (return-from handle-keys :didnt-take-turn)))))))
(defparameter *player* (make-instance 'fighter
:x 0 :y 0 :cha #\@ :name "player" :color :white :blocks t
:max-hp 30 :defense 2 :power 5 :death-fun #'player-death))
(defparameter *objects* (list *player*))
(defun make-map ()
(setf *map* (make-array (list *map-height* *map-width*)))
(dotimes (i *map-height*)
(dotimes (j *map-width*)
(setf (aref *map* i j) (make-instance 'tile :blocked t))))
(loop with num-rooms = 0
with rooms
with w
with h
with x
with y
with new-room
with failed
for r from 0 below *max-rooms* do
;; random width and height
(setf w (tcod:random-get-int tcod:+null+ *room-min-size* *room-max-size*))
(setf h (tcod:random-get-int tcod:+null+ *room-min-size* *room-max-size*))
;; random position without going out of the boundaries of the map
(setf x (tcod:random-get-int tcod:+null+ 0 (- *map-width* w 1)))
(setf y (tcod:random-get-int tcod:+null+ 0 (- *map-height* h 1)))
;; "rect" class makes rectangles easier to work with
(setf new-room (make-instance 'rect :x1 x :y1 y :width w :height h))
;; run through the other rooms and see if they intersect with this one
(setf failed nil)
(loop for other-room in rooms do
(when (intersect new-room other-room)
(setf failed t)
(return)))
(unless failed
;; this means there are no intersections, so this room is valid
(create-room new-room) ; "paint" it to the map's tiles
;; center coordinates of new room, will be useful later
(let ((new-x (car (center new-room)))
(new-y (cdr (center new-room))))
(cond ((eql num-rooms 0)
;; this is the first room, where the player starts at
(setf (x *player*) new-x
(y *player*) new-y))
;; all rooms after the first:
;; connect it to the previous room with a tunnel
(t (let ((prev-x (car (center (car (last rooms))))) ; center coordinates
(prev-y (cdr (center (car (last rooms)))))) ; of previous room
;; draw a coin (random number that is either 0 or 1)
(cond ((eql (tcod:random-get-int tcod:+null+ 0 1) 1)
;; first move horizontally, then vertically
(create-h-tunnel prev-x new-x prev-y)
(create-v-tunnel prev-y new-y new-x))
(t ; first move vertically, then horizontally
(create-v-tunnel prev-y new-y prev-x)
(create-h-tunnel prev-x new-x new-y)))))))
;; add some contents to this room, such as monsters
(place-objects new-room)
;; finally, append the new room to the list
(if (eql num-rooms 0)
(setf rooms (list new-room))
(setf rooms (append rooms (list new-room))))
(incf num-rooms))))
;; generate map (at this point it's not drawn to the screen)
(make-map)
(dotimes (y *map-height*)
(dotimes (x *map-width*)
(tcod:map-set-properties *fov-map* x y (not (block-sight (aref *map* y x)))
(not (blocked (aref *map* y x))))))
(defun render-all ()
(when *fov-recompute*
;; recompute FOV if needed (the player moved or something)
(setf *fov-recompute* nil)
(tcod:map-compute-fov *fov-map* (x *player*) (y *player*) *torch-radius*
*fov-light-walls* *fov-algo*))
;; go through all the tiles, and set their background color
(dotimes (i *map-height*)
(dotimes (j *map-width*)
(let ((visible (tcod:map-is-in-fov? *fov-map* j i))
(wall (block-sight (aref *map* i j))))
(if (not visible)
;; if it's not visible right now, the player can only see it if it's explored
(if (explored (aref *map* i j))
;; it's out of the player's FOV
(if wall
(tcod:console-set-char-background *con* j i (tcod:color :dark-wall) :set)
(tcod:console-set-char-background *con* j i (tcod:color :dark-ground) :set)))
;; it's visible
(progn (if wall
(tcod:console-set-char-background *con* j i (tcod:color :light-wall) :set)
(tcod:console-set-char-background *con* j i (tcod:color :light-ground) :set))
;; since it's visible, explore it
(setf (explored (aref *map* i j)) t))))))
;; draw all objects in the list, except the player. we want it to
;; always appear over all other objects! so it's drawn later.
(dolist (obj *objects*)
(if (not (eql obj *player*))
(draw obj)))
(draw *player*)
;; show the player's stats
(tcod:console-set-default-foreground *con* (tcod:color :white))
(tcod:console-print-ex *con* 1 (- *screen-height* 2) :none :left
"HP: ~a / ~a" (hp *player*) (max-hp *player*))
;; blit the contents of con to the root console
(tcod:console-blit *con* 0 0 *screen-width* *screen-height* tcod:*root* 0 0 1.0 1.0))
(defparameter *game-state* :playing)
(defparameter *player-action* nil)
(do ()
((tcod:console-is-window-closed?))
(render-all)
(tcod:console-flush)
(mapcar #'clear *objects*)
(setf *player-action* (handle-keys))
;; let monsters take their turn
(if (and (eql *game-state* :playing) (not (eql *player-action* :didnt-take-turn)))
(dolist (obj *objects*)
(if (ai obj)
(take-turn obj))))
(if (eql *player-action* :exit)
(return)))