diff --git a/.gitignore b/.gitignore index cdd1de3..78d36a3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ pom.xml .lein-plugins/ .repl .nrepl-port +/.cpcache/ +/.rebel_readline_history diff --git a/deps.edn b/deps.edn index 5faf14b..7ac4409 100644 --- a/deps.edn +++ b/deps.edn @@ -1,13 +1,14 @@ {:deps {org.clojure/clojurescript {:mvn/version "1.10.597"} - com.bhauman/figwheel-main {:mvn/version "0.2.3"} + com.bhauman/figwheel-main {:mvn/version "0.2.7"} binaryage/oops {:mvn/version "0.7.0"} - cljsjs/pixi {:mvn/version "5.1.4-0"}} + cljsjs/pixi {:mvn/version "5.2.0-0"} + cljsjs/howler {:mvn/version "2.0.5-0"}} :paths ["src" "target" "resources"] :aliases {:dev {:main-opts ["-m" "figwheel.main" "-b" "dev" "-r"] - :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.3"} + :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.7"} com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}}} - :awwwliens-min - {:main-opts ["-m" "figwheel.main" "-bo" "awwwliens-min"]}}} + :min + {:main-opts ["-m" "figwheel.main" "-bo" "min" "-O" "advanced"]}}} diff --git a/project.clj b/project.clj index 7a48417..8faa8b2 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,8 @@ [org.clojure/clojurescript "1.10.597"] [org.clojure/core.async "0.4.500"] [binaryage/oops "0.7.0"] - [cljsjs/pixi "5.1.2-0"]] + [cljsjs/pixi "5.1.2-0"] + [cljsjs/howler "2.0.5-0"]] :plugins [[lein-figwheel "0.5.19"] [lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]]] diff --git a/resources/public/images/shmup/game/background.png b/resources/public/images/shmup/game/background.png new file mode 100644 index 0000000..bdae57e Binary files /dev/null and b/resources/public/images/shmup/game/background.png differ diff --git a/resources/public/images/shmup/game/boss.png b/resources/public/images/shmup/game/boss.png new file mode 100644 index 0000000..9e6c94f Binary files /dev/null and b/resources/public/images/shmup/game/boss.png differ diff --git a/resources/public/images/shmup/game/bullet.png b/resources/public/images/shmup/game/bullet.png new file mode 100644 index 0000000..9b4665e Binary files /dev/null and b/resources/public/images/shmup/game/bullet.png differ diff --git a/resources/public/images/shmup/game/player.json b/resources/public/images/shmup/game/player.json new file mode 100644 index 0000000..8a3c24a --- /dev/null +++ b/resources/public/images/shmup/game/player.json @@ -0,0 +1,47 @@ +{"frames": { + +"player-0.png": +{ + "frame": {"x":1,"y":1,"w":42,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":30}, + "sourceSize": {"w":48,"h":32} +}, +"player-1.png": +{ + "frame": {"x":1,"y":65,"w":42,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":32}, + "sourceSize": {"w":48,"h":32} +}, +"player-2.png": +{ + "frame": {"x":1,"y":33,"w":42,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":30}, + "sourceSize": {"w":48,"h":32} +}, +"player-3.png": +{ + "frame": {"x":1,"y":65,"w":42,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":32}, + "sourceSize": {"w":48,"h":32} +}}, +"animations": { + "player": ["player-0.png","player-1.png","player-2.png","player-3.png"] +}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "player.png", + "format": "RGBA8888", + "size": {"w":44,"h":98}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d1185eeb86274dab3a81508ace19f526:c1437acec00efe57d100076dc4fe28ad:86d5339b780aedd87b35e2b7c0a7e11b$" +} +} diff --git a/resources/public/images/shmup/game/player.png b/resources/public/images/shmup/game/player.png new file mode 100644 index 0000000..2fcdf7b Binary files /dev/null and b/resources/public/images/shmup/game/player.png differ diff --git a/resources/public/images/shmup/game/ufo.json b/resources/public/images/shmup/game/ufo.json new file mode 100644 index 0000000..2ec5631 --- /dev/null +++ b/resources/public/images/shmup/game/ufo.json @@ -0,0 +1,47 @@ +{"frames": { + +"ufo-0.png": +{ + "frame": {"x":1,"y":1,"w":42,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":2,"w":42,"h":30}, + "sourceSize": {"w":48,"h":32} +}, +"ufo-1.png": +{ + "frame": {"x":1,"y":65,"w":42,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":32}, + "sourceSize": {"w":48,"h":32} +}, +"ufo-2.png": +{ + "frame": {"x":1,"y":33,"w":42,"h":30}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":2,"w":42,"h":30}, + "sourceSize": {"w":48,"h":32} +}, +"ufo-3.png": +{ + "frame": {"x":1,"y":65,"w":42,"h":32}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":0,"w":42,"h":32}, + "sourceSize": {"w":48,"h":32} +}}, +"animations": { + "ufo": ["ufo-0.png","ufo-1.png","ufo-2.png","ufo-3.png"] +}, +"meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "ufo.png", + "format": "RGBA8888", + "size": {"w":44,"h":98}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:a0b7e703bae679cfdaffbf9c8146d488:2906306c4ca8a91aa6d00bf87ec882d4:9a4865e58023cfbd5f985c000455d470$" +} +} diff --git a/resources/public/images/shmup/game/ufo.png b/resources/public/images/shmup/game/ufo.png new file mode 100644 index 0000000..e4f4f30 Binary files /dev/null and b/resources/public/images/shmup/game/ufo.png differ diff --git a/resources/public/sfx/.DS_Store b/resources/public/sfx/.DS_Store new file mode 100644 index 0000000..6f851ec Binary files /dev/null and b/resources/public/sfx/.DS_Store differ diff --git a/resources/public/sfx/shmup/.DS_Store b/resources/public/sfx/shmup/.DS_Store new file mode 100644 index 0000000..c271f1b Binary files /dev/null and b/resources/public/sfx/shmup/.DS_Store differ diff --git a/resources/public/sfx/shmup/game/explosion.mp3 b/resources/public/sfx/shmup/game/explosion.mp3 new file mode 100644 index 0000000..ff79100 Binary files /dev/null and b/resources/public/sfx/shmup/game/explosion.mp3 differ diff --git a/resources/public/sfx/shmup/game/explosion.ogg b/resources/public/sfx/shmup/game/explosion.ogg new file mode 100644 index 0000000..6f69a4c Binary files /dev/null and b/resources/public/sfx/shmup/game/explosion.ogg differ diff --git a/resources/public/sfx/shmup/game/gameover.mp3 b/resources/public/sfx/shmup/game/gameover.mp3 new file mode 100644 index 0000000..23b73d2 Binary files /dev/null and b/resources/public/sfx/shmup/game/gameover.mp3 differ diff --git a/resources/public/sfx/shmup/game/gameover.ogg b/resources/public/sfx/shmup/game/gameover.ogg new file mode 100644 index 0000000..83e027d Binary files /dev/null and b/resources/public/sfx/shmup/game/gameover.ogg differ diff --git a/resources/public/sfx/shmup/game/impact_metal.mp3 b/resources/public/sfx/shmup/game/impact_metal.mp3 new file mode 100644 index 0000000..06d0ca2 Binary files /dev/null and b/resources/public/sfx/shmup/game/impact_metal.mp3 differ diff --git a/resources/public/sfx/shmup/game/impact_metal.ogg b/resources/public/sfx/shmup/game/impact_metal.ogg new file mode 100644 index 0000000..5553b20 Binary files /dev/null and b/resources/public/sfx/shmup/game/impact_metal.ogg differ diff --git a/resources/public/sfx/shmup/game/shot.mp3 b/resources/public/sfx/shmup/game/shot.mp3 new file mode 100644 index 0000000..a4acbe2 Binary files /dev/null and b/resources/public/sfx/shmup/game/shot.mp3 differ diff --git a/resources/public/sfx/shmup/game/shot.ogg b/resources/public/sfx/shmup/game/shot.ogg new file mode 100644 index 0000000..209162e Binary files /dev/null and b/resources/public/sfx/shmup/game/shot.ogg differ diff --git a/resources/public/sfx/shmup/game/shot_2.mp3 b/resources/public/sfx/shmup/game/shot_2.mp3 new file mode 100644 index 0000000..f4e6fa4 Binary files /dev/null and b/resources/public/sfx/shmup/game/shot_2.mp3 differ diff --git a/resources/public/sfx/shmup/game/shot_2.ogg b/resources/public/sfx/shmup/game/shot_2.ogg new file mode 100644 index 0000000..f46bb56 Binary files /dev/null and b/resources/public/sfx/shmup/game/shot_2.ogg differ diff --git a/src/minasss_games/collision.cljs b/src/minasss_games/collision.cljs new file mode 100644 index 0000000..173df18 --- /dev/null +++ b/src/minasss_games/collision.cljs @@ -0,0 +1,25 @@ +(ns minasss-games.collision + "Utility functions for (simple, bounding rect based) collision detection") + +(defn make-rect-bounds + "Make a bounding rect based on provided position and rect size; rect is + centered around position" + [[x y] [width height]] + (let [hw (/ width 2) + hh (/ height 2)] + [(- x hw) (- y hh) (+ x hw) (+ y hh)])) + +(defn rects-overlap + "Returns true if the provided rects overlaps" + [[ax-top ay-top ax-bottom ay-bottom] [bx-top by-top bx-bottom by-bottom]] + (and (< ax-top bx-bottom) (< ay-top by-bottom) (> ax-bottom bx-top) (> ay-bottom by-top))) + +(comment + (make-rect-bounds [100 100] [10 10]) + (make-rect-bounds [105 105] [10 10]) + + (rects-overlap + (make-rect-bounds [100 100] [10 10]) + (make-rect-bounds [125 125] [10 10]) + ) + ) diff --git a/src/minasss_games/core.cljs b/src/minasss_games/core.cljs index d28b265..c67f184 100644 --- a/src/minasss_games/core.cljs +++ b/src/minasss_games/core.cljs @@ -2,8 +2,7 @@ "A small experiment with PIXI javascript library" (:require [minasss-games.pixi :as pixi] [minasss-games.director :as director] - [minasss-games.experiments.awwwliens.intro :as awwwliens])) - ;; [minasss-games.experiments.harvest-bot :as harvest-bot])) + [minasss-games.experiments.shmup.game :as shmup])) (enable-console-print!) @@ -12,7 +11,7 @@ (let [app (pixi/make-app {:width (.-innerWidth js/window) :height (.-innerHeight js/window)})] (director/init app) - (director/start-scene awwwliens/scene) + (director/start-scene shmup/scene) app)) (defonce ^:export app (init)) diff --git a/src/minasss_games/element.cljs b/src/minasss_games/element.cljs index 9928cdd..a16f6c8 100644 --- a/src/minasss_games/element.cljs +++ b/src/minasss_games/element.cljs @@ -78,8 +78,11 @@ (defmethod make-element :animated-sprite [tag attrs] (let [container (pixi/make-animated-sprite (:spritesheet attrs) (:animation-name attrs)) - cleaned-attrs (dissoc attrs :spritesheet :animation-name)] - (pixi/set-attributes container cleaned-attrs))) + cleaned-attrs (dissoc attrs :spritesheet :animation-name :autostart)] + (pixi/set-attributes container cleaned-attrs) + (when (:autostart attrs) + (.play container)) + container)) (defmethod make-element :text [tag attrs] diff --git a/src/minasss_games/experiments/shmup/enemy_behaviour.cljs b/src/minasss_games/experiments/shmup/enemy_behaviour.cljs new file mode 100644 index 0000000..3c20b5e --- /dev/null +++ b/src/minasss_games/experiments/shmup/enemy_behaviour.cljs @@ -0,0 +1,40 @@ +(ns minasss-games.experiments.shmup.enemy-behaviour + "This namespace collects all enemy ai related functions and + state management. Still not sure how this is going to be + implemented, I expect it to be higlhy experimental and prone + to changes in structure and functionality.") + +(defmulti update-state + (fn [entity] (get-in entity [::ai ::behaviour-type]))) + +(defmethod update-state :default + [entity] + entity) + +(defmethod update-state ::horizontal-ping-pong + [{:keys [position direction] :as entity}] + (let [dir-x (first direction) + pos-x (first position)] + (cond + (> dir-x 0) (if (< 350 pos-x) + (assoc entity :direction [-1 0]) + entity) + (< dir-x 0) (if (> 200 pos-x) + (assoc entity :direction [1 0]) + entity) + :else entity))) + +;; this multi method is used to create the initial state +;; for the specified behaviour type and "attach" it to the ::ai key +;; of the entity; this is needed because the main idea, for now, +;; is to change entity properties to reflect selected behaviour. +(defmulti init-state + (fn [behaviour-type _entity] behaviour-type)) + +(defmethod init-state ::horizontal-ping-pong + [behaviour-type entity] + (-> entity + (assoc + ::ai {::behaviour-type behaviour-type} + :direction [1 0] + :speed 10))) diff --git a/src/minasss_games/experiments/shmup/game.cljs b/src/minasss_games/experiments/shmup/game.cljs new file mode 100644 index 0000000..2861701 --- /dev/null +++ b/src/minasss_games/experiments/shmup/game.cljs @@ -0,0 +1,329 @@ +(ns minasss-games.experiments.shmup.game + (:require [minasss-games.director + :as + director + :refer + [scene-cleanup scene-key-down scene-key-up scene-update scene-ready]] + [minasss-games.collision :refer [make-rect-bounds rects-overlap]] + [minasss-games.element :as element] + [minasss-games.gamepad :as gamepad] + [minasss-games.math :as math] + [minasss-games.pixi :as pixi] + [minasss-games.sound :as sound] + [minasss-games.experiments.shmup.enemy-behaviour :as enemy-behaviour])) + +(def scene {:id ::game + :resources ["images/shmup/game/background.png" + "images/shmup/game/player.json" + "images/shmup/game/bullet.png" + "images/shmup/game/ufo.json" + "images/shmup/game/boss.png"] + :key-mapping {"ArrowUp" ::move-up "k" ::move-up "w" ::move-up + "ArrowDown" ::move-down "j" ::move-down "s" ::move-down + "ArrowLeft" ::move-left "h" ::move-left "a" ::move-left + "ArrowRight" ::move-right "l" ::move-right "d" ::move-right + "z" ::fire "m" ::fire} + :state (atom nil)}) + +(def main-stage (pixi/make-container)) + +(def sounds_ {:shot (sound/load "sfx/shmup/game/shot.ogg") + :explosion (sound/load "sfx/shmup/game/explosion.ogg") + :impact-metal (sound/load "sfx/shmup/game/impact_metal.ogg")}) + +(defn make-animated-ufo + "Create animated ufo element at position" + [position] + (element/render + [:animated-sprite {:spritesheet "images/shmup/game/ufo.json" + :animation-name "ufo" + :animation-speed 0.1 + :autostart true + :position position + :anchor [0.5 0.5] + :name "ufo"}])) + +(defn spawn-enemy + [position] + (let [view (make-animated-ufo position)] + (pixi/add-child main-stage view) + {:position position + :collision-rect [32 32] + :direction [0 0] + :speed 0 + :energy 10 + :view view})) + +(defn make-boss-element + [position] + (element/render + [:sprite {:texture "images/shmup/game/boss.png" + :anchor [0.5 0.5] + :position position}])) + +(defn spawn-boss-enemy + [position] + (let [view (make-boss-element position)] + (enemy-behaviour/init-state ::enemy-behaviour/horizontal-ping-pong + {:position position + :collision-rect [64 32] + :direction [0 0] + :speed 0 + :energy 400 + :view view}))) + +(defn make-player + "Create player element" + [position] + (element/render + [:animated-sprite {:spritesheet "images/shmup/game/player.json" + :animation-name "player" + :animation-speed 0.1 + :autostart true + :position position + :anchor [0.5 0.5] + :name "player"}])) + +(defn spawn-player + [position] + (let [view (make-player position)] + {:position position + :energy 100 + :speed 100 + :collision-rect [8 8] + :direction [0 0] + :view view})) + +(defn make-bullet + [position] + (element/render + [:sprite {:texture "images/shmup/game/bullet.png" + :anchor [0.5 0.5] + :position position}])) + +(defn spawn-bullet + [player direction] + (let [position (:position player) + view (make-bullet position)] + (pixi/add-child main-stage view) + (.play (:shot sounds_)) + {:position position + :speed 130 + :direction direction + :collision-rect [8 8] + :damage 1 + :view view})) + +(def FIRE-TIMEOUT-MS 0.3) +(def actions_ (atom {::fire-timeout FIRE-TIMEOUT-MS})) + +(defmethod scene-key-down ::game + [_scene _native action] + (swap! actions_ assoc action true)) + +(defmethod scene-key-up ::game + [_scene _native action] + (swap! actions_ dissoc action)) + +(def LAXE-UP 0) +(def LAXE-DOWN 1) +(def LAXE-LEFT 2) +(def LAXE-RIGHT 3) +(def DPAD-UP 12) +(def DPAD-DOWN 13) +(def DPAD-LEFT 14) +(def DPAD-RIGHT 15) +(def BUTTON-A 0) + +(defn handle-gamepad! + [] + (swap! actions_ + (fn [actions] + (assoc actions + ::fire (or (::fire actions) (gamepad/button-down 0 BUTTON-A)) + ::move-left (or (::move-left actions) + (> 0 (gamepad/axis-status 0 DPAD-LEFT)) + (> 0 (gamepad/axis-status 0 LAXE-LEFT))) + ::move-right (or (::move-right actions) + (> 0 (gamepad/axis-status 0 DPAD-RIGHT)) + (> 0 (gamepad/axis-status 0 LAXE-RIGHT))) + ::move-up (or (::move-up actions) + (> 0 (gamepad/axis-status 0 DPAD-UP)) + (> 0 (gamepad/axis-status 0 LAXE-UP))) + ::move-down (or (::move-down actions) + (> 0 (gamepad/axis-status 0 DPAD-DOWN)) + (> 0 (gamepad/axis-status 0 LAXE-DOWN))))))) + +(defn handle-actions + [state delta-time] + (let [actions (swap! actions_ update ::fire-timeout - delta-time) + dir-x (cond + (::move-left actions) -1 + (::move-right actions) 1 + :else 0) + dir-y (cond + (::move-up actions) -1 + (::move-down actions) 1 + :else 0) + ;; spawn new bullets if fire is pressed (one shot every 0.3 seconds) + #_new-bullets #_(if (and (::fire actions) (>= 0 (::fire-timeout actions))) + (let [player (nth (:entities state) 0)] + (swap! actions_ assoc ::fire-timeout FIRE-TIMEOUT-MS) + (-> (:bullets state) + (conj (spawn-bullet player [0 -1])) + (conj (spawn-bullet player (math/normalize [0.3 -1]))) + (conj (spawn-bullet player (math/normalize [-0.3 -1]))))) + (:bullets state))] + (-> state + (assoc-in [:entities 0 :direction] (math/normalize [dir-x dir-y])) + #_(assoc :bullets new-bullets)))) + +(defn update-position-by-velocity + [{:keys [direction speed position] :as entity} dt] + (assoc entity :position + (math/translate position (math/scale direction (* speed dt))))) + +(def LIMIT-TOP 0) +(def LIMIT-BOTTOM 1200) +(def LIMIT-LEFT 0) +(def LIMIT-RIGHT 640) + +(defn entity-is-out-of-boundaries + [{:keys [position boundaries]}] + (let [[x y] position + [top left bottom right] boundaries] + (or (> top y) (< bottom y) + (> left x) (< right x)))) + +(defn check-entity-against-boundaries + "If the entity goes outside of the screen + its sprite will be released and the entity + marked as deleted so it can be removed later" + [entity] + (if (and (some? (:boundaries entity)) (entity-is-out-of-boundaries entity)) + (do + (pixi/remove-container (:view entity)) + (assoc entity :deleted true)) + entity)) + +(defn entities-remove-if-out-of-boundaries + "Update entities removing the ones that are not visibile anymore" + [state] + (update state :entities + (fn [entities] + (->> entities + (map #(check-entity-against-boundaries %)) + (remove :deleted) + (into []))))) + +(defn entities-update-behavior + "Update enemies, don't do a lot at this point" + [state] + (update-in state [:entities] + (fn [entities] + (->> entities + (map #(if (some? (::enemy-behaviour/ai %)) (enemy-behaviour/update-state %) %)) + (remove :deleted) + (into []))))) + +(comment + (js/console.log (clj->js (:player @(:state scene)))) + (math/normalize [1 1]) + (math/translate [200 200] (math/scale [1 0] 0.5))) + +(defn handle-collisions + "Detect bullets colliding with enemies and remove both from the state + and scene graph" + [{:keys [bullets enemies] :as state}] + (let [collisions + (for [{:keys [position collision-rect] :as bullet} bullets + :let [bullet-rect (make-rect-bounds position collision-rect)] + {:keys [position collision-rect] :as enemy} enemies + :let [enemy-rect (make-rect-bounds position collision-rect)] + :when (rects-overlap bullet-rect enemy-rect)] + [bullet enemy])] + (if (empty? collisions) + state + (do + (let [bullets-to-remove (set (map first collisions)) + enemies-to-remove (set (map second collisions))] + (doseq [bullet bullets-to-remove] + (pixi/remove-container (:view bullet))) + (doseq [enemy enemies-to-remove] + (.play (:explosion sounds_)) + (pixi/remove-container (:view enemy))) + (assoc state + :bullets (remove bullets-to-remove (:bullets state)) + :enemies (remove enemies-to-remove (:enemies state)))))))) + +(defn update-entity-view! + [{:keys [view position]}] + (pixi/set-position view position)) + +(defn entities-update-view! + "Side effecty function that sync sprites accordingly with scene state. + Now this function returns the state even if it is not supposed to change + because this way it is easier to put this function in the + `state management pipeline` and maybe in the future it can take care of + adding or removing view elements." + [state] + (doseq [entity (:entities state)] + (update-entity-view! entity)) + state) + +(defn update-positions + [state delta-time] + (update-in state [:entities] + (fn [entities] + (mapv (fn [entity] + (if (some? (:speed entity)) + (update-position-by-velocity entity delta-time) + entity)) + entities)))) + +(defn update-game-state + "This function updates a bounch of things: + - player + - bullets + - enemies + - collisions + - view state of everything + and returns the next state of the world" + [state delta-time] + ;; (handle-gamepad!) + (-> state + (handle-actions delta-time) + (update-positions delta-time) + entities-remove-if-out-of-boundaries + ;; handle-collisions + entities-update-behavior + entities-update-view!)) + +(defmethod scene-update ::game + [scene delta-time] + (swap! (:state scene) + (fn [state] + (when (some? state) + (update-game-state state delta-time))))) + +;; setup the view adding the enemies and the player +(defmethod scene-ready ::game + [scene app-stage] + (let [background (pixi/make-sprite "images/shmup/game/background.png")] + (pixi/add-child main-stage background) + (swap! (:state scene) + (fn [state] + (let [new-state (assoc state + :entities [(spawn-player [200 400]) + (spawn-enemy [200 200]) + (spawn-enemy [260 200]) + (spawn-boss-enemy [220 120])])] + (doseq [entity (:entities new-state)] + (pixi/add-child main-stage (:view entity))) + new-state))) + (pixi/add-child app-stage main-stage))) + +(defmethod scene-cleanup ::game + [_] + (reset! (:state scene) nil) + (pixi/remove-container main-stage)) diff --git a/src/minasss_games/gamepad.cljs b/src/minasss_games/gamepad.cljs index a1f68a9..78259dc 100644 --- a/src/minasss_games/gamepad.cljs +++ b/src/minasss_games/gamepad.cljs @@ -21,9 +21,10 @@ "Return a map with the current status of the provided gamepad; right now only buttons are supported and returned in the :buttons key" [gamepad] - (let [buttons (oget gamepad "buttons")] + (let [buttons (oget gamepad "buttons") + axes (oget gamepad "axes")] {:buttons (mapv #(oget % "pressed") buttons) - :axes (oget gamepad "axes")})) + :axes (mapv #(oget % "value") axes)})) (defn gamepad-connected "Register new connected gamepad in the gamepads_ registry" @@ -81,6 +82,17 @@ old-state (button-status gamepad-index button-index :old-status)] (and old-state (not state)))) +(defn axis-status + ([gamepad-index axis-index] + (axis-status gamepad-index axis-status :status)) + ([gamepad-index axis-index which-status] + (if-let [gamepad (get @gamepads_ gamepad-index)] + (-> gamepad + which-status + :axis + (nth axis-index)) + 0))) + (defn init "Initialize gamepad system, register connected and disconnected event handlers" [] diff --git a/src/minasss_games/math.cljs b/src/minasss_games/math.cljs index ca0ae1f..2136912 100644 --- a/src/minasss_games/math.cljs +++ b/src/minasss_games/math.cljs @@ -15,7 +15,9 @@ "Given the vector v return its normalized version" [[x y]] (let [len (Math/sqrt (+ (* x x) (* y y)))] - [(/ x len) (/ y len)])) + (if (= 0 len) + [0 0] + [(/ x len) (/ y len)]))) (defn scale "Given the vector v return its scaled version" diff --git a/src/minasss_games/pixi.cljs b/src/minasss_games/pixi.cljs index 91ab21c..4a25812 100644 --- a/src/minasss_games/pixi.cljs +++ b/src/minasss_games/pixi.cljs @@ -111,12 +111,13 @@ (defn remove-container "Remove a container from the scene" [container] - (.removeChild (oget container "parent") container)) + (when-let [parent (oget container "parent")] + (.removeChild parent container))) (defn remove-child-by-name "Remove the child specified by name" [parent child-name] - (if-let [child (get-child-by-name parent child-name)] + (when-let [child (get-child-by-name parent child-name)] (.removeChild parent child))) (defn add-child-view diff --git a/src/minasss_games/sound.cljs b/src/minasss_games/sound.cljs new file mode 100644 index 0000000..9183e43 --- /dev/null +++ b/src/minasss_games/sound.cljs @@ -0,0 +1,23 @@ +(ns minasss-games.sound + "Small wrapper around a JS sound library (Howl in this case)" + (:require [cljsjs.howler])) + +(defn load + "Return a sound from the resource cache, looking up by name" + [resource-name] + (js/Howl. (clj->js {:src [resource-name] + :preload true}))) + +(comment + (def shot (load "sfx/shmup/game/shot.ogg")) + (.play shot) + ) + +(defn set-volume + "Set global volume level, 0 = mute, 1 = 100%" + [level] + (.volume js/Howler level)) + +(comment + (set-volume 1) + )