api.ps
547 lines
| 16.2 KiB
| application/postscript
|
PostScriptLexer
/ src / api.ps
r1 | ||||
r25 | (in-package sugar-qsp.api) | |||
r1 | ||||
;;; API deals with DOM manipulation and some bookkeeping for the | ||||
;;; intrinsics, namely variables | ||||
;;; API is an implementation detail and has no QSP documentation. It | ||||
;;; doesn't call intrinsics | ||||
r6 | ;;; Utils | |||
r25 | (defun make-act-html (title img) | |||
r32 | (+ "<a class='qsp-act' href='" (href-call call-act title) "' onmouseover='" (inline-call select-act title) "'>" | |||
r30 | (if img (+ "<img src='" img "'>") "") | |||
r6 | title | |||
"</a>")) | ||||
r25 | (defun make-menu-item-html (num title img loc) | |||
r32 | (+ "<a href='" (href-call finish-menu loc) "'>" | |||
r30 | (if img (+ "<img src='" img "'>") "") | |||
r11 | title | |||
"</a>")) | ||||
r32 | (defun make-obj (title img selected) | |||
r33 | (+ "<li onclick='" (inline-call select-obj title img) "'>" | |||
"<a class='qsp-obj" (if selected " qsp-obj-selected" "") "'>" | ||||
r32 | (if img (+ "<img src='" img "'>") "") | |||
r33 | title | |||
r32 | "</a>")) | |||
r30 | (defun make-menu-delimiter () | |||
"<hr>") | ||||
r25 | (defun report-error (text) | |||
r20 | (alert text)) | |||
r29 | (defun start-sleeping () | |||
r30 | (chain (by-id "qsp") class-list (add "disable"))) | |||
r29 | ||||
(defun finish-sleeping () | ||||
r30 | (chain (by-id "qsp") class-list (remove "disable"))) | |||
r29 | ||||
r25 | (defun sleep (msec) | |||
r30 | (with-sleep (resume) | |||
(set-timeout resume msec))) | ||||
r24 | ||||
r25 | (defun init-dom () | |||
r18 | ;; Save/load buttons | |||
r25 | (let ((btn (by-id "qsp-btn-save"))) | |||
(setf (@ btn onclick) savegame) | ||||
(setf (@ btn href) "#")) | ||||
(let ((btn (by-id "qsp-btn-open"))) | ||||
(setf (@ btn onclick) opengame) | ||||
(setf (@ btn href) "#")) | ||||
r18 | ;; Close image on click | |||
r25 | (setf (@ (by-id "qsp-image-container") onclick) | |||
r32 | show-image) | |||
r33 | ;; Enter in input field | |||
r32 | (setf (@ (get-frame :input) onkeyup) | |||
on-input-key) | ||||
r18 | ;; Close the dropdown on any click | |||
r25 | (setf (@ window onclick) | |||
r18 | (lambda (event) | |||
r30 | (setf (@ window mouse) | |||
(list (@ event page-x) | ||||
(@ event page-y))) | ||||
(finish-menu nil)))) | ||||
r18 | ||||
r25 | (defun call-serv-loc (var-name &rest args) | |||
(let ((loc-name (get-var var-name 0 :str))) | ||||
r20 | (when loc-name | |||
r25 | (let ((loc (getprop (root locs) loc-name))) | |||
r20 | (when loc | |||
r32 | (call-loc loc-name args)))))) | |||
r11 | ||||
r31 | (defun filename-game (filename) | |||
(let ((game-name (chain filename (match "(.*/)?([^.]+)(\\.[a-zA-Z]+)?") 2)))) | ||||
(getprop (root games) game-name)) | ||||
(defun run-game (name) | ||||
(let ((game (filename-game name))) | ||||
(setf (root main-game) name) | ||||
;; Replace locations with the new game's | ||||
(setf (root locs) game) | ||||
(funcall (getprop game | ||||
(chain *object (keys game) 0)) | ||||
(list)))) | ||||
r9 | ;;; Misc | |||
r25 | (defun newline (key) | |||
(append-id (key-to-id key) "<br>" t)) | ||||
r22 | ||||
r25 | (defun clear-id (id) | |||
(setf (inner-html (by-id id)) "")) | ||||
r22 | ||||
r25 | (defvar text-escaper (chain document (create-element :textarea))) | |||
r22 | ||||
r25 | (defun prepare-contents (s &optional force-html) | |||
(if (or force-html (get-var "USEHTML" 0 :num)) | ||||
r22 | s | |||
(progn | ||||
r25 | (setf (@ text-escaper text-content) s) | |||
(inner-html text-escaper)))) | ||||
r9 | ||||
r25 | (defun get-id (id &optional force-html) | |||
(inner-html (by-id id))) | ||||
r9 | ||||
r25 | (defun set-id (id contents &optional force-html) | |||
(setf (inner-html (by-id id)) (prepare-contents contents force-html))) | ||||
r9 | ||||
r25 | (defun append-id (id contents &optional force-html) | |||
r22 | (when contents | |||
r25 | (incf (inner-html (by-id id)) (prepare-contents contents force-html)))) | |||
r9 | ||||
r32 | (defun on-input-key (ev) | |||
(when (= 13 (@ ev key-code)) | ||||
(chain ev (prevent-default)) | ||||
(call-serv-loc "USERCOM"))) | ||||
r1 | ;;; Function calls | |||
r25 | (defun init-args (args) | |||
r1 | (dotimes (i (length args)) | |||
r16 | (let ((arg (elt args i))) | |||
(if (numberp arg) | ||||
r25 | (set-var args i :num arg) | |||
(set-var args i :str arg))))) | ||||
r1 | ||||
r25 | (defun get-result () | |||
r26 | (if (not (equal "" (get-var "RESULT" 0 :str))) | |||
(get-var "RESULT" 0 :str) | ||||
(get-var "RESULT" 0 :num))) | ||||
r1 | ||||
r25 | (defun call-loc (name args) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r23 | (with-frame | |||
r25 | (with-call-args args | |||
r32 | (funcall (getprop (root locs) name))))) | |||
r23 | ||||
r25 | (defun call-act (title) | |||
r30 | (with-frame | |||
r32 | (funcall (getprop (root acts) title :act)))) | |||
r21 | ||||
r1 | ;;; Text windows | |||
r25 | (defun key-to-id (key) | |||
r6 | (case key | |||
r32 | (:all "qsp") | |||
r6 | (:main "qsp-main") | |||
(:stat "qsp-stat") | ||||
r11 | (:objs "qsp-objs") | |||
(:acts "qsp-acts") | ||||
(:input "qsp-input") | ||||
r30 | (:image "qsp-image") | |||
r11 | (:dropdown "qsp-dropdown") | |||
r25 | (t (report-error "Internal error!")))) | |||
r6 | ||||
r25 | (defun get-frame (key) | |||
(by-id (key-to-id key))) | ||||
r11 | ||||
r25 | (defun add-text (key text) | |||
(append-id (key-to-id key) text)) | ||||
r6 | ||||
r25 | (defun get-text (key) | |||
(get-id (key-to-id key))) | ||||
r6 | ||||
r25 | (defun clear-text (key) | |||
(clear-id (key-to-id key))) | ||||
r6 | ||||
r25 | (defun enable-frame (key enable) | |||
(let ((obj (get-frame key))) | ||||
(setf (@ obj style display) (if enable "block" "none")) | ||||
r27 | (void))) | |||
r11 | ||||
r1 | ;;; Actions | |||
r25 | (defun add-act (title img act) | |||
(setf (getprop (root acts) title) | ||||
r32 | (create :title title :img img :act act :selected nil)) | |||
r25 | (update-acts)) | |||
r6 | ||||
r25 | (defun del-act (title) | |||
(delete (getprop (root acts) title)) | ||||
(update-acts)) | ||||
r6 | ||||
r25 | (defun clear-act () | |||
(setf (root acts) (create)) | ||||
r32 | (update-acts)) | |||
r6 | ||||
r25 | (defun update-acts () | |||
(clear-id "qsp-acts") | ||||
(let ((elt (by-id "qsp-acts"))) | ||||
(for-in (title (root acts)) | ||||
(let ((obj (getprop (root acts) title))) | ||||
(incf (inner-html elt) (make-act-html title (getprop obj :img))))))) | ||||
r32 | (defun select-act (title) | |||
(loop :for (k v) :of (root acts) | ||||
r33 | :do (setf (getprop v :selected) nil)) | |||
r32 | (setf (getprop (root acts) title :selected) t) | |||
(call-serv-loc "ONACTSEL")) | ||||
r1 | ||||
r19 | ;;; "Syntax" | |||
r25 | (defun qspfor (name index from to step body) | |||
(for ((i from)) | ||||
((< i to)) | ||||
((incf i step)) | ||||
(set-var name index :num i) | ||||
(unless (funcall body) | ||||
(return-from qspfor)))) | ||||
r19 | ||||
r16 | ;;; Variable class | |||
r25 | (defun *var (name) | |||
r16 | ;; From strings to numbers | |||
r32 | (setf (@ this :indexes) (create)) | |||
r16 | ;; From numbers to {num: 0, str: ""} objects | |||
r32 | (setf (@ this :values) (list)) | |||
r27 | (void)) | |||
r16 | ||||
r25 | (defun new-value () | |||
(create :num 0 :str "")) | ||||
r1 | ||||
r25 | (setf (@ *var prototype index-num) | |||
(lambda (index) | ||||
(let ((num-index | ||||
(if (stringp index) | ||||
r32 | (if (in index (@ this :indexes)) | |||
(getprop (@ this :indexes) index) | ||||
(let ((n (length (@ this :values)))) | ||||
(setf (getprop (@ this :indexes) index) n) | ||||
r25 | n)) | |||
index))) | ||||
r32 | (unless (in num-index (@ this :values)) | |||
(setf (elt (@ this :values) num-index) (new-value))) | ||||
r25 | num-index))) | |||
r16 | ||||
r25 | (setf (@ *var prototype get) | |||
(lambda (index slot) | ||||
(unless (or index (= 0 index)) | ||||
r32 | (setf index (1- (length (@ this :values))))) | |||
(getprop (@ this :values) (chain this (index-num index)) slot))) | ||||
r16 | ||||
r25 | (setf (@ *var prototype set) | |||
(lambda (index slot value) | ||||
(unless (or index (= 0 index)) | ||||
r32 | (setf index (length (@ this :values)))) | |||
r25 | (case slot | |||
(:num (setf value (chain *number (parse-int value)))) | ||||
(:str (setf value (chain value (to-string))))) | ||||
r32 | (setf (getprop (@ this :values) | |||
r25 | (chain this (index-num index)) | |||
slot) value) | ||||
r27 | (void))) | |||
r16 | ||||
r25 | (setf (@ *var prototype kill) | |||
(lambda (index) | ||||
r32 | (setf (elt (@ this :values) (chain this (index-num index))) | |||
r25 | (new-value)) | |||
r32 | (delete (getprop 'this :indexes index)))) | |||
r16 | ||||
;;; Variables | ||||
r6 | ||||
r25 | (defun var-real-name (name) | |||
(if (= (@ name 0) #\$) | ||||
(values (chain name (substr 1)) :str) | ||||
r16 | (values name :num))) | |||
r6 | ||||
r25 | (defun ensure-var (name) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r25 | (let ((store (var-ref name))) | |||
r14 | (unless store | |||
r27 | (setf store (new (*var name))) | |||
r25 | (setf (getprop (root vars) name) store)) | |||
r16 | store)) | |||
r6 | ||||
r25 | (defun var-ref (name) | |||
(let ((local-store (current-local-frame))) | ||||
r21 | (cond ((and local-store (in name local-store)) | |||
r25 | (getprop local-store name)) | |||
r16 | ((in name (root vars)) | |||
r25 | (getprop (root vars) name)) | |||
r14 | (t nil)))) | |||
r25 | (defun get-var (name index slot) | |||
(chain (ensure-var name) (get index slot))) | ||||
r6 | ||||
r25 | (defun set-var (name index slot value) | |||
(chain (ensure-var name) (set index slot value)) | ||||
r32 | (let ((serv-var (getprop serv-vars name))) | |||
(when serv-var | ||||
r33 | (funcall (@ serv-var :body) | |||
r32 | (get-var name index (@ serv-var :slot)) | |||
index))) | ||||
r27 | (void)) | |||
r6 | ||||
r25 | (defun get-array (name) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r32 | (ensure-var name)) | |||
r11 | ||||
r25 | (defun set-array (name value) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r32 | (let ((store (ensure-var name))) | |||
(setf (@ store :values) (@ value :values)) | ||||
(setf (@ store :indexes) (@ value :indexes))) | ||||
r27 | (void)) | |||
r14 | ||||
r25 | (defun kill-var (name &optional index) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r16 | (if (and index (not (= 0 index))) | |||
r25 | (chain (getprop (root vars) name) (kill index)) | |||
(delete (getprop (root vars) name))) | ||||
r27 | (void)) | |||
r6 | ||||
r25 | (defun array-size (name) | |||
r32 | (@ (var-ref name) :values length)) | |||
r16 | ||||
;;; Locals | ||||
r25 | (defun push-local-frame () | |||
(chain (root locals) (push (create))) | ||||
r27 | (void)) | |||
r16 | ||||
r25 | (defun pop-local-frame () | |||
(chain (root locals) (pop)) | ||||
r27 | (void)) | |||
r16 | ||||
r25 | (defun current-local-frame () | |||
r16 | (elt (root locals) (1- (length (root locals))))) | |||
r25 | (defun new-local (name) | |||
(let ((frame (current-local-frame))) | ||||
r16 | (unless (in name frame) | |||
r25 | (setf (getprop frame name) (create))) | |||
r27 | (void))) | |||
r11 | ||||
r6 | ;;; Objects | |||
r32 | (defun select-obj (title img) | |||
(loop :for (k v) :of (root objs) | ||||
r33 | :do (setf (getprop v :selected) nil)) | |||
r32 | (setf (getprop (root objs) title :selected) t) | |||
(call-serv-loc "ONOBJSEL" title img)) | ||||
r25 | (defun update-objs () | |||
(let ((elt (by-id "qsp-objs"))) | ||||
(setf (inner-html elt) "<ul>") | ||||
r33 | (loop :for (name obj) :of (root objs) | |||
r32 | :do (incf (inner-html elt) | |||
r33 | (make-obj name (@ obj :img) (@ obj :selected)))) | |||
r25 | (incf (inner-html elt) "</ul>"))) | |||
r11 | ||||
;;; Menu | ||||
r30 | (defun open-menu (menu-data) | |||
(let ((elt (get-frame :dropdown)) | ||||
r11 | (i 0)) | |||
(loop :for item :in menu-data | ||||
:do (incf i) | ||||
r30 | :do (incf (inner-html elt) | |||
(if (eq item :delimiter) | ||||
(make-menu-delimiter i) | ||||
(make-menu-item-html i | ||||
(@ item :text) | ||||
(@ item :icon) | ||||
(@ item :loc))))) | ||||
(let ((mouse (@ window mouse))) | ||||
(setf (@ elt style left) (+ (elt mouse 0) "px")) | ||||
(setf (@ elt style top) (+ (elt mouse 1) "px")) | ||||
;; Make sure it's inside the viewport | ||||
(when (> (@ document body inner-width) | ||||
(+ (elt mouse 0) (@ elt inner-width))) | ||||
(incf (@ elt style left) (@ elt inner-width))) | ||||
(when (> (@ document body inner-height) | ||||
(+ (elt mouse 0) (@ elt inner-height))) | ||||
(incf (@ elt style top) (@ elt inner-height)))) | ||||
r25 | (setf (@ elt style display) "block"))) | |||
r12 | ||||
r30 | (defun finish-menu (loc) | |||
(when (root menu-resume) | ||||
(let ((elt (get-frame :dropdown))) | ||||
(setf (inner-html elt) "") | ||||
(setf (@ elt style display) "none") | ||||
(funcall (root menu-resume)) | ||||
(setf (root menu-resume) nil)) | ||||
(when loc | ||||
(call-loc loc))) | ||||
(void)) | ||||
(defun menu (menu-data) | ||||
(with-sleep (resume) | ||||
(open-menu menu-data) | ||||
(setf (root menu-resume) resume)) | ||||
(void)) | ||||
r12 | ;;; Content | |||
r25 | (defun clean-audio () | |||
(loop :for k :in (chain *object (keys (root playing))) | ||||
:for v := (getprop (root playing) k) | ||||
:do (when (@ v ended) | ||||
(delete (@ (root playing) k))))) | ||||
r14 | ||||
r25 | (defun show-image (path) | |||
r30 | (let ((img (get-frame :image))) | |||
r18 | (cond (path | |||
r25 | (setf (@ img src) path) | |||
(setf (@ img style display) "flex")) | ||||
r18 | (t | |||
r25 | (setf (@ img src) "") | |||
(setf (@ img style display) "hidden"))))) | ||||
r18 | ||||
r34 | (defun show-inline-images (frame-name images) | |||
(let ((frame (get-frame frame-name)) | ||||
(text "")) | ||||
(incf text "<div style='position:relative; display: inline-block'>") | ||||
(incf text (+ "<img src='" (@ images 0) "'>")) | ||||
(loop :for image :in (chain images (slice 1)) | ||||
:do (incf text | ||||
(+ "<img style='position:absolute' src='" image "'>"))) | ||||
(incf text "</div>") | ||||
(incf (inner-html frame) text))) | ||||
r32 | (defun rgb-string (rgb) | |||
r33 | (let ((red (ps::>> rgb 16)) | |||
(green (logand (ps::>> rgb 8) 255)) | ||||
(blue (logand rgb 255))) | ||||
r32 | (flet ((rgb-to-hex (comp) | |||
(let ((hex (chain (*number comp) (to-string 16)))) | ||||
(if (< (length hex) 2) | ||||
(+ "0" hex) | ||||
hex)))) | ||||
(+ "#" (rgb-to-hex red) (rgb-to-hex green) (rgb-to-hex red))))) | ||||
r18 | ;;; Saves | |||
r25 | (defun opengame () | |||
(let ((element (chain document (create-element :input)))) | ||||
(chain element (set-attribute :type :file)) | ||||
(chain element (set-attribute :id :qsp-opengame)) | ||||
(chain element (set-attribute :tabindex -1)) | ||||
(chain element (set-attribute "aria-hidden" t)) | ||||
(setf (@ element style display) :block) | ||||
(setf (@ element style visibility) :hidden) | ||||
(setf (@ element style position) :fixed) | ||||
(setf (@ element onchange) | ||||
r18 | (lambda (event) | |||
r25 | (let* ((file (@ event target files 0)) | |||
(reader (new (*file-reader)))) | ||||
(setf (@ reader onload) | ||||
r18 | (lambda (ev) | |||
(block nil | ||||
r25 | (let ((target (@ ev current-target))) | |||
(unless (@ target result) | ||||
(return)) | ||||
(base64-to-state (@ target result)) | ||||
(unstash-state))))) | ||||
(chain reader (read-as-text file))))) | ||||
(chain document body (append-child element)) | ||||
(chain element (click)) | ||||
(chain document body (remove-child element)))) | ||||
r18 | ||||
r25 | (defun savegame () | |||
(let ((element (chain document (create-element :a)))) | ||||
(chain element (set-attribute :href (+ "data:text/plain;charset=utf-8," (state-to-base64)))) | ||||
(chain element (set-attribute :download "savegame.sav")) | ||||
(setf (@ element style display) :none) | ||||
(chain document body (append-child element)) | ||||
(chain element (click)) | ||||
(chain document body (remove-child element)))) | ||||
r20 | ||||
r25 | (defun stash-state (args) | |||
(call-serv-loc "ONGSAVE") | ||||
r20 | (setf (root state-stash) | |||
r25 | (chain *j-s-o-n (stringify | |||
r27 | (create :vars (root vars) | |||
:objs (root objs) | ||||
:loc-args args | ||||
:msecs (- (chain *date (now)) (root started-at)) | ||||
:timer-interval (root timer-interval) | ||||
:main-html (inner-html | ||||
r30 | (get-frame :main)) | |||
r27 | :stat-html (inner-html | |||
r30 | (get-frame :stat)) | |||
r27 | :next-location (root current-location))))) | |||
(void)) | ||||
r20 | ||||
r25 | (defun unstash-state () | |||
(let ((data (chain *j-s-o-n (parse (root state-stash))))) | ||||
(clear-act) | ||||
r27 | (setf (root vars) (@ data :vars)) | |||
r25 | (loop :for k :in (chain *object (keys (root vars))) | |||
:do (chain *object (set-prototype-of (getprop (root vars) k) | ||||
(@ *var prototype)))) | ||||
r27 | (setf (root started-at) (- (chain *date (now)) (@ data :msecs))) | |||
(setf (root objs) (@ data :objs)) | ||||
(setf (root current-location) (@ data :next-location)) | ||||
r30 | (setf (inner-html (get-frame :main)) | |||
r27 | (@ data :main-html)) | |||
r30 | (setf (inner-html (get-frame :stat)) | |||
r27 | (@ data :stat-html)) | |||
r25 | (update-objs) | |||
r27 | (set-timer (@ data :timer-interval)) | |||
r25 | (call-serv-loc "ONGLOAD") | |||
r27 | (call-loc (root current-location) (@ data :loc-args)) | |||
(void))) | ||||
r20 | ||||
r25 | (defun state-to-base64 () | |||
r20 | (btoa (encode-u-r-i-component (root state-stash)))) | |||
r25 | (defun base64-to-state (data) | |||
r20 | (setf (root state-stash) (decode-u-r-i-component (atob data)))) | |||
;;; Timers | ||||
r25 | (defun set-timer (interval) | |||
r20 | (setf (root timer-interval) interval) | |||
(clear-interval (root timer-obj)) | ||||
(setf (root timer-obj) | ||||
(set-interval | ||||
(lambda () | ||||
r25 | (call-serv-loc "COUNTER")) | |||
r20 | interval))) | |||
r32 | ||||
;;; Special variables | ||||
r33 | (defvar serv-vars (create)) | |||
r32 | (define-serv-var backimage (:str path) | |||
(setf (@ (get-frame :main) style background-image) path)) | ||||
(define-serv-var bcolor (:num color) | ||||
(setf (@ (get-frame :all) style background-color) (rgb-string color))) | ||||
(define-serv-var fcolor (:num color) | ||||
(setf (@ (get-frame :all) style color) (rgb-string color))) | ||||
(define-serv-var lcolor (:num color) | ||||
(setf (@ (get-frame :style) inner-text) | ||||
(+ "a { color: " (rgb-string color) ";}"))) | ||||
r33 | ||||
(define-serv-var fsize (:num size) | ||||
(setf (@ (get-frame :all) style font-size) size)) | ||||
(define-serv-var fname (:str font-name) | ||||
(setf (@ (get-frame :all) style font-family) (+ font-name ",serif"))) | ||||