Files
gitea-docker/desktop/src-common/advent/utils.clj
Bryce Covert c33950eed8 build
2017-01-19 09:17:17 -08:00

671 lines
26 KiB
Clojure

(ns advent.utils
(:refer-clojure :exclude [load])
(:require [play-clj.core :refer :all]
[play-clj.ui :refer :all]
[play-clj.utils :refer :all]
[play-clj.math :refer :all]
[play-clj.g2d :refer :all]
[play-clj.entities :refer [->TextureEntity]]
[clojure.java.io :as io]
[clojure.edn :as edn]
[clojure.tools.logging :as log]
[clojure.string :as str]
[advent.saves :as saves]
[advent.steam :as steam])
(:import [com.badlogic.gdx.graphics Pixmap Pixmap$Format Pixmap$Blending Pixmap$Filter Texture Texture$TextureFilter]
[com.badlogic.gdx.graphics.g2d TextureRegion Animation]
[com.badlogic.gdx.utils.viewport FitViewport]
[com.badlogic.gdx.utils ScreenUtils]
[com.badlogic.gdx.graphics PixmapIO$PNG]
[com.badlogic.gdx.scenes.scene2d Actor Stage]
[com.badlogic.gdx.math CatmullRomSpline]
[com.badlogic.gdx Application Audio Files Game Gdx Graphics Input
InputMultiplexer InputProcessor Net Preferences Screen]
[java.lang Object]))
(defn add-actor-to-stage [{:keys [^Stage renderer ui-listeners]} {:keys [^Actor object] :as entity}]
(.addActor renderer object)
(doseq [[_ listener] ui-listeners]
(.addListener ^Actor object listener))
entity)
(def ui-scale (Double/parseDouble (str (or (System/getProperty "ui_scale") 1.5))))
(def mobile? (= 1.5 ui-scale))
(def max-zoom (if mobile? 0.5 0.75))
(def min-zoom 0.95)
(def button-font-scale (if mobile? 1.0 0.5))
(def title-label-scale (if mobile? 0.5 0.25))
(defn clear-stage [{:keys [^Stage renderer]}]
(.clear renderer))
(defn remove-actor-from-stage
([entities id]
(do
(remove-actor-from-stage (id entities))
(dissoc entities id)))
([{:keys [^Actor object]}]
(when object
(.remove object))))
(defn log-coords [screen entities]
(let [{:keys [x y]} (input->screen screen {:x (:input-x screen) :y (:input-y screen)})]
(log/info (:input-x screen) (:input-y screen) "->" x y)))
(def ^:const +all-cursors+ [:main :wool :mushrooms :carrot :right :down :left :up :flask :flask-with-contents :trophy :ladder :stick :cat-toy :balloon :frog-legs :teddy :portrait :recipe :glass-eye :motivational-tapes :used-earplugs :grass :slobber :flask-with-strength :medal :kiss :sword :hourglass :mandrake :ball-n-chain :key :rope :crowbar :note-1 :ash :sack-lunch :flies :spear :monocle :feather :spell-component :money :charcoal :broken-clock :slingshot :camera :walkie-talkies :alarm-clock :walkie-talkie :flask-water :flask-water-stuff :flask-water-stuff-2 :note-2 :magic-slingshot :active-main :shovel :broom :tune :hand :hand-depressed :talk :talk-depressed :look :look-depressed])
(def ^:const +cursor-hotspots+ {:look [8 8]
:look-depressed [8 8]
:wool [8 8]
:mushrooms [8 8]
:right [8 8]
:down [8 8]
:left [8 8]
:up [8 8]
:flask [8 8]
:flask-with-contents [8 8]
:trophy [8 8]
:ladder [8 8]
:stick [8 8]
:cat-toy [8 8]
:balloon [3 3]
:frog-legs [8 8]
:teddy [8 8]
:portrait [5 8]
:recipe [8 8]
:glass-eye [8 3]
:motivational-tapes [8 8]
:used-earplugs [8 8]
:grass [2 2]
:slobber [8 8]
:flask-with-strength [8 8]
:medal [8 8]
:kiss [8 8]
:sword [8 8]
:hourglass [0 0]
:mandrake [8 8]
:ball-n-chain [8 8]
:key [3 8]
:rope [8 8]
:crowbar [0 0]
:note-1 [8 8]
:ash [8 10]
:sack-lunch [8 8]
:flies [8 8]
:spear [0 0]
:monocle [8 8]
:feather [8 8]
:spell-component [8 8]
:money [8 8]
:charcoal [1 1]
:broken-clock [8 8]
:slingshot [8 8]
:camera [8 8]
:walkie-talkies [8 8]
:alarm-clock [8 8]
:walkie-talkie [8 8]
:flask-water [8 8]
:flask-water-stuff [8 8]
:flask-water-stuff-2 [8 8]
:note-2 [8 8]
:magic-slingshot [8 8]
:active-main [0 0]
:shovel [8 8]
:broom [0 0]
:tune [8 8]
:hand [3 0]
:hand-depressed [3 0]
:talk [8 8]
:talk-depressed [8 8]})
(def settings (atom {:music-volume 50.0
:sound-volume 75.0
:fullscreen true}))
(defonce selected-save (atom nil))
(defn current-music-volume [& [^double factor]]
(-> (Math/pow (unchecked-multiply (:music-volume @settings) 0.01) 2)
(unchecked-multiply 0.25)
(unchecked-multiply (or factor 1.0))))
(defn current-sound-volume [& [factor]]
(-> (Math/pow (unchecked-multiply (:sound-volume @settings) 0.01) 2)
(unchecked-multiply 0.5)
(unchecked-multiply (or factor 1.0))))
(defn cursor [filename which]
(let [scale 4
base-cursor (pixmap filename)
target-width (* 16 scale)
target-height (* 16 scale)
resized (Pixmap. target-width target-height (.getFormat base-cursor))
index (.indexOf +all-cursors+ which)
[hot-x hot-y] (+cursor-hotspots+ which [0 0])
hot-x (int hot-x)
hot-y (int hot-y)]
(Pixmap/setFilter Pixmap$Filter/NearestNeighbour)
(pixmap! resized :draw-pixmap base-cursor (* index 18) 0 16 16
0 0 target-width target-height)
(graphics! :new-cursor resized (* hot-x scale) (* hot-y scale) )))
(defn get-texture [path]
(let [atlas-name (str/replace path #".png" "")
atlas (texture-atlas "packed/pack.atlas")]
(texture (texture-atlas! atlas :find-region atlas-name))))
(defn snapshot-list []
(->> (for [filename (steam/list-edn-files)]
(try
(edn/read-string (String. ^bytes (steam/get-bytes filename) "UTF-8"))
(catch Exception _ nil)))
(filter (every-pred identity :name :id))
(sort-by (fn [s]
(if (= "Autosave" (:name s))
(Long/MIN_VALUE)
(- (Long/parseLong (:id s))))))))
(defn snapshot-screenshots []
(doall (for [snapshot (snapshot-list)]
(assoc-in snapshot [:screenshot]
(try (let [bytes (steam/get-bytes (:screenshot snapshot))]
(Pixmap. ^bytes bytes 0 (count bytes)))
(catch Exception e
(Pixmap. 160 120 Pixmap$Format/RGB888)))))))
(defn save-chapter [entities chapter]
(let [prefs (.getPreferences (Gdx/app) "ticks-tales-saves")
chapters (if (.contains prefs "chapters")
(edn/read-string (.getString prefs "chapters"))
saves/chapters)
chapters (assoc chapters chapter {:state (assoc (entities :state)
:x (get-in entities [:room :entities :ego :x])
:y (get-in entities [:room :entities :ego :y]))})]
(.putString prefs "chapters" (pr-str chapters))
(.flush prefs)))
(defn get-chapters []
(let [prefs (.getPreferences (Gdx/app) "ticks-tales-saves")]
(if (.contains prefs "chapters")
(edn/read-string (.getString prefs "chapters"))
saves/chapters)))
(defn try-times [fn times]
(first (drop-while #{:fail}
(repeatedly times
#(try (fn)
(catch Exception e
(log/error "Could not save!" e)
:fail))))))
(defn save [entities id name & [blurb]]
(let [save-fn (fn []
(log/info "Saving " id name)
(let [save {:name name
:id id
:screenshot (steam/save-screenshot-file-name id)
:state (assoc (entities :state)
:x (get-in entities [:room :entities :ego :x])
:y (get-in entities [:room :entities :ego :y]))
:blurb (or blurb name)}]
(steam/write-bytes (steam/save-file-name id) (.getBytes (pr-str save) "UTF-8"))
(on-gl (let [gl-func (fn []
(let [_ (Pixmap/setFilter Pixmap$Filter/BiLinear)
viewport (-> @ (resolve 'advent.screens.scene/scene) :screen deref :viewport)
[x y w h g-l g-r] [(.getScreenX ^FitViewport viewport) (.getScreenY ^FitViewport viewport) (.getScreenWidth ^FitViewport viewport) (.getScreenHeight ^FitViewport viewport)]
pm (ScreenUtils/getFrameBufferPixmap x y w h)
resized (Pixmap. 160 120 Pixmap$Format/RGB888)
_ (.drawPixmap resized pm 0 0 w h 0 0 160 120)]
(steam/save-screenshot resized (steam/save-screenshot-file-name id))))]
(try-times gl-func 3)))))]
(try-times save-fn 3)))
(defn remove-save [id]
(doseq [f [(steam/save-screenshot-file-name id)
(steam/save-file-name id)]]
(steam/delete-file f) ))
(defn save-settings! []
(doto (.getPreferences (Gdx/app) "ticks-tales-saves")
(.putString "settings" (pr-str @settings))
.flush))
(defn has-saved-settings? []
(-> (.getPreferences (Gdx/app) "ticks-tales-saves")
(.contains "settings")))
(defn load-settings! []
(reset! settings
(-> (.getPreferences (Gdx/app) "ticks-tales-saves")
(.getString "settings")
edn/read-string)))
(defn get-font [filename]
(let [font (-> (bitmap-font filename)
#_(doto (bitmap-font! :set-font-scale 0.5 0.5)))
tr (bitmap-font! font :get-region)
tx (.getTexture tr)]
(call! ^Texture tx :set-filter Texture$TextureFilter/Linear Texture$TextureFilter/Linear)
font))
(def +screen-width+ 320)
(def +screen-height+ 240)
(defn scaler-fn-with-baseline [baseline minimum-size & [maximum-size]]
(let [maximum-size (or maximum-size 1.0)]
(fn [[_ y]]
(if (< y baseline) maximum-size
(let [percent-complete (- 1.0 (/ (- y baseline) (- +screen-height+ baseline)))
range (+ (* percent-complete (- maximum-size minimum-size)) minimum-size)]
range)))))
(defn get-scale-value [image x y]
(let [base-y (Math/floor (- 240 y))
other-y (Math/ceil (- 240 y))
base-amount (- y (Math/floor y))
other-amount (- 1.0 base-amount)
base-v (-> image
(pixmap! :get-pixel x base-y)
color
(.r))
other-v (-> image
(pixmap! :get-pixel x other-y)
color
(.r))]
(+ (* base-v base-amount)
(* other-v other-amount))))
(defn scaler-fn-from-image [image minimum-size maximum-size]
(let [image (pixmap image)
maximum-size (or maximum-size 1.0)]
(fn [[x y]]
(let [percent-complete (get-scale-value image x y)]
(if (< y 0)
maximum-size
(+ (* percent-complete (- maximum-size minimum-size)) minimum-size))))))
(defn dist [x1 y1 x2 y2 & {:keys [y-sign x-sign]}]
(let [y-sign (or y-sign 1.0)
x-sign (or x-sign 1.0)
dx (* (- x1 x2) x-sign)
dy (* y-sign (- y1 y2))]
(Math/sqrt (+ (* dx dx) (* dy dy)))))
(defn flip [^Animation anim]
(animation (animation! anim :get-frame-duration)
(for [src-frame (animation! anim :get-key-frames)
:let [frame (texture (texture! src-frame :get-texture))]]
(do
(texture! frame :set-region ^TextureRegion src-frame)
(texture! frame :flip true false)
frame))))
(defn split-texture [file [^int tile-width ^int tile-height] frames]
(let [sheet (get-texture file)
sheet-obj ^TextureRegion (:object sheet)
width ^int (int (.getRegionWidth sheet-obj))
x ^int (int (.getRegionX sheet-obj))
y ^int (int (.getRegionY sheet-obj))]
(for [frame frames
:let [new-tex ^TextureRegion (TextureRegion. sheet-obj)]]
(do
(.setRegion new-tex
^int (unchecked-add x (unchecked-multiply tile-width frame))
y
tile-width
tile-height)
(->TextureEntity new-tex)))))
(defn make-anim [file [w h] speed frames]
(animation speed (split-texture file [w h] frames)))
(defn make-anim-seq [file [w h] speed frames]
(animation speed (map #(get-texture (str file "-" (inc %) ".png")) frames)))
(defn make-bird [screen p]
(let [bird-sheet (texture! (get-texture "outside-castle/bird.png") :split 1 2)
bird-stand (animation 0.15 (for [i [0 1]]
(aget bird-sheet 0 i)))]
(assoc (animation->texture screen bird-stand)
:x 0
:y 0
:baseline 21
:anim bird-stand
:anim-start 0
:path (catmull-rom-spline (map #(apply vector-2* %) p) true)
:update-fn (fn [screen entities entity]
(let [speed 0.05
pos-f (- (* (:total-time screen) speed) (int (* (:total-time screen) speed)))
v (vector-2 0 0)
a (catmull-rom-spline! ^CatmullRomSpline (:path entity) :value-at v pos-f)]
(assoc entity :x (vector-2! v :x) :y (vector-2! v :y)))))))
(defn path-point [speed screen entities entity]
(* (- (:total-time screen) (:path-start-time entity 0.0)) speed))
(defn update-path-location [speed screen entities entity]
(if (:path entity)
(let [entity (assoc entity :path-start-time (or (:path-start-time entity) (:total-time screen)))
pos-f (- (path-point speed screen entities entity) (int (* (- (:total-time screen) (:path-start-time entity 0.0)) speed)))
v (vector-2 0 0)
a (catmull-rom-spline! ^CatmullRomSpline (:path entity) :value-at v pos-f)]
(assoc entity :x (vector-2! v :x) :y (vector-2! v :y) ))
entity))
(defn find-override [entities [x y]]
(first (concat (filter #(and ((:mouse-in? %) entities x y)
(:override %))
(get-in entities [:room :interactions]))
(filter #(and (:mouse-in? %)
((:mouse-in? %) entities x y)
(not= "ego" (:id %))
(:get-script %))
(vals (get-in entities [:room :entities])))
(filter #(and ((:mouse-in? %) entities x y)
(:get-script %))
(get-in entities [:room :interactions])))))
(defn hovering-ego [entities [x y]]
(when-let [mouse-in (get-in entities [:room :entities :ego :mouse-in?])]
(mouse-in entities x y)))
(defn remove-interaction [entities id]
(update-in entities [:room :interactions] (fn [i] (remove #(= id (:id %)) i))))
(defn play-music [snd]
(music! snd :play))
(defn stop-music [snd]
(music! snd :stop))
(defn make-music [r]
(try
(doto (music r) (music! :set-looping true))
(catch Exception _
(doto (music (str r ".mp3")) (music! :set-looping true)))))
(defn load-sound [f]
(try
(sound f)
(catch Exception e
(log/info e)
(sound (str f ".mp3")))))
(defn apply-tweens [screen entities tweens]
(reduce (fn [e f]
(f e (:total-time screen)))
entities
(vals tweens)))
(defn intersects? [e [x y]]
(when (:object e)
(let [object-width (* (:scale-x e 1.0) (or (:intersect-width e) (:width e) (when (actor? e ) (.getWidth ^Actor (:object e))) 0))
object-height (* (:scale-y e 1.0) (or (:intersect-height e) (:height e) (when (actor? e ) (.getHeight ^Actor (:object e))) 0))
origin-x (* (:scale-x e 1.0) (:origin-x e 0))
origin-y (* (:scale-y e 1.0) (:origin-y e 0))
object-x (- (or (:x e) (when (actor? e ) (.getX ^Actor (:object e))) 0) origin-x)
object-y (- (or (:y e) (when (actor? e ) (.getY ^Actor (:object e))) 0) origin-y)]
(and (:object e)
(< object-x x (+ object-x object-width))
(< object-y y (+ object-y object-height))))))
(defn get-current-music [entities]
(let [time (get-in entities [:state :time])
musics (:musics entities)
override-music (musics (get-in entities [:music-override :value]))
current-music (musics (get-in entities [:room :music]))
current-time-music (musics (get-in entities [:room :music time]))]
(or override-music current-music current-time-music)))
(defn setup-viewport [screen width height]
(let [cam (or (:cam screen) (orthographic))
viewport (FitViewport. width height cam)
stage (Stage. viewport)]
(update! screen :renderer stage :viewport viewport :camera cam)
(.apply viewport)
(assoc screen :renderer stage :viewport viewport :camera cam)))
(defn unproject
([screen options]
(unproject screen options [(:input-x options) (:input-y options)]) )
([screen options [x y]]
(let [pj (.unproject ^FitViewport (:viewport screen) (vector-2 x y))
x (.x pj)
y (.y pj)]
[x y])))
(defn project
([screen [x y]]
(let [pj (.project ^FitViewport (:viewport screen) (vector-2 x y))
x (.x pj)
y (.y pj)]
[x y])))
(defn contains-point? [x1 y1 width height x y]
(and (<= x1 x (+ x1 width))
(<= y1 y (+ y1 height))))
(defn is-unstoppable-script-running [screen entities]
(let [current-action (get-in entities [:fg-actions :current])
is-script-running (get-in entities [:fg-actions :script-running?])]
(and is-script-running
(not (#{:end :none-but-arrow} (or (get-in entities [:fg-actions :last-skip-type]) :end))))))
(defn update-override [{:keys [^FitViewport viewport] :as screen} entities options]
(let [raw-pos (get-in entities [:cursor :last-pos])
last-pos (unproject screen options raw-pos)
mouse-override (find-override entities last-pos)
out-of-bounds (not (contains-point? (.getScreenX viewport) (.getScreenY viewport)
(.getScreenWidth viewport) (.getScreenHeight viewport)
(first raw-pos) (last raw-pos)))
hovering-ego (hovering-ego entities last-pos)
current (get-in entities [:cursor :current])
entities (assoc-in entities [:label :text] "")
selected-inventory-item (@(resolve 'advent.screens.scene/get-selected-inventory-item))]
(cond
out-of-bounds
(update-in entities [:cursor] assoc :override nil :active false )
(not (get-in entities [:state :active?]))
(-> entities
(assoc-in [:cursor :override] nil))
(is-unstoppable-script-running screen entities)
(-> entities
(assoc-in [:cursor :override] :hourglass))
(get-in entities [:state :hud-active?])
(-> entities
(assoc-in [:cursor :override] nil))
(and mouse-override (:only-script mouse-override))
(-> entities
(assoc-in [:cursor :override] (:cursor mouse-override))
(assoc-in [:label :text] (:label mouse-override)))
selected-inventory-item
(-> entities
(assoc-in [:cursor :override] selected-inventory-item)
(assoc-in [:label :text] (:label mouse-override)))
mouse-override
(-> entities
(assoc-in [:cursor :override] (:cursor mouse-override))
(assoc-in [:label :text] (:label mouse-override)))
:else
(assoc-in entities [:cursor :override] :main))))
(def default-night-merge {:r 0.08 :g 0.1 :b 0.36 :multiply-amount 1.0 :hue-amount 1.0})
(def default-night-merge-sprite {:r 0.08 :g 0.1 :b 0.36 :multiply-amount 0.3 :hue-amount 0.4})
(defn clamp-volume [vol]
(max vol 0.005))
(defn proximity-volume [entities [x y] & {:keys [scale]}]
(-> (max 0.0
(- 1.0 (/ (dist x y
(get-in entities [:room :entities :ego :x])
(get-in entities [:room :entities :ego :y])
:y-sign 2.0
:x-sign (/ 1.0 (get-in entities [:room :entities :ego :scale-x])))
175.0)))
(* (or scale 1.0))
(clamp-volume)))
(defn fast-forward-particle [p]
(particle-effect! p :reset)
(particle-effect! p :start)
(particle-effect! p :update 10.0)
(dotimes [_ 160]
(particle-effect! p :update 0.2)))
(defn os->key []
(let [os-name (System/getProperty "os.name")]
(cond
(or (.startsWith os-name "Linux") (.startsWith os-name "LINUX")) :linux
(.startsWith os-name "Mac") :mac
:else :windows)))
(defmulti make-windowed os->key)
(defmulti make-fullscreen os->key)
(defmulti is-fullscreen? os->key)
(defmethod make-windowed :linux []
(do
(log/info "Making windowed 800x600 decorated window")
(System/setProperty "org.lwjgl.opengl.Window.undecorated", "false")
(.setFullscreenMode Gdx/graphics (.getDisplayMode Gdx/graphics))))
(defmethod make-fullscreen :linux []
(let [width (-> Gdx/graphics .getDisplayMode .width)
height (-> Gdx/graphics .getDisplayMode .height)]
(log/info "Making fullscreen " width "x" height " as undecorated window")
(System/setProperty "org.lwjgl.opengl.Window.undecorated", "true")
(.setFullscreenMode Gdx/graphics (.getDisplayMode Gdx/graphics))))
(defmethod is-fullscreen? :linux []
(= "true" (System/getProperty "org.lwjgl.opengl.Window.undecorated")))
(defn make-fullscreen-normal []
(let [width (-> Gdx/graphics .getDisplayMode .width)
height (-> Gdx/graphics .getDisplayMode .height)]
(log/info "Making fullscreen " width "x" height " as true fullscreen")
(.setFullscreenMode Gdx/graphics
(.getDisplayMode Gdx/graphics))
(set! (.foregroundFPS (-> (class Gdx/graphics)
(.getDeclaredField (name "config"))
(doto (.setAccessible true))
(.get Gdx/graphics))) 60)))
(defn make-windowed-normal []
(do
(log/info "Making windowed 1280x960 decorated window")
(System/setProperty "org.lwjgl.opengl.Window.undecorated", "false")
(.setWindowedMode Gdx/graphics 1280 960)))
(defn is-fullscreen?-normal []
(.isFullscreen Gdx/graphics))
(defmethod make-windowed :mac [] (make-windowed-normal))
(defmethod make-fullscreen :mac [] (make-fullscreen-normal))
(defmethod is-fullscreen? :mac [] (is-fullscreen?-normal))
(defmethod make-windowed :windows [] (make-windowed-normal))
(defmethod make-fullscreen :windows [] (make-fullscreen-normal))
(defmethod is-fullscreen? :windows [] (is-fullscreen?-normal))
(defn set-fullscreen! [val]
(try
(if val
(on-gl (make-fullscreen))
(on-gl (make-windowed)))
(on-gl
(swap! settings assoc :fullscreen val)
(save-settings!))
nil
(catch Exception e
(log/error e))))
(defn toggle-fullscreen! []
(if (is-fullscreen?)
(set-fullscreen! false)
(set-fullscreen! true)))
(defn get-sound-pan [x]
(/ (- x 160 ) 160))
(defn sourced-volume-fn [target vol-scale [x y]]
(fn [entities]
(if (= target :ego)
(-> (* (/ (get-in entities [:room :entities :ego :scale-x]) 1.5) 0.9)
(* (or vol-scale 1.0))
(clamp-volume))
(proximity-volume entities [x y] :scale vol-scale))
))
(defn scale-vol-from-fade [entities vol]
(* vol
(- 1.0 (get-in entities [:fade :opacity]))))
(defn play-sound!
([screen entities snd volume-fn]
(play-sound! screen entities snd volume-fn 0.5))
([screen entities snd volume-fn pan]
(play-sound! screen entities snd volume-fn pan :once))
([screen entities snd volume-fn pan type]
(play-sound! screen entities snd volume-fn pan type 1.0))
([screen entities snd volume-fn pan type pitch]
(let [vol (volume-fn entities)
snd (if (keyword? snd)
(or (-> entities :room :sounds snd)
(-> entities :sounds snd))
snd)
sound-id (if (= :once type)
(sound! snd :play (scale-vol-from-fade entities (current-sound-volume vol)) (or pitch 1.0) pan )
(sound! snd :loop 0.0 (or pitch 1.0) pan )) ]
(update-in entities [:current-sounds :value]
conj {:id sound-id
:sound snd
:volume-fn volume-fn
:type type
:ends-at (if (= type :once)
(+ (:total-time screen) 1.0 #_(sound! snd :duration))
nil)}))))
(defn stop-all-sounds! [entities]
(doseq [snd (get-in entities [:current-sounds :value])]
(sound! (:sound snd) :stop (:id snd))))
(defn translate-depressed [cursor depressed?]
(if depressed?
(get {:hand :hand-depressed
:talk :talk-depressed
:look :look-depressed} cursor cursor)
cursor))
(defn bound-to-camera [x length zoom]
(min (- length (* length 0.5 zoom ))
(max (* length 0.5 zoom )
x)))