;;;; ;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game ;;;; set in Africa. ;;;; ;;;; This is the main program file with the user interface. ;;;; ;;;; (c) 2018 Daniel Vedder, MIT license ;;;; ;;XXX move this file to src/ncurses.lisp? (in-package :naledi-ya-africa) ;(use-package :croatoan) (defun start-game () "Start the game logic and UI" (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t :input-echoing nil :cursor-visibility nil :input-reading :unbuffered) (splash-screen scr) (choose-local-or-remote scr) (user-interface scr))) (defun splash-screen (scr) "Display the splash screen with the `Naledi ya Africa' logo" (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) (logo (load-text-file "LOGO")) (y (halve (- height (length logo)))) (xoff (halve (- width 80)))) (croatoan:clear scr) (dolist (l logo) (croatoan:move scr y xoff) (croatoan:add-string scr l) (incf y)) (croatoan:move scr (1- height) 0) (croatoan:add-string scr "Press any key to continue.") (croatoan:move scr (1- height) (- width 22)) (croatoan:add-string scr "(c) 2018 Daniel Vedder") (croatoan:event-case (scr event) ((nil) nil) (otherwise (return-from croatoan:event-case))))) (defun choose-local-or-remote (scr) "Choose whether to start a local game or connect to a remote server" (croatoan:clear scr) (croatoan:refresh scr) (let ((mw (make-instance 'croatoan:dialog-window :title "Welcome!" :center t :border t :width 50 :max-item-length 42 :input-blocking t :cyclic-selection t :current-item-mark "* " :items '("Start a local game" ;;TODO "Load a saved game" "Connect to a remote server")))) ;;XXX I have to effectively reimplement (select-item menu) because ;; the screen grabs all user input and none arrives at the window (croatoan:draw-menu mw) (croatoan:event-case (scr event) ((nil) nil) ((:up :down) (croatoan:update-menu mw event) (croatoan:draw-menu mw)) (#\newline (cond ((equalp (croatoan:current-item mw) "Start a local game") (start-server) (sleep 0.5) ;;give the server time to start (connect-server)) ((equalp (croatoan:current-item mw) "Connect to a remote server") (connect-server (enter-server-address scr)))) (return-from croatoan:event-case))))) ;;TODO (choose-world-size) (defun enter-server-address (scr) "Enter the IP and port of the server to connect to" ;;TODO rewrite this with individual, successive forms? (croatoan:clear scr) (croatoan:refresh scr) (let* ((x0 (- (halve (croatoan:.width scr)) 25)) (y0 (- (halve (croatoan:.height scr)) 5)) (ip-field (make-instance 'croatoan:field :width 20 :position '(4 8))) (port-field (make-instance 'croatoan:field :width 20 :position '(5 10))) (form (make-instance 'croatoan:form :fields (list ip-field port-field))) (inputwin (make-instance 'croatoan:window :position (list y0 x0) :width 50 :height 10))) (setf (croatoan:.visible inputwin) t) (croatoan:add-string inputwin "Enter the servers IP address and port." :y 2 :x 4) (croatoan:add-string inputwin "IP:" :y 4 :x 4) (croatoan:add-string inputwin "Port:" :y 5 :x 4) (croatoan:add-string inputwin "(Jump with Tab, confirm with control-A)" :y 7 :x 4) (setf (croatoan:.buffer ip-field) (reverse (to-list "127.0.0.1"))) (setf (croatoan:.buffer port-field) (reverse (to-list (to-string *port*)))) (croatoan:box inputwin) (croatoan:edit inputwin form) (croatoan:move inputwin 5 8) (croatoan:refresh inputwin) ;;FIXME The keymap doesn't work and throws an error (setf (croatoan:.event-handlers scr) (croatoan:get-keymap :form-default-keymap)))) (defun user-interface (scr) "Create the screen on the ncurses interface and hand over to window functions" (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) (me (list (round (/ width 4)) (halve height)))) (croatoan:clear scr) (croatoan:refresh scr) (croatoan:with-windows ((mapwin :position '(0 0) :input-blocking *framerate* :border t :width (- width 51) :height height) (playerwin :position (list 0 (- width 50)) :input-blocking *framerate* :border t :width 50 :height (halve height 'down)) (placewin :input-blocking *framerate* :border t :position (list (halve height) (- width 50)) :width 50 :height (halve height 'down)) (newswin :input-blocking *framerate* :position (list height 0) :width width :height 1)) (update-ui mapwin playerwin placewin newswin me) ;;TODO (croatoan:event-case (scr event) (#\q (disconnect) (terminate) (return-from croatoan:event-case)) ;XXX (#\n (croatoan:draw-menu (message-window))) (:up (decf (second me)) (update-ui mapwin playerwin placewin newswin me)) (:down (incf (second me)) (update-ui mapwin playerwin placewin newswin me)) (:left (decf (first me)) (update-ui mapwin playerwin placewin newswin me)) (:right (incf (first me)) (update-ui mapwin playerwin placewin newswin me)) ((nil) (update-ui mapwin playerwin placewin newswin me)) (otherwise (notify (string event))))))) (defun update-ui (mapwin playerwin placewin newswin me) "Update all four UI elements" (draw-map mapwin me) (draw-player-panel playerwin) (draw-place-panel placewin me) (draw-news-panel newswin)) (defun draw-map (win me) "Draw a portion of the game map in an ncurses window" (setf (croatoan:.color-pair win) '(:white :black)) (croatoan:box win) (croatoan:move win 1 1) (let ((x0 (- (first me) (round (/ (croatoan:.width win) 4)))) (y0 (- (second me) (halve (croatoan:.height win))))) ;; NB. x0 and w are calculated differently to y0 and h because we insert ;; a space after each character (dotimes (h (1- (croatoan:.height win))) (dotimes (w (- (halve (croatoan:.width win) 'floor) 2)) (let ((p (coord (+ w x0 3) (+ h y0 1)))) (if (null p) (croatoan:add-char win #\space) (if (and (= (first (patch-pos p)) (first me)) (= (second (patch-pos p)) (second me))) (progn (setf (croatoan:.color-pair win) '(:white :black)) (croatoan:add-char win #\@)) (if (patch-occupant p) (progn (setf (croatoan:.color-pair win) (list (.color (patch-occupant p)) :black)) (croatoan:add-char win (.char (patch-occupant p)))) (progn (setf (croatoan:.color-pair win) (list (biome-col (patch-biome p)) :black)) (croatoan:add-char win (biome-char (patch-biome p))))))) (croatoan:add-char win #\space))) (croatoan:move win (1+ h) 1)) (croatoan:refresh win))) (defun draw-player-panel (win) "Draw a panel with information about the player character." ;;TODO (croatoan:box win) (croatoan:move win 1 1) (croatoan:add-string win "This is the player panel.") (croatoan:refresh win)) (defun draw-place-panel (win me) "Draw a panel with information about the player's current location." (let ((descr (break-lines (describe-patch me) (- (croatoan:.width win) 2)))) (croatoan:clear win) (croatoan:box win) (croatoan:move win 1 1) (dolist (d descr) (croatoan:add-string win d) (croatoan:move win (1+ (first (croatoan:.cursor-position win))) 1)) (croatoan:refresh win))) (let ((news '("Press h for help."))) (defun draw-news-panel (win) "Draw a thin panel at the bottom of the screen to display news items." (croatoan:clear win) (croatoan:move win 0 0) (croatoan:add-string win (car news)) (croatoan:refresh win)) (defun notify (news-string &rest formats) "Append a string to the news to notify the user." ;;A bit of a kluge, but means that `notify' supports formatting (setf news (cons (apply #'format (cons NIL (cons news-string formats))) news))) (defun message-window () "Return a dialog window with the last game messages." ;;TODO complete (make-instance 'croatoan:dialog-window :input-blocking t :items (break-lines (mapcar #'(lambda (n) (string-from-list (list "*" n))) news) 50) :center t :border t :stacked t :layout nil :title "Game messages" :max-item-length 50 :message-height 2 :message-text "Press b to go back."))) ;;:event-handlers '((#\b #'exit-event-loop))))) (defun process-command (event) ;;TODO )