(ns topdown2d.core (:require [topdown2d.demoscene :as demoscene] [topdown2d.input :as input] [topdown2d.dom :refer [by-id]])) (enable-console-print!) (def gamestate {:canvas (by-id "gamecanvas") :ctx (.getContext (by-id "gamecanvas") "2d") :continue? true :timing {;; msecs of previous frame :prev 0 ;; msecs of current frame :now 0 ;; fps resulting of prev and now :fps 0 ;; difference between prev and now in seconds :elapsed 0} ;; width and height of the canvas :dimensions {:w (* 16 11 3) :h (* 16 9 3)} :input {:dir :?} ;; currently active scene :scene :demo :scenes {:demo {:update demoscene/update-scene :draw demoscene/draw-scene :init demoscene/init}}}) (def reloaded (atom false)) (defn curr-fps "calculates the current fps using the elapsed time" [elapsed] (/ 1 elapsed)) (defn elapsed-seconds "calculates the elapsed seconds since the last frame" [gamestate now] (/ (- now (-> gamestate :timing :prev)) 1000)) (defn curr-scene "returns the current scene" [gamestate] (get-in gamestate [:scenes (:scene gamestate)])) (defn run-scene-update "updates the current scene using its udpate function" [gamestate scene] ((:update scene) gamestate scene)) (defn continue-running? "checks if the gameloop should keep running, based on input" [prev-continue?] (cond (and prev-continue? (input/keydown? :Digit2) (input/keydown? :ControlLeft)) false (and (not prev-continue?) (input/keydown? :Digit3) (input/keydown? :ControlLeft)) true :else prev-continue?)) (defn update-step "updates timing information and the current scene" [gamestate] (let [now (.now js/performance) secs (elapsed-seconds gamestate now) scene (curr-scene gamestate) continue? (continue-running? (:continue? gamestate))] (as-> gamestate $ (assoc-in $ [:input :dir] (input/dir)) (assoc $ :timing {:now now :elapsed secs :fps (curr-fps secs)}) (assoc-in $ [:scenes (:scene gamestate)] (if continue? (run-scene-update $ scene) scene)) (assoc-in $ [:timing :prev] (.now js/performance))))) (defn draw-fps "draws the current fps" [gamestate] (let [ctx (:ctx gamestate)] (aset ctx "fillStyle" "white") (.fillRect ctx 0 0 13 13) (aset ctx "fillStyle" "black") (aset ctx "font" "10px monospace") (.fillText (:ctx gamestate) (int (get-in gamestate [:timing :fps])) 0 10))) (defn draw-step "clears the canvas, draws fps and invokes the scene draw function" [gamestate] (.clearRect (:ctx gamestate) 0 0 (get-in gamestate [:dimensions :w]) (get-in gamestate [:dimensions :h])) (let [scenekey (:scene gamestate) {:keys [draw] :as scene} (-> gamestate :scenes scenekey)] (draw gamestate scene)) (draw-fps gamestate)) (defn mainloop "transforms the given gamestate by invoking a series of update functions and draws it using the 2d context of the gamestate. then, it calls itself again using requestAnimationFrame" [gamestate] (let [newstate (update-step gamestate)] (draw-step newstate) (when-not @reloaded (.requestAnimationFrame js/window #(mainloop newstate))))) (defn init-scenes "initiates the scene data maps using their respective init functions" [] (set! (.-width (:canvas gamestate)) (-> gamestate :dimensions :w)) (set! (.-height (:canvas gamestate)) (-> gamestate :dimensions :h)) (set! (.-imageSmoothingEnabled (:ctx gamestate)) false) (update gamestate :scenes #(reduce (fn [carr [key scene]] (assoc carr key ((:init scene) gamestate scene))) {} %))) (defn fig-reload [] (reset! reloaded true) (.setTimeout js/window (fn [] (reset! reloaded false) (mainloop (init-scenes))) 1000)) (mainloop (init-scenes))