dd9a7ef86a21

Add ruins
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sat, 07 Jan 2017 20:50:14 +0000
parents 01ab77c9d46a
children 50624f6d57d7
branches/tags (none)
files antipodes.asd data/vegetables.lisp data/venues.lisp package.lisp src/aspects/coordinates.lisp src/aspects/trigger.lisp src/aspects/visible.lisp src/config.lisp src/entities/ruin.lisp src/main.lisp

Changes

--- a/antipodes.asd	Sat Jan 07 19:17:56 2017 +0000
+++ b/antipodes.asd	Sat Jan 07 20:50:14 2017 +0000
@@ -26,9 +26,11 @@
                  (:module "aspects" :serial t
                   :components ((:file "coordinates")
                                (:file "holdable")
+                               (:file "trigger")
                                (:file "visible")))
                  (:module "entities" :serial t
                   :components ((:file "food")
+                               (:file "ruin")
                                (:file "player")))
                  (:file "flavor")
                  (:file "main")))))
--- a/data/vegetables.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/data/vegetables.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -38,7 +38,7 @@
   "hubbard squash"
   "pickled jalapenos"
   "kale"
-  "kidney bean"
+  "kidney beans"
   "kohlrabi"
   "lavender"
   "leeks"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/venues.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -0,0 +1,363 @@
+#("an Acupuncturist"
+  "an Adult Boutique"
+  "an Advertising Agency"
+  "an Afghan Restaurant"
+  "an African Restaurant"
+  "an American Restaurant"
+  "an Animal Shelter"
+  "an Antique Shop"
+  "an Aquarium"
+  "an Arcade"
+  "an Arepa Restaurant"
+  "an Argentinian Restaurant"
+  "an Art Gallery"
+  "an Art Museum"
+  "an Arts & Crafts Store"
+  "an Asian Restaurant"
+  "an Auditorium"
+  "an Australian Restaurant"
+  "an Austrian Restaurant"
+  "an Auto Garage"
+  "an Automotive Shop"
+  "a Baby Store"
+  "a Badminton Court"
+  "a Bagel Shop"
+  "a Bakery"
+  "a Bank"
+  "a Bar"
+  "a Baseball Field"
+  "a Baseball Stadium"
+  "a Basketball Court"
+  "a Basketball Stadium"
+  "a Bath House"
+  "a BBQ Joint"
+  "a Beer Garden"
+  "a Beer Store"
+  "a Belarusian Restaurant"
+  "a Belgian Restaurant"
+  "a Bike Shop"
+  "a Bistro"
+  "a Board Shop"
+  "a Bookstore"
+  "a Botanical Garden"
+  "a Bowling Alley"
+  "a Bowling Green"
+  "a Boxing Gym"
+  "a Brazilian Restaurant"
+  "a Brewery"
+  "a Bridal Shop"
+  "a Bridge"
+  "a Bubble Tea Shop"
+  "a Buddhist Temple"
+  "a Building"
+  "a Burger Joint"
+  "a Burrito Place"
+  "a Business Service"
+  "a Butcher"
+  "a Cafeteria"
+  "a Café"
+  "a Cajun Restaurant"
+  "a Cambodian Restaurant"
+  "a Camera Store"
+  "a Campaign Office"
+  "a Campground"
+  "a Candy Store"
+  "a Car Dealership"
+  "a Car Wash"
+  "a Caribbean Restaurant"
+  "a Carpet Store"
+  "a Casino"
+  "a Castle"
+  "a Caucasian Restaurant"
+  "a Cemetery"
+  "a Champagne Bar"
+  "a Check Cashing Service"
+  "a Cheese Shop"
+  "a Chinese Restaurant"
+  "a Chiropractor"
+  "a Chocolate Shop"
+  "a Christmas Market"
+  "a Church"
+  "a Circus"
+  "a City Hall"
+  "a Climbing Gym"
+  "a Clothing Store"
+  "a Club House"
+  "a Cocktail Bar"
+  "a Coffee Shop"
+  "a College Bookstore"
+  "a College Cafeteria"
+  "a College Classroom"
+  "a College Gym"
+  "a College Lab"
+  "a College Library"
+  "a College Residence Hall"
+  "a Comedy Club"
+  "a Comic Shop"
+  "a Community Center"
+  "a Concert Hall"
+  "a Conference Room"
+  "a Convenience Store"
+  "a Convention Center"
+  "a Cosmetics Shop"
+  "a Costume Shop"
+  "a Country Dance Club"
+  "a Courthouse"
+  "a Coworking Space"
+  "a Credit Union"
+  "a Creperie"
+  "a Cricket Ground"
+  "a Cuban Restaurant"
+  "a Cultural Center"
+  "a Cupcake Shop"
+  "a Czech Restaurant"
+  "a Dance Studio"
+  "a Daycare"
+  "a Deli"
+  "a Dentist's Office"
+  "a Department Store"
+  "a Design Studio"
+  "a Dessert Shop"
+  "a Dim Sum Restaurant"
+  "a Diner"
+  "a Disc Golf"
+  "a Discount Store"
+  "a Distillery"
+  "a Dive Bar"
+  "a Doctor's Office"
+  "a Donut Shop"
+  "a Driving School"
+  "a Drugstore"
+  "a Dry Cleaner"
+  "a Dumpling Restaurant"
+  "a Eastern European Restaurant"
+  "an Electronics Store"
+  "an Elementary School"
+  "an Embassy"
+  "an Emergency Room"
+  "an English Restaurant"
+  "an Ethiopian Restaurant"
+  "an Eye Doctor"
+  "a Fabric Shop"
+  "a Factory"
+  "a Falafel Restaurant"
+  "a Farm"
+  "a Farmers Market"
+  "a Fast Food Restaurant"
+  "a Filipino Restaurant"
+  "a Fire Station"
+  "a Fireworks Store"
+  "a Fish & Chips Shop"
+  "a Fish Market"
+  "a Fishing Store"
+  "a Flea Market"
+  "a Flower Shop"
+  "a Fondue Restaurant"
+  "a Food Court"
+  "a Food Truck"
+  "a Football Stadium"
+  "a Fraternity House"
+  "a French Restaurant"
+  "a Fried Chicken Joint"
+  "a Funeral Home"
+  "a Gas Station"
+  "a Gastropub"
+  "a Gay Bar"
+  "a German Restaurant"
+  "a Gift Shop"
+  "a Gluten-free Restaurant"
+  "a Go Kart Track"
+  "a Golf Course"
+  "a Greek Restaurant"
+  "a Grocery Store"
+  "a Gun Range"
+  "a Gun Shop"
+  "a Gym"
+  "a Hardware Store"
+  "a Hawaiian Restaurant"
+  "a Health Food Store"
+  "a High School"
+  "a Himalayan Restaurant"
+  "a Hindu Temple"
+  "a History Museum"
+  "a Hobby Shop"
+  "a Hockey Arena"
+  "a Hockey Field"
+  "a Hookah Bar"
+  "a Hospital"
+  "a Hot Dog Joint"
+  "a Hotel Bar"
+  "a Hotpot Restaurant"
+  "a Hungarian Restaurant"
+  "an Ice Cream Shop"
+  "an Indian Restaurant"
+  "an Indie Movie Theater"
+  "an Indie Theater"
+  "an Indonesian Restaurant"
+  "an Internet Cafe"
+  "an Irish Pub"
+  "an Italian Restaurant"
+  "a Japanese Restaurant"
+  "a Jazz Club"
+  "a Jewelry Store"
+  "a Juice Bar"
+  "a Karaoke Bar"
+  "a Knitting Store"
+  "a Korean Restaurant"
+  "a Kosher Restaurant"
+  "a Laboratory"
+  "a Laundromat"
+  "a Library"
+  "a Lighthouse"
+  "a Lingerie Store"
+  "a Liquor Store"
+  "a Locksmith"
+  "a Lounge"
+  "a Mac & Cheese Joint"
+  "a Malaysian Restaurant"
+  "a Marijuana Dispensary"
+  "a Martial Arts Dojo"
+  "a Massage Studio"
+  "a Mattress Store"
+  "a Medical Center"
+  "a Mediterranean Restaurant"
+  "a Mental Health Office"
+  "a Mexican Restaurant"
+  "a Middle Eastern Restaurant"
+  "a Middle School"
+  "a Mini Golf Course"
+  "a Mobile Phone Shop"
+  "a Modern European Restaurant"
+  "a Molecular Gastronomy Restaurant"
+  "a Monastery"
+  "a Mongolian Restaurant"
+  "a Moroccan Restaurant"
+  "a Mosque"
+  "a Motorcycle Shop"
+  "a Movie Theater"
+  "a Multiplex"
+  "a Museum"
+  "a Music School"
+  "a Music Store"
+  "a Nail Salon"
+  "a New American Restaurant"
+  "a Newsstand"
+  "a Nightclub"
+  "a Noodle House"
+  "a Nursery School"
+  "an Opera House"
+  "an Optical Shop"
+  "an Organic Grocery"
+  "an Outlet Store"
+  "a Paintball Field"
+  "a Pakistani Restaurant"
+  "a Pawn Shop"
+  "a Perfume Shop"
+  "a Persian Restaurant"
+  "a Peruvian Restaurant"
+  "a Pet Store"
+  "a Photography Lab"
+  "a Piano Bar"
+  "a Pie Shop"
+  "a Piercing Parlor"
+  "a Pizza Place"
+  "a Planetarium"
+  "a Police Station"
+  "a Polish Restaurant"
+  "a Pool Hall"
+  "a Pool"
+  "a Portuguese Restaurant"
+  "a Post Office"
+  "a Prayer Room"
+  "a Preschool"
+  "a Private School"
+  "a Pub"
+  "a Public Art"
+  "a Racetrack"
+  "a Radio Station"
+  "a Record Shop"
+  "a Recording Studio"
+  "a Recreation Center"
+  "a Recruiting Agency"
+  "a Religious School"
+  "a Rock Club"
+  "a Roller Rink"
+  "a Romanian Restaurant"
+  "a Rugby Pitch"
+  "a Russian Restaurant"
+  "a Sake Bar"
+  "a Salon"
+  "a Salsa Club"
+  "a Sandwich Place"
+  "a Scandinavian Restaurant"
+  "a Science Museum"
+  "a Sculpture Garden"
+  "a Seafood Restaurant"
+  "a Shoe Repair"
+  "a Shoe Store"
+  "a Shrine"
+  "a Skate Park"
+  "a Skating Rink"
+  "a Snack Place"
+  "a Soccer Field"
+  "a Soccer Stadium"
+  "a Sorority House"
+  "a Soup Place"
+  "a South American Restaurant"
+  "a Southern Restaurant"
+  "a Souvenir Shop"
+  "a Souvlaki Shop"
+  "a Spa"
+  "a Spanish Restaurant"
+  "a Speakeasy"
+  "a Sporting Goods Shop"
+  "a Sports Bar"
+  "a Sports Club"
+  "a Squash Court"
+  "a Sri Lankan Restaurant"
+  "a Stadium"
+  "a Stationery Store"
+  "a Steakhouse"
+  "a Storage Facility"
+  "a Supermarket"
+  "a Sushi Restaurant"
+  "a Swiss Restaurant"
+  "a Synagogue"
+  "a Taco Place"
+  "a Tanning Salon"
+  "a Tapas Restaurant"
+  "a Tatar Restaurant"
+  "a Tattoo Parlor"
+  "a Tea Room"
+  "a Temple"
+  "a Tennis Court"
+  "a Thai Restaurant"
+  "a Theater"
+  "a Theme Park"
+  "a Thrift Store"
+  "a Tibetan Restaurant"
+  "a Town Hall"
+  "a Toy Store"
+  "a Track Stadium"
+  "a Travel Agency"
+  "a Turkish Restaurant"
+  "a TV Station"
+  "a Ukrainian Restaurant"
+  "a Used Bookstore"
+  "a Vegan Restaurant"
+  "a Veterinarian's Office"
+  "a Video Game Store"
+  "a Video Store"
+  "a Vietnamese Restaurant"
+  "a Volleyball Court"
+  "a Warehouse Store"
+  "a Warehouse"
+  "a Watch Repair Shop"
+  "a Water Park"
+  "a Whisky Bar"
+  "a Wine Bar"
+  "a Wine Shop"
+  "a Winery"
+  "a Wings Joint"
+  "a Yoga Studio"
+  "a Zoo")
--- a/package.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/package.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -57,11 +57,19 @@
     :make-food
     :food/energy
 
+    :trigger
+    :trigger?
+    :trigger/text
+
+    :ruin
+    :make-ruin
+
     :coords
     :coords/x
     :coords/y
     :coords?
     :coords-lookup
+    :coords-nearby
     :coords-move-entity
 
     :holdable
--- a/src/aspects/coordinates.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/src/aspects/coordinates.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -28,7 +28,7 @@
   (when (within-bounds-p x y)
     (aref *world-contents* x y)))
 
-(defun nearby (entity &optional (radius 1))
+(defun coords-nearby (entity &optional (radius 1))
   (remove entity
           (iterate (with x = (coords/x entity))
                    (with y = (coords/y entity))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/aspects/trigger.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -0,0 +1,3 @@
+(in-package :ap.entities)
+
+(define-aspect trigger text)
--- a/src/aspects/visible.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/src/aspects/visible.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -1,4 +1,3 @@
 (in-package :ap.entities)
 
-
 (define-aspect visible glyph color)
--- a/src/config.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/src/config.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -6,3 +6,8 @@
 (defparameter *noise-scale* 0.03)
 (defparameter *noise-seed-x* (random 1000.0))
 (defparameter *noise-seed-y* (random 1000.0))
+
+(defparameter *ruin-density* 1/1000)
+(defparameter *ruin-size-mean* 10.0)
+(defparameter *ruin-size-dev* 2.0)
+(defparameter *graffiti-chance* 1/10)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/entities/ruin.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -0,0 +1,39 @@
+(in-package :ap.entities)
+
+(defparameter *venues*
+  (read-file-into-form "data/venues.lisp"))
+
+(define-entity ruin (coords trigger))
+
+(defun random-ruin-text ()
+  (format nil "You see the ruins of ~A.~2%~A"
+          (random-elt *venues*)
+          (if (randomp ap::*graffiti-chance*)
+            (format nil "Someone has graffitied \"~A\" on the wall."
+                    (string-upcase (random-elt #("give up"
+                                                 "why me?"
+                                                 "tom <3 alice"
+                                                 "dave was here"
+                                                 "420"
+                                                 "goodbye"
+                                                 "5823"
+                                                 "jesus saves"
+                                                 "hail satan"
+                                                 "run"
+                                                 "head north"
+                                                 "head south"
+                                                 "were damned"
+                                                 "hope is lost"
+                                                 "turn back"))))
+            (random-elt #("Maybe there's something useful left."
+                          "Perhaps you should scavenge?"
+                          "It brings back fond memories."
+                          "Your parents used to live near one of these."
+                          "The world has suddenly gotten quiet."
+                          "A remnant of a happier time.")))))
+
+(defun make-ruin (x y)
+  (create-entity 'ruin
+    :coords/x x
+    :coords/y y
+    :trigger/text (random-ruin-text)))
--- a/src/main.lisp	Sat Jan 07 19:17:56 2017 +0000
+++ b/src/main.lisp	Sat Jan 07 20:50:14 2017 +0000
@@ -17,6 +17,7 @@
 (defparameter *height* nil)
 
 (defparameter *terrain* nil)
+(defparameter *structures* nil)
 
 (defparameter *view-x* nil)
 (defparameter *view-y* nil)
@@ -24,6 +25,19 @@
 (defparameter *player* nil)
 
 
+;;;; Colors -------------------------------------------------------------------
+(defcolors
+  (+white-black+  charms/ll:COLOR_WHITE   charms/ll:COLOR_BLACK)
+  (+blue-black+   charms/ll:COLOR_BLUE    charms/ll:COLOR_BLACK)
+  (+cyan-black+   charms/ll:COLOR_CYAN    charms/ll:COLOR_BLACK)
+  (+yellow-black+ charms/ll:COLOR_YELLOW  charms/ll:COLOR_BLACK)
+  (+green-black+  charms/ll:COLOR_GREEN   charms/ll:COLOR_BLACK)
+  (+pink-black+   charms/ll:COLOR_MAGENTA charms/ll:COLOR_BLACK)
+
+  (+black-white+  charms/ll:COLOR_BLACK   charms/ll:COLOR_WHITE)
+  )
+
+
 ;;;; Heightmap ----------------------------------------------------------------
 ;;; TODO: Switch to something less samey
 
@@ -49,18 +63,79 @@
     (noise-heightmap heightmap)
     heightmap))
 
+(defun random-coord ()
+  (random *map-size*))
 
-;;;; Colors -------------------------------------------------------------------
-(defcolors
-  (+white-black+  charms/ll:COLOR_WHITE   charms/ll:COLOR_BLACK)
-  (+blue-black+   charms/ll:COLOR_BLUE    charms/ll:COLOR_BLACK)
-  (+cyan-black+   charms/ll:COLOR_CYAN    charms/ll:COLOR_BLACK)
-  (+yellow-black+ charms/ll:COLOR_YELLOW  charms/ll:COLOR_BLACK)
-  (+green-black+  charms/ll:COLOR_GREEN   charms/ll:COLOR_BLACK)
-  (+pink-black+   charms/ll:COLOR_MAGENTA charms/ll:COLOR_BLACK)
+(defun underwaterp (height)
+  (< height -0.05))
+
+(defun deepwaterp (height)
+  (< height -0.20))
+
+
+;;;; Ruins --------------------------------------------------------------------
+(defun make-empty-structures ()
+  (make-array (list *map-size* *map-size*)))
+
+(defun passablep (structure)
+  (if (member structure '(:wall))
+    nil
+    t))
+
+(defun add-intact-ruin (width height start-x start-y)
+  (iterate (for-nested ((x :from start-x :below (+ start-x width))
+                        (y :from start-y :below (+ start-y height))))
+           (setf (aref *structures* x y) :floor))
+  (iterate (repeat width)
+           (for x :from start-x)
+           (setf (aref *structures* x start-y) :wall
+                 (aref *structures* x (+ start-y height -1)) :wall))
+  (iterate (repeat height)
+           (for y :from start-y)
+           (setf (aref *structures* start-x y) :wall
+                 (aref *structures* (+ start-x width) y) :wall)))
 
-  (+black-white+  charms/ll:COLOR_BLACK   charms/ll:COLOR_WHITE)
-  )
+(defun add-ruin-door (width height start-x start-y)
+  (setf (aref *structures* (+ start-x (random width))
+              (if (randomp)
+                start-y
+                (+ start-y height -1)))
+        nil))
+
+(defun decay-ruin (width height start-x start-y condition)
+  (iterate (for-nested ((x :from start-x :to (+ start-x width))
+                        (y :from start-y :below (+ start-y height))))
+           (when (or (randomp condition)
+                     (and (deepwaterp (aref *terrain* x y))
+                          (not (eq :wall (aref *structures* x y)))))
+             (setf (aref *structures* x y) nil))))
+
+(defun place-ruin-food (width height start-x start-y)
+  (iterate (repeat (random 4))
+           (make-food
+             (random-range (1+ start-x) (+ start-x width))
+             (random-range (1+ start-y) (+ start-y height)))))
+
+(defun add-ruin-trigger (width height start-x start-y)
+  (make-ruin (+ start-x (truncate width 2))
+             (+ start-y (truncate height 2))))
+
+(defun add-ruin ()
+  (let ((x (clamp 0 (- *map-size* 50) (random-coord)))
+        (y (clamp 0 (- *map-size* 50) (random-coord)))
+        (width (max 5 (truncate (random-gaussian *ruin-size-mean* *ruin-size-dev*))))
+        (height (max 5 (truncate (random-gaussian *ruin-size-mean* *ruin-size-dev*))))
+        (condition (random-range 0.1 0.8)))
+    (add-intact-ruin width height x y)
+    (add-ruin-door width height x y)
+    (decay-ruin width height x y condition)
+    (place-ruin-food width height x y)
+    (add-ruin-trigger width height x y)))
+
+(defun fill-ruins ()
+  (iterate
+    (repeat (round (* *ruin-density* *map-size* *map-size*)))
+    (add-ruin)))
 
 
 ;;;; Intro --------------------------------------------------------------------
@@ -124,9 +199,6 @@
 
 
 ;;;; World Generation ---------------------------------------------------------
-(defun underwaterp (height)
-  (< height 0.05))
-
 (defun generate-terrain ()
   (setf *terrain* (generate-heightmap)
         *view-x* 0 *view-y* 0))
@@ -140,15 +212,19 @@
                                 *map-size*
                                 *map-size*)))
     (until (zerop remaining))
-    (for x = (random *map-size*))
-    (for y = (random *map-size*))
+    (for x = (random-coord))
+    (for y = (random-coord))
     (when (not (underwaterp (aref *terrain* x y)))
       (make-food x y)
       (decf remaining))))
 
+(defun generate-structures ()
+  (setf *structures* (make-empty-structures))
+  (fill-ruins))
+
 (defun generate-world ()
   (clear-entities)
-  (with-dims (30 (+ 2 3))
+  (with-dims (30 (+ 2 4))
     (with-panel-and-window
         (pan win *width* *height*
              (center *width* *screen-width*)
@@ -157,10 +233,13 @@
       (progn (write-string-left win "Generating terrain..." 1 1)
              (redraw)
              (generate-terrain))
-      (progn (write-string-left win "Placing food..." 1 2)
+      (progn (write-string-left win "Generating structures..." 1 2)
+             (redraw)
+             (generate-structures))
+      (progn (write-string-left win "Placing food..." 1 3)
              (redraw)
              (place-food))
-      (progn (write-string-left win "Spawning player..." 1 3)
+      (progn (write-string-left win "Spawning player..." 1 4)
              (redraw)
              (spawn-player))))
   (world-map))
@@ -190,7 +269,13 @@
         ((< height  0.05) (values #\` +yellow-black+)) ; sand
         ((< height  0.40) (values #\. +white-black+)) ; dirt
         ((< height  0.55) (values #\^ +white-black+)) ; hills
-        (t                (values #\# +white-black+)))) ; mountains
+        (t                (values #\* +white-black+))))
+
+(defun structure-char (contents)
+  (case contents
+    (:wall #\#)
+    (:floor #\_)))
+
 
 (defun clamp-view (coord size)
   (clamp 0 (- *map-size* size 1) coord))
@@ -204,18 +289,27 @@
                (coords/x *player*)
                (coords/y *player*)))
 
+
 (defun render-items (window)
-  (let ((items (-<> (coords-lookup (coords/x *player*)
-                                   (coords/y *player*))
-                 (remove-if-not #'holdable? <>))))
+  (let* ((x (coords/x *player*))
+         (y (coords/y *player*))
+         (items (-<> (coords-lookup x y)
+                  (remove-if-not #'holdable? <>)))
+         (here-string (if (underwaterp (aref *terrain* x y))
+                        "floating here"
+                        "here")))
     (when items
       (if (= (length items) 1)
         (write-string-left
           window
-          (format nil "You see ~A here" (holdable/description (first items)))
+          (format nil "You see ~A ~A"
+                  (holdable/description (first items))
+                  here-string)
           0 0)
         (progn
-          (write-string-left window "The following things are here:" 0 0)
+          (write-string-left window (format nil "The following things are ~A:"
+                                            here-string)
+                             0 0)
           (iterate
             (for item :in items)
             (for y :from 1)
@@ -226,15 +320,22 @@
 (defun render-map (window)
   (iterate
     (with terrain = *terrain*)
+    (with structures = *structures*)
     (with vx = *view-x*)
     (with vy = *view-y*)
-    (for-nested ((sx :from 0 :below *width*)
-                 (sy :from 0 :below *height*)))
+    (for-nested ((sx :from 0 :below (1- *width*))
+                 (sy :from 0 :below (1- *height*))))
     (for x = (+ sx vx))
     (for y = (+ sy vy))
-    (for (values glyph color) = (terrain-char (aref terrain x y)))
-    (with-color (window color)
-      (charms:write-char-at-point window glyph sx sy))
+
+    (for (values terrain-glyph terrain-color) = (terrain-char (aref terrain x y)))
+    (with-color (window terrain-color)
+      (charms:write-char-at-point window terrain-glyph sx sy))
+
+    (for structure-glyph = (structure-char (aref structures x y)))
+    (when structure-glyph
+      (charms:write-char-at-point window structure-glyph sx sy))
+
     (for entities = (coords-lookup x y))
     (for entity = (if (member *player* entities)
                     *player*
@@ -268,10 +369,11 @@
 
 
 (defun move-player (dx dy)
-  (let ((player *player*))
-    (coords-move-entity player
-                        (+ (coords/x player) dx)
-                        (+ (coords/y player) dy))))
+  (let* ((player *player*)
+         (dest-x (+ (coords/x player) dx))
+         (dest-y (+ (coords/y player) dy)))
+    (when (passablep (aref *structures* dest-x dest-y))
+      (coords-move-entity player dest-x dest-y))))
 
 (defun world-map-input (window)
   (case (charms:get-char window)
@@ -283,6 +385,13 @@
     (:down  (move-player 0 1) :tick)))
 
 
+(defun check-triggers ()
+  (iterate (for trigger :in (-<> *player*
+                              (coords-nearby <> 10)
+                              (remove-if-not #'trigger? <>)))
+           (popup (trigger/text trigger))
+           (destroy-entity trigger)))
+
 (defun world-map ()
   (with-dims ((- *screen-width* 2) (- *screen-height* 1))
     (with-panels-and-windows
@@ -301,7 +410,8 @@
           (if (ap.flavor:flavorp)
             (popup (ap.flavor:random-flavor))
             (case (world-map-input bar-win)
-              (:tick (tick-player *player*))
+              (:tick (tick-player *player*)
+                     (check-triggers))
               (:quit (return))
               (:help (popup *help*))))))))
   nil)