diff --git a/project.clj b/project.clj index 0a14375..cc12e07 100644 --- a/project.clj +++ b/project.clj @@ -1,5 +1,5 @@ (defproject il "0.1.0-SNAPSHOT" - :description "Iron Legacy, a browser based sci-fi MMORPG" + :description "Iron Legacy, a browser based high-fantasy MMORPG" :url "http://example.com/FIXME" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" :url "https://www.eclipse.org/legal/epl-2.0/"} @@ -8,6 +8,7 @@ [org.clojure/data.json "2.5.2"] [compojure "1.7.2"] [hiccup "2.0.0"] + [metosin/jsonista "1.0.0"] [org.slf4j/slf4j-simple "2.0.16"] [org.clojure/data.json "2.5.2"] [ring/ring-core "1.15.4"] diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 733955d..e7c0538 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -1,5 +1,5 @@ {:source-paths ["src"] - :dependencies [] + :dependencies [[org.clojure/core.async "1.9.865"]] :builds {:app {:target :browser diff --git a/src/frontend/il/app.cljs b/src/frontend/il/app.cljs index a81ae75..61fadcd 100644 --- a/src/frontend/il/app.cljs +++ b/src/frontend/il/app.cljs @@ -3,6 +3,9 @@ [clojure.string :as str] ["pixi.js" :as pixi] [goog.dom :as dom] + [cljs.core.async :as async :refer [go go-loop !]] + [il.common.race :as race] + [il.common.class :as class] [frontend.il.entity :as entity] [frontend.il.update :as update])) @@ -31,16 +34,28 @@ (js/window.addEventListener "keydown" (partial handle-key state velocity-for-key-down)) (js/window.addEventListener "keyup" (partial handle-key state (fn [_] 0)))) -(defn ticker [app state layers entity-id t] +(defn- drain-outbox! [{:keys [outbox] :as state} tx-chan] + (println "Draining outbox: " outbox) + (when (count outbox) + (go + (doseq [msg outbox] + (println "Writing event: " msg) + (>! tx-chan (js/JSON.stringify (clj->js msg)))))) + (assoc state :outbox [])) + +(defn ticker [app state layers entity-id rx-chan tx-chan t] (let [dt (.-deltaTime t) {world-layer :world-layer entity-layer :entity-layer} layers global-mouse-pos (.. app -renderer -events -pointer -global) entity-relative-mouse-pos (.toLocal ^js entity-layer global-mouse-pos) - _ (println @state) - {:keys [player entities]} (-> @state + send-interval 25 ; Send every 25ms (10 tps) + elapsed (.-elapsedMS t) + {:keys [player entities]} (-> (update @state :elapsed + elapsed) (update/update-entities dt layers) - (update/update-player dt layers entity-id entity-relative-mouse-pos) + (update/update-player dt send-interval layers entity-id entity-relative-mouse-pos) + (update/update-from-server rx-chan) + (drain-outbox! tx-chan) (->> (swap! state merge))) sx (- (/ (.. app -screen -width) 2) (player :x)) sy (- (/ (.. app -screen -height) 2) (player :y)) @@ -67,29 +82,38 @@ state (atom {:player {:velocity {:x 0 :y 0} :keymap {:w 0 :a 0 :s 0 :d 0 :lmb 0} :last-attack -1 - :attack-speed 666 - :meta {:type "player" - :name "Player" - :race "Human" - :class "Priest"} + :attack-speed 0.66 + :meta {:name "Player" + :race race/human + :class class/priest} :sprite "https://pixijs.com/assets/bunny.png" :speed 10 :renderable nil} - :entities {(-> entity-id (swap! inc) (keyword)) {:x 500 :y 500 :sprite "https://pixijs.com/assets/bunny.png"}}}) + :elapsed 0 + :outbox [] + :entities {}}) width (.-width js/window.screen) height (.-height js/window.screen) stage (.-stage app) world-layer (pixi/Container.) entity-layer (pixi/Container.) ui-layer (pixi/Container.) + tx-chan (async/chan (async/sliding-buffer 10)) + rx-chan (async/chan (async/sliding-buffer 2)) layers {:entity-layer entity-layer :world-layer world-layer :ui-layer ui-layer} websocket (doto (js/WebSocket. (str "ws://" js/location.host "/ws")) - (.addEventListener "open" #(println "Websocket opened.")) - (.addEventListener "message" #(println "Received message " (.-data %))) + (.addEventListener "open" #(go-loop [] + (js/console.log %) + (when-let [msg ( % + (.-data) + (js/JSON.parse) + (js->clj :keywordize-keys true)))) (.addEventListener "close" #(println "Websocket closed.")))] - (.. app (init #js {:width width :height height :backgroundColor 0x222}) (then (fn [_] @@ -110,4 +134,4 @@ (partial sb-callback false)) player (assoc e :renderable p :shootbox sb)] (swap! state update-in [:player] merge player) - (.. app -ticker (add (partial ticker app state layers entity-id))))))))) + (.. app -ticker (add (partial ticker app state layers entity-id rx-chan tx-chan))))))))) diff --git a/src/frontend/il/update.cljs b/src/frontend/il/update.cljs index 4f20f39..d9d31f4 100644 --- a/src/frontend/il/update.cljs +++ b/src/frontend/il/update.cljs @@ -1,6 +1,7 @@ (ns frontend.il.update (:require - [frontend.il.entity :as entity])) + [frontend.il.entity :as entity] + [cljs.core.async :as async])) (defn apply-velocity [e dt] (if (contains? e :velocity) @@ -21,7 +22,7 @@ (defn apply-player-shoot [state entity-id mouse-pos] (if (and (get-in state [:player :keymap :lmb] false) (< (+ (get-in state [:player :last-attack] -1) - (get-in state [:player :attack-speed] 888)) + (* 1000 (get-in state [:player :attack-speed] 0.75))) (js/Date.now))) (let [player-sprite (get-in state [:player :renderable]) speed 15 @@ -36,17 +37,31 @@ h (js/Math.sqrt (+ (* dx dx) (* dy dy))) vx (* (/ dx h) speed) vy (* (/ dy h) speed) - eid (swap! entity-id inc) + eid (-> (swap! entity-id inc) + (str) + (keyword)) e {:sprite image - :hyp h + :hypotenuse h :x start-x :y start-y :velocity {:x vx :y vy}}] (-> state - (assoc-in [:entities (keyword (str eid))] e) + (update :outbox conj {:type "shoot"}) + (assoc-in [:entities eid] e) (assoc-in [:player :last-attack] (js/Date.now)))) state)) +(defn apply-player-position [state send-interval] + (let [{elapsed :elapsed + {x :x y :y lx :last-x ly :last-y} :player} state] + (if (and (>= elapsed send-interval) + (not (and (= lx x) (= ly y)))) + (-> state + (assoc :elapsed (mod elapsed send-interval)) + (update :player assoc :last-x x :last-y y) + (update :outbox conj {:type "position" :x x :y y})) + state))) + (defn update-entities [state dt layers] (let [{entities :entities} state new-entities (update-vals entities #(-> % @@ -54,10 +69,25 @@ (apply-velocity dt)))] (assoc state :entities new-entities))) -(defn update-player [state dt layers entity-id mouse-pos] +(defn update-player [state dt send-interval layers entity-id mouse-pos] (let [{player :player} state updated-player (-> player (apply-renderable layers) (apply-velocity dt))] (-> (assoc state :player updated-player) + (apply-player-position send-interval) (apply-player-shoot entity-id mouse-pos)))) + +(defn- deep-merge [a b] + (if (and (map? a) (map? b)) + (merge-with deep-merge a b) + b)) + +(defn update-from-server [state rx-chan] + (loop [state-acc state + {:keys [kind] :as msg} (async/poll! rx-chan)] + (if (and msg (= kind "sync")) + (recur + (deep-merge state-acc msg) + (async/poll! rx-chan)) + state-acc))) diff --git a/src/il/class.clj b/src/il/class.clj deleted file mode 100644 index 927ac3f..0000000 --- a/src/il/class.clj +++ /dev/null @@ -1,24 +0,0 @@ -(ns il.class) - -(def priest {:name "priest" - :stats { :atkspeed 0.5 }}) -(def warden "warden") -(def sorcerer "sorcerer") -(def magician "magician") -(def thief "thief") -(def ranger "ranger") -(def artificer "artificer") -(def corsair "corsair") -(def astromancer "astromancer") - -(def valid-class? [class] - (let [classes [priest - warden - sorcerer - magician - thief - ranger - artificer - corsair - astromancer]] - (contains? classes class))) diff --git a/src/il/common/class.cljc b/src/il/common/class.cljc new file mode 100644 index 0000000..b34b28b --- /dev/null +++ b/src/il/common/class.cljc @@ -0,0 +1,33 @@ +(ns il.common.class) + +(def priest {:name "priest" + :stats {:hp 90}}) +(def warden {:name "priest" + :stats {:hp 120}}) +(def sorcerer {:name "sorcerer" + :stats {:hp 90}}) +(def magician {:name "magician" + :stats {:hp 80}}) +(def thief {:name "thief" + :stats {:hp 80}}) +(def ranger {:name "ranger" + :stats {:hp 80}}) +(def artificer {:name "artificer" + :stats {:hp 90}}) +(def corsair {:name "corsair" + :stats {:hp 80}}) +(def astromancer {:name "astromancer" + :stats {:hp 100}}) + +(def all [priest + warden + sorcerer + magician + thief + ranger + artificer + corsair + astromancer]) + +(defn valid-class? [c] + (some #{c} (map :name all))) diff --git a/src/il/common/race.cljc b/src/il/common/race.cljc new file mode 100644 index 0000000..e228493 --- /dev/null +++ b/src/il/common/race.cljc @@ -0,0 +1,3 @@ +(ns il.common.race) + +(def human "human") diff --git a/src/il/core.clj b/src/il/core.clj index 4709ddf..15376bc 100644 --- a/src/il/core.clj +++ b/src/il/core.clj @@ -3,24 +3,31 @@ [ring.websocket.async :refer [go-websocket]] [compojure.core :as c] [compojure.route :as route] - [clojure.core.async :refer [! go-loop]] - [clojure.data.json :as json] - [hiccup.core :as h])) + [clojure.core.async :as async :refer [! go-loop]] + [hiccup.core :as h] + [il.state :as s] + [il.json :as j] + [il.handler :as handler])) -(defn ws-handler [_] +(defn ws-handler [state state-chan player-id] (go-websocket [in out err] + (s/change state-chan update :players assoc player-id {:out out :in in :err err}) (go-loop [] (when-let [e (! out "Hello World!")) + (= type "shoot") (handler/handle-shoot state state-chan player-id msg) + (= type "invalid") (>! out "Hello World!")) (println msg) (recur)) - (println "Closed."))))) + (do + (s/change state-chan update :players dissoc player-id) + (println "Closed: " player-id)))))) (defn index-handler [_] (h/html @@ -32,14 +39,42 @@ [:script {:src "/js/app.js" :defer true}]] [:body]])) -(c/defroutes app - (c/GET "/" r (index-handler r)) - (c/GET "/ws" r (ws-handler r)) - (route/resources "/") - (route/not-found "