stuff
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
(defproject il "0.1.0-SNAPSHOT"
|
(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"
|
:url "http://example.com/FIXME"
|
||||||
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
|
: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/"}
|
:url "https://www.eclipse.org/legal/epl-2.0/"}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
[org.clojure/data.json "2.5.2"]
|
[org.clojure/data.json "2.5.2"]
|
||||||
[compojure "1.7.2"]
|
[compojure "1.7.2"]
|
||||||
[hiccup "2.0.0"]
|
[hiccup "2.0.0"]
|
||||||
|
[metosin/jsonista "1.0.0"]
|
||||||
[org.slf4j/slf4j-simple "2.0.16"]
|
[org.slf4j/slf4j-simple "2.0.16"]
|
||||||
[org.clojure/data.json "2.5.2"]
|
[org.clojure/data.json "2.5.2"]
|
||||||
[ring/ring-core "1.15.4"]
|
[ring/ring-core "1.15.4"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{:source-paths ["src"]
|
{:source-paths ["src"]
|
||||||
:dependencies []
|
:dependencies [[org.clojure/core.async "1.9.865"]]
|
||||||
:builds
|
:builds
|
||||||
{:app
|
{:app
|
||||||
{:target :browser
|
{:target :browser
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
["pixi.js" :as pixi]
|
["pixi.js" :as pixi]
|
||||||
[goog.dom :as dom]
|
[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.entity :as entity]
|
||||||
[frontend.il.update :as update]))
|
[frontend.il.update :as update]))
|
||||||
|
|
||||||
@@ -31,16 +34,28 @@
|
|||||||
(js/window.addEventListener "keydown" (partial handle-key state velocity-for-key-down))
|
(js/window.addEventListener "keydown" (partial handle-key state velocity-for-key-down))
|
||||||
(js/window.addEventListener "keyup" (partial handle-key state (fn [_] 0))))
|
(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)
|
(let [dt (.-deltaTime t)
|
||||||
{world-layer :world-layer
|
{world-layer :world-layer
|
||||||
entity-layer :entity-layer} layers
|
entity-layer :entity-layer} layers
|
||||||
global-mouse-pos (.. app -renderer -events -pointer -global)
|
global-mouse-pos (.. app -renderer -events -pointer -global)
|
||||||
entity-relative-mouse-pos (.toLocal ^js entity-layer global-mouse-pos)
|
entity-relative-mouse-pos (.toLocal ^js entity-layer global-mouse-pos)
|
||||||
_ (println @state)
|
send-interval 25 ; Send every 25ms (10 tps)
|
||||||
{:keys [player entities]} (-> @state
|
elapsed (.-elapsedMS t)
|
||||||
|
{:keys [player entities]} (-> (update @state :elapsed + elapsed)
|
||||||
(update/update-entities dt layers)
|
(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)))
|
(->> (swap! state merge)))
|
||||||
sx (- (/ (.. app -screen -width) 2) (player :x))
|
sx (- (/ (.. app -screen -width) 2) (player :x))
|
||||||
sy (- (/ (.. app -screen -height) 2) (player :y))
|
sy (- (/ (.. app -screen -height) 2) (player :y))
|
||||||
@@ -67,29 +82,38 @@
|
|||||||
state (atom {:player {:velocity {:x 0 :y 0}
|
state (atom {:player {:velocity {:x 0 :y 0}
|
||||||
:keymap {:w 0 :a 0 :s 0 :d 0 :lmb 0}
|
:keymap {:w 0 :a 0 :s 0 :d 0 :lmb 0}
|
||||||
:last-attack -1
|
:last-attack -1
|
||||||
:attack-speed 666
|
:attack-speed 0.66
|
||||||
:meta {:type "player"
|
:meta {:name "Player"
|
||||||
:name "Player"
|
:race race/human
|
||||||
:race "Human"
|
:class class/priest}
|
||||||
:class "Priest"}
|
|
||||||
:sprite "https://pixijs.com/assets/bunny.png"
|
:sprite "https://pixijs.com/assets/bunny.png"
|
||||||
:speed 10
|
:speed 10
|
||||||
:renderable nil}
|
: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)
|
width (.-width js/window.screen)
|
||||||
height (.-height js/window.screen)
|
height (.-height js/window.screen)
|
||||||
stage (.-stage app)
|
stage (.-stage app)
|
||||||
world-layer (pixi/Container.)
|
world-layer (pixi/Container.)
|
||||||
entity-layer (pixi/Container.)
|
entity-layer (pixi/Container.)
|
||||||
ui-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
|
layers {:entity-layer entity-layer
|
||||||
:world-layer world-layer
|
:world-layer world-layer
|
||||||
:ui-layer ui-layer}
|
:ui-layer ui-layer}
|
||||||
websocket (doto (js/WebSocket. (str "ws://" js/location.host "/ws"))
|
websocket (doto (js/WebSocket. (str "ws://" js/location.host "/ws"))
|
||||||
(.addEventListener "open" #(println "Websocket opened."))
|
(.addEventListener "open" #(go-loop []
|
||||||
(.addEventListener "message" #(println "Received message " (.-data %)))
|
(js/console.log %)
|
||||||
|
(when-let [msg (<! tx-chan)]
|
||||||
|
(.send (.-target %) msg)
|
||||||
|
(recur))))
|
||||||
|
(.addEventListener "message" #(async/put! rx-chan (-> %
|
||||||
|
(.-data)
|
||||||
|
(js/JSON.parse)
|
||||||
|
(js->clj :keywordize-keys true))))
|
||||||
(.addEventListener "close" #(println "Websocket closed.")))]
|
(.addEventListener "close" #(println "Websocket closed.")))]
|
||||||
|
|
||||||
(.. app
|
(.. app
|
||||||
(init #js {:width width :height height :backgroundColor 0x222})
|
(init #js {:width width :height height :backgroundColor 0x222})
|
||||||
(then (fn [_]
|
(then (fn [_]
|
||||||
@@ -110,4 +134,4 @@
|
|||||||
(partial sb-callback false))
|
(partial sb-callback false))
|
||||||
player (assoc e :renderable p :shootbox sb)]
|
player (assoc e :renderable p :shootbox sb)]
|
||||||
(swap! state update-in [:player] merge player)
|
(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)))))))))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns frontend.il.update
|
(ns frontend.il.update
|
||||||
(:require
|
(:require
|
||||||
[frontend.il.entity :as entity]))
|
[frontend.il.entity :as entity]
|
||||||
|
[cljs.core.async :as async]))
|
||||||
|
|
||||||
(defn apply-velocity [e dt]
|
(defn apply-velocity [e dt]
|
||||||
(if (contains? e :velocity)
|
(if (contains? e :velocity)
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
(defn apply-player-shoot [state entity-id mouse-pos]
|
(defn apply-player-shoot [state entity-id mouse-pos]
|
||||||
(if (and (get-in state [:player :keymap :lmb] false)
|
(if (and (get-in state [:player :keymap :lmb] false)
|
||||||
(< (+ (get-in state [:player :last-attack] -1)
|
(< (+ (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)))
|
(js/Date.now)))
|
||||||
(let [player-sprite (get-in state [:player :renderable])
|
(let [player-sprite (get-in state [:player :renderable])
|
||||||
speed 15
|
speed 15
|
||||||
@@ -36,17 +37,31 @@
|
|||||||
h (js/Math.sqrt (+ (* dx dx) (* dy dy)))
|
h (js/Math.sqrt (+ (* dx dx) (* dy dy)))
|
||||||
vx (* (/ dx h) speed)
|
vx (* (/ dx h) speed)
|
||||||
vy (* (/ dy h) speed)
|
vy (* (/ dy h) speed)
|
||||||
eid (swap! entity-id inc)
|
eid (-> (swap! entity-id inc)
|
||||||
|
(str)
|
||||||
|
(keyword))
|
||||||
e {:sprite image
|
e {:sprite image
|
||||||
:hyp h
|
:hypotenuse h
|
||||||
:x start-x
|
:x start-x
|
||||||
:y start-y
|
:y start-y
|
||||||
:velocity {:x vx :y vy}}]
|
:velocity {:x vx :y vy}}]
|
||||||
(-> state
|
(-> 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))))
|
(assoc-in [:player :last-attack] (js/Date.now))))
|
||||||
state))
|
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]
|
(defn update-entities [state dt layers]
|
||||||
(let [{entities :entities} state
|
(let [{entities :entities} state
|
||||||
new-entities (update-vals entities #(-> %
|
new-entities (update-vals entities #(-> %
|
||||||
@@ -54,10 +69,25 @@
|
|||||||
(apply-velocity dt)))]
|
(apply-velocity dt)))]
|
||||||
(assoc state :entities new-entities)))
|
(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
|
(let [{player :player} state
|
||||||
updated-player (-> player
|
updated-player (-> player
|
||||||
(apply-renderable layers)
|
(apply-renderable layers)
|
||||||
(apply-velocity dt))]
|
(apply-velocity dt))]
|
||||||
(-> (assoc state :player updated-player)
|
(-> (assoc state :player updated-player)
|
||||||
|
(apply-player-position send-interval)
|
||||||
(apply-player-shoot entity-id mouse-pos))))
|
(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)))
|
||||||
|
|||||||
@@ -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)))
|
|
||||||
33
src/il/common/class.cljc
Normal file
33
src/il/common/class.cljc
Normal file
@@ -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)))
|
||||||
3
src/il/common/race.cljc
Normal file
3
src/il/common/race.cljc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
(ns il.common.race)
|
||||||
|
|
||||||
|
(def human "human")
|
||||||
@@ -3,24 +3,31 @@
|
|||||||
[ring.websocket.async :refer [go-websocket]]
|
[ring.websocket.async :refer [go-websocket]]
|
||||||
[compojure.core :as c]
|
[compojure.core :as c]
|
||||||
[compojure.route :as route]
|
[compojure.route :as route]
|
||||||
[clojure.core.async :refer [<! >! go-loop]]
|
[clojure.core.async :as async :refer [<! >! go-loop]]
|
||||||
[clojure.data.json :as json]
|
[hiccup.core :as h]
|
||||||
[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]
|
(go-websocket [in out err]
|
||||||
|
(s/change state-chan update :players assoc player-id {:out out :in in :err err})
|
||||||
(go-loop []
|
(go-loop []
|
||||||
(when-let [e (<! err)]
|
(when-let [e (<! err)]
|
||||||
(println (.getMessage e))
|
(println (.getMessage e))
|
||||||
(recur)))
|
(recur)))
|
||||||
(go-loop []
|
(go-loop []
|
||||||
(if-let [msg (<! in)]
|
(if-let [raw (<! in)]
|
||||||
(do
|
(let [msg (j/from-json raw)
|
||||||
|
type (get msg :type "invalid")]
|
||||||
(cond
|
(cond
|
||||||
(= msg "world") (>! out "Hello World!"))
|
(= type "shoot") (handler/handle-shoot state state-chan player-id msg)
|
||||||
|
(= type "invalid") (>! out "Hello World!"))
|
||||||
(println msg)
|
(println msg)
|
||||||
(recur))
|
(recur))
|
||||||
(println "Closed.")))))
|
(do
|
||||||
|
(s/change state-chan update :players dissoc player-id)
|
||||||
|
(println "Closed: " player-id))))))
|
||||||
|
|
||||||
(defn index-handler [_]
|
(defn index-handler [_]
|
||||||
(h/html
|
(h/html
|
||||||
@@ -32,14 +39,42 @@
|
|||||||
[:script {:src "/js/app.js" :defer true}]]
|
[:script {:src "/js/app.js" :defer true}]]
|
||||||
[:body]]))
|
[:body]]))
|
||||||
|
|
||||||
(c/defroutes app
|
(defn build-application [state state-chan]
|
||||||
|
(c/routes
|
||||||
(c/GET "/" r (index-handler r))
|
(c/GET "/" r (index-handler r))
|
||||||
(c/GET "/ws" r (ws-handler r))
|
(c/GET "/ws" [] (ws-handler @state state-chan (random-uuid))) ; TODO: Auth here
|
||||||
(route/resources "/")
|
(route/resources "/")
|
||||||
(route/not-found "<h1>The requested resource does not exist...</h1>"))
|
(route/not-found "<h1>The requested resource does not exist...</h1>")))
|
||||||
|
|
||||||
|
(defn game-loop [state state-chan]
|
||||||
|
(go-loop []
|
||||||
|
(let [start-time (System/currentTimeMillis)
|
||||||
|
max-inputs-per-tick 15000 ; Stops the game from spinning endlessly, dropping ticks
|
||||||
|
events-to-apply (loop [inputs []
|
||||||
|
count 0]
|
||||||
|
(if (>= count max-inputs-per-tick)
|
||||||
|
inputs
|
||||||
|
(if-let [args (async/poll! state-chan)]
|
||||||
|
(recur (conj inputs args) (inc count))
|
||||||
|
inputs)))]
|
||||||
|
(doseq [event-args events-to-apply]
|
||||||
|
(apply swap! state event-args))
|
||||||
|
(doseq [{out :out} (vals (:players @state))]
|
||||||
|
(>! out (j/to-json {:type "sync"
|
||||||
|
:entities (:entities @state)})))
|
||||||
|
(let [elapsed (- (System/currentTimeMillis) start-time)
|
||||||
|
sleep-time (max 0 (- 50 elapsed))]
|
||||||
|
(<! (async/timeout sleep-time))))
|
||||||
|
(recur)))
|
||||||
|
|
||||||
(defn -main []
|
(defn -main []
|
||||||
|
(let [state (atom {:systems []
|
||||||
|
:entities {}
|
||||||
|
:players {}})
|
||||||
|
state-chan (async/chan)
|
||||||
|
app (build-application state state-chan)]
|
||||||
|
(game-loop state state-chan)
|
||||||
(let [port 5000]
|
(let [port 5000]
|
||||||
(jetty/run-jetty
|
(jetty/run-jetty
|
||||||
app
|
app
|
||||||
{:port port :join? true :max-idle-timeout 60000}))) ; Allow 60 second idle on websockets
|
{:port port :join? true :max-idle-timeout 60000})))) ; Allow 60 second idle on websockets
|
||||||
|
|||||||
18
src/il/handler.clj
Normal file
18
src/il/handler.clj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
(ns il.handler
|
||||||
|
(:require [il.state :as s]))
|
||||||
|
|
||||||
|
(defn handle-shoot [state state-chan player-id msg]
|
||||||
|
(let [bullet-id (random-uuid)
|
||||||
|
{x :x y :y} (get-in state [:players player-id])
|
||||||
|
{tx :x ty :y} msg
|
||||||
|
dx (- tx x)
|
||||||
|
dy (- ty y)
|
||||||
|
h (Math/sqrt (+ (Math/pow dx 2) (Math/pow dy 2)))
|
||||||
|
speed 15 ; TODO: Calculate projectile speed based on player stats
|
||||||
|
vx (* (/ dx h) speed)
|
||||||
|
vy (* (/ dy h) speed)
|
||||||
|
bullet {:sprite "/sprite/projectile.png" ; TODO: Render sprite based on player weapon
|
||||||
|
:x x
|
||||||
|
:y y
|
||||||
|
:velocity {:x vx :y vy}}]
|
||||||
|
(s/change state-chan update :entities assoc bullet-id bullet)))
|
||||||
10
src/il/json.clj
Normal file
10
src/il/json.clj
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
(ns il.json
|
||||||
|
(:require [jsonista.core :as j]))
|
||||||
|
|
||||||
|
(def keyword-mapper (j/object-mapper {:decode-key-fn true}))
|
||||||
|
|
||||||
|
(defn from-json [str]
|
||||||
|
(j/read-value str keyword-mapper))
|
||||||
|
|
||||||
|
(defn to-json [data]
|
||||||
|
(j/write-value-as-string data))
|
||||||
5
src/il/state.clj
Normal file
5
src/il/state.clj
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(ns il.state
|
||||||
|
(:require [clojure.core.async :refer [>! go]]))
|
||||||
|
|
||||||
|
(defn change [state-chan & args]
|
||||||
|
(go (>! state-chan args)))
|
||||||
Reference in New Issue
Block a user