├── 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 | [](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 |
--------------------------------------------------------------------------------