├── compiled-tests └── dummy.html ├── tests ├── runner.cljs ├── utils.cljs └── GUI.cljs ├── .travis.yml ├── runtime └── index.html ├── src └── novelette │ ├── syntax.cljs │ ├── utils.cljs │ ├── storage.cljs │ ├── GUI │ ├── panel.cljs │ ├── canvas.cljs │ ├── button.cljs │ ├── label.cljs │ └── story-ui.cljs │ ├── sound.cljs │ ├── input.cljs │ ├── screens │ ├── dialoguescreen.cljs │ ├── loadingscreen.cljs │ └── storyscreen.cljs │ ├── screen.cljs │ ├── render.cljs │ ├── syntax.clj │ ├── schemas.cljs │ ├── GUI.cljs │ └── storyteller.cljs ├── project.clj ├── README.md └── src-js └── lzstr └── lz-string.js /compiled-tests/dummy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is just a dummy HTML file with which to load the unit tests. 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.tests.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [novelette.tests.utils] 4 | [novelette.tests.GUI])) 5 | 6 | (doo-tests 7 | 'novelette.tests.utils 8 | 'novelette.tests.GUI) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: false 3 | lein: lein2 4 | script: xvfb-run -a -- lein2 doo slimer tests once 5 | jdk: 6 | - openjdk7 7 | 8 | before_install: 9 | - echo "Installing slimer..." 10 | - wget http://download.slimerjs.org/releases/0.9.6/slimerjs-0.9.6.zip 11 | - unzip slimerjs-0.9.6.zip 12 | - export PATH="$PWD/slimerjs-0.9.6/:$PATH" 13 | -------------------------------------------------------------------------------- /runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 13 | 14 | -------------------------------------------------------------------------------- /src/novelette/syntax.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.syntax 2 | (:require-macros [novelette.syntax :as stx])) 3 | 4 | (defn option 5 | [id jump & args] 6 | (loop [opt args res {:jump jump}] 7 | (cond 8 | (empty? opt) {:options {id res}} 9 | (= :pre (ffirst opt)) 10 | (recur (rest opt) (assoc res :pre (second (first opt)))) 11 | (= :post (ffirst opt)) 12 | (recur (rest opt) (assoc res :post (second (first opt)))) 13 | :else ; ignore it 14 | (recur (rest opt) res)))) 15 | 16 | (defn choice 17 | [& args] 18 | (if (string? (first args)) 19 | (stx/choice-explicit* (first args) (rest args)) 20 | (stx/choice-implicit* args))) 21 | -------------------------------------------------------------------------------- /src/novelette/utils.cljs: -------------------------------------------------------------------------------- 1 | ; This file contains mostly re-used self-contained utility functions. 2 | (ns novelette.utils 3 | (:require-macros [schema.core :as s]) 4 | (:require [novelette-sprite.schemas :as scs] 5 | [schema.core :as s])) 6 | 7 | (s/defn sort-z-index 8 | "Returns a list of sprites priority sorted by z-index" 9 | [sprites :- [scs/Sprite]] 10 | (reverse (sort-by :z-index sprites))) 11 | 12 | (s/defn inside-bounds? 13 | "Check if a given point is within bounds." 14 | [[x1 y1] :- scs/pos 15 | [x2 y2 w2 h2] :- scs/pos] 16 | (and (< x2 x1 (+ x2 w2)) 17 | (< y2 y1 (+ y2 h2)))) 18 | 19 | (s/defn get-center-coordinates 20 | "Given an x,y,w,h area return the center coordinates." 21 | [bounds :- scs/pos] 22 | [(+ (bounds 0) (/ (bounds 2) 2)) ; X coordinate of the center 23 | (+ (bounds 1) (/ (bounds 3) 2))]) ; Y coordinate of the center 24 | -------------------------------------------------------------------------------- /src/novelette/storage.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.storage 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [cljs.reader :as reader] 5 | [schema.core :as s] 6 | [lzstr.LZString :as LZString])) 7 | 8 | ; This file contains all the functions related to storing and retrieving data 9 | ; from Javascript's LocalStorage 10 | 11 | (s/defn save! 12 | "Save the given data into the local storage with the specified name." 13 | [data :- s/Any 14 | name :- s/Str] 15 | (.setItem js/localStorage name (LZString/compress (pr-str data)))) 16 | 17 | (s/defn clear! 18 | "Clear the data at the specified name location." 19 | [name :- s/Str] 20 | (.removeItem js/localStorage name)) 21 | 22 | (s/defn load 23 | "Retrieve the data from the specified name location and try to convert it to a 24 | Clojure datatype." 25 | [name :- s/Str] 26 | (let [to-decompress (.getItem js/localStorage name)] 27 | (if (nil? to-decompress) 28 | nil 29 | (reader/read-string (LZString/decompress to-decompress))))) 30 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject novelette "0.1.0-indev" 2 | :description "ClojureScript engine for visual novels." 3 | :dependencies [[org.clojure/clojure "1.8.0"] 4 | [org.clojure/clojurescript "1.8.51"] 5 | [prismatic/schema "1.0.1"] 6 | [lein-doo "0.1.6"] 7 | [novelette-text "0.1.3"] 8 | [novelette-sprite "0.1.0.2"]] 9 | :plugins [[lein-cljsbuild "1.1.3"] 10 | [lein-doo "0.1.6"]] 11 | :hooks [leiningen.cljsbuild] 12 | :clean-targets ["runtime/js/*"] 13 | :cljsbuild 14 | { 15 | :builds 16 | [ 17 | {:id "novelette" 18 | :source-paths ["src/"] 19 | :compiler 20 | {:optimizations :simple 21 | :output-dir "runtime/js" 22 | :output-to "runtime/js/novelette.js" 23 | :pretty-print true 24 | :libs ["src-js/lzstr/lz-string.js"] 25 | :source-map "runtime/js/novelette.js.map" 26 | :closure-output-charset "US-ASCII" 27 | }} 28 | {:id "tests" 29 | :source-paths ["src/" "tests"] 30 | :compiler {:output-to "compiled-tests/tests.js" 31 | :libs ["src-js/lzstr/lz-string.js"] 32 | :optimizations :whitespace 33 | :main "novelette.tests.runner" 34 | :pretty-print true}}]}) 35 | -------------------------------------------------------------------------------- /src/novelette/GUI/panel.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI.panel 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [novelette-sprite.schemas :as scs] 5 | [schema.core :as s] 6 | [novelette.render] 7 | [novelette.utils :as u] 8 | [novelette.GUI :as GUI])) 9 | 10 | (s/defn render 11 | [{{ctx :context 12 | bg-color :bg-color} :content 13 | position :position} :- sc/GUIElement 14 | ancestors :- [sc/GUIElement]] 15 | (let [abs-position (GUI/absolute-position ancestors position)] 16 | (novelette.render/draw-rectangle ctx bg-color abs-position) 17 | nil)) 18 | 19 | (s/defn create 20 | "Creates a new panel GUI element with sane defaults." 21 | [ctx :- js/CanvasRenderingContext2D 22 | id :- sc/id 23 | position :- scs/pos 24 | z-index :- s/Int 25 | extra :- {s/Any s/Any}] 26 | (let [content (merge {:context ctx 27 | :bg-color "white"} extra)] 28 | (sc/GUIElement. :panel 29 | id 30 | position 31 | content 32 | [] 33 | {} 34 | false ; focus 35 | false ; hover 36 | z-index 37 | render))) 38 | -------------------------------------------------------------------------------- /tests/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.tests.utils 2 | (:require [cljs.test :refer-macros [deftest is run-tests]] 3 | [novelette.utils :as u])) 4 | 5 | (deftest get-center-coordinates-test 6 | (is (= [50 50](u/get-center-coordinates [0 0 100 100]))) 7 | (is (= [200 100] (u/get-center-coordinates [100 0 200 200])))) 8 | 9 | (deftest inside-bounds?-test 10 | (is (= true (u/inside-bounds? [10 10] [0 0 100 100]))) 11 | (is (= true (u/inside-bounds? [50 50] [20 10 150 300]))) 12 | (is (= false (u/inside-bounds? [0 0] [10 10 100 100]))) 13 | (is (= false (u/inside-bounds? [1000 500] [0 10 200 1000])))) 14 | 15 | (def sprite-test-1 16 | {:id :sprite-test-1 17 | :z-index 100 18 | :position [0 0 0 0]}) 19 | 20 | (def sprite-test-2 21 | {:id :sprite-test-2 22 | :z-index 50 23 | :position [0 0 0 0]}) 24 | 25 | (def sprite-test-3 26 | {:id :sprite-test-3 27 | :z-index 1000 28 | :position [0 0 0 0]}) 29 | 30 | (def sprite-test-4 31 | {:id :sprite-test-4 32 | :z-index 1 33 | :position [0 0 0 0]}) 34 | 35 | (deftest sort-z-index 36 | (is (= [sprite-test-3 sprite-test-1 sprite-test-2 sprite-test-4] 37 | (u/sort-z-index [sprite-test-1 sprite-test-2 38 | sprite-test-3 sprite-test-4]))) 39 | (is (not (= (reverse [sprite-test-3 sprite-test-1 sprite-test-2 sprite-test-4]) 40 | (u/sort-z-index [sprite-test-1 sprite-test-2 41 | sprite-test-3 sprite-test-4]))))) 42 | -------------------------------------------------------------------------------- /src/novelette/sound.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.sound 2 | (:require-macros [schema.core :as s]) 3 | (:require [goog.dom :as dom] 4 | [schema.core :as s])) 5 | 6 | (def SOUND-MAP (atom {})) 7 | 8 | (def BGM (atom {})) 9 | 10 | (declare load-sound) 11 | 12 | (s/defn load-error 13 | [uri :- s/Str 14 | sym :- s/Keyword] 15 | (let [window (dom/getWindow)] 16 | (.setTimeout window #(load-sound uri sym) 200))) 17 | 18 | (s/defn load-sound 19 | [uri :- s/Str 20 | sym :- s/Keyword] 21 | (let [sound (js/Audio.)] 22 | (set! (.-loop sound) true) 23 | (.addEventListener sound "loadeddata" #(swap! SOUND-MAP assoc sym sound)) 24 | (set! (.-onerror sound) #(load-error uri sym)) 25 | (set! (.-src sound) uri))) 26 | 27 | (s/defn stop-audio 28 | [sound :- js/Audio] 29 | (.pause sound)) 30 | 31 | (s/defn play-audio 32 | [sound :- js/Audio 33 | loop? :- s/Bool] 34 | (set! (.-currentTime sound) 0) 35 | (set! (.-loop sound) loop?) 36 | (.play sound)) 37 | 38 | (defn stop-bgm 39 | [] 40 | (let [sym (first (keys @BGM)) 41 | bgm (@BGM sym)] 42 | (when sym 43 | (stop-audio bgm)) 44 | (reset! BGM {}))) 45 | 46 | (s/defn play-bgm 47 | [sym :- s/Keyword] 48 | (let [curr-sym (first (keys @BGM)) 49 | sound1 (@BGM curr-sym) 50 | sound2 (sym @SOUND-MAP)] 51 | (cond 52 | (nil? curr-sym) 53 | (do 54 | (play-audio sound2 true) 55 | (reset! BGM {sym sound2})) 56 | (= curr-sym sym) 57 | nil 58 | :else 59 | (do 60 | (stop-bgm) 61 | (play-bgm sym))))) 62 | -------------------------------------------------------------------------------- /src/novelette/GUI/canvas.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI.canvas 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [schema.core :as s] 5 | [novelette.GUI :as GUI] 6 | [novelette.render])) 7 | 8 | (s/defn render 9 | [{{ctx :context 10 | canvas :entity 11 | base-color :base-color} :content} :- sc/GUIElement 12 | ancestors :- [sc/GUIElement]] 13 | ; TODO - Fix the canvas rendering, should we have the canvas rendered at all? 14 | ;(novelette.render/fill-clear canvas ctx base-color ) 15 | nil) 16 | 17 | (s/defn canvas-clicked 18 | [element :- sc/GUIElement 19 | screen :- sc/Screen] 20 | ; If we reached this event it means that no other widget captured the clicked 21 | ; event, so we just need to pass it down to the storyteller instead. 22 | [(assoc-in screen [:storyteller :clicked?] 23 | (get-in screen [:state :input-state :clicked?])) false]) 24 | 25 | (s/defn create 26 | "Creates a new canvas GUI element with sane defaults." 27 | [canvas :- js/HTMLCanvasElement 28 | ctx :- js/CanvasRenderingContext2D 29 | base-color :- s/Str] 30 | (let [content {:entity canvas 31 | :context ctx 32 | :base-color base-color}] 33 | (-> (sc/GUIElement. :canvas 34 | :canvas ; id 35 | [0 0 (.-width canvas) (.-height canvas)] 36 | content 37 | [] ; no children yet 38 | {} ; no events yet 39 | true ; focus 40 | false ; hover 41 | 10000 ; very low priority in depth 42 | render) 43 | (GUI/add-event-listener :clicked canvas-clicked)))) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Novelette 2 | 3 | [![Build Status](https://travis-ci.org/Morgawr/Novelette.svg?branch=master)](https://travis-ci.org/Morgawr/Novelette) 4 | 5 | A ClojureScript engine used to develop Visual Novels and graphic storytelling on 6 | the browser and node-webkit. 7 | 8 | ## Usage 9 | 10 | The project is still heavily in development and is not yet ready for release. 11 | 12 | There is a [planned roadmap/feature track list](https://docs.google.com/document/d/1YNBiB1H9DMh2VBY9-hdHCaoOaOZXpdn_k2SEazDmqLY) on Google docs where I keep track 13 | of what needs to be done and the development of new features. 14 | 15 | Alternatively, a quick grep for "TODO" string on the source code will show what 16 | minor or structural/refactoring issues need to be worked on. 17 | 18 | ## License 19 | 20 | Copyright © 2014 Federico "Morgawr" Pareschi 21 | 22 | This software is provided 'as-is', without any express or implied 23 | warranty. In no event will the authors be held liable for any damages 24 | arising from the use of this software. 25 | Permission is granted to anyone to use this software for any purpose, 26 | including commercial applications, and to alter it and redistribute it 27 | freely, subject to the following restrictions: 28 | 29 | 1. The origin of this software must not be misrepresented; you must not 30 | claim that you wrote the original software. 31 | 32 | 2. Altered source versions must be plainly marked as such, and must not be 33 | misrepresented as being the original software. 34 | 35 | 3. This notice may not be removed or altered from any source 36 | distribution 37 | 38 | 4. In case of use or inclusion of this software in a commercial product, a 39 | proper acknowledgment of this software inside the final product is required. 40 | -------------------------------------------------------------------------------- /src/novelette/GUI/button.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI.button 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [novelette-sprite.schemas :as scs] 5 | [schema.core :as s] 6 | [novelette.render] 7 | [novelette.utils :as u] 8 | [novelette.GUI :as GUI])) 9 | 10 | (s/defn render 11 | [{{ctx :context 12 | bg-color :bg-color 13 | fg-color :fg-color 14 | text :text 15 | font-size :font-size} :content 16 | position :position} :- sc/GUIElement 17 | ancestors :- [sc/GUIElement]] 18 | (let [abs-position (GUI/absolute-position ancestors position) 19 | text-center (GUI/absolute-position ancestors 20 | (update (u/get-center-coordinates position) 21 | 1 #(- % (/ font-size 2))))] 22 | (novelette.render/draw-rectangle ctx bg-color abs-position) 23 | (novelette.render/draw-text-centered ctx text-center text (str font-size "px") fg-color)) 24 | nil) 25 | 26 | (s/defn create 27 | "Creates a new button GUI element with sane defaults." 28 | [ctx :- js/CanvasRenderingContext2D 29 | id :- sc/id 30 | text :- s/Str 31 | position :- scs/pos 32 | z-index :- s/Int 33 | extra :- {s/Any s/Any}] 34 | (let [content (merge {:context ctx 35 | :bg-color "white" 36 | :fg-color "black" 37 | :text text 38 | :font-size 20} extra)] 39 | (sc/GUIElement. :button 40 | id 41 | position 42 | content 43 | [] 44 | {} 45 | false ; focus 46 | false ; hover 47 | z-index 48 | render))) 49 | -------------------------------------------------------------------------------- /src/novelette/GUI/label.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI.label 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [novelette-sprite.schemas :as scs] 5 | [schema.core :as s] 6 | [novelette.render] 7 | [novelette.utils :as u] 8 | [novelette.GUI :as GUI])) 9 | 10 | (s/defn render 11 | [{{ctx :context 12 | bg-color :bg-color 13 | fg-color :fg-color 14 | text :text 15 | font-size :font-size 16 | transparent? :transparent?} :content 17 | position :position} :- sc/GUIElement 18 | ancestors :- [sc/GUIElement]] 19 | (let [abs-position (GUI/absolute-position ancestors position) 20 | text-center (GUI/absolute-position ancestors 21 | (update (u/get-center-coordinates position) 22 | 1 #(- % (/ font-size 2))))] 23 | (when-not transparent? 24 | (novelette.render/draw-rectangle ctx bg-color abs-position)) 25 | (novelette.render/draw-text-centered ctx text-center text (str font-size "px") fg-color)) 26 | nil) 27 | 28 | (s/defn create 29 | "Creates a new label GUI element with sane defaults." 30 | [ctx :- js/CanvasRenderingContext2D 31 | id :- sc/id 32 | text :- s/Str 33 | position :- scs/pos 34 | z-index :- s/Int 35 | extra :- {s/Any s/Any}] 36 | (let [content (merge {:context ctx 37 | :bg-color "white" 38 | :fg-color "black" 39 | :text text 40 | :font-size 20} extra)] 41 | (sc/GUIElement. :label 42 | id 43 | position 44 | content 45 | [] 46 | {} 47 | false ; focus 48 | false ; hover 49 | z-index 50 | render))) 51 | -------------------------------------------------------------------------------- /src/novelette/input.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.input 2 | (:require-macros [schema.core :as s]) 3 | (:require [goog.events.EventType :as event-type] 4 | [goog.events :as events] 5 | [schema.core :as s])) 6 | 7 | (def INPUT-STATE (atom {:x 0 8 | :y 0 9 | :clicked? [false false] 10 | :down? [false false] 11 | :enabled? true 12 | })) 13 | 14 | (def mouse-key-map {0 0 15 | 2 1}) 16 | 17 | (def BOUNDING-BOX (atom nil)) 18 | 19 | ; TODO - Maybe add keyboard stuff too 20 | (defn mouse-move-listener 21 | [event] 22 | (if (nil? @BOUNDING-BOX) 23 | (.log js/console "WARNING: Input not properly initialized.") 24 | (let [x (- (.-clientX event) (.-left @BOUNDING-BOX)) 25 | y (- (.-clientY event) (.-top @BOUNDING-BOX))] 26 | (swap! INPUT-STATE assoc :x x :y y)))) 27 | 28 | (defn mouse-disable 29 | [event] 30 | (swap! INPUT-STATE assoc :enabled? false)) 31 | 32 | (defn mouse-enable 33 | [event] 34 | (swap! INPUT-STATE assoc :enabled? true)) 35 | 36 | (defn mouse-lclick-listener 37 | [event] 38 | (swap! INPUT-STATE assoc-in [:clicked? 0] true)) 39 | 40 | ; This is necessary to mask the contextmenu right-click 41 | ; default event listener. 42 | (defn mouse-rclick-listener 43 | [event] 44 | (.preventDefault event) 45 | (swap! INPUT-STATE assoc-in [:clicked? 1] true) 46 | false) 47 | 48 | (defn mouse-down 49 | [event] 50 | (let [key (.-button event)] 51 | (swap! INPUT-STATE assoc-in [:down? (mouse-key-map key)] true))) 52 | 53 | (defn mouse-up 54 | [event] 55 | (let [key (.-button event)] 56 | (swap! INPUT-STATE assoc-in [:down? (mouse-key-map key)] false))) 57 | 58 | (defn mouse-declick 59 | [] 60 | (swap! INPUT-STATE assoc :clicked? [false false])) 61 | 62 | (s/defn init 63 | [canvas :- js/HTMLCanvasElement] 64 | (reset! BOUNDING-BOX (.getBoundingClientRect canvas)) 65 | (.addEventListener canvas "contextmenu" mouse-rclick-listener) 66 | (events/listen canvas event-type/MOUSEMOVE mouse-move-listener) 67 | (events/listen canvas event-type/MOUSEOUT mouse-disable) 68 | (events/listen canvas event-type/MOUSEENTER mouse-enable) 69 | (events/listen canvas event-type/MOUSEDOWN mouse-down) 70 | (events/listen canvas event-type/MOUSEUP mouse-up) 71 | (events/listen canvas event-type/CLICK mouse-lclick-listener)) 72 | -------------------------------------------------------------------------------- /src/novelette/screens/dialoguescreen.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.screens.dialoguescreen 2 | (:require [novelette.render :as r] 3 | [novelette.screen :as gscreen])) 4 | 5 | (defn printed-full-message? 6 | [screen] 7 | (let [letters (:letters screen) 8 | msg (first (:messages screen))] 9 | (>= letters (count msg)))) 10 | 11 | (defn handle-input 12 | [screen mouse] 13 | (let [{:keys [x y clicked]} mouse] 14 | (if clicked 15 | (if (printed-full-message? screen) 16 | (assoc screen :advance true) 17 | (assoc screen :letters (count (first (:messages screen))))) 18 | screen))) 19 | 20 | (defn maybe-handle-input 21 | [screen on-top mouse] 22 | (if on-top 23 | (handle-input screen mouse) 24 | screen)) 25 | 26 | (defn end-message 27 | [screen] 28 | (assoc screen 29 | :next-frame 30 | (fn [state] 31 | (let [screen-list (:screen-list state) 32 | new-list (gscreen/pop-screen screen-list)] 33 | (assoc state :screen-list new-list))))) 34 | 35 | (defn next-letter 36 | [screen] 37 | (if (printed-full-message? screen) 38 | (:letters screen) 39 | (inc (:letters screen)))) 40 | 41 | (defn screen-update 42 | [screen elapsed-time] 43 | (let [adv (:advance screen) 44 | interval (:interval screen) 45 | letters (:letters screen) 46 | msgs (:messages screen) 47 | speed (:speed screen) 48 | interval (+ (:interval screen) elapsed-time) 49 | newinterval (- interval speed)] 50 | (cond 51 | (and adv (empty? (rest msgs))) 52 | (end-message screen) 53 | adv 54 | (assoc screen 55 | :interval 0 56 | :messages (rest msgs) 57 | :advance false 58 | :letters 0) 59 | (pos? newinterval) 60 | (assoc screen 61 | :interval newinterval 62 | :letters (next-letter screen)) 63 | :else 64 | (assoc screen :interval interval)))) 65 | 66 | (defn maybe-update 67 | [screen on-top elapsed-time] 68 | (if on-top (screen-update screen elapsed-time) screen)) 69 | 70 | (defn draw 71 | [screen] 72 | (let [bg (:background screen) 73 | ctx (:context screen) 74 | msg (first (:messages screen)) 75 | letters (:letters screen)] 76 | (r/draw-image ctx [0 400] bg) 77 | (r/draw-multiline-center-text ctx [400 450] 78 | (subs msg 0 letters) 79 | "25px" "white" 65 30) 80 | screen)) 81 | 82 | (defn maybe-draw 83 | [screen on-top] 84 | (if on-top (draw screen) screen)) 85 | 86 | (defn init [ctx canvas event-after messages] 87 | (-> gscreen/BASE-SCREEN 88 | (into { 89 | :id "DialogueScreen" 90 | :update maybe-update 91 | :render maybe-draw 92 | :handle-input maybe-handle-input 93 | :next-frame nil 94 | :background :dialoggui 95 | :context ctx 96 | :canvas canvas 97 | :deinit (fn [s] nil) 98 | :event-after event-after 99 | :messages messages ; list of messages to print 100 | :letters 0 ; letters of current message already printed 101 | :interval 0 ; milliseconds passed since last letter printed 102 | :speed 50 ; number of milliseconds before printing another letter 103 | }))) 104 | -------------------------------------------------------------------------------- /src/novelette/screen.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.screen 2 | (:require-macros [schema.core :as s]) 3 | (:require [goog.dom :as dom] 4 | [novelette.schemas :as sc] 5 | [novelette.input] 6 | [novelette.render] 7 | [schema.core :as s])) 8 | 9 | (def BASE-SCREEN (sc/Screen. "" nil nil nil nil nil nil nil)) 10 | 11 | (defn get-animation-method [] 12 | (let [window (dom/getWindow) 13 | methods ["requestAnimationFrame" 14 | "webkitRequestAnimationFrame" 15 | "mozRequestAnimationFrame" 16 | "oRequestAnimationFrame" 17 | "msRequestAnimationFrame"] 18 | options (map (fn [name] #(aget window name)) 19 | methods)] 20 | ((fn [[current & remaining]] 21 | (cond 22 | (nil? current) 23 | #((.-setTimeout window) % (/ 1000 30)) 24 | (fn? (current)) 25 | (current) 26 | :else 27 | (recur remaining))) 28 | options))) 29 | 30 | (s/defn screen-loop 31 | [screen :- sc/Screen 32 | on-top :- s/Bool 33 | elapsed-time :- s/Num] 34 | (let [update (:update screen) 35 | render (:render screen) 36 | input (:handle-input screen)] 37 | (-> screen 38 | (input on-top @novelette.input/INPUT-STATE) 39 | (update on-top elapsed-time) 40 | (render on-top)))) 41 | 42 | (s/defn iterate-screens 43 | [screen-list :- [sc/Screen] 44 | elapsed-time :- s/Num] 45 | (let [length (count screen-list)] 46 | (doall 47 | (map-indexed (fn [n s] 48 | (screen-loop s (= (dec length) n) 49 | elapsed-time)) 50 | screen-list)))) 51 | 52 | (s/defn remove-next-step 53 | [screen-list :- [sc/Screen]] 54 | (if (empty? screen-list) 55 | [] 56 | (doall 57 | (map #(assoc % :next-frame nil) screen-list)))) 58 | 59 | (s/defn push-screen 60 | [screen :- sc/Screen 61 | screen-list :- [sc/Screen]] 62 | (-> screen-list 63 | (remove-next-step) 64 | (#(conj (vec %) screen)))) 65 | 66 | (s/defn pop-screen 67 | [screen-list :- [sc/Screen]] 68 | (let [oldscreen (last screen-list)] 69 | ((:deinit oldscreen) oldscreen)) 70 | (pop (vec screen-list))) 71 | 72 | (s/defn replace-screen 73 | [screen :- sc/Screen 74 | screen-list :- [sc/Screen]] 75 | (->> screen-list 76 | (pop-screen) 77 | (push-screen screen))) 78 | 79 | (s/defn restart-screens 80 | [screen :- sc/Screen 81 | screen-list :- []] 82 | (loop [s screen-list] 83 | (if (empty? s) 84 | [screen] 85 | (recur (pop-screen s))))) 86 | 87 | (s/defn schedule-next-frame 88 | [state :- sc/State] 89 | "This function executes the scheduled init for the next screen if it is 90 | required." 91 | (let [next-frame (:next-frame (last (:screen-list state)))] 92 | (cond 93 | (nil? next-frame) state ; Nothing to be done 94 | (fn? next-frame) (next-frame state) 95 | :else (.log js/console "ERROR: next frame was something else?!")))) 96 | 97 | (s/defn update-time 98 | [state :- sc/State 99 | curr-time :- s/Num] 100 | (assoc state :curr-time curr-time)) 101 | 102 | (s/defn clear-screen 103 | [state :- sc/State] 104 | (novelette.render/fill-clear (:canvas state) (:context state) "black") 105 | state) 106 | 107 | (s/defn main-game-loop 108 | [state :- sc/State] 109 | (let [screen-list (:screen-list state) 110 | curr-time (.getTime (js/Date.)) 111 | elapsed-time (- curr-time (:curr-time state))] 112 | (-> state 113 | (clear-screen) 114 | (assoc :screen-list (iterate-screens screen-list elapsed-time)) 115 | (update-time curr-time) 116 | (schedule-next-frame) 117 | ((fn [s] ((get-animation-method) #(main-game-loop s))))) 118 | (novelette.input/mouse-declick))) 119 | -------------------------------------------------------------------------------- /src/novelette/render.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.render 2 | (:require-macros [schema.core :as s]) 3 | (:require [goog.dom :as dom] 4 | [clojure.string] 5 | [novelette.schemas :as sc] 6 | [novelette-sprite.schemas :as scs] 7 | [novelette-sprite.render] 8 | [schema.core :as s] 9 | [novelette.utils :as u])) 10 | 11 | ; TODO - re-work the entire text rendering engine 12 | ; TODO - re-work the color system so we support alpha values 13 | ; TODO - properly implement ctx.save() ctx.restore() 14 | 15 | (s/defn clear-context 16 | "Refresh and clear the whole canvas surface context." 17 | [screen :- sc/Screen] 18 | (let [ctx (:context screen) 19 | canvas (:canvas screen) 20 | width (.-width canvas) 21 | height (.-height canvas)] 22 | (.clearRect ctx 0 0 width height))) 23 | 24 | (def IMAGE-MAP (atom {})) ; Global state buffer for image data 25 | 26 | ; TODO - Add support for multiple fonts 27 | (def FONT "sans-serif") 28 | 29 | (s/defn draw-image 30 | "Draw the image on the canvas given the coordinates and the id of the image." 31 | [ctx :- js/CanvasRenderingContext2D 32 | pos :- scs/pos 33 | name :- sc/id] 34 | (let [image (name @IMAGE-MAP)] 35 | (.drawImage ctx image 36 | (first pos) 37 | (second pos)))) 38 | 39 | (s/defn draw-sprite 40 | "Draw a given sprite on the canvas." 41 | [ctx :- js/CanvasRenderingContext2D ; XXX - Temporary wrapper to Novelette-sprite 42 | sprite :- scs/Sprite] 43 | (novelette-sprite.render/draw-sprite ctx sprite @IMAGE-MAP)) 44 | 45 | (s/defn fill-clear 46 | "Fill the whole canvas with a given color." 47 | [canvas :- js/HTMLCanvasElement 48 | ctx :- js/CanvasRenderingContext2D 49 | color :- s/Str] 50 | (set! (.-fillStyle ctx) color) 51 | (.fillRect ctx 0 0 52 | (.-width canvas) 53 | (.-height canvas))) 54 | 55 | (s/defn draw-rectangle 56 | "Draw a rectangle shape on the canvas at the given coordinates." 57 | [ctx :- js/CanvasRenderingContext2D 58 | color :- s/Str 59 | pos :- scs/pos] 60 | (set! (.-fillStyle ctx) color) 61 | (.fillRect ctx (pos 0) (pos 1) (pos 2) (pos 3))) 62 | 63 | (s/defn draw-text 64 | "Draw a string of text on the canvas with the given properties." 65 | [ctx :- js/CanvasRenderingContext2D 66 | pos :- scs/pos 67 | text :- s/Str 68 | attr ; TODO - Figure out the data type of this 69 | color :- s/Str] 70 | (.save ctx) 71 | (set! (.-textBaseline ctx) "top") 72 | (set! (.-font ctx) (str attr " " FONT)) 73 | (set! (.-fillStyle ctx) color) 74 | (.fillText ctx text 75 | (first pos) 76 | (second pos)) 77 | (.restore ctx)) 78 | 79 | (s/defn measure-text-length 80 | "Measure the length of a given string in canvas pixel units." 81 | [ctx :- js/CanvasRenderingContext2D 82 | text :- s/Str] 83 | (.-width (.measureText ctx text))) 84 | 85 | (s/defn draw-text-centered 86 | "Draw a string centered at the given origin." 87 | [ctx :- js/CanvasRenderingContext2D 88 | pos :- scs/pos 89 | text :- s/Str 90 | attr ; TODO - Figure out the data type of this 91 | color :- s/Str] 92 | (.save ctx) 93 | (set! (.-font ctx) (str attr " " FONT)) 94 | (let [width (measure-text-length ctx text) 95 | newx (int (- (first pos) (/ width 2)))] 96 | (draw-text ctx [newx (second pos)] text attr color)) 97 | (.restore ctx)) 98 | 99 | (s/defn split-index 100 | [msg :- s/Str 101 | length :- s/Int] 102 | (loop [idx 0 prevs 0] 103 | (cond 104 | (< (count msg) length) 105 | [msg ""] 106 | (= (str (nth msg idx)) " ") 107 | (recur (inc idx) idx) 108 | (>= idx length) 109 | [(take prevs msg) (drop prevs msg)] 110 | :else 111 | (recur (inc idx) prevs)))) 112 | 113 | (s/defn draw-multiline-center-text 114 | "Draw a multiline string centered at the given origin." 115 | [ctx :- js/CanvasRenderingContext2D 116 | pos :- scs/pos 117 | msg :- s/Str 118 | attr ; TODO - Figure out the data type of this 119 | color :- s/Str 120 | maxlength :- s/Int 121 | spacing :- s/Int] 122 | (loop [msgs msg 123 | [_ y] pos] 124 | (let [res (split-index msgs maxlength) 125 | first-msg (clojure.string/join (first res)) 126 | rest-msg (clojure.string/join (second res))] 127 | (if (empty? rest-msg) 128 | (draw-text-centered ctx [(first pos) y] first-msg attr color) 129 | (do 130 | (draw-text-centered ctx [(first pos) y] first-msg attr color) 131 | (recur rest-msg (+ spacing y))))))) 132 | 133 | (s/defn get-center 134 | "Return the center position of the canvas." 135 | [canvas :- js/CanvasRenderingContext2D] 136 | (let [width (.-width canvas) 137 | height (.-height canvas) 138 | position [0 0 width height]] 139 | (u/get-center-coordinates position))) 140 | -------------------------------------------------------------------------------- /src/novelette/screens/loadingscreen.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.screens.loadingscreen 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.render :as r] 4 | [novelette-sprite.loader] 5 | [novelette.screen :as gscreen] 6 | [novelette.schemas :as sc] 7 | [novelette.sound :as gsound] 8 | [novelette.screens.storyscreen] 9 | [schema.core :as s])) 10 | 11 | ; This is the loading screen, it is the first screen that we load in the game 12 | ; and its task is to load all the resources (images, sounds, etc etc) of the 13 | ; game before we can begin playing. 14 | (s/defn can-play? 15 | [type :- s/Str] 16 | (not (empty? (. (js/Audio.) canPlayType type)))) 17 | 18 | (s/defn set-audio-extension 19 | [audio :- {s/Keyword s/Str} 20 | ext :- s/Str] 21 | (into {} (for [[k v] audio] [k (str v ext)]))) 22 | 23 | (s/defn generate-audio-list 24 | [audio :- {s/Keyword s/Str}] 25 | (let [canplayogg (can-play? "audio/ogg; codecs=vorbis") 26 | canplaymp3 (can-play? "audio/mpeg")] 27 | (cond 28 | canplayogg (set-audio-extension audio ".ogg") 29 | canplaymp3 (set-audio-extension audio ".mp3") 30 | :else (throw (js/Error. "Your browser does not seem to support the proper audio standards. The game will not work."))))) 31 | 32 | (s/defn handle-input 33 | [screen :- sc/Screen 34 | input :- {s/Any s/Any}] 35 | (if (and (:complete screen) ((:clicked? input) 0) (not (:advance screen))) 36 | (assoc screen :advance true) 37 | screen)) 38 | 39 | (s/defn maybe-handle-input 40 | [screen :- sc/Screen 41 | on-top :- s/Bool 42 | input :- {s/Any s/Any}] 43 | (if on-top 44 | (handle-input screen input) 45 | screen)) 46 | 47 | (s/defn load-main-menu ; XXX - Misleading function name! 48 | [screen :- sc/Screen] 49 | (let [ctx (:context screen) 50 | canvas (:canvas screen)] 51 | (assoc screen 52 | :next-frame 53 | (fn [state] 54 | (let [screen-list (:screen-list state) 55 | new-list (gscreen/replace-screen (:to-load screen) screen-list)] 56 | (assoc state :screen-list new-list)))))) 57 | 58 | (s/defn percentage-loaded 59 | [imgcount :- s/Int 60 | sndcount :- s/Int] 61 | (let [max (+ (count @r/IMAGE-MAP) ; TODO - With novelette-sprite this is broken 62 | (count @gsound/SOUND-MAP)) 63 | ptg (* (/ max (+ imgcount sndcount)) 100)] 64 | (int ptg))) 65 | 66 | (s/defn everything-loaded 67 | [screen :- sc/Screen] 68 | (let [complete true 69 | message "Finished loading, click to continue"] 70 | (assoc screen :complete complete :message message 71 | :percentage (percentage-loaded 72 | (count (:image-list screen)) (count (:audio-list screen)))))) 73 | 74 | (s/defn has-loaded? 75 | [num :- s/Int 76 | res-map :- {s/Any s/Any}] 77 | (= num (count res-map))) 78 | 79 | (s/defn all-loaded? 80 | [imgcount :- s/Int 81 | sndcount :- s/Int] 82 | (and (novelette-sprite.loader/loading-complete?) 83 | (has-loaded? sndcount @gsound/SOUND-MAP))) 84 | 85 | (s/defn load-sounds 86 | [screen :- sc/Screen] 87 | (doseq [[k v] (:audio-list screen)] (gsound/load-sound v k)) 88 | (assoc screen :loading-status 1)) 89 | 90 | (s/defn screen-update 91 | [screen :- sc/Screen 92 | elapsed-time :- s/Int] 93 | (let [images (count (:image-list screen)) 94 | sounds (count (:audio-list screen))] 95 | (cond 96 | (:advance screen) 97 | (load-main-menu screen) 98 | (:complete screen) 99 | screen 100 | (and (zero? (:loading-status screen)) 101 | (novelette-sprite.loader/loading-complete?)) 102 | (do 103 | (reset! r/IMAGE-MAP (novelette-sprite.loader/get-images!)) ; XXX - This is very ugly, it should be streamlined 104 | (load-sounds screen)) 105 | (all-loaded? images sounds) 106 | (everything-loaded screen) 107 | :else 108 | (assoc screen :percentage (percentage-loaded images sounds))))) 109 | 110 | (s/defn draw 111 | [screen :- sc/Screen] ; TODO - set coordinates to proper resolution 112 | (r/draw-text-centered (:context screen) [690 310] 113 | (:message screen) "25px" "white") 114 | (r/draw-text-centered (:context screen) [690 360] 115 | (str (:percentage screen) "%") "25px" "white") 116 | screen) 117 | 118 | (s/defn init 119 | [ctx :- js/CanvasRenderingContext2D 120 | canvas :- js/HTMLCanvasElement 121 | images :- {sc/id s/Str} 122 | audios :- {s/Any s/Any} 123 | to-load :- sc/Screen] 124 | (novelette-sprite.loader/load-images! images) 125 | (-> gscreen/BASE-SCREEN 126 | (into { 127 | :id "LoadingScreen" 128 | :update (fn [screen _ elapsed-time] (screen-update screen elapsed-time)) 129 | :render (fn [screen _] (draw screen)) 130 | :handle-input maybe-handle-input 131 | :next-frame nil 132 | :context ctx 133 | :canvas canvas 134 | :deinit (fn [s] nil) 135 | :advance false 136 | :complete false 137 | :message "Loading..." 138 | :percentage 0 139 | :loading-status 0 ; 0 = image, 1 = sound 140 | :audio-list (generate-audio-list audios) 141 | :image-list images 142 | :to-load to-load 143 | }))) 144 | -------------------------------------------------------------------------------- /src/novelette/syntax.clj: -------------------------------------------------------------------------------- 1 | ; This is a standalone file providing all the syntax transformation for the novelette 2 | ; syntactical engine. 3 | (ns novelette.syntax) 4 | 5 | ; TODO - add an optional init function? 6 | 7 | (defmacro parse-format 8 | [s] 9 | `(loop [messages# '() actions# '() remaining# ~s] 10 | (let [idx1# (.indexOf remaining# "{(") 11 | idx2# (.indexOf remaining# ")}")] 12 | (if (or (= -1 idx1#) 13 | (= -1 idx2#)) 14 | [(reverse (conj messages# remaining#)) (reverse actions#)] 15 | (let [half1# (->> remaining# 16 | (split-at idx1#) 17 | (first) 18 | (apply str)) 19 | half2# (->> remaining# 20 | (split-at (+ idx2# 2)) 21 | (second) 22 | (apply str)) 23 | match# (subs remaining# idx1# (+ idx2# 2))] 24 | (if (empty? half1#) 25 | (recur (conj messages# :pop) 26 | (conj actions# match#) 27 | half2#) 28 | (recur (concat [:pop half1#] messages#) 29 | (conj actions# match#) 30 | half2#))))))) 31 | 32 | (defmacro speak* 33 | [name text color] ; TODO - add support for font 34 | `(let [result# { :name ~name 35 | :color ~color 36 | :type :speech} 37 | msgs# (parse-format ~text)] 38 | (assoc result# 39 | :messages (first msgs#) 40 | :actions (second msgs#)))) 41 | 42 | (defmacro speaker 43 | [name color] 44 | `(fn [text#] 45 | (speak* ~name text# ~color))) 46 | 47 | (defmacro narrate 48 | [text] 49 | `(speak* "" ~text :red)) 50 | 51 | (defmacro defspeaker 52 | [symbol name color] ; TODO - add support for font 53 | `(def ~symbol 54 | (speaker ~name ~color))) 55 | 56 | (defmacro default 57 | [id] 58 | `{:default ~id}) 59 | 60 | (defmacro choice-explicit* 61 | [text args] 62 | `(let [args# ~args 63 | text# ~text] 64 | (apply merge-with 65 | (fn [a# b#] 66 | (assoc a# (first (keys b#)) (first (vals b#)))) 67 | {:type :explicit-choice :text text#} 68 | args#))) 69 | 70 | (defmacro choice-implicit* 71 | [args] 72 | `{:type :implicit-choice 73 | :options (map :options ~args)}) 74 | 75 | (defmacro defscene 76 | [name & body] 77 | `(def ~name { :name ~(str name) 78 | :body (into [] (filter seq (flatten [~@body])))})) 79 | 80 | (defmacro defblock 81 | [name & body] 82 | `(defn ~name [] 83 | [~@body])) 84 | 85 | (defmacro clear-sprites 86 | "Removes all sprites from the screen." 87 | [] 88 | `{:type :function 89 | :hook :clear-sprites}) 90 | 91 | (defmacro sprite 92 | "Adds a sprite on screen." 93 | [id] 94 | `{:type :function 95 | :hook :add-sprite 96 | :params [~id]}) 97 | 98 | (defmacro no-sprite 99 | "Removes a sprite from the screen." 100 | [id] 101 | `{:type :function 102 | :hook :remove-sprite 103 | :params [~id]}) 104 | 105 | (defmacro teleport-sprite 106 | "Teleports a sprite at a given position." 107 | [id position] 108 | `{:type :function 109 | :hook :teleport-sprite 110 | :params [~id ~position]}) 111 | 112 | (defmacro move-sprite 113 | "Moves a sprite at a given position in a given time (milliseconds)." 114 | [id position duration] 115 | `{:type :function 116 | :hook :move-sprite 117 | :params [~id ~position ~duration]}) 118 | 119 | (defmacro bgm 120 | "Starts playing new bgm." 121 | [id] 122 | `{:type :function 123 | :hook :play-bgm 124 | :params [~id]}) 125 | 126 | (defmacro no-bgm 127 | "Stops playing the bgm." 128 | [] 129 | `{:type :function 130 | :hook :stop-bgm 131 | :params []}) 132 | 133 | (defmacro declare-sprite 134 | "Declares a sprite bound to the current screen" 135 | [id sprite-model position z-index] 136 | `{:type :function 137 | :hook :decl-sprite 138 | :params [~id ~sprite-model ~position ~z-index]}) 139 | 140 | (defmacro clear-backgrounds 141 | "Empties the background stack" 142 | [] 143 | `{:type :function 144 | :hook :clear-backgrounds 145 | :params []}) 146 | 147 | (defmacro background 148 | "Adds a new background on top of the stack" 149 | [id] 150 | `{:type :function 151 | :hook :push-background 152 | :params [~id]}) 153 | 154 | (defmacro no-background 155 | "Removes the topmost background from the stack" 156 | [] 157 | `{:type :function 158 | :hook :pop-background 159 | :params []}) 160 | 161 | (defmacro show-ui 162 | "Shows UI on screen." 163 | [] 164 | `{:type :function 165 | :hook :show-ui 166 | :params []}) 167 | 168 | (defmacro hide-ui 169 | "Hides UI on screen." 170 | [] 171 | `{:type :function 172 | :hook :hide-ui 173 | :params []}) 174 | 175 | (defmacro set-ui 176 | "Sets the UI image to be used." 177 | [id pos] 178 | `{:type :function 179 | :hook :set-ui 180 | :params [~id ~pos]}) 181 | 182 | (defmacro set-cursor 183 | "Sets the glyph image used as cursor." 184 | [id] 185 | `{:type :function 186 | :hook :set-cursor 187 | :params [~id]}) 188 | 189 | (defmacro set-cps 190 | "Sets the characters per second." 191 | [amount] 192 | `{:type :function 193 | :hook :set-cps 194 | :params [~amount]}) 195 | 196 | (defmacro set-bounds 197 | "Sets the dialogue bounds for the UI textbox." 198 | [x y w h] 199 | `{:type :function 200 | :hook :set-dialogue-bounds 201 | :params [~x ~y ~w ~h]}) 202 | 203 | (defmacro set-nametag-position 204 | "Sets the nametag position in the UI textbox." 205 | [pos] 206 | `{:type :function 207 | :hook :set-nametag-position 208 | :params [~pos]}) 209 | 210 | (defmacro wait 211 | "Waits for a given amount of milliseconds." 212 | [msec] 213 | `{:type :function 214 | :hook :wait 215 | :params [~msec]}) 216 | 217 | (defmacro jump-to-scene 218 | "Transitions onto the next scene." 219 | [name] 220 | `{:type :function 221 | :hook :get-next-scene 222 | :params [~name]}) 223 | 224 | (defmacro add-label 225 | "Adds a label to the screen." 226 | [id parent text position] 227 | `{:type :add-label 228 | :id ~id 229 | :parent (if (nil? ~parent) 230 | :canvas ~parent) 231 | :text ~text 232 | :position ~position 233 | }) 234 | -------------------------------------------------------------------------------- /tests/GUI.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.tests.GUI 2 | (:require [cljs.test :refer-macros [deftest is run-tests]] 3 | [novelette.GUI :as GUI])) 4 | 5 | (def data-paths 6 | {:id :first-layer 7 | :children [ 8 | {:id :second-layer-1 9 | :children []} 10 | {:id :second-layer-2 11 | :children [ 12 | {:id :third-layer-1 13 | :children []} 14 | {:id :third-layer-2 15 | :children [ 16 | {:id :fourth-layer-1 17 | :children []} 18 | {:id :fourth-layer-2 19 | :children []} 20 | {:id :fourth-layer-3 21 | :children [ 22 | {:id :fifth-layer-1 23 | :children []} 24 | {:id :fifth-layer-2 25 | :children []}]} 26 | {:id :fourth-layer-4 27 | :children []}]} 28 | {:id :third-layer-3 29 | :children []}]} 30 | {:id :second-layer-3 31 | :children []} 32 | {:id :second-layer-4 33 | :children [ 34 | {:id :third-layer-4 35 | :children [ 36 | {:id :fourth-layer-5 37 | :children [ 38 | {:id :fifth-layer-3 39 | :children []}]} 40 | {:id :fourth-layer-6 41 | :chilren []}]} 42 | {:id :third-layer-5 43 | :children []}]}]}) 44 | 45 | 46 | (deftest find-element-path-test 47 | (is (= [:first-layer] 48 | (GUI/find-element-path :second-layer-1 data-paths []))) 49 | (is (= [:first-layer] 50 | (GUI/find-element-path :second-layer-2 data-paths []))) 51 | (is (= [:first-layer :second-layer-2] 52 | (GUI/find-element-path :third-layer-3 data-paths []))) 53 | (is (= [:first-layer :second-layer-2 :third-layer-2] 54 | (GUI/find-element-path :fourth-layer-1 data-paths []))) 55 | (is (not (= [:first-layer :second-layer-2 :third-layer-3] 56 | (GUI/find-element-path :fourth-layer-1 data-paths [])))) 57 | (is (= [] (GUI/find-element-path :first-layer data-paths []))) 58 | (is (= nil (GUI/find-element-path :non-existent-layer data-paths [])))) 59 | 60 | (deftest create-children-path 61 | (is (= [:children 1 :children 2] 62 | (GUI/create-children-path 63 | data-paths [:first-layer :second-layer-2 :third-layer-3]))) 64 | (is (= [] (GUI/create-children-path data-paths [:first-layer]))) 65 | (is (= nil (GUI/create-children-path data-paths [:first-layer :non-existent-layer :test])))) 66 | 67 | (def data-elements 68 | {:id :first-layer 69 | :children [ 70 | {:id :second-layer-1 71 | :children [ 72 | {:id :third-layer-1 73 | :children []} 74 | {:id :third-layer-2 75 | :children []}]} 76 | {:id :second-layer-2 77 | :children []}]}) 78 | 79 | (def data-elements-replace 80 | {:id :first-layer 81 | :children [ 82 | {:id :second-layer-1 83 | :children [ 84 | {:id :third-layer-1 85 | :children [ 86 | {:id :fourth-layer-1 87 | :children []}]} 88 | {:id :third-layer-2 89 | :children []}]} 90 | {:id :second-layer-2 91 | :children []}]}) 92 | 93 | (def data-elements-remove 94 | {:id :first-layer 95 | :children [ 96 | {:id :second-layer-1 97 | :children [ 98 | {:id :third-layer-2 99 | :children []}]} 100 | {:id :second-layer-2 101 | :children []}]}) 102 | 103 | (def data-elements-add 104 | {:id :first-layer 105 | :children [ 106 | {:id :second-layer-1 107 | :children [ 108 | {:id :third-layer-1 109 | :children []} 110 | {:id :third-layer-2 111 | :children []} 112 | {:id :third-layer-3 113 | :children []}]} 114 | {:id :second-layer-2 115 | :children []}]}) 116 | 117 | (def to-replace 118 | {:id :third-layer-1 119 | :children [ 120 | {:id :fourth-layer-1 121 | :children []}]}) 122 | 123 | (deftest add-element-test 124 | (is (= {:GUI data-elements-add} 125 | (GUI/add-element {:id :third-layer-3 126 | :children []} :second-layer-1 {:GUI data-elements})))) 127 | (deftest replace-element-test 128 | (is (= {:GUI data-elements-replace} 129 | (GUI/replace-element to-replace :third-layer-1 {:GUI data-elements})))) 130 | 131 | (deftest remove-element-test 132 | (is (= {:GUI data-elements-remove} 133 | (GUI/remove-element :third-layer-1 {:GUI data-elements})))) 134 | 135 | (deftest add-event-listener-test 136 | (let [func (fn [a b] (+ a b))] 137 | (is (= func (:clicked (:events (GUI/add-event-listener 138 | {:id :button 139 | :events {}} 140 | :clicked func))))) 141 | (is (= func (:clicked (:events (GUI/add-event-listener 142 | {:id :button 143 | :events {:clicked identity}} 144 | :clicked func))))))) 145 | 146 | (deftest remove-event-listener-test 147 | (let [func (fn [a b] (+ a b))] 148 | (is (= {:id :button 149 | :events {}} 150 | (GUI/remove-event-listener 151 | {:id :button 152 | :events {:clicked func}} 153 | :clicked))))) 154 | 155 | (run-tests) 156 | -------------------------------------------------------------------------------- /src/novelette/schemas.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.schemas 2 | (:require-macros [schema.core :as s]) 3 | (:require [schema.core :as s] 4 | [novelette-sprite.schemas :as scs] 5 | [cljs.reader])) 6 | 7 | ; This file contains all the schemas used by the Novelette VN engine so it can 8 | ; be easily referred from any namespace without dependency problems. 9 | 10 | ; TODO - Write a more comprehensive documentation on the data structures and 11 | ; their differences, composition and which data they can contain. 12 | 13 | (s/defschema function (s/pred fn? 'fn?)) 14 | 15 | ; The id of an element can either be a string or a keyword (prefer using keywords). 16 | (s/defschema id (s/cond-pre s/Str s/Keyword)) 17 | 18 | ; A GUIEvent is one of the following enums. 19 | (s/defschema GUIEvent (s/enum :clicked :on-focus 20 | :off-focus :on-hover 21 | :off-hover)) 22 | 23 | ; This is the screen, a screen is the base data structure that contains 24 | ; all the data for updating, rendering and handling a single state instance 25 | ; in the game. Multiple screens all packed together make the full state of the 26 | ; game. 27 | (s/defrecord Screen [id :- (s/maybe id) ; Unique identifier of the screen 28 | handle-input :- (s/maybe function) ; Function to handle input 29 | update :- (s/maybe function) ; Function to update state 30 | render :- (s/maybe function) ; Function to render on screen 31 | deinit :- (s/maybe function) ; Function to destroy screen 32 | canvas :- js/HTMLCanvasElement ; canvas of the game 33 | context :- js/CanvasRenderingContext2D ; context of the canvas 34 | next-frame :- (s/maybe function) ; What to do on the next game-loop 35 | ; TODO add GUI to all screens and base data structure 36 | ] 37 | {s/Any s/Any}) 38 | (cljs.reader/register-tag-parser! "novelette.schemas.Screen" 39 | map->Screen) 40 | 41 | ; TODO - purge a lot of old data and cruft 42 | 43 | (s/defrecord State [screen-list :- [Screen] 44 | curr-time :- s/Num 45 | context :- js/CanvasRenderingContext2D ; TODO - maybe invert order of this and canvas for consistency 46 | canvas :- js/HTMLCanvasElement] 47 | {s/Any s/Any}) 48 | (cljs.reader/register-tag-parser! "novelette.schemas.State" 49 | map->State) 50 | 51 | ; A GUIElement is the basic datatype used to handle GUI operations. 52 | (s/defrecord GUIElement [type :- s/Keyword; The element type. 53 | id :- id ; Name/id of the GUI element 54 | position :- scs/pos ; Coordinates of the element [x y w h] 55 | content :- {s/Keyword s/Any} ; Local state of the element (i.e.: checkbox checked? radio selected? etc) 56 | children :- [s/Any] ; Vector of children GUIElements. TODO - maybe turn this into a map 57 | events :- {GUIEvent function} ; Map of events. 58 | focus? :- s/Bool ; Whether or not the element has focus status. 59 | hover? :- s/Bool ; Whether or not the element has hover status. 60 | z-index :- s/Int ; Depth of the Element in relation to its siblings. lower = front 61 | render :- function ; Render function called on the element. 62 | ]) 63 | (cljs.reader/register-tag-parser! "novelette.schemas.GUIElement" 64 | map->GUIElement) 65 | 66 | ; TODO - Move on-top and elapsed-time into the screen structure 67 | ; This is the storytelling state of the game. It is an object containing the whole set of 68 | ; past, present and near-future state. It keeps track of stateful actions like scrollback, 69 | ; sprites being rendered, bgm playing and other stuff. Ideally, it should be easy to 70 | ; save/load transparently. 71 | (s/defrecord StoryState [scrollback :- [s/Any] ; Complete history of events for scrollback purposes, as a stack (this should contain the previous state of the storyscreen too) 72 | scrollfront :- [{s/Any s/Any}] ; Stack of events yet to be interpreted. 73 | spriteset :- #{s/Keyword} ; Set of sprite id currently displayed on screen. 74 | sprites :- {s/Keyword scs/Sprite} ; Map of sprites globally defined on screen. 75 | backgrounds :- [scs/Sprite] ; Stack of sprites currently in use as backgrounds. 76 | points :- {s/Keyword s/Int} ; Map of points that the player obtained during the game 77 | cps :- s/Int ; characters per second 78 | next-step? :- s/Bool ; Whether or not to advance to next step for storytelling 79 | show-ui? :- s/Bool ; Whether or not we show the game UI on screen. 80 | ui-img :- scs/Sprite; UI image to show TODO: Maybe integrate sprite in UI? 81 | input-state :- {s/Keyword s/Any} ; State of the input for the current frame. 82 | cursor :- s/Keyword ; Image of glyph used to advance text 83 | cursor-delta :- s/Num ; Delta to make the cursor float, just calculate % 4 84 | dialogue-bounds :- [s/Int] ; x,y, width and height of the boundaries of text to be displayed 85 | nametag-position :- [s/Int] ; x,y coordinates of the nametag in the UI 86 | ; TODO - add a "seen" map with all the dialogue options already seen 87 | ; to facilitate skipping of text. 88 | ] 89 | {s/Any s/Any}) 90 | (cljs.reader/register-tag-parser! "novelette.schemas.StoryState" 91 | map->StoryState) 92 | 93 | (s/defrecord StoryTeller [runtime-hooks :- {s/Any s/Any} ; Map of runtime hooks to in-text macros 94 | current-token :- {s/Any s/Any} ; Current token to parse. 95 | timer :- s/Num ; Amount of milliseconds passed since last token transition 96 | state :- {s/Any s/Any} ; Local storyteller state for transitioning events 97 | first? :- s/Bool ; Is this the first frame for this state? TODO - I dislike this, I need a better option. 98 | ] 99 | {s/Any s/Any}) 100 | (cljs.reader/register-tag-parser! "novelette.schemas.StoryTeller" 101 | map->StoryTeller) 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/novelette/GUI/story-ui.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI.story-ui 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [novelette-sprite.schemas :as scs] 5 | [schema.core :as s] 6 | [novelette.render] 7 | [novelette.utils :as u] 8 | [novelette.GUI :as GUI] 9 | novelette.GUI.button 10 | novelette.GUI.panel 11 | novelette.GUI.label)) 12 | 13 | (s/defn create-dialogue-panel 14 | [screen :- sc/Screen] 15 | (GUI/add-element (novelette.GUI.panel/create 16 | (:context screen) 17 | :dialogue-panel 18 | [30 500 1240 200] 10 ; TODO - Remove this hardcoded stuff and make it more general-purpose 19 | {:bg-color "#304090"}) 20 | :canvas screen)) 21 | 22 | (s/defn create-multiple-choice-panel 23 | [id :- sc/id 24 | ctx :- js/CanvasRenderingContext2D] 25 | (novelette.GUI.panel/create ctx id [320 180 640 360] 26 | 20 {:bg-color "#405599"})) 27 | 28 | (s/defn mult-clicked 29 | [element :- sc/GUIElement 30 | screen :- sc/Screen] 31 | [(assoc-in screen [:storyteller :state :choice] (:id element)) false]) 32 | 33 | (s/defn on-hover 34 | [element :- sc/GUIElement 35 | screen :- sc/Screen] 36 | (let [on-hover-color-map (get-in element [:content :on-hover-color])] 37 | [(-> screen 38 | (#(GUI/assoc-element (:id element) % [:hover?] true)) 39 | (#(GUI/update-element (:id element) % [:content] 40 | merge on-hover-color-map))) 41 | false])) 42 | 43 | (s/defn off-hover 44 | [element :- sc/GUIElement 45 | screen :- sc/Screen] 46 | (let [off-hover-color-map (get-in element [:content :off-hover-color])] 47 | [(-> screen 48 | (#(GUI/assoc-element (:id element) % [:hover?] false)) 49 | (#(GUI/update-element (:id element) % [:content] 50 | merge off-hover-color-map))) 51 | false])) 52 | 53 | (s/defn add-multiple-choice-buttons 54 | [ids :- [sc/id] 55 | {:keys [context] :as screen} :- sc/Screen] 56 | (let [gen-button-data (fn [id position] 57 | (-> (novelette.GUI.button/create 58 | context id id position 20 59 | {:bg-color "#222222" ; TODO - this is ugly, fix it 60 | :fg-color "#772222" 61 | :on-hover-color {:bg-color "#FFFFFF" 62 | :fg-color "#000000"} 63 | :off-hover-color {:bg-color "#222222" 64 | :fg-color "#772222"} 65 | :font-size 20}) 66 | (GUI/add-event-listener :clicked 67 | mult-clicked) 68 | (GUI/add-event-listener :on-hover on-hover) 69 | (GUI/add-event-listener :off-hover off-hover))) 70 | offset-y 60 71 | starting-pos [160 80 310 50]] 72 | (loop [screen screen counter 0 opts ids] 73 | (if (seq opts) 74 | (recur (GUI/add-element 75 | (gen-button-data (first opts) 76 | (update starting-pos 1 77 | (partial + (* offset-y counter)))) 78 | :choice-panel screen) 79 | (inc counter) 80 | (rest opts)) 81 | screen)))) 82 | 83 | (s/defn spawn-explicit-choice-gui 84 | [{:keys [storyteller state] :as screen} :- sc/Screen] 85 | (let [{{:keys [choice-text option-names]} :state} storyteller] 86 | (-> screen 87 | (#(GUI/add-element (create-multiple-choice-panel 88 | :choice-panel (:context %)) :canvas %)) 89 | (#(GUI/add-element (novelette.GUI.panel/create (:context %) 90 | :choice-panel-title 91 | [0 0 640 60] 19 92 | {:bg-color "#607070"}) 93 | :choice-panel %)) 94 | (#(GUI/add-element (novelette.GUI.label/create (:context %) 95 | :choice-label-title 96 | choice-text 97 | [0 0 640 60] 19 98 | {:fg-color "#000000" 99 | :transparent? true 100 | :font-size 30}) 101 | :choice-panel-title %)) 102 | (#(add-multiple-choice-buttons option-names %))))) 103 | 104 | (s/defn qsave-qload-clicked 105 | [element :- sc/GUIElement 106 | to-call-fn :- s/Keyword 107 | screen :- sc/Screen] 108 | [((get-in element [:content to-call-fn]) screen) false]) 109 | 110 | (s/defn init-story-ui 111 | [{:keys [canvas context] :as screen} :- sc/Screen 112 | qsave-fn :- sc/function 113 | qload-fn :- sc/function] 114 | (let [button-state {:bg-color "#FFFFFF" ; TODO - this is ugly, fix it 115 | :fg-color "#000000" 116 | :on-hover-color {:bg-color "#000000" 117 | :fg-color "#FFFFFF"} 118 | :off-hover-color {:bg-color "#FFFFFF" 119 | :fg-color "#000000"} 120 | :font-size 15} 121 | width (.-width canvas)] 122 | (->> screen 123 | (GUI/add-element 124 | (-> 125 | (novelette.GUI.button/create context :q-save "Q. Save" 126 | [(- width 130) 5 60 20] 1 127 | (assoc button-state 128 | :save-fn qsave-fn)) 129 | (GUI/add-event-listener :on-hover on-hover) 130 | (GUI/add-event-listener :off-hover off-hover) 131 | (GUI/add-event-listener :clicked #(qsave-qload-clicked 132 | %1 :save-fn %2))) 133 | :canvas) 134 | (GUI/add-element 135 | (-> 136 | (novelette.GUI.button/create context :q-load "Q. Load" 137 | [(- width 65) 5 60 20] 1 138 | (assoc button-state 139 | :load-fn qload-fn)) 140 | (GUI/add-event-listener :on-hover on-hover) 141 | (GUI/add-event-listener :off-hover off-hover) 142 | (GUI/add-event-listener :clicked #(qsave-qload-clicked 143 | %1 :load-fn %2))) 144 | :canvas)))) 145 | -------------------------------------------------------------------------------- /src/novelette/screens/storyscreen.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.screens.storyscreen 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.render :as r] 4 | [novelette.GUI :as GUI] 5 | [novelette.GUI.canvas] 6 | [novelette.GUI.story-ui :as ui] 7 | [novelette.screen :as gscreen] 8 | [novelette.sound :as gsound] 9 | [novelette.storyteller :as st] 10 | [novelette.schemas :as sc] 11 | [novelette-sprite.schemas :as scs] 12 | [novelette-sprite.loader] 13 | [novelette-sprite.render] 14 | [novelette-text.renderer :as text] 15 | [novelette.utils :as utils] 16 | [novelette.storage :as storage] 17 | [clojure.string :as string] 18 | [schema.core :as s])) 19 | 20 | (def dialogue-ui-model (novelette-sprite.loader/create-model 21 | :dialogue-ui [(scs/Keyframe. [0 0 1280 300] 0)])) 22 | 23 | ; TODO - Maybe remove the base state? It's only used in the init. 24 | (def BASE-STATE (sc/StoryState. '() '() #{} {} '() {} 0 true false 25 | (novelette-sprite.loader/create-sprite 26 | dialogue-ui-model [0 0] 0) {} :cursor 0 27 | [0 0 0 0] [0 0])) 28 | 29 | 30 | (s/defn update-cursor ; TODO - move this into the GUI 31 | [{:keys [state] :as screen} :- sc/Screen 32 | elapsed-time :- s/Int] 33 | (let [cursor-delta (:cursor-delta state)] 34 | (if (> (+ cursor-delta elapsed-time) 800) 35 | (assoc-in screen [:state :cursor-delta] 36 | (+ cursor-delta elapsed-time -800)) 37 | (assoc-in screen [:state :cursor-delta] 38 | (+ cursor-delta elapsed-time))))) 39 | 40 | (s/defn update-gui 41 | [screen :- sc/Screen 42 | elapsed-time :- s/Int] 43 | (update-cursor screen elapsed-time)) ; TODO - move this into the GUI 44 | 45 | (s/defn update-sprites 46 | [screen :- sc/Screen 47 | elapsed-time :- s/Int] 48 | (update-in screen [:state :sprites] 49 | #(into {} (for [[k s] %] 50 | [k (novelette-sprite.render/update-sprite 51 | s elapsed-time)])))) 52 | 53 | (s/defn screen-update 54 | [screen :- sc/Screen 55 | on-top :- s/Bool 56 | elapsed-time :- s/Int] 57 | (cond-> screen 58 | on-top 59 | (-> (st/screen-update elapsed-time) 60 | (update-sprites elapsed-time) 61 | (update-gui elapsed-time)))) 62 | 63 | (s/defn render-dialogue 64 | [{:keys [state storyteller context] :as screen} :- sc/Screen] 65 | (let [{:keys [dialogue-bounds nametag-position]} state 66 | [x y w _] dialogue-bounds 67 | dialogue (get-in storyteller [:state :display-message]) 68 | text-renderer (get-in screen [:text-renderer :renderer]) 69 | font-class (get-in screen [:text-renderer :font-class]) 70 | name-class (get-in screen [:text-renderer :name-class]) 71 | {{nametag :name 72 | namecolor :color} :current-token} storyteller] ; TODO refactor this 73 | (when (seq nametag) 74 | (text/draw-text nametag nametag-position 200 (assoc name-class :color (name namecolor)) text-renderer)) 75 | (let [returned-pos (text/draw-text dialogue [x y] w font-class text-renderer) 76 | cursor-delta (:cursor-delta state) 77 | offset (cond (< 0 cursor-delta 101) -2 ; TODO - this is terrible I hate myself. 78 | (or (< 100 cursor-delta 201) 79 | (< 700 cursor-delta 801)) -1 80 | (or (< 200 cursor-delta 301) 81 | (< 600 cursor-delta 701)) 0 82 | (or (< 300 cursor-delta 401) 83 | (< 500 cursor-delta 601)) 1 84 | (< 400 cursor-delta 501) 2 85 | :else 0) 86 | cursor-pos [(+ (first returned-pos) 2) ; TODO - maybe fix these hardcoded parameters (the x value offset) 87 | (+ offset (second returned-pos) -3)]] 88 | (r/draw-image context cursor-pos (:cursor state))))) 89 | 90 | ; TODO - Commented out this code as a reminder, but it needs to go. 91 | ;(s/defn render-choice 92 | ; [{:keys [state storyteller context] :as screen} :- sc/Screen] 93 | ; (.save context) 94 | ; (set! (.-shadowColor context) "black") 95 | ; (set! (.-shadowOffsetX context) 1.5) 96 | ; (set! (.-shadowOffsetY context) 1.5) 97 | ; (let [{{name :choice-text 98 | ; options :option-names} :state} storyteller 99 | ; pos-w (int (/ (r/measure-text-length context name) 2))] 100 | ; (.restore context))) 101 | 102 | (s/defn render ; TODO - move this to GUI 103 | [{:keys [state context] :as screen} :- sc/Screen 104 | on-top :- s/Bool] 105 | (let [bgs (reverse (:backgrounds state)) 106 | sps (if (seq (:spriteset state)) 107 | (utils/sort-z-index ((apply juxt (:spriteset state)) 108 | (:sprites state))) [])] 109 | (doseq [s bgs] 110 | (r/draw-image context [0 0] s)) ; TODO - Merge backgrounds into the sprite system 111 | (when on-top 112 | (doseq [s sps] 113 | (r/draw-sprite context s)) 114 | (GUI/render screen) 115 | (when (get-in screen [:storyteller :state :display-message]) 116 | (render-dialogue screen)) 117 | screen))) ; TODO This might just return nothing? render in any case shouldn't be stateful 118 | 119 | (s/defn handle-input 120 | [screen :- sc/Screen 121 | on-top :- s/Bool 122 | input :- {s/Any s/Any}] 123 | (cond-> screen 124 | on-top (#(GUI/handle-input 125 | (assoc-in % [:state :input-state] input))))) 126 | 127 | (defn init-text-engine ; TODO - Move this in a more reasonable place. Make it parameterized. 128 | [] 129 | {:renderer (text/create-renderer "surface") 130 | :font-class (text/create-font-class {:font-size "25px" 131 | :color "white" 132 | :line-spacing "7" 133 | :text-shadow "1.5 1.5 0 black" 134 | }) 135 | :name-class (text/create-font-class {:font-size "29px" 136 | :font-style "bold" 137 | :color "white"})}) 138 | 139 | 140 | (s/defn q-save 141 | [screen :- sc/Screen] 142 | (let [to-save {:state (:state screen) 143 | :storyteller (dissoc (:storyteller screen) :runtime-hooks)}] 144 | (storage/save! to-save "quickslot")) 145 | screen) 146 | 147 | ; TODO - Implement restoring of music/bgm as it was during the save. 148 | ; TODO - The multiple-choice options seem to be loaded in a different order, I need to investigate it 149 | (s/defn q-load 150 | [screen :- sc/Screen] 151 | (let [to-load (storage/load "quickslot") 152 | new-storyteller (merge (:storyteller screen) (:storyteller to-load))] 153 | (-> screen 154 | (assoc :storyteller new-storyteller) 155 | (assoc :state (:state to-load))))) 156 | 157 | (s/defn init 158 | [ctx :- js/CanvasRenderingContext2D 159 | canvas :- js/HTMLCanvasElement 160 | gamestate :- sc/StoryState] 161 | (let [screen (into gscreen/BASE-SCREEN 162 | { 163 | :id "StoryScreen" 164 | :update screen-update 165 | :render render 166 | :handle-input handle-input 167 | :context ctx 168 | :canvas canvas 169 | :deinit (fn [s] nil) 170 | :state gamestate 171 | :storyteller (sc/StoryTeller. @st/RT-HOOKS 172 | {:type :dummy} 0 {} false) 173 | :text-renderer (init-text-engine) 174 | :GUI (novelette.GUI.canvas/create canvas ctx "black")})] 175 | (-> screen 176 | (ui/create-dialogue-panel) 177 | (ui/init-story-ui q-save q-load)))) 178 | ; TODO - Find a way to properly pass user-provided init data to the canvas 179 | ; and other possible GUI elements. In this case it's the 'black' color. 180 | -------------------------------------------------------------------------------- /src/novelette/GUI.cljs: -------------------------------------------------------------------------------- 1 | (ns novelette.GUI 2 | (:require-macros [schema.core :as s]) 3 | (:require [novelette.schemas :as sc] 4 | [novelette-sprite.schemas :as scs] 5 | [schema.core :as s] 6 | [novelette.input] 7 | [novelette.utils :as utils])) 8 | 9 | ; This file contains the whole codebase for the Novelette GUI engine. 10 | 11 | ; Note on IDs: When creating a new GUI element it is possible to specify 12 | ; its parent ID. In case of duplicate IDs the function walks through the 13 | ; GUI entity graph as depth-first search and adds the new element to the 14 | ; first matching ID. This search's behavior is undefined if two or more 15 | ; elements have the same ID. 16 | 17 | ; Element types can be: 18 | ; :button 19 | ; :label 20 | ; :slider 21 | ; :radio 22 | ; :checkbox 23 | ; :dialog 24 | ; :canvas <-- There can only be one canvas in the whole game, it is the base GUI element 25 | ; used to catch all the base events when nothing else is hit. It is the root of the tree. 26 | 27 | ; TODO - Change name of functions for GUI widgets so that they don't repeat themselves. 28 | ; For example novelette.GUI.button/create-button-element -> novelette.GUI.button/create 29 | 30 | (s/defn absolute-position 31 | "Calculate the absolute position given a list of ancestors and the currently 32 | relative position within the parent element." 33 | [ancestors :- [sc/GUIElement] 34 | position :- scs/pos] 35 | (vec (reduce #(update (update %1 0 + ((:position %2) 0)) 36 | 1 + ((:position %2) 1)) position ancestors))) 37 | 38 | (s/defn handle-input-event 39 | "Given a GUIElement, screen and event type, act on the individual event." 40 | [element :- sc/GUIElement 41 | event :- sc/GUIEvent 42 | screen :- sc/Screen] 43 | (if (some? (event (:events element))) 44 | ((event (:events element)) element screen) 45 | [screen true])) 46 | 47 | ; Event types are :clicked :on-focus :off-focus :on-hover :off-hover 48 | ; as specified in the schema.cljs file 49 | (s/defn retrieve-event-trigger 50 | "Analyze the current input state to apply the correct event to the given 51 | GUI element." 52 | [element :- sc/GUIElement 53 | ancestors :- [sc/GUIElement] 54 | screen :- sc/Screen] 55 | (let [{x :x y :y 56 | clicked? :clicked?} @novelette.input/INPUT-STATE 57 | bounding-box (absolute-position ancestors (:position element)) 58 | in-bounds? (utils/inside-bounds? [x y] bounding-box)] 59 | ; if the mouse is inside the bounding box: 60 | ; -> check if the element has hover? as true 61 | ; -> if yes, do nothing 62 | ; -> if no, enable hover? and call the :on-hover event trigger 63 | ; if the mouse is outside the bounding box: 64 | ; -> check if the element has hover? as true 65 | ; -> if yes, disable hover? and call the :off-hover event trigger 66 | ; -> if no, do nothing 67 | ; if the mouse has any button state as clicked: 68 | ; -> check if the mouse is inside the bounding box: 69 | ; -> if yes, call the :clicked event on the element 70 | ; -> if no, do nothing 71 | ; TODO: figure out how to deal with on/off focus (keyboard tab-selection?) 72 | (cond 73 | (and (some true? clicked?) in-bounds?) 74 | (handle-input-event element :clicked screen) 75 | (and (not (:hover? element)) in-bounds?) 76 | (handle-input-event element :on-hover screen) 77 | (and (:hover? element) (not in-bounds?)) 78 | (handle-input-event element :off-hover screen) 79 | :else 80 | [screen true]))) 81 | 82 | (s/defn walk-GUI-events 83 | "Walk through the GUI tree dispatching events." 84 | [element :- sc/GUIElement 85 | ancestors :- [sc/GUIElement] 86 | screen :- sc/Screen] 87 | (let [walk-fn (fn [[screen continue?] child] 88 | (if continue? 89 | (walk-GUI-events 90 | child (conj ancestors element) 91 | screen) ; Recursion, be careful on the depth! 92 | [screen continue?])) 93 | [new-screen continue?] (reduce walk-fn [screen true] (:children element))] 94 | (if continue? 95 | (retrieve-event-trigger element ancestors new-screen) 96 | [new-screen continue?]))) 97 | 98 | (s/defn handle-input 99 | "Handle the input on the GUI engine. It delves into the subtree branch 100 | of the GUI element tree in a DFS and calls the appropriate handle-input 101 | function for each element until one of them returns false and stops 102 | propagating upwards." 103 | [{:keys [GUI] :as screen}] 104 | (if (:enabled? @novelette.input/INPUT-STATE) 105 | (first (walk-GUI-events GUI '() screen)) 106 | screen)) 107 | 108 | (s/defn render-gui 109 | "Generic render function called recursively on all GUI elements on the screen." 110 | [element :- sc/GUIElement 111 | ancestors :- [sc/GUIElement]] 112 | ((:render element) element ancestors) 113 | (doseq [x (reverse (sort-by :z-index (:children element)))] 114 | ; This could cause a stack-overflow but the depth of the children tree 115 | ; will never get that big. If it gets that big then you probably should 116 | ; rethink your lifestyle and choices that brought you to do this in the 117 | ; first place. Please be considerate with the amount of alcohol you drink 118 | ; and don't do drugs. 119 | ; 120 | ; Tests on a (java) repl showed no stackoverflow until the tree reached 121 | ; a level of 1000-1500 nested elements. I'm not sure about clojurescript's 122 | ; stack but it shouldn't be worse... I hope. 123 | (render-gui x (conj ancestors element)))) 124 | 125 | (s/defn find-element-path 126 | "Recursively look for a specific ID of a GUI element in a GUI tree. 127 | If the element is found, return a path of IDs to reach it, otherwise nil." 128 | [id :- sc/id 129 | GUI-tree :- sc/GUIElement 130 | walk-list :- [s/Any]] 131 | (let [found? (seq (filter #(= (:id %) id) (:children GUI-tree))) 132 | next-walk-list (conj walk-list (:id GUI-tree))] 133 | (cond 134 | (= id (:id GUI-tree)) [] 135 | (empty? (:children GUI-tree)) nil 136 | found? next-walk-list 137 | :else (let [result 138 | (keep identity 139 | (map #(find-element-path id % next-walk-list) 140 | (:children GUI-tree)))] 141 | (when (seq result) (first result)))))) 142 | 143 | (s/defn create-children-path 144 | "Given a list of IDs and the root of the tree, create a path." 145 | [root :- sc/GUIElement 146 | ancestors :- [s/Any]] 147 | (loop [element root remaining (rest ancestors) path []] 148 | (cond 149 | (not (seq remaining)) path 150 | :else 151 | (let [result (keep-indexed 152 | (fn [idx el] 153 | (when (= (:id el) (first remaining)) idx)) 154 | (:children element))] 155 | (when (seq result) 156 | (recur ((:children element) (first result)) 157 | (rest remaining) 158 | (conj (conj path :children) (first result)))))))) 159 | 160 | (s/defn render 161 | "Recursively calls into the registered elements render functions and displays 162 | them on the screen." 163 | [{:keys [GUI state]} :- sc/Screen] 164 | (when (:show-ui? state) 165 | (render-gui GUI '()))) ; TODO - Pass the whole screen to the rendering functions 166 | ; because we might want to handle transitions in the rendering. 167 | 168 | (s/defn replace-element 169 | "Find an element in the GUI tree and replace it with the new one." 170 | [element :- sc/GUIElement 171 | id :- sc/id 172 | {:keys [GUI] :as screen} :- sc/Screen] 173 | (let [search (find-element-path id GUI [])] 174 | (when (nil? search) 175 | (throw (js/Error. (str id " id not found in GUI element list.")))) 176 | (if (seq search) 177 | (let [path (create-children-path GUI (conj search id))] 178 | (assoc-in screen (concat [:GUI] path) element)) 179 | (assoc screen :GUI element)))) 180 | 181 | (s/defn add-element 182 | "Add a GUIElement to the GUI tree given the specified parent ID." 183 | [element :- sc/GUIElement 184 | parent :- sc/id 185 | {:keys [GUI] :as screen} :- sc/Screen] 186 | (let [search (find-element-path parent GUI [])] 187 | (when (nil? search) 188 | (throw (js/Error. (str parent " id not found in GUI element list.")))) 189 | (let [path (create-children-path GUI (conj search parent))] 190 | (update-in screen (concat [:GUI] path [:children]) 191 | conj element)))) 192 | 193 | (s/defn remove-element 194 | "Remove a GUIElement from the GUI tree given the ID." 195 | [id :- sc/id 196 | {:keys [GUI] :as screen} :- sc/Screen] 197 | (let [search (find-element-path id GUI [])] 198 | (when (nil? search) 199 | (throw (js/Error. (str id " id not found in GUI element list.")))) 200 | (let [path (create-children-path GUI (conj search id)) 201 | removal-path (concat [:GUI] (pop path)) 202 | split-index (last path) 203 | children (get-in screen removal-path) 204 | new-children (vec (concat (subvec children 0 split-index) 205 | (subvec children (inc split-index))))] 206 | (assoc-in screen removal-path new-children)))) 207 | 208 | ; TODO - Maybe write test for this? 209 | (s/defn update-element 210 | "Update the state of a given element inside the active GUI tree." 211 | [id :- sc/id 212 | screen :- sc/Screen 213 | keys :- [s/Keyword] 214 | func :- sc/function 215 | & args] 216 | (let [search (find-element-path id (:GUI screen) [])] 217 | (when (nil? search) 218 | (throw (js/Error. (str id " id not found in GUI element list.")))) 219 | (let [path (create-children-path (:GUI screen) (conj search id))] 220 | (update-in screen (concat [:GUI] path keys) #(apply func % args)))) ) 221 | 222 | ; TODO - Maybe write test for this? 223 | (s/defn assoc-element 224 | "Replace the state of a given element inside the active GUI tree." 225 | [id :- sc/id 226 | screen :- sc/Screen 227 | keys :- [s/Keyword] 228 | newstate :- s/Any] 229 | (let [search (find-element-path id (:GUI screen) [])] 230 | (when (nil? search) 231 | (throw (js/Error. (str id " id not found in GUI element list.")))) 232 | (let [path (create-children-path (:GUI screen) (conj search id))] 233 | (assoc-in screen (concat [:GUI] path keys) newstate)))) 234 | 235 | (s/defn add-event-listener 236 | "Add a new event listener to the GUI element. It overrides any previous one." 237 | [element :- sc/GUIElement 238 | event-type :- sc/GUIEvent 239 | target :- sc/function] 240 | (assoc-in element [:events event-type] target)) 241 | 242 | (s/defn remove-event-listener 243 | "Remove the event listener of the given type from the GUI element." 244 | [element :- sc/GUIElement 245 | event-type :- sc/GUIEvent] 246 | (update element :events dissoc event-type)) 247 | -------------------------------------------------------------------------------- /src/novelette/storyteller.cljs: -------------------------------------------------------------------------------- 1 | ; Storyteller is the underlying engine used for parsing, macroexpnding and 2 | ; setting up the storytelling engine with event hooks and storytelling 3 | ; routines. 4 | (ns novelette.storyteller 5 | (:require-macros [schema.core :as s]) 6 | (:require [novelette.sound :as snd] 7 | [clojure.string] 8 | [novelette.schemas :as sc] 9 | [novelette-sprite.schemas :as scs] 10 | novelette-sprite.loader 11 | novelette-sprite.render 12 | [novelette.GUI :as GUI] 13 | [novelette.GUI.story-ui :as ui] 14 | [schema.core :as s])) 15 | 16 | ; storyteller just takes a stack of instructions and executes them in order. 17 | ; It constantly fetches tokens from the script and interprets them. When it requires a user 18 | ; action or some time to pass (or other interactive stuff), it yields back to the storyscreen 19 | ; which continues with the rendering and updating. 20 | 21 | ;(.log js/console (str "Added: " (pr-str (:current-token storyteller)))) ; TODO - debug flag 22 | (s/defn init-new 23 | [screen :- sc/Screen] 24 | (update-in screen [:storyteller] merge {:state {} :first? true :timer 0})) 25 | 26 | (s/defn mark-initialized 27 | [screen :- sc/Screen] 28 | (assoc-in screen [:storyteller :first?] false)) 29 | 30 | (s/defn init-dialogue-state 31 | [{:keys [storyteller state] :as screen} :- sc/Screen] 32 | (cond-> screen 33 | (:first? storyteller) 34 | (update-in [:storyteller :state] merge {:cps (:cps state) 35 | :end? false}))) 36 | 37 | (s/defn advance-step 38 | ([{:keys [storyteller state] :as screen} :- sc/Screen 39 | yield? :- s/Bool] 40 | [(-> screen 41 | (assoc-in [:storyteller :current-token] (first (:scrollfront state))) ; TODO - add support for end-of-script 42 | (update-in [:state :scrollback] conj (:current-token storyteller)) ; TODO - conj entire state screen onto history 43 | (update-in [:state :scrollfront] rest) 44 | (init-new)) yield?]) 45 | ([screen :- sc/Screen] (advance-step screen false))) 46 | 47 | ; TODO - Probably extract these message utilities into their own module for 48 | ; simpler re-use (easier with memoization too!) 49 | (s/defn interpolate-message 50 | "Take the message string and split it into multiple groups." 51 | [msg :- s/Str] 52 | (let [regex-match (re-pattern "<[/]?style[=]?[^>]*>") 53 | matches (clojure.string/split msg regex-match) 54 | seqs (re-seq regex-match msg)] 55 | {:msg-list matches 56 | :sequences seqs})) 57 | 58 | ; TODO - I am adding unnecessary " " in-between the style tags because of a bug 59 | ; with the novelette-text library -> Novelette-text/issues/1 60 | (s/defn extract-message 61 | "Given a message sequence and a character count, retrieve a well-formatted 62 | message." 63 | [{:keys [msg-list sequences]} :- s/Any ; TODO - Do we want proper typing here? 64 | char-count :- s/Int] 65 | (cond 66 | (>= char-count (count (apply str msg-list))) 67 | (str (apply str (map str msg-list sequences)) ; Merge together the whole message + styles 68 | (if (> (count msg-list) (count sequences)) ; add closing tags if any :) 69 | (apply str (drop (count sequences) msg-list)) 70 | (str " " (clojure.string/join 71 | " " (drop (count msg-list) sequences))))) 72 | :else 73 | (loop [taken 0 msg "" msg-list msg-list sequences sequences] 74 | (let [next-msg (first msg-list)] 75 | (cond 76 | (or (>= taken char-count) 77 | (not (seq next-msg))) 78 | (str msg " " (clojure.string/join " " sequences)) 79 | (> (+ taken (count next-msg)) char-count) 80 | (str msg (apply str (take (- char-count taken) next-msg)) 81 | (clojure.string/join " " sequences)) 82 | :else 83 | (let [msg (apply str msg next-msg (first sequences)) 84 | taken (+ taken (count next-msg))] 85 | (recur taken msg (rest msg-list) (rest sequences))) ))))) 86 | 87 | ; This is for optimization purposes, memory usage might raise unnecessarily 88 | ; so it might be nice to have a "purge" command to free memoization resources. 89 | ; TODO - implement a "purge" command inbetween screen transitions 90 | (def memo-interpolate-message (atom (memoize interpolate-message))) 91 | (def memo-extract-message (atom (memoize extract-message))) 92 | (s/defn update-dialogue 93 | [{:keys [state storyteller] :as screen} :- sc/Screen] 94 | (let [{{current-token :current-token timer :timer 95 | {:keys [cps end? display-message]} :state} :storyteller 96 | {:keys [input-state]} :state} screen 97 | message (first (:messages current-token)) 98 | cpms (if (zero? cps) 0 (/ 1000 cps)) ; TODO fix this shit 99 | char-count (if (zero? cpms) 10000 (int (/ timer cpms))) 100 | clicked? (get-in storyteller [:clicked? 0])] 101 | (cond 102 | end? 103 | (let [next (assoc-in screen 104 | [:storyteller :state :display-message] message)] 105 | (if clicked? 106 | (advance-step next true) 107 | [next true])) 108 | (or clicked? 109 | (> char-count ((comp count str :msg-list) 110 | (@memo-interpolate-message message)))) 111 | [(update-in screen [:storyteller :state] 112 | merge {:display-message message :end? true}) true] 113 | :else ; Update display-message according to cps 114 | (let [msg (@memo-extract-message 115 | (@memo-interpolate-message message) char-count)] 116 | [(assoc-in screen [:storyteller :state :display-message] msg) true])))) 117 | 118 | (s/defn reset-clicked 119 | "Reset the clicked? status in the storyteller for future events." 120 | [[screen yield?] :- [(s/cond-pre sc/Screen s/Bool)]] 121 | [(assoc-in screen [:storyteller :clicked?] [false false]) yield?]) 122 | 123 | (s/defn init-explicit-choice 124 | [{:keys [storyteller state] :as screen} :- sc/Screen] 125 | (cond-> screen 126 | (:first? storyteller) 127 | (-> 128 | (update-in [:storyteller :state] 129 | merge {:choice-text (get-in storyteller [:current-token :text]) 130 | :option-names 131 | (keys (get-in storyteller [:current-token :options]))}) 132 | (ui/spawn-explicit-choice-gui)))) 133 | 134 | ; 1 - Storyteller must create GUI elements for multiple choice 135 | ; 2 - Storyteller must not be aware of the GUI composition and/or interface details 136 | ; 3 - Storyteller must retrieve multiple choice data from a shared variable with each button 137 | 138 | (s/defn update-explicit-choice 139 | [{:keys [storyteller] :as screen} :- sc/Screen] 140 | (let [storyteller-state (:state storyteller)] 141 | (if (:choice storyteller-state) 142 | (let [next (-> storyteller 143 | (get-in [:current-token :options]) 144 | ((fn [s] (s (:choice storyteller-state)))) 145 | (get-in [:jump :body]))] 146 | (-> screen 147 | (assoc-in [:state :scrollfront] next) 148 | (#(GUI/remove-element :choice-panel %)) 149 | (advance-step true))) 150 | [screen true]))) 151 | 152 | ; TODO - change this into self-hosted parsing with :update, maybe. 153 | (s/defn parse-event 154 | [screen :- sc/Screen] 155 | (let [{{step :current-token} :storyteller} screen] 156 | (cond 157 | (= :function (:type step)) 158 | (let [{{hooks :runtime-hooks} :storyteller} screen 159 | {fn-id :hook params :params} step] 160 | (apply (fn-id hooks) screen params)) 161 | (= :implicit-choice (:type step)) 162 | [screen false] ; TODO - Add implicit choices. 163 | (= :explicit-choice (:type step)) 164 | (-> screen 165 | (init-explicit-choice) 166 | (mark-initialized) 167 | (update-explicit-choice)) 168 | (= :speech (:type step)) 169 | (-> screen 170 | (init-dialogue-state) 171 | (mark-initialized) 172 | (update-dialogue) 173 | (reset-clicked)) 174 | (= :dummy (:type step)) 175 | (do 176 | (.log js/console "Dummy step") 177 | (advance-step screen)) 178 | :else 179 | (do 180 | (.log js/console "Storyteller: ") 181 | (.log js/console (pr-str (:storyteller screen))) 182 | (.log js/console "State: ") 183 | (.log js/console (pr-str (:state screen))) 184 | (.log js/console "Screen: ") 185 | (.log js/console (pr-str (dissoc screen :state :storyteller))) 186 | (throw 187 | (js/Error. (str "Error: unknown type -> " (pr-str (:type step))))))))) 188 | 189 | (s/defn screen-update 190 | [{:keys [storyteller] :as screen} :- sc/Screen 191 | elapsed-time :- s/Num] 192 | (-> screen 193 | (update-in [:storyteller :timer] + elapsed-time) 194 | ((fn [screen yield?] 195 | (cond 196 | yield? screen 197 | :else (let [[screen yield?] (parse-event screen)] 198 | (recur screen yield?)))) false))) 199 | 200 | ; RUNTIME HOOKS 201 | 202 | (s/defn add-sprite 203 | [screen :- sc/Screen 204 | id :- sc/id] 205 | (-> screen 206 | (update-in [:state :spriteset] conj id) 207 | (update-in [:state :sprites id] novelette-sprite.render/start-sprite) 208 | (advance-step))) 209 | 210 | (s/defn remove-sprite 211 | [screen :- sc/Screen 212 | id :- sc/id] 213 | (-> screen 214 | (update-in [:state :spriteset] disj id) 215 | (update-in [:state :sprites id] novelette-sprite.render/stop-sprite) 216 | (advance-step))) 217 | 218 | (s/defn clear-sprites 219 | [screen :- sc/Screen 220 | id :- sc/id] 221 | (-> screen 222 | (assoc-in [:state :spriteset] #{}) 223 | (advance-step))) 224 | 225 | (s/defn teleport-sprite 226 | [screen :- sc/Screen 227 | id :- sc/id 228 | position :- scs/pos] 229 | (-> screen 230 | (assoc-in [:state :sprites id :position] position) 231 | (advance-step))) 232 | 233 | (s/defn decl-sprite 234 | [screen :- sc/Screen 235 | id :- sc/id 236 | model :- scs/SpriteModel 237 | pos :- scs/pos 238 | z-index :- s/Int] 239 | (-> screen 240 | (assoc-in [:state :sprites id] (novelette-sprite.loader/create-sprite 241 | model pos z-index)) 242 | (update-in [:state :sprites id] novelette-sprite.render/pause-sprite) 243 | (advance-step))) 244 | 245 | (s/defn pop-background 246 | [screen :- sc/Screen] 247 | (-> screen 248 | (update-in [:state :backgrounds] rest) 249 | (advance-step))) 250 | 251 | (s/defn push-background 252 | [screen :- sc/Screen 253 | id :- sc/id] 254 | (-> screen 255 | (update-in [:state :backgrounds] conj id) 256 | (advance-step))) 257 | 258 | (s/defn clear-backgrounds 259 | [screen :- sc/Screen] 260 | (-> screen 261 | (assoc-in [:state :backgrounds] '()) 262 | (advance-step))) 263 | 264 | ; TODO: Add an extra show-dialogue because show-ui is too generic and it messes 265 | ; up with the q.save/q.load and system menu options 266 | (s/defn show-ui 267 | [screen :- sc/Screen] 268 | (-> screen 269 | (assoc-in [:state :show-ui?] true) 270 | (advance-step))) 271 | 272 | (s/defn hide-ui 273 | [screen :- sc/Screen] 274 | (-> screen 275 | (assoc-in [:state :show-ui?] false) 276 | (advance-step))) 277 | 278 | (s/defn set-ui 279 | [screen :- sc/Screen 280 | id :- sc/id 281 | pos :- scs/pos] 282 | (-> screen 283 | (update-in [:state :ui-img] merge {:id id :position pos}) 284 | (advance-step))) 285 | 286 | (s/defn wait 287 | [screen :- sc/Screen 288 | msec :- s/Int] 289 | (cond 290 | (<= msec (get-in screen [:storyteller :timer])) 291 | (advance-step screen) 292 | :else 293 | [screen true])) 294 | 295 | (s/defn get-next-scene 296 | [screen :- sc/Screen 297 | {:keys [body]} :- {s/Keyword s/Any}] 298 | (-> screen 299 | (assoc-in [:state :scrollfront] body) 300 | (advance-step))) 301 | 302 | (s/defn set-cps 303 | [screen :- sc/Screen 304 | amount :- s/Int] 305 | (-> screen 306 | (assoc-in [:state :cps] amount) 307 | (advance-step))) 308 | 309 | (s/defn set-dialogue-bounds 310 | [screen :- sc/Screen 311 | x :- s/Int y :- s/Int 312 | w :- s/Int h :- s/Int] 313 | (-> screen 314 | (assoc-in [:state :dialogue-bounds] [x y w h]) 315 | (advance-step))) 316 | 317 | (s/defn set-nametag-position 318 | [screen :- sc/Screen 319 | pos :- scs/pos] 320 | (-> screen 321 | (assoc-in [:state :nametag-position] pos) 322 | (advance-step))) 323 | 324 | (s/defn play-bgm 325 | [screen :- sc/Screen 326 | id :- sc/id] 327 | (snd/play-bgm id) 328 | (advance-step screen)) 329 | 330 | (s/defn stop-bgm 331 | [screen :- sc/Screen] 332 | (snd/stop-bgm) 333 | (advance-step screen)) 334 | 335 | (def RT-HOOKS (atom { 336 | :play-bgm play-bgm 337 | :stop-bgm stop-bgm 338 | :add-sprite add-sprite 339 | :remove-sprite remove-sprite 340 | :teleport-sprite teleport-sprite 341 | :clear-sprites clear-sprites 342 | :decl-sprite decl-sprite 343 | :pop-background pop-background 344 | :push-background push-background 345 | :clear-backgrounds clear-backgrounds 346 | :show-ui show-ui 347 | :hide-ui hide-ui 348 | :set-ui set-ui 349 | :wait wait 350 | :get-next-scene get-next-scene 351 | :set-cps set-cps 352 | :set-dialogue-bounds set-dialogue-bounds 353 | :set-nametag-position set-nametag-position 354 | })) 355 | -------------------------------------------------------------------------------- /src-js/lzstr/lz-string.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Pieroxy 2 | // This work is free. You can redistribute it and/or modify it 3 | // under the terms of the WTFPL, Version 2 4 | // For more information see LICENSE.txt or http://www.wtfpl.net/ 5 | // 6 | // For more information, the home page: 7 | // http://pieroxy.net/blog/pages/lz-string/testing.html 8 | // 9 | // LZ-based compression algorithm, version 1.4.4 10 | 11 | goog.provide("lzstr.LZString"); 12 | 13 | var LZString = (function() { 14 | 15 | // private property 16 | var f = String.fromCharCode; 17 | var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 18 | var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; 19 | var baseReverseDic = {}; 20 | 21 | function getBaseValue(alphabet, character) { 22 | if (!baseReverseDic[alphabet]) { 23 | baseReverseDic[alphabet] = {}; 24 | for (var i=0 ; i>> 8; 69 | buf[i*2+1] = current_value % 256; 70 | } 71 | return buf; 72 | }, 73 | 74 | //decompress from uint8array (UCS-2 big endian format) 75 | decompressFromUint8Array:function (compressed) { 76 | if (compressed===null || compressed===undefined){ 77 | return LZString.decompress(compressed); 78 | } else { 79 | var buf=new Array(compressed.length/2); // 2 bytes per character 80 | for (var i=0, TotalLen=buf.length; i> 1; 162 | } 163 | } else { 164 | value = 1; 165 | for (i=0 ; i> 1; 187 | } 188 | } 189 | context_enlargeIn--; 190 | if (context_enlargeIn == 0) { 191 | context_enlargeIn = Math.pow(2, context_numBits); 192 | context_numBits++; 193 | } 194 | delete context_dictionaryToCreate[context_w]; 195 | } else { 196 | value = context_dictionary[context_w]; 197 | for (i=0 ; i> 1; 207 | } 208 | 209 | 210 | } 211 | context_enlargeIn--; 212 | if (context_enlargeIn == 0) { 213 | context_enlargeIn = Math.pow(2, context_numBits); 214 | context_numBits++; 215 | } 216 | // Add wc to the dictionary. 217 | context_dictionary[context_wc] = context_dictSize++; 218 | context_w = String(context_c); 219 | } 220 | } 221 | 222 | // Output the code for w. 223 | if (context_w !== "") { 224 | if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { 225 | if (context_w.charCodeAt(0)<256) { 226 | for (i=0 ; i> 1; 247 | } 248 | } else { 249 | value = 1; 250 | for (i=0 ; i> 1; 272 | } 273 | } 274 | context_enlargeIn--; 275 | if (context_enlargeIn == 0) { 276 | context_enlargeIn = Math.pow(2, context_numBits); 277 | context_numBits++; 278 | } 279 | delete context_dictionaryToCreate[context_w]; 280 | } else { 281 | value = context_dictionary[context_w]; 282 | for (i=0 ; i> 1; 292 | } 293 | 294 | 295 | } 296 | context_enlargeIn--; 297 | if (context_enlargeIn == 0) { 298 | context_enlargeIn = Math.pow(2, context_numBits); 299 | context_numBits++; 300 | } 301 | } 302 | 303 | // Mark the end of the stream 304 | value = 2; 305 | for (i=0 ; i> 1; 315 | } 316 | 317 | // Flush the last char 318 | while (true) { 319 | context_data_val = (context_data_val << 1); 320 | if (context_data_position == bitsPerChar-1) { 321 | context_data.push(getCharFromInt(context_data_val)); 322 | break; 323 | } 324 | else context_data_position++; 325 | } 326 | return context_data.join(''); 327 | }, 328 | 329 | decompress: function (compressed) { 330 | if (compressed == null) return ""; 331 | if (compressed == "") return null; 332 | return LZString._decompress(compressed.length, 32768, function(index) { return compressed.charCodeAt(index); }); 333 | }, 334 | 335 | _decompress: function (length, resetValue, getNextValue) { 336 | var dictionary = [], 337 | next, 338 | enlargeIn = 4, 339 | dictSize = 4, 340 | numBits = 3, 341 | entry = "", 342 | result = [], 343 | i, 344 | w, 345 | bits, resb, maxpower, power, 346 | c, 347 | data = {val:getNextValue(0), position:resetValue, index:1}; 348 | 349 | for (i = 0; i < 3; i += 1) { 350 | dictionary[i] = i; 351 | } 352 | 353 | bits = 0; 354 | maxpower = Math.pow(2,2); 355 | power=1; 356 | while (power!=maxpower) { 357 | resb = data.val & data.position; 358 | data.position >>= 1; 359 | if (data.position == 0) { 360 | data.position = resetValue; 361 | data.val = getNextValue(data.index++); 362 | } 363 | bits |= (resb>0 ? 1 : 0) * power; 364 | power <<= 1; 365 | } 366 | 367 | switch (next = bits) { 368 | case 0: 369 | bits = 0; 370 | maxpower = Math.pow(2,8); 371 | power=1; 372 | while (power!=maxpower) { 373 | resb = data.val & data.position; 374 | data.position >>= 1; 375 | if (data.position == 0) { 376 | data.position = resetValue; 377 | data.val = getNextValue(data.index++); 378 | } 379 | bits |= (resb>0 ? 1 : 0) * power; 380 | power <<= 1; 381 | } 382 | c = f(bits); 383 | break; 384 | case 1: 385 | bits = 0; 386 | maxpower = Math.pow(2,16); 387 | power=1; 388 | while (power!=maxpower) { 389 | resb = data.val & data.position; 390 | data.position >>= 1; 391 | if (data.position == 0) { 392 | data.position = resetValue; 393 | data.val = getNextValue(data.index++); 394 | } 395 | bits |= (resb>0 ? 1 : 0) * power; 396 | power <<= 1; 397 | } 398 | c = f(bits); 399 | break; 400 | case 2: 401 | return ""; 402 | } 403 | dictionary[3] = c; 404 | w = c; 405 | result.push(c); 406 | while (true) { 407 | if (data.index > length) { 408 | return ""; 409 | } 410 | 411 | bits = 0; 412 | maxpower = Math.pow(2,numBits); 413 | power=1; 414 | while (power!=maxpower) { 415 | resb = data.val & data.position; 416 | data.position >>= 1; 417 | if (data.position == 0) { 418 | data.position = resetValue; 419 | data.val = getNextValue(data.index++); 420 | } 421 | bits |= (resb>0 ? 1 : 0) * power; 422 | power <<= 1; 423 | } 424 | 425 | switch (c = bits) { 426 | case 0: 427 | bits = 0; 428 | maxpower = Math.pow(2,8); 429 | power=1; 430 | while (power!=maxpower) { 431 | resb = data.val & data.position; 432 | data.position >>= 1; 433 | if (data.position == 0) { 434 | data.position = resetValue; 435 | data.val = getNextValue(data.index++); 436 | } 437 | bits |= (resb>0 ? 1 : 0) * power; 438 | power <<= 1; 439 | } 440 | 441 | dictionary[dictSize++] = f(bits); 442 | c = dictSize-1; 443 | enlargeIn--; 444 | break; 445 | case 1: 446 | bits = 0; 447 | maxpower = Math.pow(2,16); 448 | power=1; 449 | while (power!=maxpower) { 450 | resb = data.val & data.position; 451 | data.position >>= 1; 452 | if (data.position == 0) { 453 | data.position = resetValue; 454 | data.val = getNextValue(data.index++); 455 | } 456 | bits |= (resb>0 ? 1 : 0) * power; 457 | power <<= 1; 458 | } 459 | dictionary[dictSize++] = f(bits); 460 | c = dictSize-1; 461 | enlargeIn--; 462 | break; 463 | case 2: 464 | return result.join(''); 465 | } 466 | 467 | if (enlargeIn == 0) { 468 | enlargeIn = Math.pow(2, numBits); 469 | numBits++; 470 | } 471 | 472 | if (dictionary[c]) { 473 | entry = dictionary[c]; 474 | } else { 475 | if (c === dictSize) { 476 | entry = w + w.charAt(0); 477 | } else { 478 | return null; 479 | } 480 | } 481 | result.push(entry); 482 | 483 | // Add w+entry[0] to the dictionary. 484 | dictionary[dictSize++] = w + entry.charAt(0); 485 | enlargeIn--; 486 | 487 | w = entry; 488 | 489 | if (enlargeIn == 0) { 490 | enlargeIn = Math.pow(2, numBits); 491 | numBits++; 492 | } 493 | 494 | } 495 | } 496 | }; 497 | return LZString; 498 | })(); 499 | 500 | if (typeof define === 'function' && define.amd) { 501 | define(function () { return LZString; }); 502 | } else if( typeof module !== 'undefined' && module != null ) { 503 | module.exports = LZString 504 | } 505 | 506 | goog.exportSymbol("lzstr.LZString", LZString); 507 | --------------------------------------------------------------------------------