stuff
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{:source-paths ["src"]
|
||||
:dependencies []
|
||||
:dependencies [[org.clojure/core.async "1.9.865"]]
|
||||
:builds
|
||||
{:app
|
||||
{:target :browser
|
||||
|
||||
@@ -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 (<! 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.")))]
|
||||
|
||||
(.. 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)))))))))
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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]]
|
||||
[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 (<! err)]
|
||||
(println (.getMessage e))
|
||||
(recur)))
|
||||
(go-loop []
|
||||
(if-let [msg (<! in)]
|
||||
(do
|
||||
(if-let [raw (<! in)]
|
||||
(let [msg (j/from-json raw)
|
||||
type (get msg :type "invalid")]
|
||||
(cond
|
||||
(= msg "world") (>! 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
|
||||
(defn build-application [state state-chan]
|
||||
(c/routes
|
||||
(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/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 []
|
||||
(let [state (atom {:systems []
|
||||
:entities {}
|
||||
:players {}})
|
||||
state-chan (async/chan)
|
||||
app (build-application state state-chan)]
|
||||
(game-loop state state-chan)
|
||||
(let [port 5000]
|
||||
(jetty/run-jetty
|
||||
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