src/robots/rpg-shopkeeper.lisp @ bb25eaad4d17
Add bin to .hgignore.
author |
Steve Losh <steve@stevelosh.com> |
date |
Sat, 04 Nov 2017 13:49:42 -0400 |
parents |
7dd4cf568944 |
children |
af9390b6947b |
(in-package :magitek.robots.rpg-shopkeeper)
(named-readtables:in-readtable :nutbags-readtable)
; https://twitter.com/rpg_shopkeeper
;;;; Utils --------------------------------------------------------------------
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun muffenize (str)
(symb '* (string-upcase (substitute #\- #\space str)) '*)))
(defmacro maybe (expr &optional (chance 0.5))
`(when (randomp ,chance)
,expr))
;;;; General ------------------------------------------------------------------
(define-rule (bonus :distribution (:zipf :exponent 1.8))
1 2 3 4 5)
;;;; Monsters -----------------------------------------------------------------
(defclass* monster ()
(singular multiplier plural adjective))
(defun make-monster (singular multiplier plural adjective)
(make-instance 'monster
:singular singular
:multiplier multiplier
:plural plural
:adjective adjective))
(defmethod print-object ((o monster) s)
(print-unreadable-object (o s :type t)
(princ (monster-singular o) s)))
(defmacro define-monster (singular multiplier plural adjective)
`(defparameter ,(muffenize singular)
(make-monster
,singular
,multiplier
,plural
,adjective)))
(define-monster "goblin" 1.00 "goblins" "goblin")
(define-monster "kobold" 1.00 "kobolds" "kobold")
(define-monster "elf" 1.00 "elves" "elven")
(define-monster "dwarf" 1.00 "dwarves" "dwarven")
(define-monster "halfling" 1.00 "halflings" "halfling")
(define-monster "gnome" 1.00 "gnomes" "gnomish")
(define-monster "gnoll" 1.10 "gnolls" "gnollish")
(define-monster "ogre" 1.20 "ogres" "ogre")
(define-monster "troll" 1.60 "trolls" "troll")
(define-monster "vampire" 5.00 "vampires" "vampire")
(define-monster "dragon" 9.00 "dragons" "dragon")
(define-rule (monster :distribution :weighted)
(1.00 !*goblin*)
(1.00 !*kobold*)
(1.00 !*elf*)
(1.00 !*dwarf*)
(1.00 !*halfling*)
(1.00 !*gnome*)
(1.00 !*ogre*)
(1.00 !*troll*)
(1.00 !*gnoll*)
(0.10 !*vampire*)
(0.01 !*dragon*))
;;;; Materials ----------------------------------------------------------------
(defclass* material ()
(kind name multiplier))
(defun make-material (kind name multiplier)
(make-instance 'material :kind kind :name name :multiplier multiplier))
(defmethod print-object ((o material) s)
(print-unreadable-object (o s :type t)
(princ (material-name o) s)))
(defmacro define-material (kind name multiplier)
`(defparameter ,(muffenize name)
(make-material ,kind ,name ,multiplier)))
(define-material :metal "iron" 1.0)
(define-material :metal "steel" 1.5)
(define-material :metal "silver" 5.0)
(define-material :metal "meteoric iron" 10.0)
(define-material :metal "mithril" 50.0)
(define-material :metal "adamantine" 60.0)
(define-material :textile "cloth" 1.0)
(define-material :textile "leather" 1.5)
(define-material :textile "silk" 2.0)
(define-material :textile "spider silk" 6.0)
(define-material :wood "pine" 1.0) ; http://www.wood-database.com/wood-articles/bow-woods/
(define-material :wood "poplar" 1.2)
(define-material :wood "walnut" 1.3)
(define-material :wood "maple" 2.0)
(define-material :wood "oak" 3.0)
(define-material :wood "yew" 5.0)
(define-material :wood "rosewood" 10.0)
(define-material :skin "leather" 1.0)
(define-rule (metal :distribution :zipf)
!*iron*
!*steel*
!*silver*
!*meteoric-iron*
!*mithril*
!*adamantine*)
(define-rule (textile :distribution :weighted)
(1.00 !*cloth*)
(0.80 !*silk*)
(0.05 !*spider-silk*))
(define-rule (wood :distribution (:zipf :exponent 0.8))
!*pine*
!*poplar*
!*walnut*
!*maple*
!*oak*
!*yew*
!*rosewood*)
(define-rule (skin :distribution :weighted)
(3 !*leather*)
(1 !(let ((monster (monster)))
(make-material :skin
$(!(monster-singular monster) :. "skin")
(1+ (monster-multiplier monster))))))
(define-string material-picture-wood
"carved with images of"
"covered in carvings of"
"painted with pictures of")
(define-string material-picture-metal
"engraved with an image of"
"etched with a picture of"
"painted with images of")
(define-string material-picture-textile
"sewn with pictures of"
"embroidered with images of")
(define-string material-picture-skin
"dyed with images of"
"sewn with pictures of"
"embossed with drawings of")
(defun material-picture (material)
(ecase (material-kind material)
(:metal (material-picture-metal))
(:wood (material-picture-wood))
(:textile (material-picture-textile))
(:skin (material-picture-skin))))
;;;; Roles --------------------------------------------------------------------
(define-rule role
"cleric"
"warrior"
"wizard"
"bard"
"jester"
"priest"
"hermit"
"rogue")
;;;; Elements -----------------------------------------------------------------
(define-rule element
("good" "holy")
("evil" "unholy")
("ice" "icy")
("fire" "flaming")
("lightning" "electrified")
("poison" "venomous"))
;;;; Pieces -------------------------------------------------------------------
(defclass* piece ()
(name base-value))
(defmethod print-object ((o piece) stream)
(print-unreadable-object (o stream :type t)
(princ (piece-name o) stream)))
(defmacro define-piece (name base-value)
`(defparameter ,(muffenize name)
(make-instance 'piece
:name ,name
:base-value ,base-value)))
;;;; Ornaments ----------------------------------------------------------------
(define-string ornament-scene
"a strange land"
([monster monster-plural] "fighting" [monster monster-plural])
("a famous" [monster monster-singular] role))
(define-string word-adjective
"mysterious"
"ancient"
"glowing")
(define-string word-noun
"runes"
"symbols")
(define-string ornament-words
("covered in" word-adjective word-noun))
(define-rule (ornament :arguments (material))
!$(!(material-picture material) ornament-scene)
!$ornament-words
nil)
;;;; Enchantments -------------------------------------------------------------
(define-rule enchant-armor
(:protection monster)
(:resistance element)
(:bonus bonus))
(define-rule enchant-weapon
(:slaying monster)
(:element element)
(:bonus bonus))
(defun enchantment-multiplier (enchantment)
(declare (optimize (debug 3)))
(ecase (first enchantment)
((nil) 1.0)
((:protection :slaying) (+ 2.0 (monster-multiplier (second enchantment))))
((:resistance :element) 5.0)
(:bonus (expt 2.0 (second enchantment)))))
(defun enchanted-armor-description (base enchantment)
(trivia:ematch enchantment
(`(:protection ,monster)
(format nil "~A of protection from ~A" base (monster-plural monster)))
(`(:resistance (,element ,_))
(format nil "~A of ~A resistance" base element))
(`(:bonus ,val)
(format nil "+~D ~A" val base))))
(defun enchanted-weapon-description (base enchantment)
(trivia:ematch enchantment
(`(:slaying ,monster)
(format nil "~A of ~A-slaying" base (monster-singular monster)))
(`(:element (,_ ,element))
(format nil "~A ~A" element base))
(`(:bonus ,val)
(format nil "+~D ~A" val base))))
;;;; Armor --------------------------------------------------------------------
(defclass* armor ()
(material piece enchantment ornament))
(define-with-macro armor
material piece enchantment ornament)
(define-piece "scale mail" 50)
(define-piece "ring mail" 50)
(define-piece "chain mail" 150)
(define-piece "breastplate" 200)
(define-piece "plate mail" 350)
(define-rule (piece-armor :distribution (:zipf :exponent 0.6))
!*scale-mail*
!*ring-mail*
!*chain-mail*
!*breastplate*
!*plate-mail*)
(define-piece "robe" 10)
(define-piece "shirt" 5)
(define-piece "pants" 5)
(define-piece "dress" 9)
(define-piece "skirt" 5)
(define-rule piece-clothing
!*robe*
!*shirt*
!*pants*
!*dress*
!*skirt*)
(define-rule (vanilla-armor :distribution :weighted)
(1 (metal piece-armor))
(1 (textile piece-clothing))
(0.2 (skin piece-clothing)))
(define-rule (armor% :distribution :weighted)
(2 (vanilla-armor nil))
(1 (vanilla-armor enchant-armor)))
(defun armor ()
(destructuring-bind ((material piece) enchantment)
(armor%)
(make-instance 'armor
:material material
:piece piece
:enchantment enchantment
:ornament (ornament material))))
(defun armor-value (armor)
(with-armor (armor)
(* (+ (* (-<> piece piece-base-value)
(-<> material material-multiplier))
(if enchantment 100 0)
(if ornament 10 0))
(enchantment-multiplier enchantment)
(if ornament 1.5 1.0))))
(defun vanilla-armor-description (vanilla-armor)
(format nil "~A ~A"
(-<> vanilla-armor armor-material material-name)
(-<> vanilla-armor armor-piece piece-name)))
(defun armor-description (armor)
(let ((vanilla-description (vanilla-armor-description armor))
(enchantment (armor-enchantment armor))
(ornament (armor-ornament armor)))
(concatenate 'string
(if enchantment
(enchanted-armor-description vanilla-description
enchantment)
vanilla-description)
(if ornament
(format nil ", ~A" ornament)
""))))
; ;;;; Weapons ------------------------------------------------------------------
(defclass* weapon ()
(material piece enchantment ornament))
(define-with-macro weapon
material piece enchantment ornament)
(define-piece "dagger" 5)
(define-piece "longsword" 50)
(define-piece "short sword" 20)
(define-piece "hand axe" 30)
(define-piece "battleaxe" 80)
(define-piece "spear" 20)
(define-piece "halberd" 80)
(define-piece "scythe" 50)
(define-piece "scimitar" 50)
(define-piece "lance" 70)
(define-piece "warhammer" 80)
(define-piece "staff" 5)
(define-piece "mace" 25)
(define-piece "flail" 45)
(define-piece "sling" 5)
(define-rule piece-melee
!*dagger*
!*longsword*
!*short-sword*
!*hand-axe*
!*battleaxe*
!*spear*
!*halberd*
!*scythe*
!*scimitar*
!*lance*
!*warhammer*
!*staff*
!*mace*
!*flail*)
(define-piece "shortbow" 35)
(define-piece "longbow" 60)
(define-piece "crossbow" 80)
(define-piece "compound bow" 80)
(define-rule (piece-bow :distribution :weighted)
(2 !*shortbow*)
(2 !*longbow*)
(1 !*crossbow*)
(1 !*compound-bow*))
(define-rule piece-sling
!*sling*)
(define-rule (vanilla-weapon :distribution :weighted)
(2 (metal piece-melee))
(1 (wood piece-bow))
(0.5 (skin piece-sling)))
(define-rule (weapon% :distribution :weighted)
(2 (vanilla-weapon nil))
(1 (vanilla-weapon enchant-weapon)))
(defun weapon ()
(destructuring-bind ((material piece) enchantment)
(weapon%)
(make-instance 'weapon
:material material
:piece piece
:enchantment enchantment
:ornament (ornament material))))
(defun weapon-value (weapon)
(with-weapon (weapon)
(* (+ (* (-<> piece piece-base-value)
(-<> material material-multiplier))
(if enchantment 100 0)
(if ornament 10 0))
(enchantment-multiplier enchantment)
(if ornament 1.5 1.0))))
(defun vanilla-weapon-description (vanilla-weapon)
(with-weapon (vanilla-weapon)
(format nil "~A ~A"
(-<> material material-name)
(-<> piece piece-name))))
(defun weapon-description (weapon)
(let ((vanilla-description (vanilla-weapon-description weapon))
(enchantment (weapon-enchantment weapon))
(ornament (weapon-ornament weapon)))
(concatenate 'string
(if enchantment
(enchanted-weapon-description vanilla-description
enchantment)
vanilla-description)
(if ornament
(format nil ", ~A" ornament)
""))))
;;;; Flavor -------------------------------------------------------------------
(define-string fluid
"blood"
"slime")
(define-string sales-pitch
"Only used once!"
"Brand new!"
("That" fluid "will wash right off...")
"You won't find a better deal!"
"This could save your life!"
"It breaks my heart to part with it..."
"Its last owner... doesn't need it anymore."
("I bought it from" [monster monster-adjective a] role :. ".")
"Look at the workmanship!"
"This is gonna go fast!")
;;;; Prices -------------------------------------------------------------------
(defun round-to (n sigfigs)
(let* ((digits (ceiling (log n 10)))
(div (expt 10 (max 0 (- digits sigfigs)))))
(-<> n
(round <> div)
(* <> div))))
(defun sanitize-price (price)
(let ((price (round-to price 3)))
(cond
((< price 50) price)
((in-range-p 50 price 100) (* 5 (round price 5)))
((in-range-p 100 price 1000) (* 10 (round price 10)))
(t price))))
(defun item-value (item)
(-<> (etypecase item
(armor (armor-value item))
(weapon (weapon-value item)))
(sanitize-price <>)
(format nil "~:D" <>)))
(define-string for-the-low-price
"only"
"just"
"yours for the low, low price of"
"for you, only"
"on clearance for just"
"a steal at"
"on sale for")
;;;; Main ---------------------------------------------------------------------
(define-rule item
weapon
armor)
(defun item-description (item)
(etypecase item
(armor (armor-description item))
(weapon (weapon-description item))))
(defun offer ()
(let ((item (item)))
$("FOR SALE:" [!item item-description cap] :. "."
[for-the-low-price cap] [!item item-value] "GP."
:. #\newline #\newline :.
sales-pitch)))
(defun dump ()
(loop :repeat 10 :do
(terpri)
(terpri)
(print '-------------------------------)
(terpri)
(princ (offer))))
;;;; API ----------------------------------------------------------------------
(defun random-string ()
(offer))