;;; ;;; Atlantis is a framework for creating multi-user dungeon worlds. ;;; This is the Common Lisp implementation. ;;; ;;; This module is responsible for the interactive user interface. All ;;; in-game UI should be done here. (Pre-game UI goes into atlantis.lisp.) ;;; ;;; Licensed under the terms of the MIT license. ;;; author: Daniel Vedder ;;; date: 21/05/2015 ;;; ;; TODO Out-source all game logic to other modules ;; (This module should be purely UI) (defun play-game (player-name) "The main game loop" (let ((player (get-game-object 'player player-name))) (clear-screen) ;; Initialize the player if necessary (unless player (setf player (create-player player-name)) (add-player player)) ;; The actual game loop (clear-screen) (let ((place (get-game-object 'place (player-place player)))) (describe-place place (player-night-vision player)) (input-string command) (while (not (or (equalp command "quit") (equalp command "exit"))) (game-command command player) (input-string command)) (format t "~&Goodbye!")))) (defun create-player (player-name) "The user creates a new player" ;; XXX This function feels somewhat ugly - any possibility of a cleanup? (let* ((start-player (get-game-object 'player "Start")) (player (if start-player (copy-player start-player) (make-player :name player-name :place (random-elt (list-world-objects 'place))))) (char-attr '((strength 0) (dexterity 0) (constitution 0) (intelligence 0))) (items NIL) (weapon "") (character-points NIL)) (format t "~&The name you have chosen is not registered on this game.") (unless (y-or-n-p "~&Create a new player?") (start-menu) (quit)) ;; Chose race and class (format t "~&Please chose a race:") (setf (player-race player) (choose-option (list-world-objects 'race))) (format t "~&Please chose a class:") (setf (player-class player) (choose-option (list-world-objects 'character-class))) (dolist (i (character-class-special-item (get-game-object 'character-class (player-class player)))) (set-object-attribute player 'item i)) ;; Set character attributes (while (or (< (reduce #'+ character-points) 24) ; XXX magic number! (not (set-p character-points))) (set-list (1+ (random 20)) a b c d) (setf character-points (list a b c d))) (setf text " Now distribute your attribute points. Random numbers have been chosen, you may assign one number to each of the following attributes:") (format t "~&~A~%~A~%~%The numbers are:" text (string-from-list (keys char-attr))) ;; XXX I should replace simple-input with something offering 'magic' (?) (do* ((i 0 (1+ i)) (attr (safe-nth i (keys char-attr)) (safe-nth i (keys char-attr))) (val (cassoc attr char-attr) (cassoc attr char-attr))) ((= i (length char-attr)) player) (format t "~&~A" (string-from-list character-points)) (simple-input val (concatenate 'string (symbol-name attr) ":")) (while (not (member val character-points)) (format t "~&Sorry, invalid number chosen. Please reenter:") (simple-input val (concatenate 'string (symbol-name attr) ":"))) (let ((player-fn (build-symbol "player-" attr))) ;; XXX Kludge ?! (eval `(setf (,player-fn ,player) ,val))) (setf character-points (remove-if #'(lambda (x) (= x val)) character-points))))) (defun describe-place (p &optional (show-dark t)) "Print out a complete description of place p" ;; XXX This has become slightly ugly with the addition of darkness... (when (stringp p) (setf p (get-game-object 'place p))) (objectify-place-monsters p) (format t "~&~A" (string-upcase (place-name p))) (format t "~&~%~A" (if (and (place-dark p) (not show-dark)) "You do not see a thing in here. It's too dark!" (place-description p))) (format t "~&~%Neighbouring places: ~A" (string-from-list (place-neighbour p))) (unless (and (place-dark p) (not show-dark)) (format t "~&Players present: ~A" (string-from-list (place-player p))) (format t "~&Items: ~A" (string-from-list (place-item p))) (format t "~&NPCs: ~A" (string-from-list (place-npc p))) (format t "~&Monsters: ~A" (string-from-list (list-place-objects 'monster p))))) (defun game-command (cmd player) "Execute a typed-in game command" (let* ((command (read-from-string cmd)) (space (position #\Space cmd)) (arg (if space (second (cut-string cmd (1+ space))) NIL))) (if (member command *commands*) (if space (funcall command player arg) (funcall command player)) (progn (format t "~&Sorry, this command does not exist!") (format t "~&Type 'help' for a list of commands."))))) ;;; ;;; Here follow the functions that define the in-game commands. ;;; ;; A list of all in-game commands. Each new command must be registered here. (defvar *commands* '(help place player goto pickup drop talk trade equip attack spell about save clear)) ;;; The following commands don't take any arguments except for a player (defun help (player) "Print out a list of in-game commands" (setf help-text " Commands: help - Show this list of game commands quit/exit - Exit the game clear - Clear the screen place - Describe the current location player - Describe your player goto <place> - Go to a neighbouring location about <object> - Show a description of this entity talk to <npc> - Talk to an NPC pickup <item> - Pick up an item lying around drop <item> - Drop the item equip <weapon> - Equip this item as your weapon attack <monster> - Fight a monster save <game-file> - Save the game to file") (format t "~A" help-text)) (defun clear (player) "Clear the screen (wrapper function)" (clear-screen) (place player)) ;; XXX Will the following two functions give problems? (Their name is ;; identical with the struct name) Probably not, but best to be aware. (defun place (player) "Describe the player's current location (wrapper function)" (describe-place (player-place player) (player-night-vision player))) (defun player (p) "Print a description of this player" (let ((tab (string #\tab))) (when (stringp p) (setf p (get-game-object 'player p))) (format t "~&Player ~A:" (player-name p)) (format t "~&~%Current place: ~A" (player-place p)) (format t "~&Race: ~A~AClass: ~A" (player-race p) tab (player-class p)) (format t "~&=====") (format t "~&Attributes:") (format t "~&Intelligence: ~A~AStrength: ~A" (player-intelligence p) tab (player-strength p)) (format t "~&Constitution: ~A~ADexterity: ~A" (player-constitution p) tab (player-dexterity p)) (format t "~&Night vision: ~A" (if (player-night-vision p) "yes" "no")) (format t "~&=====") (format t "~&Weapon: ~A" (player-weapon p)) ;; XXX This will need adjusting for large item numbers (format t "~&Items: ~A" (string-from-list (player-item p))) (format t "~&=====") (format t "~&Max health: ~A~ACurrent health: ~A" (player-max-health p) tab (player-health p)) (format t "~&Experience: ~A~AMoney: ~A" (player-experience p) tab (player-money p)))) ;;; These next functions have to take two arguments (the argument ;;; to the function and a player instance). (let ((last-save NIL)) (defun save (player &optional game-file) "Save a game to file (wrapper method around save-world)" ;; XXX Include a permissions check (only allow admins to save)? ;; Could give problems in single-player mode. (cond (game-file (setf last-save game-file)) ((and last-save (not game-file)) (setf game-file last-save)) ((not (or last-save game-file)) (format t "~&Where do you want to save the game?") (input-string game-file) (setf last-save game-file))) (when (y-or-n-p "Save game to ~A?" game-file) (save-world game-file) (format t "~&Game saved.")))) (defun goto (player &optional location) "Go to the specified location" ;; Look before you leap ;-) (unless location (format t "~&Please specify a location!") (return-from goto)) (when (not (member location (place-neighbour (get-game-object 'place (player-place player))) :test #'equalp)) (format t "~&This place does not border your current location!") (return-from goto)) ;; Change places (clear-screen) (debugging "~&~A is going to ~A." (player-name player) location) (change-player-location player location) (add-player-experience player 1) (describe-place location (player-night-vision player))) (defun about (player &optional object-name) "Print a description of this object" (unless object-name (format t "~&Please specify the object you wish to inspect!") (return-from about)) ;; A bit of syntactic sugar... (cond ((equalp object-name "me") (player player) (return-from about)) ((equalp object-name "here") (place player) (return-from about))) (let ((description (get-object-description object-name (player-place player)))) ;; Don't forget items the player is carrying (when (member object-name (player-item player) :test #'equalp) (setf description (item-description (get-game-object 'item object-name)))) (if description (format t "~&(~A) ~A" object-name description) (format t "~&Could not find ~A!" object-name)))) (defun talk (player &optional npc-name) "Talk to the desired NPC" ;; TODO Add interactive facility (unless npc-name (format t "~&Please specify an NPC to talk to!") (return-from talk)) ;; Allow for a bit of syntactic sugar (note: interface inconsistency?) (let ((split-name (cut-string npc-name 3))) (when (equalp (first split-name) "to ") (setf npc-name (second split-name)))) (let* ((place (get-game-object 'place (player-place player))) (npc (when (member npc-name (place-npc place) :test #'equalp) (get-game-object 'npc npc-name)))) ;; Check if the NPC is here (unless npc (format t "~&~A is not here!" npc-name) (return-from talk)) (format t "~&~A: ~A" (string-upcase npc-name) (npc-says npc)) ;; Trade with the NPC (when (and (npc-sells npc) (y-or-n-p "Trade with ~A?" npc-name)) (trade player npc)) ;; Handle quests (let ((quest (get-game-object 'quest (npc-quest npc)))) (when quest (if (dolist (i (quest-proof-item quest)) (unless (member i (player-item player) :test #'equalp) (return T))) (when (y-or-n-p "~%~A has a quest. Accept it?" npc-name) (format t "~&~A: ~A" (string-upcase npc-name) (quest-say-before quest))) (when (y-or-n-p "~%Give to ~A: ~A?" npc-name (string-from-list (quest-proof-item quest) ", ")) (dolist (j (quest-proof-item quest)) (remove-object-attribute player 'item j)) (dolist (k (quest-reward-item quest)) (set-object-attribute player 'item k)) (add-player-experience player (quest-experience quest)) (add-player-money player (quest-money quest)) (format t "~&~A: ~A" (string-upcase npc-name) (quest-say-after quest)) (format t "~&~%Quest complete. You gain:") (format t "~&Money: ~A Experience: ~A~&Items: ~A" (quest-money quest) (quest-experience quest) (string-from-list (quest-reward-item quest))))))))) (defun trade (player npc) "The player trades with this NPC" ;; TODO Implement player-sells-to-npc feature (when (and (stringp npc) (member npc (place-npc (get-game-object 'place (player-place player))) :test #'equalp)) (setf npc (get-game-object 'npc npc)) (unless (npc-sells npc) (format t "~&This NPC doesn't sell anything!") (return-from trade))) (format t "~&~%What do you want to buy? Your money: ~S" (player-money player)) (let* ((choice (choose-option (append (npc-sells npc) (list "None")))) (item (get-game-object 'item choice)) (cost (if item (item-cost item) NIL))) (cond ((equalp choice "None") NIL) ;; XXX Do we have to insure that every object referenced by the ;; world exists, or is that the responsibility of the person ;; who writes the ATL code? ((not item) (format t "~&This object does not exist!") (format t "(Talk to the world creator)") NIL) ((< (player-money player) cost) (format t "~&You do not have enough money!")) ((y-or-n-p "Buy ~A for ~S?" choice cost) (decf (player-money player) cost) (set-object-attribute player 'item choice) (format t "~&Bought ~A for ~A." choice cost)))) (if (y-or-n-p "Buy something else?") (trade player npc) (clear player))) (defun pickup (player &optional item-name) "The player picks up an item" (unless item-name (format t "~&Please specify an item to pick up!") (return-from pickup)) (let ((place (get-game-object 'place (player-place player))) (item (get-game-object 'item item-name))) (if (member item-name (place-item place) :test #'equalp) (progn (set-object-attribute player 'item item-name) (when (item-function item) (funcall (item-function item))) (remove-object-attribute place 'item item-name) (format t "~&You have picked up: ~A" item-name)) (format t "~&Sorry, this item is not here!")))) (defun drop (player &optional item) "The player drops the specified item" (unless item (format t "~&Please specify an item to drop!") (return-from drop)) (if (member item (player-item player) :test #'equalp) (progn (remove-object-attribute player 'item item) (when (equalp (item-weapon (get-game-object 'item item)) "yes") (set-object-attribute player 'weapon "")) (set-object-attribute (get-game-object 'place (player-place player)) 'item item) (format t "~&You have dropped: ~A" item)) (format t "~&You do not possess this item!"))) (defun equip (player &optional new-weapon) "The player sets another item to be his weapon" (when (or (not new-weapon) (equalp new-weapon "none")) (setf (player-weapon player) "") (format t "~&You no longer have any weapon equipped.") (return-from equip)) (if (and (member new-weapon (player-item player) :test #'equalp) (equalp (item-weapon (get-game-object 'item new-weapon)) "yes")) (progn (setf (player-weapon player) new-weapon) (format t "~&You have equipped: ~A" new-weapon)) (format t "~&Sorry, this item is not available as a weapon!"))) (defun attack (player &optional opponent) "The player launches an attack at a monster" ;; Check input for validity (unless opponent (format t "~&Please specify an opponent!") (return-from attack)) (unless (member opponent (list-place-objects 'monster (get-game-object 'place (player-place player))) :test #'equalp) (format t "~&This monster is not here!") (return-from attack)) ;; Bind all relevant values to variables (saves typing later) (let* ((place (get-game-object 'place (player-place player))) (monster (dolist (m (place-monster place)) (when (equalp (monster-name m) opponent) (return m)))) (m-str (monster-strength monster)) (m-dex (monster-dexterity monster)) (m-ac (monster-armour-class monster)) (m-weapon (get-game-object 'weapon (monster-weapon monster))) (p-str (player-strength player)) (p-dex (player-dexterity player)) (p-ac (player-armour-class player)) (p-weapon (if (not (equalp (player-weapon player) "")) ;lbyl (get-game-object 'weapon (player-weapon player)) (make-weapon :name "Fists" :damage 0))) (damage 0)) ;; Print information about the combattants (format t "~&Health ~A: ~A Health ~A: ~A" (player-name player) (player-health player) opponent (monster-health monster)) ;; Determine whether the player or the monster strikes (if (> (+ (random 10) p-dex) (+ (random 10) m-dex)) ;XXX magic numbers! (setf damage (calculate-damage p-str p-weapon m-dex m-ac)) (setf damage (- 0 (calculate-damage m-str m-weapon p-dex p-ac)))) ;; Negative damage denotes that the player has been hit (cond ((plusp damage) (decf (monster-health monster) damage) (format t "~&You hit! ~A points damage." damage) (when (> 1 (monster-health monster)) (let ((experience (round (average m-str m-dex)))) (remove-object-attribute place 'monster monster) (add-player-experience player experience) (format t "~&You killed the monster! ") (format t "~A points experience." experience)))) ((minusp damage) (change-player-health player damage) (format t "~&You missed. Your opponent hit! ") (format t "~A points damage." damage)) (t (format t "~&You missed. Your opponent missed."))))) (defun calculate-damage (att-str att-weapon def-dex def-ac) "A private function to calculate the damage caused by an attack" (let ((damage 0)) (incf damage (random att-str)) (incf damage (weapon-damage att-weapon)) (decf damage (random def-dex)) (decf damage def-ac) (if (minusp damage) 0 damage))) (defun spell (player &optional spell) "Call this game function" ;; TODO Remove this/change it so that not all game-functions can be called (cond ((null spell) (format t "~&Please specify a spell to cast!") (return-from spell)) ((not (member spell (list-world-objects 'game-function) :test #'equalp)) (format t "~&This spell does not exist!") (return-from spell)) (t (format t "~&~A" (game-function-print (get-game-object 'game-function spell))) (run-game-function spell player))))