switch repo to just cleaning game focus
BIN
game/assets/battery_bar.aseprite
Normal file
BIN
game/assets/battery_bar.png
Normal file
|
After Width: | Height: | Size: 225 B |
BIN
game/assets/charging_station.aseprite
Normal file
BIN
game/assets/curiosities-1x.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
game/assets/doors.aseprite
Normal file
BIN
game/assets/dust_001.aseprite
Normal file
BIN
game/assets/dust_001.png
Normal file
|
After Width: | Height: | Size: 510 B |
107
game/assets/gen_levels.bb
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env bb
|
||||
(ns gen-levels
|
||||
(:require
|
||||
[babashka.fs :as fs]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.data.xml :as xml]
|
||||
[clojure.string :as str]
|
||||
[clojure.java.io :as io])
|
||||
(:import
|
||||
'java.io.StringReader))
|
||||
|
||||
(defn load-tiled-xml-file [path]
|
||||
(-> (slurp (io/reader path))
|
||||
(str/replace #">\s+" ">")
|
||||
(StringReader.)
|
||||
(xml/parse)))
|
||||
|
||||
(defn parse-tile-set [tsx]
|
||||
(->>
|
||||
(:content tsx)
|
||||
(filter #(= (:tag %) :tile))
|
||||
(reduce (fn [acc tile]
|
||||
(conj acc
|
||||
{(Integer/parseInt (get-in tile [:attrs :id]))
|
||||
(vec
|
||||
(->> (:content tile)
|
||||
(map :content)
|
||||
(flatten)
|
||||
(map :attrs)
|
||||
(map #(select-keys % [:x :y :height :width]))
|
||||
(map #(update-vals % (fn [v] (Math/round (Double/parseDouble v)))))))}))
|
||||
|
||||
{})))
|
||||
|
||||
(def walls-tsx (load-tiled-xml-file "./tiled/walls.tsx"))
|
||||
(def objects-tsx (load-tiled-xml-file "./tiled/objects.tsx"))
|
||||
;; (def colliders {:walls (parse-tile-set walls-tsx) :objects (parse-tile-set objects-tsx)})
|
||||
;; (into {} (map (fn [[k v]]
|
||||
;; [(transform-key k) v])
|
||||
;; your-map))
|
||||
(def colliders
|
||||
"collider boxes by tile GID"
|
||||
(let [walls (into {} (map (fn [[k v]] [(+ k 1) v]) (parse-tile-set walls-tsx)))
|
||||
objects (into {} (map (fn [[k v]] [(+ k 21) v]) (parse-tile-set objects-tsx)))]
|
||||
(merge walls objects)))
|
||||
|
||||
(def tutorial-map-tmx (load-tiled-xml-file "./tiled/tutorial.tmx"))
|
||||
(def level-001-map-tmx (load-tiled-xml-file "./tiled/level_001.tmx"))
|
||||
(def dev-map-tmx (load-tiled-xml-file "./tiled/dev.tmx"))
|
||||
|
||||
(defn parse-gid [gid]
|
||||
(let [gid-long (Long/parseLong (str gid))
|
||||
h-flip (bit-test gid-long 31) ; Bit 32 (horizontal flip)
|
||||
v-flip (bit-test gid-long 30) ; Bit 31 (vertical flip)
|
||||
d-flip (bit-test gid-long 29) ; Bit 30 (diagonal flip)
|
||||
tile-id (bit-and gid-long 0x0FFFFFFF)] ; Clear top 4 bits to get actual tile ID
|
||||
{:tile-id tile-id
|
||||
:colliders (get colliders tile-id)
|
||||
:h-flip h-flip
|
||||
:v-flip v-flip
|
||||
:d-flip d-flip}))
|
||||
|
||||
(defn parse-map
|
||||
[map-tmx]
|
||||
(let [tiles (-> (filter #(= (:tag %) :layer) (:content map-tmx))
|
||||
first :content first :content first str/split-lines
|
||||
(->>
|
||||
(mapv #(str/split % #","))
|
||||
(mapv (fn [row] (mapv parse-gid row)))))]
|
||||
{:tiles
|
||||
(vec (map-indexed
|
||||
(fn [y row]
|
||||
(vec (map-indexed
|
||||
(fn [x tile] (conj {:x (* x 25) :y (* y 25)} tile))
|
||||
row))) tiles))
|
||||
|
||||
:objects
|
||||
(let [objects (:content (first (filter #(= (get-in % [:attrs :name]) "objects") (:content map-tmx))))]
|
||||
(vec (map (fn [obj]
|
||||
(let [attrs (assoc (:attrs obj) :x (Double. (get-in obj [:attrs :x])) :y (Double. (get-in obj [:attrs :y])))
|
||||
tags (flatten (map :content (:content obj)))
|
||||
properties (map #(hash-map (keyword (get-in % [:attrs :name])) (get-in % [:attrs :value])) tags)]
|
||||
|
||||
(into attrs properties))) objects)))
|
||||
:spawns
|
||||
(let [spawns (first (filter #(= (get-in % [:attrs :name]) "spawns") (:content map-tmx)))
|
||||
player-1 (first (filter #(= (get-in % [:attrs :name]) "player_1") (:content spawns)))
|
||||
player-2 (first (filter #(= (get-in % [:attrs :name]) "player_2") (:content spawns)))
|
||||
round-num-str #(Double/parseDouble (format "%.2f" (Double/parseDouble %)))
|
||||
extract-pos (fn [tag] {:x (round-num-str (get-in tag [:attrs :x]))
|
||||
:y (round-num-str (get-in tag [:attrs :y]))})]
|
||||
{:player-1 (extract-pos player-1)
|
||||
:player-2 (extract-pos player-2)})}))
|
||||
(def levels {:levels {:level-001 (parse-map level-001-map-tmx)
|
||||
:tutorial (parse-map tutorial-map-tmx)
|
||||
:dev (parse-map dev-map-tmx)}
|
||||
|
||||
:colliders colliders})
|
||||
|
||||
(pprint (get-in levels [:levels :tutorial :objects]))
|
||||
;; (pprint/pprint {:row (first (:tiles level-001))})
|
||||
;; (pprint/pprint (pr-str level-001))
|
||||
|
||||
(fs/write-lines "../levels.fnl" ["(local levels"
|
||||
(str/replace (pr-str levels) #",+" "")
|
||||
")\n\n"
|
||||
"levels"])
|
||||
BIN
game/assets/level_002.png
Normal file
|
After Width: | Height: | Size: 220 B |
BIN
game/assets/objects.aseprite
Normal file
BIN
game/assets/objects.png
Normal file
|
After Width: | Height: | Size: 649 B |
BIN
game/assets/player.aseprite
Normal file
BIN
game/assets/player.png
Normal file
|
After Width: | Height: | Size: 436 B |
83
game/assets/tiled/dev.tmx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="50" height="50" tilewidth="25" tileheight="25" infinite="0" nextlayerid="4" nextobjectid="12">
|
||||
<tileset firstgid="1" source="walls.tsx"/>
|
||||
<layer id="1" name="Tile Layer 1" width="50" height="50">
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2147483651,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,10,1,1,1,0,0,1,1,1,2147483658,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,4,11,2147483652,0,0,0,0,0,0,4,11,2147483652,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2147483650,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="2" name="spawns">
|
||||
<object id="1" name="player_1" x="612.159" y="713.357">
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="player_2" x="638.166" y="712.787">
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="5" type="info_pad" x="600" y="700" width="25" height="25">
|
||||
<properties>
|
||||
<property name="content" value="[a] is left wheel forward [d] is right wheel forward"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="6" x="625" y="700" width="25" height="25">
|
||||
<properties>
|
||||
<property name="content" value="[a] is left wheel forward [d] is right wheel forward"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="11" type="info_pad" x="600" y="600" width="25" height="25">
|
||||
<properties>
|
||||
<property name="content" value="[q] is left wheel reverse [e] is right wheel reverse"/>
|
||||
</properties>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
70
game/assets/tiled/level_001.tmx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="50" height="50" tilewidth="25" tileheight="25" infinite="0" nextlayerid="3" nextobjectid="4">
|
||||
<editorsettings>
|
||||
<export target="level_001.tmj" format="json"/>
|
||||
</editorsettings>
|
||||
<tileset firstgid="1" source="walls.tsx"/>
|
||||
<tileset firstgid="21" source="objects.tsx"/>
|
||||
<layer id="1" name="Tile Layer 1" width="50" height="50">
|
||||
<data encoding="csv">
|
||||
5,1,1,1,1,1,1,1,1,1,1,1,12,1,1,1,1,1,1,1,12,1,1,1,1,1,1,1,1,12,1,1,1,1,1,1,1,1,1,12,1,1,1,1,1,1,1,1,1,4,
|
||||
10,0,0,0,21,22,21,0,0,0,0,0,10,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,21,0,0,0,0,0,0,10,0,0,0,0,0,0,0,8,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
10,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
14,1,1,1,1,1,1,1,1,1,1,1,15,0,0,0,0,0,0,0,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,0,0,0,0,0,0,0,2,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,13,1,1,4,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,8,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,0,0,0,0,0,0,0,11,11,11,11,11,11,11,11,11,11,11,11,11,5,1,1,1,1,1,1,1,1,13,1,18,11,11,17,1,15,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
14,1,1,1,1,1,1,1,1,1,1,1,15,11,11,11,11,11,11,11,5,1,1,1,1,1,1,1,1,4,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,10,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,8,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,2147483650,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,9,11,11,11,14,18,11,11,17,1,1,1,1,12,1,1,1,18,11,11,20,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,2,1,1,1,1,1,1,1,1,3,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
14,1,1,1,1,1,1,1,1,1,1,1,15,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,14,1,1,1,1,1,18,11,11,10,11,11,17,1,1,1,15,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,5,1,1,1,1,1,1,1,1,1,12,1,1,3,11,11,17,1,1,1,1,1,16,1,1,1,18,11,11,20,
|
||||
14,1,1,1,1,1,1,1,1,1,1,1,15,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,8,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,8,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,9,11,11,11,11,11,11,10,
|
||||
10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,11,11,11,11,11,10,11,11,11,11,11,11,10,
|
||||
2,1,1,1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,3
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="2" name="spawns">
|
||||
<object id="1" name="player_1" x="569.372" y="697.644">
|
||||
<point/>
|
||||
</object>
|
||||
<object id="3" name="player_2" x="679.319" y="590.314">
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
30
game/assets/tiled/objects.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.1" name="objects" tilewidth="25" tileheight="25" tilecount="16" columns="4">
|
||||
<image source="../objects.png" width="100" height="100"/>
|
||||
<tile id="0">
|
||||
<properties>
|
||||
<property name="name" value="charging_pad"/>
|
||||
</properties>
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="2.08266" y="2.08266" width="21.0002" height="21.087"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="1">
|
||||
<properties>
|
||||
<property name="name" value="charging_station"/>
|
||||
</properties>
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="1.12811" y="2.08266" width="22.8225" height="20.6531"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="3">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="2.0113" y="1.87259" width="20.9453" height="21.084"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="5">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="0.0892797" y="4.0196" width="24.8292" height="17.8243"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
</tileset>
|
||||
79
game/assets/tiled/tutorial.tmx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="50" height="50" tilewidth="25" tileheight="25" infinite="0" nextlayerid="5" nextobjectid="19">
|
||||
<tileset firstgid="1" source="walls.tsx"/>
|
||||
<tileset firstgid="21" source="objects.tsx"/>
|
||||
<layer id="1" name="bg" width="50" height="50">
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2147483651,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,1,1,1,1,8,1,1,1,0,0,1,1,1,8,1,1,1,1,1,1,1,1,1,2147483650,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,1,1,1,1,1,2147483658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,1,1,1,1,1,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,1,1,1,1,1,2147483658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,1,1,1,1,1,0,0,1,2147483658,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,1,1,1,2147483650,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="2" name="spawns">
|
||||
<object id="1" name="player_1" x="562.031" y="1164.44">
|
||||
<point/>
|
||||
</object>
|
||||
<object id="2" name="player_2" x="687.512" y="1162.99">
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
<objectgroup id="3" name="objects">
|
||||
<object id="3" type="charging_station" x="550" y="1025" width="75" height="50">
|
||||
<properties>
|
||||
<property name="dir" value="n"/>
|
||||
</properties>
|
||||
</object>
|
||||
<object id="4" name="wait_for_2_players" type="h_door" x="650" y="1000" width="50" height="25"/>
|
||||
<object id="6" name="waiting_for_player2" type="info_pad" x="650" y="1025" width="25" height="25"/>
|
||||
<object id="7" name="waiting_for_player2" type="info_pad" x="675" y="1025" width="25" height="25"/>
|
||||
<object id="15" name="controls" type="info_pad" x="550" y="1150" width="25" height="25"/>
|
||||
<object id="16" name="controls" type="info_pad" x="675" y="1150" width="25" height="25"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
14
game/assets/tiled/untitled.tiled-project
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"automappingRulesFile": "",
|
||||
"commands": [
|
||||
],
|
||||
"compatibilityVersion": 1100,
|
||||
"extensionsPath": "extensions",
|
||||
"folders": [
|
||||
"../../"
|
||||
],
|
||||
"properties": [
|
||||
],
|
||||
"propertyTypes": [
|
||||
]
|
||||
}
|
||||
86
game/assets/tiled/untitled.tiled-session
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"activeFile": "tutorial.tmx",
|
||||
"expandedProjectPaths": [
|
||||
".",
|
||||
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game",
|
||||
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets"
|
||||
],
|
||||
"fileStates": {
|
||||
"dev.tmx": {
|
||||
"expandedObjectLayers": [
|
||||
2
|
||||
],
|
||||
"scale": 2.9815,
|
||||
"selectedLayer": 2,
|
||||
"viewCenter": {
|
||||
"x": 635.7538151936944,
|
||||
"y": 649.3375817541506
|
||||
}
|
||||
},
|
||||
"level_001.tmx": {
|
||||
"expandedObjectLayers": [
|
||||
2
|
||||
],
|
||||
"scale": 0.92,
|
||||
"selectedLayer": 1,
|
||||
"viewCenter": {
|
||||
"x": 528.804347826087,
|
||||
"y": 1036.9565217391303
|
||||
}
|
||||
},
|
||||
"map_tileset.tsx": {
|
||||
"dynamicWrapping": false,
|
||||
"scaleInDock": 1,
|
||||
"scaleInEditor": 8
|
||||
},
|
||||
"objects.tsx": {
|
||||
"scaleInDock": 1,
|
||||
"scaleInEditor": 2.8063
|
||||
},
|
||||
"tutorial.tmx": {
|
||||
"expandedObjectLayers": [
|
||||
3,
|
||||
2
|
||||
],
|
||||
"scale": 3.1565,
|
||||
"selectedLayer": 2,
|
||||
"viewCenter": {
|
||||
"x": 612.8623475368288,
|
||||
"y": 1141.1373356565816
|
||||
}
|
||||
},
|
||||
"walls.tsx": {
|
||||
"scaleInDock": 1.4397,
|
||||
"scaleInEditor": 5.9643
|
||||
}
|
||||
},
|
||||
"last.exportedFilePath": "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets/tiled",
|
||||
"last.imagePath": "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets",
|
||||
"map.height": 50,
|
||||
"map.lastUsedExportFilter": "JSON map files (*.tmj *.json)",
|
||||
"map.lastUsedFormat": "tmx",
|
||||
"map.tileHeight": 25,
|
||||
"map.tileWidth": 25,
|
||||
"map.width": 50,
|
||||
"openFiles": [
|
||||
"level_001.tmx",
|
||||
"walls.tsx",
|
||||
"objects.tsx",
|
||||
"tutorial.tmx"
|
||||
],
|
||||
"project": "untitled.tiled-project",
|
||||
"recentFiles": [
|
||||
"level_001.tmx",
|
||||
"walls.tsx",
|
||||
"objects.tsx",
|
||||
"tutorial.tmx",
|
||||
"map_tileset.tsx"
|
||||
],
|
||||
"textEdit.monospace": true,
|
||||
"tileset.lastUsedFormat": "tsx",
|
||||
"tileset.tileSize": {
|
||||
"height": 25,
|
||||
"width": 25
|
||||
},
|
||||
"tileset.type": 0
|
||||
}
|
||||
116
game/assets/tiled/walls.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.1" name="walls" tilewidth="25" tileheight="25" tilecount="20" columns="13">
|
||||
<image source="../walls.png" width="325" height="25"/>
|
||||
<tile id="0">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="0.141111" y="4.02527" width="24.9767" height="17.7059"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="1">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.16" y="4.23334" width="14.6756" height="17.6389"/>
|
||||
<object id="2" x="10.3011" y="-1.77636e-15" width="4.51556" height="5.22112"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="2">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.0692" y="4.10365" width="15.0247" height="17.8045"/>
|
||||
<object id="2" x="10.13" y="20.5323" width="4.83172" height="4.30221"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="3">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="9.99984" y="3.9681" width="15.157" height="17.8707"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="4">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="2" x="10.13" y="0.0242897" width="4.83172" height="21.8801"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="5">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="9.99437" y="4.10365" width="4.89882" height="20.853"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="6">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.1259" y="0.0116848" width="4.70022" height="24.9386"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="7">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="0.0701754" y="4.0888" width="24.8722" height="17.7532"/>
|
||||
<object id="2" x="10.0565" y="16.9227" width="4.85487" height="7.90651"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="8">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="0.0734307" y="4.10365" width="24.7335" height="17.9363"/>
|
||||
<object id="2" x="10.1952" y="0" width="4.71616" height="7.21296"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="9">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="9.99437" y="-0.0661879" width="4.96409" height="24.8867"/>
|
||||
<object id="2" x="14.0098" y="4.09197" width="10.9581" height="17.963"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="10">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.0565" y="1.77636e-15" width="4.85487" height="24.9679"/>
|
||||
<object id="2" x="0.0693553" y="4.02261" width="24.9679" height="17.963"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="11">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="2.98861" y="4.03746" width="21.898" height="17.7384"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="12">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="9.984" y="3.88925" width="11.9834" height="18.1041"/>
|
||||
<object id="2" x="9.99437" y="0.0661879" width="4.89791" height="24.83"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="13">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.1267" y="4.16984" width="14.9585" height="17.7384"/>
|
||||
<object id="2" x="10.0606" y="-0.0661879" width="4.76553" height="25.019"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="14">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="0.0661879" y="4.03746" width="14.8923" height="17.9369"/>
|
||||
<object id="2" x="10.0606" y="0.0661879" width="4.89791" height="24.9528"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="15">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="-0.264752" y="4.10365" width="25.1514" height="17.8045"/>
|
||||
<object id="2" x="10.1267" y="0" width="4.63315" height="24.8205"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="16">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="3.04464" y="4.03746" width="21.9082" height="17.6722"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="17">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="2" x="0.132376" y="4.10365" width="21.7096" height="17.7384"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="18">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="9.99437" y="-0.132376" width="4.89791" height="25.1514"/>
|
||||
<object id="2" x="13.7671" y="4.03746" width="8.14111" height="17.606"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="19">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="10.0606" y="0" width="4.76553" height="24.9528"/>
|
||||
<object id="2" x="3.04464" y="4.03746" width="11.6491" height="17.9369"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
</tileset>
|
||||
BIN
game/assets/ui.aseprite
Normal file
BIN
game/assets/ui.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
game/assets/walls.aseprite
Normal file
BIN
game/assets/walls.png
Normal file
|
After Width: | Height: | Size: 419 B |
1
game/fennel.lua
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/Users/she0001t/personal_projects/fennel_love2d_experiments/fennel-1.5.3.lua
|
||||
6
game/levels.fnl
Normal file
175
game/libs/beholder.lua
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
-- beholder.lua - v2.1.1 (2011-11)
|
||||
|
||||
-- Copyright (c) 2011 Enrique García Cota
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN callback OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
local function copy(t)
|
||||
local c = {}
|
||||
for i = 1, #t do c[i] = t[i] end
|
||||
return c
|
||||
end
|
||||
|
||||
local function hash2array(t)
|
||||
local arr, i = {}, 0
|
||||
for _, v in pairs(t) do
|
||||
i = i + 1
|
||||
arr[i] = v
|
||||
end
|
||||
return arr, i
|
||||
end
|
||||
-- private Node class
|
||||
|
||||
local nodesById = nil
|
||||
local root = nil
|
||||
|
||||
local function newNode()
|
||||
return { callbacks = {}, children = setmetatable({}, { __mode = "k" }) }
|
||||
end
|
||||
|
||||
|
||||
local function findNodeById(id)
|
||||
return nodesById[id]
|
||||
end
|
||||
|
||||
local function findOrCreateChildNode(self, key)
|
||||
self.children[key] = self.children[key] or newNode()
|
||||
return self.children[key]
|
||||
end
|
||||
|
||||
local function findOrCreateDescendantNode(self, keys)
|
||||
local node = self
|
||||
for i = 1, #keys do
|
||||
node = findOrCreateChildNode(node, keys[i])
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
local function invokeNodeCallbacks(self, params)
|
||||
-- copy the hash into an array, for safety (self-erasures)
|
||||
local callbacks, count = hash2array(self.callbacks)
|
||||
for i = 1, #callbacks do
|
||||
callbacks[i](unpack(params))
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function invokeAllNodeCallbacksInSubTree(self, params)
|
||||
local counter = invokeNodeCallbacks(self, params)
|
||||
for _, child in pairs(self.children) do
|
||||
counter = counter + invokeAllNodeCallbacksInSubTree(child, params)
|
||||
end
|
||||
return counter
|
||||
end
|
||||
|
||||
local function invokeNodeCallbacksFromPath(self, path)
|
||||
local node = self
|
||||
local params = copy(path)
|
||||
local counter = invokeNodeCallbacks(node, params)
|
||||
|
||||
for i = 1, #path do
|
||||
node = node.children[path[i]]
|
||||
if not node then break end
|
||||
table.remove(params, 1)
|
||||
counter = counter + invokeNodeCallbacks(node, params)
|
||||
end
|
||||
|
||||
return counter
|
||||
end
|
||||
|
||||
local function addCallbackToNode(self, callback)
|
||||
local id = {}
|
||||
self.callbacks[id] = callback
|
||||
nodesById[id] = self
|
||||
return id
|
||||
end
|
||||
|
||||
local function removeCallbackFromNode(self, id)
|
||||
self.callbacks[id] = nil
|
||||
nodesById[id] = nil
|
||||
end
|
||||
|
||||
|
||||
------ beholder table
|
||||
|
||||
local beholder = {}
|
||||
|
||||
|
||||
-- beholder private functions/vars
|
||||
|
||||
local groups = nil
|
||||
local currentGroupId = nil
|
||||
|
||||
local function addIdToCurrentGroup(id)
|
||||
if currentGroupId then
|
||||
groups[currentGroupId] = groups[currentGroupId] or setmetatable({}, { __mode = "k" })
|
||||
local group = groups[currentGroupId]
|
||||
group[#group + 1] = id
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
local function stopObservingGroup(group)
|
||||
local count = #group
|
||||
for i = 1, count do
|
||||
beholder.stopObserving(group[i])
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
local function falseIfZero(n)
|
||||
return n > 0 and n
|
||||
end
|
||||
|
||||
local function extractEventAndCallbackFromParams(params)
|
||||
assert(#params > 0,
|
||||
"beholder.observe requires at least one parameter - the callback. You usually want to use two, i.e.: beholder.observe('EVENT', callback)")
|
||||
local callback = table.remove(params, #params)
|
||||
return params, callback
|
||||
end
|
||||
|
||||
|
||||
------ Public interface
|
||||
|
||||
function beholder.observe(...)
|
||||
local event, callback = extractEventAndCallbackFromParams({ ... })
|
||||
local node = findOrCreateDescendantNode(root, event)
|
||||
return addIdToCurrentGroup(addCallbackToNode(node, callback))
|
||||
end
|
||||
|
||||
function beholder.stopObserving(id)
|
||||
local node = findNodeById(id)
|
||||
if node then removeCallbackFromNode(node, id) end
|
||||
|
||||
local group, count = groups[id], 0
|
||||
if group then count = stopObservingGroup(group) end
|
||||
|
||||
return (node or count > 0) and true or false
|
||||
end
|
||||
|
||||
function beholder.group(groupId, f)
|
||||
assert(not currentGroupId, "beholder.group can not be nested!")
|
||||
currentGroupId = groupId
|
||||
f()
|
||||
currentGroupId = nil
|
||||
end
|
||||
|
||||
function beholder.trigger(...)
|
||||
return falseIfZero(invokeNodeCallbacksFromPath(root, { ... }))
|
||||
end
|
||||
|
||||
function beholder.triggerAll(...)
|
||||
return falseIfZero(invokeAllNodeCallbacksInSubTree(root, { ... }))
|
||||
end
|
||||
|
||||
function beholder.reset()
|
||||
root = newNode()
|
||||
nodesById = setmetatable({}, { __mode = "k" })
|
||||
groups = {}
|
||||
currentGroupId = nil
|
||||
end
|
||||
|
||||
beholder.reset()
|
||||
|
||||
return beholder
|
||||
1
game/libs/bump.lua
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/Users/she0001t/personal_projects/fennel_love2d_experiments/lib/bump.lua
|
||||
546
game/libs/nata.lua
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
--- Entity management for Lua.
|
||||
-- @module nata
|
||||
|
||||
local nata = {
|
||||
_VERSION = 'Nata v0.3.3',
|
||||
_DESCRIPTION = 'Entity management for Lua.',
|
||||
_URL = 'https://github.com/tesselode/nata',
|
||||
_LICENSE = [[
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Andrew Minnich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
-- gets the error level needed to make an error appear
|
||||
-- in the user's code, not the library code
|
||||
local function getUserErrorLevel()
|
||||
local source = debug.getinfo(1).source
|
||||
local level = 1
|
||||
while debug.getinfo(level).source == source do
|
||||
level = level + 1
|
||||
end
|
||||
--[[
|
||||
we return level - 1 here and not just level
|
||||
because the level was calculated one function
|
||||
deeper than the function that will actually
|
||||
use this value. if we produced an error *inside*
|
||||
this function, level would be correct, but
|
||||
for the function calling this function, level - 1
|
||||
is correct.
|
||||
]]
|
||||
return level - 1
|
||||
end
|
||||
|
||||
-- gets the name of the function that the user called
|
||||
-- that eventually caused an error
|
||||
local function getUserCalledFunctionName()
|
||||
return debug.getinfo(getUserErrorLevel() - 1).name
|
||||
end
|
||||
|
||||
local function checkCondition(condition, message)
|
||||
if condition then return end
|
||||
error(message, getUserErrorLevel())
|
||||
end
|
||||
|
||||
-- changes a list of types into a human-readable phrase
|
||||
-- i.e. string, table, number -> "string, table, or number"
|
||||
local function getAllowedTypesText(...)
|
||||
local numberOfArguments = select('#', ...)
|
||||
if numberOfArguments >= 3 then
|
||||
local text = ''
|
||||
for i = 1, numberOfArguments - 1 do
|
||||
text = text .. string.format('%s, ', select(i, ...))
|
||||
end
|
||||
text = text .. string.format('or %s', select(numberOfArguments, ...))
|
||||
return text
|
||||
elseif numberOfArguments == 2 then
|
||||
return string.format('%s or %s', select(1, ...), select(2, ...))
|
||||
end
|
||||
return select(1, ...)
|
||||
end
|
||||
|
||||
-- checks if an argument is of the correct type, and if not,
|
||||
-- throws a "bad argument" error consistent with the ones
|
||||
-- lua and love produce
|
||||
local function checkArgument(argumentIndex, argument, ...)
|
||||
for i = 1, select('#', ...) do
|
||||
-- allow tables with the __call metamethod to be treated like functions
|
||||
if select(i, ...) == 'function' then
|
||||
if type(argument) == 'table' and getmetatable(argument).__call then
|
||||
return
|
||||
end
|
||||
end
|
||||
if type(argument) == select(i, ...) then return end
|
||||
end
|
||||
error(
|
||||
string.format(
|
||||
"bad argument #%i to '%s' (expected %s, got %s)",
|
||||
argumentIndex,
|
||||
getUserCalledFunctionName(),
|
||||
getAllowedTypesText(...),
|
||||
type(argument)
|
||||
),
|
||||
getUserErrorLevel()
|
||||
)
|
||||
end
|
||||
|
||||
local function checkOptionalArgument(argumentIndex, argument, ...)
|
||||
if argument == nil then return end
|
||||
checkArgument(argumentIndex, argument, ...)
|
||||
end
|
||||
|
||||
local function removeByValue(t, v)
|
||||
for i = #t, 1, -1 do
|
||||
if t[i] == v then table.remove(t, i) end
|
||||
end
|
||||
end
|
||||
|
||||
local function entityHasKeys(entity, keys)
|
||||
for _, key in ipairs(keys) do
|
||||
if not entity[key] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function filterEntity(entity, filter)
|
||||
if type(filter) == 'table' then
|
||||
return entityHasKeys(entity, filter)
|
||||
elseif type(filter) == 'function' then
|
||||
return filter(entity)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Defines the behaviors of a system.
|
||||
--
|
||||
-- There's no constructor for SystemDefinitions. Rather, you simply
|
||||
-- define a table with functions that correspond to events. These
|
||||
-- events can be named anything you like. Below are the built-in events
|
||||
-- that the pool will automatically call.
|
||||
-- @type SystemDefinition
|
||||
|
||||
--- Called when the pool is first created.
|
||||
-- @function SystemDefinition:init
|
||||
-- @param ... additional arguments that were passed to `nata.new`.
|
||||
|
||||
--- Called when an entity is added to the pool.
|
||||
-- @function SystemDefinition:add
|
||||
-- @tparam table e the entity that was added
|
||||
|
||||
--- Called when an entity is removed from the pool.
|
||||
-- @function SystemDefinition:remove
|
||||
-- @tparam table e the entity that was removed
|
||||
|
||||
--- Called when an entity is added to a group.
|
||||
-- @function SystemDefinition:addToGroup
|
||||
-- @string groupName the name of the group that the entity was added to
|
||||
-- @tparam table e the entity that was added
|
||||
|
||||
--- Called when an entity is removed from a group.
|
||||
-- @function SystemDefinition:removeFromGroup
|
||||
-- @string groupName the name of the group that the entity was removed from
|
||||
-- @tparam table e the entity that was removed
|
||||
|
||||
--- Responds to events in a pool.
|
||||
--
|
||||
-- Systems are not created directly. They're created by the @{Pool}
|
||||
-- according to the @{SystemDefinition}s passed to `nata.new`.
|
||||
-- Each system instance inherits all of the functions of its
|
||||
-- @{SystemDefinition}.
|
||||
-- @type System
|
||||
|
||||
--- The @{Pool} that this system is running on.
|
||||
-- @tfield Pool pool
|
||||
|
||||
--- Manages a subset of entities.
|
||||
-- @type Group
|
||||
|
||||
--- The filter that defines which entities are added to this group.
|
||||
-- Can be either:
|
||||
--
|
||||
-- - A list of required keys
|
||||
-- - A function that takes the entity as the first argument
|
||||
-- and returns true if the entity should be added to the group
|
||||
-- @tfield[opt] table|function filter
|
||||
|
||||
--- A function that specifies how the entities in this group should be sorted.
|
||||
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||
-- @tfield[opt] function sort
|
||||
|
||||
--- A list of all the entities in the group.
|
||||
-- @tfield table entities
|
||||
|
||||
--- A set of all the entities in the group.
|
||||
-- @tfield table hasEntity
|
||||
-- @usage
|
||||
-- print(pool.groups.physical.hasEntity[e]) -- prints "true" if the entity is in the "physical" group, or "nil" if not
|
||||
|
||||
--- Manages entities in a game world.
|
||||
-- @type Pool
|
||||
local Pool = {}
|
||||
Pool.__index = Pool
|
||||
|
||||
--- A list of all the entities in the pool.
|
||||
-- @tfield table entities
|
||||
|
||||
--- A set of all the entities in the pool.
|
||||
-- @tfield table hasEntity
|
||||
-- @usage
|
||||
-- print(pool.hasEntity[e]) -- prints "true" if the entity is in the pool, or "nil" if not
|
||||
|
||||
--- A dictionary of the @{Group}s in the pool.
|
||||
-- @tfield table groups
|
||||
|
||||
--- A field containing any data you want.
|
||||
-- @field data
|
||||
|
||||
---
|
||||
|
||||
function Pool:_validateOptions(options)
|
||||
checkOptionalArgument(1, options, 'table')
|
||||
if not options then return end
|
||||
if options.groups then
|
||||
checkCondition(type(options.groups) == 'table', "groups must be a table")
|
||||
for groupName, groupOptions in pairs(options.groups) do
|
||||
checkCondition(type(groupOptions) == 'table',
|
||||
string.format("options for group '$s' must be a table", groupName))
|
||||
local filter = groupOptions.filter
|
||||
if filter ~= nil then
|
||||
checkCondition(type(filter) == 'table' or type(filter) == 'function',
|
||||
string.format("filter for group '%s' must be a table or function", groupName))
|
||||
end
|
||||
local sort = groupOptions.sort
|
||||
if sort ~= nil then
|
||||
checkCondition(type(sort) == 'function',
|
||||
string.format("sort for group '%s' must be a function", groupName))
|
||||
end
|
||||
end
|
||||
end
|
||||
if options.systems then
|
||||
checkCondition(type(options.systems) == 'table', "systems must be a table")
|
||||
for _, system in ipairs(options.systems) do
|
||||
checkCondition(type(system) == 'table', "all systems must be tables")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Pool:_init(options, ...)
|
||||
self:_validateOptions(options)
|
||||
options = options or {}
|
||||
-- entities that will be added to the pool on the next flush
|
||||
self._queue = {}
|
||||
-- a temporary table for entities that will be added to the pool
|
||||
-- on the current flush (see Pool.flush for more details)
|
||||
self._entitiesToFlush = {}
|
||||
self.entities = {}
|
||||
self.hasEntity = {}
|
||||
self.groups = {}
|
||||
self._systems = {}
|
||||
self._events = {}
|
||||
self.data = options.data or {}
|
||||
local groups = options.groups or {}
|
||||
local systems = options.systems or { nata.oop() }
|
||||
for groupName, groupOptions in pairs(groups) do
|
||||
self.groups[groupName] = {
|
||||
filter = groupOptions.filter,
|
||||
sort = groupOptions.sort,
|
||||
entities = {},
|
||||
hasEntity = {},
|
||||
}
|
||||
end
|
||||
for _, systemDefinition in ipairs(systems) do
|
||||
local system = setmetatable({
|
||||
pool = self,
|
||||
}, { __index = systemDefinition })
|
||||
table.insert(self._systems, system)
|
||||
end
|
||||
self:emit('init', ...)
|
||||
end
|
||||
|
||||
--- Queues an entity to be added to the pool.
|
||||
-- @tparam table entity the entity to add
|
||||
-- @treturn table the queued entity
|
||||
function Pool:queue(entity)
|
||||
table.insert(self._queue, entity)
|
||||
return entity
|
||||
end
|
||||
|
||||
--- Adds the queued entities to the pool. Entities are added
|
||||
-- in the order they were queued.
|
||||
function Pool:flush()
|
||||
--[[
|
||||
Move the currently queued entities to a temporary
|
||||
table. This way, if an add/addToGroup/removeToGroup
|
||||
event emission leads to another entity being queued,
|
||||
it will be saved for the next flush, rather than
|
||||
adding entities to the table we're in the middle
|
||||
of iterating over, which would lead to an array with
|
||||
holes and screw everything up.
|
||||
]]
|
||||
for i = 1, #self._queue do
|
||||
local entity = self._queue[i]
|
||||
self._entitiesToFlush[i] = entity
|
||||
self._queue[i] = nil
|
||||
end
|
||||
for i = 1, #self._entitiesToFlush do
|
||||
local entity = self._entitiesToFlush[i]
|
||||
-- check if the entity belongs in each group and
|
||||
-- add it to/remove it from the group as needed
|
||||
for groupName, group in pairs(self.groups) do
|
||||
if filterEntity(entity, group.filter) then
|
||||
if not group.hasEntity[entity] then
|
||||
table.insert(group.entities, entity)
|
||||
group.hasEntity[entity] = true
|
||||
self:emit('addToGroup', groupName, entity)
|
||||
end
|
||||
if group.sort then group._needsResort = true end
|
||||
elseif group.hasEntity[entity] then
|
||||
removeByValue(group.entities, entity)
|
||||
group.hasEntity[entity] = nil
|
||||
self:emit('removeFromGroup', groupName, entity)
|
||||
end
|
||||
end
|
||||
-- add the entity to the pool if it hasn't been added already
|
||||
if not self.hasEntity[entity] then
|
||||
table.insert(self.entities, entity)
|
||||
self.hasEntity[entity] = true
|
||||
self:emit('add', entity)
|
||||
end
|
||||
self._entitiesToFlush[i] = nil
|
||||
end
|
||||
-- re-sort groups
|
||||
for _, group in pairs(self.groups) do
|
||||
if group._needsResort then
|
||||
table.sort(group.entities, group.sort)
|
||||
group._needsResort = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes entities from the pool.
|
||||
-- @tparam function f the condition upon which an entity should be
|
||||
-- removed. The function should take an entity as the first argument
|
||||
-- and return `true` if the entity should be removed.
|
||||
function Pool:remove(f)
|
||||
checkArgument(1, f, 'function')
|
||||
for groupName, group in pairs(self.groups) do
|
||||
for i = #group.entities, 1, -1 do
|
||||
local entity = group.entities[i]
|
||||
if f(entity) then
|
||||
self:emit('removeFromGroup', groupName, entity)
|
||||
table.remove(group.entities, i)
|
||||
group.hasEntity[entity] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = #self.entities, 1, -1 do
|
||||
local entity = self.entities[i]
|
||||
if f(entity) then
|
||||
self:emit('remove', entity)
|
||||
table.remove(self.entities, i)
|
||||
self.hasEntity[entity] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Registers a function to be called when an event is emitted.
|
||||
-- @string event the event to listen for
|
||||
-- @tparam function f the function to call
|
||||
-- @treturn function the function that was registered
|
||||
function Pool:on(event, f)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
checkArgument(2, f, 'function')
|
||||
self._events[event] = self._events[event] or {}
|
||||
table.insert(self._events[event], f)
|
||||
return f
|
||||
end
|
||||
|
||||
--- Unregisters a function from an event.
|
||||
-- @string event the event to unregister the function from
|
||||
-- @tparam function f the function to unregister
|
||||
function Pool:off(event, f)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
checkArgument(2, f, 'function')
|
||||
if self._events[event] then
|
||||
removeByValue(self._events[event], f)
|
||||
end
|
||||
end
|
||||
|
||||
--- Emits an event. The `system[event]` function will be called
|
||||
-- for each system that has it, and functions registered
|
||||
-- to the event will be called as well.
|
||||
-- @string event the event to emit
|
||||
-- @param ... additional arguments to pass to the functions that are called
|
||||
function Pool:emit(event, ...)
|
||||
checkCondition(event ~= nil, "event cannot be nil")
|
||||
for _, system in ipairs(self._systems) do
|
||||
if type(system[event]) == 'function' then
|
||||
system[event](system, ...)
|
||||
end
|
||||
end
|
||||
if self._events[event] then
|
||||
for _, f in ipairs(self._events[event]) do
|
||||
f(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets this pool's instance of a system.
|
||||
-- @tparam SystemDefinition systemDefinition the system class to get the instance of
|
||||
-- @treturn System the instance of the system running in this pool
|
||||
function Pool:getSystem(systemDefinition)
|
||||
checkArgument(1, systemDefinition, 'table')
|
||||
for _, system in ipairs(self._systems) do
|
||||
if getmetatable(system).__index == systemDefinition then
|
||||
return system
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
local function validateOopOptions(options)
|
||||
checkOptionalArgument(1, options, 'table')
|
||||
if not options then return end
|
||||
if options.include then
|
||||
checkCondition(type(options.include) == 'table', "include must be a table")
|
||||
end
|
||||
if options.exclude then
|
||||
checkCondition(type(options.exclude) == 'table', "exclude must be a table")
|
||||
end
|
||||
end
|
||||
|
||||
--- Defines the behavior of an OOP system.
|
||||
-- @type OopOptions
|
||||
-- @see nata.oop
|
||||
|
||||
--- A list of events to forward to entities. If not defined,
|
||||
-- the system will forward all events to entities (except
|
||||
-- for the ones in the exclude list).
|
||||
-- @tfield table include
|
||||
|
||||
--- A list of events *not* to forward to entities.
|
||||
-- @tfield table exclude
|
||||
|
||||
--- The name of the group of entities to forward events to.
|
||||
-- If not defined, the system will forward events to all entities.
|
||||
-- @tfield string group
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
--- Creates a new OOP system definition.
|
||||
-- An OOP system, upon receiving an event, will call
|
||||
-- the function of the same name on each entity it monitors
|
||||
-- (if it exists). This facilitates a more traditional, OOP-style
|
||||
-- entity management, where you loop over a table of entities and
|
||||
-- call update and draw functions on them.
|
||||
-- @tparam[opt] OopOptions options how to set up the OOP system
|
||||
-- @treturn SystemDefinition the new OOP system definition
|
||||
function nata.oop(options)
|
||||
validateOopOptions(options)
|
||||
local group = options and options.group
|
||||
local include, exclude
|
||||
if options and options.include then
|
||||
include = {}
|
||||
for _, event in ipairs(options.include) do
|
||||
include[event] = true
|
||||
end
|
||||
end
|
||||
if options and options.exclude then
|
||||
exclude = {}
|
||||
for _, event in ipairs(options.exclude) do
|
||||
exclude[event] = true
|
||||
end
|
||||
end
|
||||
return setmetatable({ _cache = {} }, {
|
||||
__index = function(t, event)
|
||||
t._cache[event] = t._cache[event] or function(self, ...)
|
||||
local shouldCallEvent = true
|
||||
if include and not include[event] then shouldCallEvent = false end
|
||||
if exclude and exclude[event] then shouldCallEvent = false end
|
||||
if shouldCallEvent then
|
||||
local entities
|
||||
-- not using ternary here because if the group doesn't exist,
|
||||
-- i'd rather it cause an error than just silently falling back
|
||||
-- to the main entity pool
|
||||
if group then
|
||||
entities = self.pool.groups[group].entities
|
||||
else
|
||||
entities = self.pool.entities
|
||||
end
|
||||
for _, entity in ipairs(entities) do
|
||||
if type(entity[event]) == 'function' then
|
||||
entity[event](entity, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return t._cache[event]
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
--- Defines the filter and sort function for a @{Group}.
|
||||
-- @type GroupOptions
|
||||
|
||||
--- The filter that defines which entities are added to this group.
|
||||
-- Can be either:
|
||||
--
|
||||
-- - A list of required keys
|
||||
-- - A function that takes the entity as the first argument
|
||||
-- and returns true if the entity should be added to the group
|
||||
-- @tfield[opt] table|function filter
|
||||
|
||||
--- A function that specifies how the entities in this group should be sorted.
|
||||
-- Has the same requirements as the function argument to Lua's built-in `table.sort`.
|
||||
-- @tfield[opt] function sort
|
||||
|
||||
--- Defines the groups and systems for a @{Pool}.
|
||||
-- @type PoolOptions
|
||||
|
||||
--- A dictionary of groups for the pool to have.
|
||||
-- Each key is the name of the group, and each value
|
||||
-- should be a @{GroupOptions} table.
|
||||
-- @tfield[opt={}] table groups
|
||||
|
||||
--- A list of @{SystemDefinition}s for the pool to use.
|
||||
-- @tfield[opt={nata.oop()}] table systems
|
||||
|
||||
--- An initial value to set @{Pool.data} to.
|
||||
-- @field[opt={}] data
|
||||
|
||||
---
|
||||
-- @section end
|
||||
|
||||
--- Creates a new @{Pool}.
|
||||
-- @tparam[opt] PoolOptions options how to set up the pool
|
||||
-- @param[opt] ... additional arguments to pass to the pool's init event
|
||||
-- @treturn Pool the new pool
|
||||
function nata.new(options, ...)
|
||||
local pool = setmetatable({}, Pool)
|
||||
pool:_init(options, ...)
|
||||
return pool
|
||||
end
|
||||
|
||||
return nata
|
||||
863
game/libs/tiny.lua
Normal file
|
|
@ -0,0 +1,863 @@
|
|||
--[[
|
||||
Copyright (c) 2016 Calvin Rose
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
|
||||
--- @module tiny-ecs
|
||||
-- @author Calvin Rose
|
||||
-- @license MIT
|
||||
-- @copyright 2016
|
||||
local tiny = {}
|
||||
|
||||
-- Local versions of standard lua functions
|
||||
local tinsert = table.insert
|
||||
local tremove = table.remove
|
||||
local tsort = table.sort
|
||||
local setmetatable = setmetatable
|
||||
local type = type
|
||||
local select = select
|
||||
|
||||
-- Local versions of the library functions
|
||||
local tiny_manageEntities
|
||||
local tiny_manageSystems
|
||||
local tiny_addEntity
|
||||
local tiny_addSystem
|
||||
local tiny_add
|
||||
local tiny_removeEntity
|
||||
local tiny_removeSystem
|
||||
|
||||
--- Filter functions.
|
||||
-- A Filter is a function that selects which Entities apply to a System.
|
||||
-- Filters take two parameters, the System and the Entity, and return a boolean
|
||||
-- value indicating if the Entity should be processed by the System. A truthy
|
||||
-- value includes the entity, while a falsey (nil or false) value excludes the
|
||||
-- entity.
|
||||
--
|
||||
-- Filters must be added to Systems by setting the `filter` field of the System.
|
||||
-- Filter's returned by tiny-ecs's Filter functions are immutable and can be
|
||||
-- used by multiple Systems.
|
||||
--
|
||||
-- local f1 = tiny.requireAll("position", "velocity", "size")
|
||||
-- local f2 = tiny.requireAny("position", "velocity", "size")
|
||||
--
|
||||
-- local e1 = {
|
||||
-- position = {2, 3},
|
||||
-- velocity = {3, 3},
|
||||
-- size = {4, 4}
|
||||
-- }
|
||||
--
|
||||
-- local entity2 = {
|
||||
-- position = {4, 5},
|
||||
-- size = {4, 4}
|
||||
-- }
|
||||
--
|
||||
-- local e3 = {
|
||||
-- position = {2, 3},
|
||||
-- velocity = {3, 3}
|
||||
-- }
|
||||
--
|
||||
-- print(f1(nil, e1), f1(nil, e2), f1(nil, e3)) -- prints true, false, false
|
||||
-- print(f2(nil, e1), f2(nil, e2), f2(nil, e3)) -- prints true, true, true
|
||||
--
|
||||
-- Filters can also be passed as arguments to other Filter constructors. This is
|
||||
-- a powerful way to create complex, custom Filters that select a very specific
|
||||
-- set of Entities.
|
||||
--
|
||||
-- -- Selects Entities with an "image" Component, but not Entities with a
|
||||
-- -- "Player" or "Enemy" Component.
|
||||
-- filter = tiny.requireAll("image", tiny.rejectAny("Player", "Enemy"))
|
||||
--
|
||||
-- @section Filter
|
||||
|
||||
-- A helper function to compile filters.
|
||||
local filterJoin
|
||||
|
||||
-- A helper function to filters from string
|
||||
local filterBuildString
|
||||
|
||||
do
|
||||
local loadstring = loadstring or load
|
||||
local function getchr(c)
|
||||
return "\\" .. c:byte()
|
||||
end
|
||||
local function make_safe(text)
|
||||
return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr)
|
||||
end
|
||||
|
||||
local function filterJoinRaw(prefix, seperator, ...)
|
||||
local accum = {}
|
||||
local build = {}
|
||||
for i = 1, select('#', ...) do
|
||||
local item = select(i, ...)
|
||||
if type(item) == 'string' then
|
||||
accum[#accum + 1] = ("(e[%s] ~= nil)"):format(make_safe(item))
|
||||
elseif type(item) == 'function' then
|
||||
build[#build + 1] = ('local subfilter_%d_ = select(%d, ...)')
|
||||
:format(i, i)
|
||||
accum[#accum + 1] = ('(subfilter_%d_(system, e))'):format(i)
|
||||
else
|
||||
error 'Filter token must be a string or a filter function.'
|
||||
end
|
||||
end
|
||||
local source = ('%s\nreturn function(system, e) return %s(%s) end')
|
||||
:format(
|
||||
table.concat(build, '\n'),
|
||||
prefix,
|
||||
table.concat(accum, seperator))
|
||||
local loader, err = loadstring(source)
|
||||
if err then error(err) end
|
||||
return loader(...)
|
||||
end
|
||||
|
||||
function filterJoin(...)
|
||||
local state, value = pcall(filterJoinRaw, ...)
|
||||
if state then return value else return nil, value end
|
||||
end
|
||||
|
||||
local function buildPart(str)
|
||||
local accum = {}
|
||||
local subParts = {}
|
||||
str = str:gsub('%b()', function(p)
|
||||
subParts[#subParts + 1] = buildPart(p:sub(2, -2))
|
||||
return ('\255%d'):format(#subParts)
|
||||
end)
|
||||
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do
|
||||
if part:match('^\255%d+$') then
|
||||
local partIndex = tonumber(part:match(part:sub(2)))
|
||||
accum[#accum + 1] = ('%s(%s)')
|
||||
:format(invert == '' and '' or 'not', subParts[partIndex])
|
||||
else
|
||||
accum[#accum + 1] = ("(e[%s] %s nil)")
|
||||
:format(make_safe(part), invert == '' and '~=' or '==')
|
||||
end
|
||||
if sep ~= '' then
|
||||
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ')
|
||||
end
|
||||
end
|
||||
return table.concat(accum)
|
||||
end
|
||||
|
||||
function filterBuildString(str)
|
||||
local source = ("return function(_, e) return %s end")
|
||||
:format(buildPart(str))
|
||||
local loader, err = loadstring(source)
|
||||
if err then
|
||||
error(err)
|
||||
end
|
||||
return loader()
|
||||
end
|
||||
end
|
||||
|
||||
--- Makes a Filter that selects Entities with all specified Components and
|
||||
-- Filters.
|
||||
function tiny.requireAll(...)
|
||||
return filterJoin('', ' and ', ...)
|
||||
end
|
||||
|
||||
--- Makes a Filter that selects Entities with at least one of the specified
|
||||
-- Components and Filters.
|
||||
function tiny.requireAny(...)
|
||||
return filterJoin('', ' or ', ...)
|
||||
end
|
||||
|
||||
--- Makes a Filter that rejects Entities with all specified Components and
|
||||
-- Filters, and selects all other Entities.
|
||||
function tiny.rejectAll(...)
|
||||
return filterJoin('not', ' and ', ...)
|
||||
end
|
||||
|
||||
--- Makes a Filter that rejects Entities with at least one of the specified
|
||||
-- Components and Filters, and selects all other Entities.
|
||||
function tiny.rejectAny(...)
|
||||
return filterJoin('not', ' or ', ...)
|
||||
end
|
||||
|
||||
--- Makes a Filter from a string. Syntax of `pattern` is as follows.
|
||||
--
|
||||
-- * Tokens are alphanumeric strings including underscores.
|
||||
-- * Tokens can be separated by |, &, or surrounded by parentheses.
|
||||
-- * Tokens can be prefixed with !, and are then inverted.
|
||||
--
|
||||
-- Examples are best:
|
||||
-- 'a|b|c' - Matches entities with an 'a' OR 'b' OR 'c'.
|
||||
-- 'a&!b&c' - Matches entities with an 'a' AND NOT 'b' AND 'c'.
|
||||
-- 'a|(b&c&d)|e - Matches 'a' OR ('b' AND 'c' AND 'd') OR 'e'
|
||||
-- @param pattern
|
||||
function tiny.filter(pattern)
|
||||
local state, value = pcall(filterBuildString, pattern)
|
||||
if state then return value else return nil, value end
|
||||
end
|
||||
|
||||
--- System functions.
|
||||
-- A System is a wrapper around function callbacks for manipulating Entities.
|
||||
-- Systems are implemented as tables that contain at least one method;
|
||||
-- an update function that takes parameters like so:
|
||||
--
|
||||
-- * `function system:update(dt)`.
|
||||
--
|
||||
-- There are also a few other optional callbacks:
|
||||
--
|
||||
-- * `function system:filter(entity)` - Returns true if this System should
|
||||
-- include this Entity, otherwise should return false. If this isn't specified,
|
||||
-- no Entities are included in the System.
|
||||
-- * `function system:onAdd(entity)` - Called when an Entity is added to the
|
||||
-- System.
|
||||
-- * `function system:onRemove(entity)` - Called when an Entity is removed
|
||||
-- from the System.
|
||||
-- * `function system:onModify(dt)` - Called when the System is modified by
|
||||
-- adding or removing Entities from the System.
|
||||
-- * `function system:onAddToWorld(world)` - Called when the System is added
|
||||
-- to the World, before any entities are added to the system.
|
||||
-- * `function system:onRemoveFromWorld(world)` - Called when the System is
|
||||
-- removed from the world, after all Entities are removed from the System.
|
||||
-- * `function system:preWrap(dt)` - Called on each system before update is
|
||||
-- called on any system.
|
||||
-- * `function system:postWrap(dt)` - Called on each system in reverse order
|
||||
-- after update is called on each system. The idea behind `preWrap` and
|
||||
-- `postWrap` is to allow for systems that modify the behavior of other systems.
|
||||
-- Say there is a DrawingSystem, which draws sprites to the screen, and a
|
||||
-- PostProcessingSystem, that adds some blur and bloom effects. In the preWrap
|
||||
-- method of the PostProcessingSystem, the System could set the drawing target
|
||||
-- for the DrawingSystem to a special buffer instead the screen. In the postWrap
|
||||
-- method, the PostProcessingSystem could then modify the buffer and render it
|
||||
-- to the screen. In this setup, the PostProcessingSystem would be added to the
|
||||
-- World after the drawingSystem (A similar but less flexible behavior could
|
||||
-- be accomplished with a single custom update function in the DrawingSystem).
|
||||
--
|
||||
-- For Filters, it is convenient to use `tiny.requireAll` or `tiny.requireAny`,
|
||||
-- but one can write their own filters as well. Set the Filter of a System like
|
||||
-- so:
|
||||
-- system.filter = tiny.requireAll("a", "b", "c")
|
||||
-- or
|
||||
-- function system:filter(entity)
|
||||
-- return entity.myRequiredComponentName ~= nil
|
||||
-- end
|
||||
--
|
||||
-- All Systems also have a few important fields that are initialized when the
|
||||
-- system is added to the World. A few are important, and few should be less
|
||||
-- commonly used.
|
||||
--
|
||||
-- * The `world` field points to the World that the System belongs to. Useful
|
||||
-- for adding and removing Entities from the world dynamically via the System.
|
||||
-- * The `active` flag is whether or not the System is updated automatically.
|
||||
-- Inactive Systems should be updated manually or not at all via
|
||||
-- `system:update(dt)`. Defaults to true.
|
||||
-- * The `entities` field is an ordered list of Entities in the System. This
|
||||
-- list can be used to quickly iterate through all Entities in a System.
|
||||
-- * The `interval` field is an optional field that makes Systems update at
|
||||
-- certain intervals using buffered time, regardless of World update frequency.
|
||||
-- For example, to make a System update once a second, set the System's interval
|
||||
-- to 1.
|
||||
-- * The `index` field is the System's index in the World. Lower indexed
|
||||
-- Systems are processed before higher indices. The `index` is a read only
|
||||
-- field; to set the `index`, use `tiny.setSystemIndex(world, system)`.
|
||||
-- * The `indices` field is a table of Entity keys to their indices in the
|
||||
-- `entities` list. Most Systems can ignore this.
|
||||
-- * The `modified` flag is an indicator if the System has been modified in
|
||||
-- the last update. If so, the `onModify` callback will be called on the System
|
||||
-- in the next update, if it has one. This is usually managed by tiny-ecs, so
|
||||
-- users should mostly ignore this, too.
|
||||
--
|
||||
-- There is another option to (hopefully) increase performance in systems that
|
||||
-- have items added to or removed from them often, and have lots of entities in
|
||||
-- them. Setting the `nocache` field of the system might improve performance.
|
||||
-- It is still experimental. There are some restriction to systems without
|
||||
-- caching, however.
|
||||
--
|
||||
-- * There is no `entities` table.
|
||||
-- * Callbacks such onAdd, onRemove, and onModify will never be called
|
||||
-- * Noncached systems cannot be sorted (There is no entities list to sort).
|
||||
--
|
||||
-- @section System
|
||||
|
||||
-- Use an empty table as a key for identifying Systems. Any table that contains
|
||||
-- this key is considered a System rather than an Entity.
|
||||
local systemTableKey = { "SYSTEM_TABLE_KEY" }
|
||||
|
||||
-- Checks if a table is a System.
|
||||
local function isSystem(table)
|
||||
return table[systemTableKey]
|
||||
end
|
||||
|
||||
-- Update function for all Processing Systems.
|
||||
local function processingSystemUpdate(system, dt)
|
||||
local preProcess = system.preProcess
|
||||
local process = system.process
|
||||
local postProcess = system.postProcess
|
||||
|
||||
if preProcess then
|
||||
preProcess(system, dt)
|
||||
end
|
||||
|
||||
if process then
|
||||
if system.nocache then
|
||||
local entities = system.world.entities
|
||||
local filter = system.filter
|
||||
if filter then
|
||||
for i = 1, #entities do
|
||||
local entity = entities[i]
|
||||
if filter(system, entity) then
|
||||
process(system, entity, dt)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local entities = system.entities
|
||||
for i = 1, #entities do
|
||||
process(system, entities[i], dt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if postProcess then
|
||||
postProcess(system, dt)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sorts Systems by a function system.sortDelegate(entity1, entity2) on modify.
|
||||
local function sortedSystemOnModify(system)
|
||||
local entities = system.entities
|
||||
local indices = system.indices
|
||||
local sortDelegate = system.sortDelegate
|
||||
if not sortDelegate then
|
||||
local compare = system.compare
|
||||
sortDelegate = function(e1, e2)
|
||||
return compare(system, e1, e2)
|
||||
end
|
||||
system.sortDelegate = sortDelegate
|
||||
end
|
||||
tsort(entities, sortDelegate)
|
||||
for i = 1, #entities do
|
||||
indices[entities[i]] = i
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates a new System or System class from the supplied table. If `table` is
|
||||
-- nil, creates a new table.
|
||||
function tiny.system(table)
|
||||
table = table or {}
|
||||
table[systemTableKey] = true
|
||||
return table
|
||||
end
|
||||
|
||||
--- Creates a new Processing System or Processing System class. Processing
|
||||
-- Systems process each entity individual, and are usually what is needed.
|
||||
-- Processing Systems have three extra callbacks besides those inheritted from
|
||||
-- vanilla Systems.
|
||||
--
|
||||
-- function system:preProcess(dt) -- Called before iteration.
|
||||
-- function system:process(entity, dt) -- Process each entity.
|
||||
-- function system:postProcess(dt) -- Called after iteration.
|
||||
--
|
||||
-- Processing Systems have their own `update` method, so don't implement a
|
||||
-- a custom `update` callback for Processing Systems.
|
||||
-- @see system
|
||||
function tiny.processingSystem(table)
|
||||
table = table or {}
|
||||
table[systemTableKey] = true
|
||||
table.update = processingSystemUpdate
|
||||
return table
|
||||
end
|
||||
|
||||
--- Creates a new Sorted System or Sorted System class. Sorted Systems sort
|
||||
-- their Entities according to a user-defined method, `system:compare(e1, e2)`,
|
||||
-- which should return true if `e1` should come before `e2` and false otherwise.
|
||||
-- Sorted Systems also override the default System's `onModify` callback, so be
|
||||
-- careful if defining a custom callback. However, for processing the sorted
|
||||
-- entities, consider `tiny.sortedProcessingSystem(table)`.
|
||||
-- @see system
|
||||
function tiny.sortedSystem(table)
|
||||
table = table or {}
|
||||
table[systemTableKey] = true
|
||||
table.onModify = sortedSystemOnModify
|
||||
return table
|
||||
end
|
||||
|
||||
--- Creates a new Sorted Processing System or Sorted Processing System class.
|
||||
-- Sorted Processing Systems have both the aspects of Processing Systems and
|
||||
-- Sorted Systems.
|
||||
-- @see system
|
||||
-- @see processingSystem
|
||||
-- @see sortedSystem
|
||||
function tiny.sortedProcessingSystem(table)
|
||||
table = table or {}
|
||||
table[systemTableKey] = true
|
||||
table.update = processingSystemUpdate
|
||||
table.onModify = sortedSystemOnModify
|
||||
return table
|
||||
end
|
||||
|
||||
--- World functions.
|
||||
-- A World is a container that manages Entities and Systems. Typically, a
|
||||
-- program uses one World at a time.
|
||||
--
|
||||
-- For all World functions except `tiny.world(...)`, object-oriented syntax can
|
||||
-- be used instead of the documented syntax. For example,
|
||||
-- `tiny.add(world, e1, e2, e3)` is the same as `world:add(e1, e2, e3)`.
|
||||
-- @section World
|
||||
|
||||
-- Forward declaration
|
||||
local worldMetaTable
|
||||
|
||||
--- Creates a new World.
|
||||
-- Can optionally add default Systems and Entities. Returns the new World along
|
||||
-- with default Entities and Systems.
|
||||
function tiny.world(...)
|
||||
local ret = setmetatable({
|
||||
|
||||
-- List of Entities to remove
|
||||
entitiesToRemove = {},
|
||||
|
||||
-- List of Entities to change
|
||||
entitiesToChange = {},
|
||||
|
||||
-- List of Entities to add
|
||||
systemsToAdd = {},
|
||||
|
||||
-- List of Entities to remove
|
||||
systemsToRemove = {},
|
||||
|
||||
-- Set of Entities
|
||||
entities = {},
|
||||
|
||||
-- List of Systems
|
||||
systems = {}
|
||||
|
||||
}, worldMetaTable)
|
||||
|
||||
tiny_add(ret, ...)
|
||||
tiny_manageSystems(ret)
|
||||
tiny_manageEntities(ret)
|
||||
|
||||
return ret, ...
|
||||
end
|
||||
|
||||
--- Adds an Entity to the world.
|
||||
-- Also call this on Entities that have changed Components such that they
|
||||
-- match different Filters. Returns the Entity.
|
||||
function tiny.addEntity(world, entity)
|
||||
local e2c = world.entitiesToChange
|
||||
e2c[#e2c + 1] = entity
|
||||
return entity
|
||||
end
|
||||
|
||||
tiny_addEntity = tiny.addEntity
|
||||
|
||||
--- Adds a System to the world. Returns the System.
|
||||
function tiny.addSystem(world, system)
|
||||
assert(system.world == nil, "System already belongs to a World.")
|
||||
local s2a = world.systemsToAdd
|
||||
s2a[#s2a + 1] = system
|
||||
system.world = world
|
||||
return system
|
||||
end
|
||||
|
||||
tiny_addSystem = tiny.addSystem
|
||||
|
||||
--- Shortcut for adding multiple Entities and Systems to the World. Returns all
|
||||
-- added Entities and Systems.
|
||||
function tiny.add(world, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local obj = select(i, ...)
|
||||
if obj then
|
||||
if isSystem(obj) then
|
||||
tiny_addSystem(world, obj)
|
||||
else -- Assume obj is an Entity
|
||||
tiny_addEntity(world, obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
tiny_add = tiny.add
|
||||
|
||||
--- Removes an Entity from the World. Returns the Entity.
|
||||
function tiny.removeEntity(world, entity)
|
||||
local e2r = world.entitiesToRemove
|
||||
e2r[#e2r + 1] = entity
|
||||
return entity
|
||||
end
|
||||
|
||||
tiny_removeEntity = tiny.removeEntity
|
||||
|
||||
--- Removes a System from the world. Returns the System.
|
||||
function tiny.removeSystem(world, system)
|
||||
assert(system.world == world, "System does not belong to this World.")
|
||||
local s2r = world.systemsToRemove
|
||||
s2r[#s2r + 1] = system
|
||||
return system
|
||||
end
|
||||
|
||||
tiny_removeSystem = tiny.removeSystem
|
||||
|
||||
--- Shortcut for removing multiple Entities and Systems from the World. Returns
|
||||
-- all removed Systems and Entities
|
||||
function tiny.remove(world, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local obj = select(i, ...)
|
||||
if obj then
|
||||
if isSystem(obj) then
|
||||
tiny_removeSystem(world, obj)
|
||||
else -- Assume obj is an Entity
|
||||
tiny_removeEntity(world, obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ...
|
||||
end
|
||||
|
||||
-- Adds and removes Systems that have been marked from the World.
|
||||
function tiny_manageSystems(world)
|
||||
local s2a, s2r = world.systemsToAdd, world.systemsToRemove
|
||||
|
||||
-- Early exit
|
||||
if #s2a == 0 and #s2r == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
world.systemsToAdd = {}
|
||||
world.systemsToRemove = {}
|
||||
|
||||
local worldEntityList = world.entities
|
||||
local systems = world.systems
|
||||
|
||||
-- Remove Systems
|
||||
for i = 1, #s2r do
|
||||
local system = s2r[i]
|
||||
local index = system.index
|
||||
local onRemove = system.onRemove
|
||||
if onRemove and not system.nocache then
|
||||
local entityList = system.entities
|
||||
for j = 1, #entityList do
|
||||
onRemove(system, entityList[j])
|
||||
end
|
||||
end
|
||||
tremove(systems, index)
|
||||
for j = index, #systems do
|
||||
systems[j].index = j
|
||||
end
|
||||
local onRemoveFromWorld = system.onRemoveFromWorld
|
||||
if onRemoveFromWorld then
|
||||
onRemoveFromWorld(system, world)
|
||||
end
|
||||
s2r[i] = nil
|
||||
|
||||
-- Clean up System
|
||||
system.world = nil
|
||||
system.entities = nil
|
||||
system.indices = nil
|
||||
system.index = nil
|
||||
end
|
||||
|
||||
-- Add Systems
|
||||
for i = 1, #s2a do
|
||||
local system = s2a[i]
|
||||
if systems[system.index or 0] ~= system then
|
||||
if not system.nocache then
|
||||
system.entities = {}
|
||||
system.indices = {}
|
||||
end
|
||||
if system.active == nil then
|
||||
system.active = true
|
||||
end
|
||||
system.modified = true
|
||||
system.world = world
|
||||
local index = #systems + 1
|
||||
system.index = index
|
||||
systems[index] = system
|
||||
local onAddToWorld = system.onAddToWorld
|
||||
if onAddToWorld then
|
||||
onAddToWorld(system, world)
|
||||
end
|
||||
|
||||
-- Try to add Entities
|
||||
if not system.nocache then
|
||||
local entityList = system.entities
|
||||
local entityIndices = system.indices
|
||||
local onAdd = system.onAdd
|
||||
local filter = system.filter
|
||||
if filter then
|
||||
for j = 1, #worldEntityList do
|
||||
local entity = worldEntityList[j]
|
||||
if filter(system, entity) then
|
||||
local entityIndex = #entityList + 1
|
||||
entityList[entityIndex] = entity
|
||||
entityIndices[entity] = entityIndex
|
||||
if onAdd then
|
||||
onAdd(system, entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
s2a[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Adds, removes, and changes Entities that have been marked.
|
||||
function tiny_manageEntities(world)
|
||||
local e2r = world.entitiesToRemove
|
||||
local e2c = world.entitiesToChange
|
||||
|
||||
-- Early exit
|
||||
if #e2r == 0 and #e2c == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
world.entitiesToChange = {}
|
||||
world.entitiesToRemove = {}
|
||||
|
||||
local entities = world.entities
|
||||
local systems = world.systems
|
||||
|
||||
-- Change Entities
|
||||
for i = 1, #e2c do
|
||||
local entity = e2c[i]
|
||||
-- Add if needed
|
||||
if not entities[entity] then
|
||||
local index = #entities + 1
|
||||
entities[entity] = index
|
||||
entities[index] = entity
|
||||
end
|
||||
for j = 1, #systems do
|
||||
local system = systems[j]
|
||||
if not system.nocache then
|
||||
local ses = system.entities
|
||||
local seis = system.indices
|
||||
local index = seis[entity]
|
||||
local filter = system.filter
|
||||
if filter and filter(system, entity) then
|
||||
if not index then
|
||||
system.modified = true
|
||||
index = #ses + 1
|
||||
ses[index] = entity
|
||||
seis[entity] = index
|
||||
local onAdd = system.onAdd
|
||||
if onAdd then
|
||||
onAdd(system, entity)
|
||||
end
|
||||
end
|
||||
elseif index then
|
||||
system.modified = true
|
||||
local tmpEntity = ses[#ses]
|
||||
ses[index] = tmpEntity
|
||||
seis[tmpEntity] = index
|
||||
seis[entity] = nil
|
||||
ses[#ses] = nil
|
||||
local onRemove = system.onRemove
|
||||
if onRemove then
|
||||
onRemove(system, entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
e2c[i] = nil
|
||||
end
|
||||
|
||||
-- Remove Entities
|
||||
for i = 1, #e2r do
|
||||
local entity = e2r[i]
|
||||
e2r[i] = nil
|
||||
local listIndex = entities[entity]
|
||||
if listIndex then
|
||||
-- Remove Entity from world state
|
||||
local lastEntity = entities[#entities]
|
||||
entities[lastEntity] = listIndex
|
||||
entities[entity] = nil
|
||||
entities[listIndex] = lastEntity
|
||||
entities[#entities] = nil
|
||||
-- Remove from cached systems
|
||||
for j = 1, #systems do
|
||||
local system = systems[j]
|
||||
if not system.nocache then
|
||||
local ses = system.entities
|
||||
local seis = system.indices
|
||||
local index = seis[entity]
|
||||
if index then
|
||||
system.modified = true
|
||||
local tmpEntity = ses[#ses]
|
||||
ses[index] = tmpEntity
|
||||
seis[tmpEntity] = index
|
||||
seis[entity] = nil
|
||||
ses[#ses] = nil
|
||||
local onRemove = system.onRemove
|
||||
if onRemove then
|
||||
onRemove(system, entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Manages Entities and Systems marked for deletion or addition. Call this
|
||||
-- before modifying Systems and Entities outside of a call to `tiny.update`.
|
||||
-- Do not call this within a call to `tiny.update`.
|
||||
function tiny.refresh(world)
|
||||
tiny_manageSystems(world)
|
||||
tiny_manageEntities(world)
|
||||
local systems = world.systems
|
||||
for i = #systems, 1, -1 do
|
||||
local system = systems[i]
|
||||
if system.active then
|
||||
local onModify = system.onModify
|
||||
if onModify and system.modified then
|
||||
onModify(system, 0)
|
||||
end
|
||||
system.modified = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates the World by dt (delta time). Takes an optional parameter, `filter`,
|
||||
-- which is a Filter that selects Systems from the World, and updates only those
|
||||
-- Systems. If `filter` is not supplied, all Systems are updated. Put this
|
||||
-- function in your main loop.
|
||||
function tiny.update(world, dt, filter)
|
||||
tiny_manageSystems(world)
|
||||
tiny_manageEntities(world)
|
||||
|
||||
local systems = world.systems
|
||||
|
||||
-- Iterate through Systems IN REVERSE ORDER
|
||||
for i = #systems, 1, -1 do
|
||||
local system = systems[i]
|
||||
if system.active then
|
||||
-- Call the modify callback on Systems that have been modified.
|
||||
local onModify = system.onModify
|
||||
if onModify and system.modified then
|
||||
onModify(system, dt)
|
||||
end
|
||||
local preWrap = system.preWrap
|
||||
if preWrap and
|
||||
((not filter) or filter(world, system)) then
|
||||
preWrap(system, dt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Iterate through Systems IN ORDER
|
||||
for i = 1, #systems do
|
||||
local system = systems[i]
|
||||
if system.active and ((not filter) or filter(world, system)) then
|
||||
-- Update Systems that have an update method (most Systems)
|
||||
local update = system.update
|
||||
if update then
|
||||
local interval = system.interval
|
||||
if interval then
|
||||
local bufferedTime = (system.bufferedTime or 0) + dt
|
||||
while bufferedTime >= interval do
|
||||
bufferedTime = bufferedTime - interval
|
||||
update(system, interval)
|
||||
end
|
||||
system.bufferedTime = bufferedTime
|
||||
else
|
||||
update(system, dt)
|
||||
end
|
||||
end
|
||||
|
||||
system.modified = false
|
||||
end
|
||||
end
|
||||
|
||||
-- Iterate through Systems IN ORDER AGAIN
|
||||
for i = 1, #systems do
|
||||
local system = systems[i]
|
||||
local postWrap = system.postWrap
|
||||
if postWrap and system.active and
|
||||
((not filter) or filter(world, system)) then
|
||||
postWrap(system, dt)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes all Entities from the World.
|
||||
function tiny.clearEntities(world)
|
||||
local el = world.entities
|
||||
for i = 1, #el do
|
||||
tiny_removeEntity(world, el[i])
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes all Systems from the World.
|
||||
function tiny.clearSystems(world)
|
||||
local systems = world.systems
|
||||
for i = #systems, 1, -1 do
|
||||
tiny_removeSystem(world, systems[i])
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets number of Entities in the World.
|
||||
function tiny.getEntityCount(world)
|
||||
return #world.entities
|
||||
end
|
||||
|
||||
--- Gets number of Systems in World.
|
||||
function tiny.getSystemCount(world)
|
||||
return #world.systems
|
||||
end
|
||||
|
||||
--- Sets the index of a System in the World, and returns the old index. Changes
|
||||
-- the order in which they Systems processed, because lower indexed Systems are
|
||||
-- processed first. Returns the old system.index.
|
||||
function tiny.setSystemIndex(world, system, index)
|
||||
tiny_manageSystems(world)
|
||||
local oldIndex = system.index
|
||||
local systems = world.systems
|
||||
|
||||
if index < 0 then
|
||||
index = tiny.getSystemCount(world) + 1 + index
|
||||
end
|
||||
|
||||
tremove(systems, oldIndex)
|
||||
tinsert(systems, index, system)
|
||||
|
||||
for i = oldIndex, index, index >= oldIndex and 1 or -1 do
|
||||
systems[i].index = i
|
||||
end
|
||||
|
||||
return oldIndex
|
||||
end
|
||||
|
||||
-- Construct world metatable.
|
||||
worldMetaTable = {
|
||||
__index = {
|
||||
add = tiny.add,
|
||||
addEntity = tiny.addEntity,
|
||||
addSystem = tiny.addSystem,
|
||||
remove = tiny.remove,
|
||||
removeEntity = tiny.removeEntity,
|
||||
removeSystem = tiny.removeSystem,
|
||||
refresh = tiny.refresh,
|
||||
update = tiny.update,
|
||||
clearEntities = tiny.clearEntities,
|
||||
clearSystems = tiny.clearSystems,
|
||||
getEntityCount = tiny.getEntityCount,
|
||||
getSystemCount = tiny.getSystemCount,
|
||||
setSystemIndex = tiny.setSystemIndex
|
||||
},
|
||||
__tostring = function()
|
||||
return "<tiny-ecs_World>"
|
||||
end
|
||||
}
|
||||
|
||||
return tiny
|
||||
191
game/main.fnl
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
;; deps
|
||||
; (local levels (. (require "levels.fnl") :levels))
|
||||
; (local bump (require "libs/bump"))
|
||||
; (local tiny (require "libs/tiny"))
|
||||
; (local assets (require "src/assets.fnl"))
|
||||
(local utils (require "src/utils.fnl"))
|
||||
; (local colors (require "src/colors.fnl"))
|
||||
; (local tutorial (require "src/levels/tutorial.fnl"))
|
||||
(local dev (require "src/levels/dev.fnl"))
|
||||
|
||||
(local level dev)
|
||||
(local debug false)
|
||||
|
||||
(var pool nil)
|
||||
|
||||
(local screen
|
||||
(let [scale 2 canvas-w 800 canvas-h 450]
|
||||
{ :screen-w (* canvas-w scale)
|
||||
:screen-h (* canvas-h scale)
|
||||
:canvas-w canvas-w
|
||||
:canvas-h canvas-h
|
||||
:scale scale}))
|
||||
|
||||
|
||||
; (lambda create-charging-station [x y]
|
||||
; (let [
|
||||
; station {: x : y :type :charging-station :active-pads {1 false 2 false 3 false}}
|
||||
; pads {
|
||||
; 1 {:x (- x 25) :y y :width 25 :height 25 :light-x (+ x 5)}
|
||||
; 3 {:x (+ x 25) :y y :width 25 :height 25 :light-x (+ x 5 6 6)}
|
||||
; 2 {:x x :y (+ y 25) :width 25 :height 25 :light-x (+ x 5 6)}
|
||||
; }]
|
||||
; (bump-world:add {: x : y :name :charging-station :behavior :block} x y 25 25)
|
||||
; (add-collider-debug-box x y 25 25)
|
||||
; (fn station.draw [self]
|
||||
; (reset-color)
|
||||
; (love.graphics.draw objects.sprite objects.quads.charging-station self.x self.y)
|
||||
; (for [i 1 3 1]
|
||||
; (let [pad (. pads i)]
|
||||
; (if (. self.active-pads i)
|
||||
; (do (love.graphics.draw objects.sprite objects.quads.charging-pad-active pad.x pad.y)
|
||||
; (set-color :light-pink)
|
||||
; (love.graphics.rectangle "fill" pad.light-x (+ y 18) 2 2)
|
||||
; (reset-color))
|
||||
; (love.graphics.draw objects.sprite objects.quads.charging-pad pad.x pad.y))))
|
||||
; )
|
||||
; (lambda pad-hover-cb [self dt]
|
||||
; (tset station.active-pads self.id true)
|
||||
; (when (< player.battery 100) (set player.battery (+ player.battery (* dt 6)))))
|
||||
; (lambda station.update [self _dt]
|
||||
; (set self.active-pads {1 false 2 false 3 false}))
|
||||
; (for [i 1 3 1]
|
||||
; (let [pad (. pads i)]
|
||||
; (bump-world:add {:name :charging-pad :behavior :hover :hover-cb pad-hover-cb :id i} pad.x pad.y 25 25)
|
||||
; (add-collider-debug-box pad.x pad.y 25 25)))
|
||||
; (table.insert objects.list station)))
|
||||
|
||||
; (fn load-objects []
|
||||
; (let [(w h) (objects.sprite:getDimensions)]
|
||||
; (set objects.quads.charging-pad (love.graphics.newQuad 0 0 25 25 w h))
|
||||
; (set objects.quads.charging-pad-active (love.graphics.newQuad 50 0 25 25 w h))
|
||||
; (set objects.quads.charging-station (love.graphics.newQuad 25 0 25 25 w h))
|
||||
; (set objects.quads.door (love.graphics.newQuad 25 25 25 25 w h)))
|
||||
; (each [_ object (pairs (. levels :levels level-key :objects))]
|
||||
; ; (debug-print object)
|
||||
; (case object.type
|
||||
; "h_door" (load-h-door object))
|
||||
; ))
|
||||
|
||||
; (lambda load-level [lvl-name]
|
||||
; (set bump-world (bump.newWorld 25))
|
||||
; (set level-key :tutorial)
|
||||
; ;; set player 1 location
|
||||
; (load-player)
|
||||
; (load-walls)
|
||||
; (load-objects))
|
||||
|
||||
(fn love.load []
|
||||
(love.window.setMode screen.screen-w screen.screen-h)
|
||||
; (tset screen :canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h))
|
||||
(set pool (level.load screen))
|
||||
(pool:flush)
|
||||
(pool:emit :load)
|
||||
; (utils.debug-print pool)
|
||||
; (utils.debug-print world)
|
||||
; (load-assets)
|
||||
; (load-level :tutorial)
|
||||
;; Initialize network
|
||||
; (network.init)
|
||||
; (network.send-msg "REGISTER")
|
||||
|
||||
;; start a thread listening on stdin
|
||||
(: (love.thread.newThread "require('love.event')
|
||||
while 1 do love.event.push('stdin', io.read('*line')) end") :start))
|
||||
|
||||
(fn love.handlers.stdin [line]
|
||||
;; evaluate lines read from stdin as fennel code
|
||||
(let [(ok val) (pcall fennel.eval line)]
|
||||
(print (if ok (fennel.view val) val))))
|
||||
|
||||
;; drawing
|
||||
; (fn draw-world []
|
||||
; (reset-color)
|
||||
; (love.graphics.draw walls.batch)
|
||||
; ;; draw collider debug boxes
|
||||
; (when debug
|
||||
; (set-color :black)
|
||||
; (each [_ collider (pairs collider-debug-boxes)]
|
||||
; (love.graphics.rectangle "line" collider.x collider.y collider.width collider.height))))
|
||||
|
||||
|
||||
(fn love.update [dt]
|
||||
(pool:flush)
|
||||
(pool:emit :update dt))
|
||||
|
||||
; (fn love.update [dt]
|
||||
; (each [_ obj (pairs objects.list)]
|
||||
; (obj:update dt))
|
||||
; ; move / rotate player
|
||||
; (let [
|
||||
; d-key (love.keyboard.isDown :d)
|
||||
; a-key (love.keyboard.isDown :a)
|
||||
; e-key (love.keyboard.isDown :e)
|
||||
; q-key (love.keyboard.isDown :q)]
|
||||
; ; (print (fennel.view {: d-key : a-key : e-key : q-key}))
|
||||
; (match {:d-key d-key :a-key a-key :e-key e-key :q-key q-key}
|
||||
; {:d-key true :a-key false :e-key false :q-key false} (tset player :rot (+ player.rot (* dt 2)))
|
||||
; {:d-key false :a-key true :e-key false :q-key false} (tset player :rot (- player.rot (* dt 2)))
|
||||
; {:d-key false :a-key false :e-key true :q-key false} (tset player :rot (- player.rot (* dt 2)))
|
||||
; {:d-key false :a-key false :e-key false :q-key true} (tset player :rot (+ player.rot (* dt 2)))
|
||||
; )
|
||||
; (when (and (> player.battery 0) (or d-key a-key e-key q-key))
|
||||
; (let [
|
||||
; dir-fn (if (or d-key a-key) #(+ $1 $2) #(- $1 $2))
|
||||
; new-x (dir-fn player.x (* player.speed dt (math.cos player.rot)))
|
||||
; new-y (dir-fn player.y (* player.speed dt (math.sin player.rot)))
|
||||
; col-filter-fn (lambda [item other]
|
||||
; (if (= other.behavior "block") :slide :cross))
|
||||
; (x y cols len) (bump-world:move player new-x new-y col-filter-fn)]
|
||||
; (tset player :x x)
|
||||
; (tset player :y y))
|
||||
; (if (> player.battery 0)
|
||||
; (set player.battery (- player.battery (* dt 2))))))
|
||||
; ;; Update camera to follow player (keep player centered on screen)
|
||||
; (tset camera :x (- player.x (/ screen.canvas-w 2)))
|
||||
; (tset camera :y (- player.y (/ screen.canvas-h 2)))
|
||||
|
||||
; (let
|
||||
; [(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))]
|
||||
; (each [_ item (pairs items)]
|
||||
; (item:hover-cb dt)))
|
||||
|
||||
; ;; Network updates
|
||||
; (network.update dt))
|
||||
; (set net-state.net-update-timer (+ net-state.net-update-timer dt))
|
||||
; (when (>= net-state.net-update-timer net-state.net-update-interval)
|
||||
; (when net-state.connected
|
||||
; (network.send-update player.x player.y player.rot player.battery))
|
||||
; (set net-state.net-update-timer 0)))
|
||||
|
||||
|
||||
|
||||
(fn love.draw []
|
||||
(for [i 1 99 1]
|
||||
(let [draw-trigger (.. "draw" i)]
|
||||
(pool:emit draw-trigger)
|
||||
(when (and (= i 88) debug) (pool:emit "draw-debug")))))
|
||||
|
||||
; (fn love.draw []
|
||||
; (love.graphics.setCanvas screen.canvas)
|
||||
; (love.graphics.clear)
|
||||
; (set-color :cream)
|
||||
; (love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h)
|
||||
; (love.graphics.push) ; stores the default coordinate system
|
||||
; (love.graphics.translate (* -1 camera.x) (* -1 camera.y))
|
||||
; (draw-world)
|
||||
; (draw-objects)
|
||||
; (draw-player)
|
||||
; (love.graphics.pop)
|
||||
; (draw-ui)
|
||||
; (love.graphics.setCanvas)
|
||||
; (reset-color)
|
||||
; (love.graphics.draw screen.canvas 0 0 0 screen.scale screen.scale))
|
||||
; (love.graphics.print "Hello from Fennel!\nPress any key to quit" 10 10))
|
||||
|
||||
(fn love.keypressed [key] nil )
|
||||
|
||||
(fn love.quit []
|
||||
"Clean up before game closes"
|
||||
; (network.close)
|
||||
false)
|
||||
1
game/main.lua
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/Users/she0001t/personal_projects/fennel_love2d_experiments/fennel_bootstrap.lua
|
||||
9
game/src/assets.fnl
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
(local assets {})
|
||||
|
||||
(love.graphics.setDefaultFilter "nearest" "nearest")
|
||||
(set assets.objects-sprite (love.graphics.newImage "assets/objects.png"))
|
||||
(set assets.walls-sprite (love.graphics.newImage "assets/walls.png"))
|
||||
(set assets.battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png"))
|
||||
(set assets.player-sprite (love.graphics.newImage "assets/player.png"))
|
||||
|
||||
assets
|
||||
32
game/src/colors.fnl
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
(local utils (require "src/utils.fnl"))
|
||||
|
||||
;; colors
|
||||
(lambda color [full-r full-g full-b]
|
||||
(let [(r g b) (love.math.colorFromBytes full-r full-g full-b)]
|
||||
[r g b]
|
||||
))
|
||||
|
||||
(local colors {
|
||||
:cream (color 255 238 204)
|
||||
:dark-purple (color 70 66 94)
|
||||
:dark-pink (color 255 105 115)
|
||||
:dark-blue (color 21 120 140)
|
||||
:light-blue (color 0 185 190)
|
||||
:light-pink (color 255 176 163)
|
||||
:black [0 0 0]
|
||||
:black-half-tone [0 0 0 0.25]
|
||||
})
|
||||
|
||||
(fn reset-color []
|
||||
"reset color to white (no tinting)"
|
||||
(love.graphics.setColor 1 1 1))
|
||||
|
||||
(lambda set-color [color-name]
|
||||
"set color to the given color from the color pallet"
|
||||
(love.graphics.setColor (unpack (. colors color-name))))
|
||||
|
||||
{
|
||||
: reset-color
|
||||
: set-color
|
||||
: colors
|
||||
}
|
||||
38
game/src/entities/info-pad.fnl
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
(local utils (require "src/utils.fnl"))
|
||||
(local beholder (require "libs/beholder"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
(local levels (require "levels.fnl"))
|
||||
(local assets (require "src/assets.fnl"))
|
||||
|
||||
(lambda info-pad [obj]
|
||||
(let [
|
||||
x obj.x
|
||||
y obj.y
|
||||
content obj.content
|
||||
pad {: x : y : content :hitbox [x y 25 25]}
|
||||
(w h) (assets.objects-sprite:getDimensions)]
|
||||
(lambda pad.load [self]
|
||||
(set self.quad (love.graphics.newQuad 75 0 25 25 w h)))
|
||||
|
||||
(lambda pad.on-hit [self]
|
||||
(beholder.trigger "NOTIFICATION" self.content))
|
||||
|
||||
(fn pad.draw-debug [self]
|
||||
(color.set-color :black)
|
||||
(love.graphics.rectangle "line" self.x self.y 25 25))
|
||||
|
||||
(lambda pad.draw49 [self]
|
||||
(color.reset-color)
|
||||
(love.graphics.draw assets.objects-sprite self.quad self.x self.y))
|
||||
pad
|
||||
|
||||
|
||||
))
|
||||
|
||||
|
||||
info-pad
|
||||
|
||||
; (lambda [{:bump-world bw :level-key key}]
|
||||
; (set bump-world bw)
|
||||
; (set level-key key)
|
||||
; player)
|
||||
117
game/src/entities/player.fnl
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
(local utils (require "src/utils.fnl"))
|
||||
(local beholder (require "libs/beholder"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
(local levels (require "levels.fnl"))
|
||||
(local assets (require "src/assets.fnl"))
|
||||
|
||||
(var bump-world nil)
|
||||
(var level-key nil)
|
||||
|
||||
(lambda bump-filter [item other]
|
||||
(if (= other.behavior "block") :slide :cross))
|
||||
|
||||
(lambda angle-to-direction [angle]
|
||||
"Convert angle (radians) to compass direction keyword"
|
||||
(local tau (* 2 math.pi))
|
||||
; Normalize angle to 0-2π range
|
||||
(local normalized (% (+ angle tau) tau))
|
||||
; Offset by 22.5° (π/8) so section boundaries align with directions
|
||||
(local offset (+ normalized (/ math.pi 8)))
|
||||
; Find which 45° section (π/4) it falls into
|
||||
(local section (math.floor (/ offset (/ math.pi 4))))
|
||||
; Map to compass directions
|
||||
(local directions [:e :se :s :sw :w :nw :n :ne])
|
||||
(. directions (+ (% section 8) 1)))
|
||||
|
||||
|
||||
(local width 19)
|
||||
(local height 15)
|
||||
(local player {
|
||||
:x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0
|
||||
:hitbox [50 50 width height]
|
||||
:init-move false ; nudge the player at start of game to trigger starting collisions
|
||||
})
|
||||
|
||||
|
||||
(lambda handle-collisions [cols]
|
||||
(each [_ col (pairs cols)]
|
||||
(when col.other.on-hit (col.other:on-hit))
|
||||
(beholder.trigger "PLAYER.HIT" col.other)))
|
||||
|
||||
(fn player.update [self dt]
|
||||
(when (= self.init-move false)
|
||||
(set self.init-move true)
|
||||
(let [(x y cols len) (bump-world:move self self.x self.y bump-filter)]
|
||||
(handle-collisions cols)))
|
||||
(let [
|
||||
d-key (love.keyboard.isDown :d)
|
||||
a-key (love.keyboard.isDown :a)
|
||||
e-key (love.keyboard.isDown :e)
|
||||
q-key (love.keyboard.isDown :q)]
|
||||
(match {:d-key d-key :a-key a-key :e-key e-key :q-key q-key}
|
||||
{:d-key true :a-key false :e-key false :q-key false} (set self.rot (+ self.rot (* dt 2)))
|
||||
{:d-key false :a-key true :e-key false :q-key false} (set self.rot (- self.rot (* dt 2)))
|
||||
{:d-key false :a-key false :e-key true :q-key false} (set self.rot (- self.rot (* dt 2)))
|
||||
{:d-key false :a-key false :e-key false :q-key true} (set self.rot (+ self.rot (* dt 2)))
|
||||
)
|
||||
(when (and (> self.battery 0) (or d-key a-key e-key q-key))
|
||||
(let [
|
||||
dir-fn (if (or d-key a-key) #(+ $1 $2) #(- $1 $2))
|
||||
new-x (dir-fn self.x (* self.speed dt (math.cos self.rot)))
|
||||
new-y (dir-fn self.y (* self.speed dt (math.sin self.rot)))
|
||||
(x y cols len) (bump-world:move self new-x new-y bump-filter)]
|
||||
(handle-collisions cols)
|
||||
(set self.x x)
|
||||
(set self.y y))
|
||||
(if (> self.battery 0)
|
||||
(set self.battery (- self.battery (* dt 2))))))
|
||||
(beholder.trigger "PLAYER.POS" self.x self.y)
|
||||
(beholder.trigger "PLAYER.BATTERY" self.battery))
|
||||
|
||||
(fn player.load [self]
|
||||
(set self.battery 100)
|
||||
(let [
|
||||
spawn-x (-> (. levels :levels level-key :spawns :player-1 :x) (- (/ width 2)))
|
||||
spawn-y (-> (. levels :levels level-key :spawns :player-1 :y) (- (/ height 2)))
|
||||
(x y cols len) (bump-world:move self spawn-x spawn-y bump-filter)]
|
||||
(set self.x x)
|
||||
(set self.y y))
|
||||
(set self.quads
|
||||
(let [
|
||||
(w h) (assets.player-sprite:getDimensions)]
|
||||
{
|
||||
:n (love.graphics.newQuad 0 0 25 25 w h)
|
||||
:s (love.graphics.newQuad 25 0 25 25 w h)
|
||||
:ne (love.graphics.newQuad 50 0 25 25 w h)
|
||||
:e (love.graphics.newQuad 75 0 25 25 w h)
|
||||
:se (love.graphics.newQuad 100 0 25 25 w h)
|
||||
:sw (love.graphics.newQuad 125 0 25 25 w h)
|
||||
:w (love.graphics.newQuad 150 0 25 25 w h)
|
||||
:nw (love.graphics.newQuad 175 0 25 25 w h)
|
||||
})))
|
||||
|
||||
(fn player.draw50 [self]
|
||||
"draw player sprite and hitbox"
|
||||
(color:reset-color)
|
||||
(love.graphics.draw
|
||||
assets.player-sprite
|
||||
(. self.quads (angle-to-direction player.rot))
|
||||
(- self.x 3) (- self.y 7)))
|
||||
|
||||
(fn player.draw-debug [self]
|
||||
"draw player hitbox and direction line"
|
||||
(color.set-color :black)
|
||||
(love.graphics.rectangle "line" self.x self.y width height)
|
||||
(love.graphics.rectangle "line" self.x self.y 1 1)
|
||||
(love.graphics.push)
|
||||
(let [ox (+ self.x (/ width 2)) oy (+ self.y (/ height 2))]
|
||||
(love.graphics.translate ox oy)
|
||||
(love.graphics.rotate self.rot)
|
||||
(love.graphics.line 0 0 35 0))
|
||||
(love.graphics.pop))
|
||||
|
||||
|
||||
(lambda [{:bump-world bw :level-key key}]
|
||||
(set bump-world bw)
|
||||
(set level-key key)
|
||||
player)
|
||||
72
game/src/levels/dev.fnl
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
(local bump (require "libs/bump"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
(local nata (require "libs/nata"))
|
||||
(local utils (require "src/utils.fnl"))
|
||||
(local camera (require "src/systems/camera.fnl"))
|
||||
(local beholder (require "libs/beholder"))
|
||||
(local player (require "src/entities/player.fnl"))
|
||||
(local walls (require "src/systems/walls.fnl"))
|
||||
(local hud (require "src/systems/hud.fnl"))
|
||||
(local levels (require "levels.fnl"))
|
||||
(local info-pad (require "src/entities/info-pad.fnl"))
|
||||
|
||||
(lambda load-objects [level-key pool]
|
||||
(each [_ object (pairs (. levels :levels level-key :objects))]
|
||||
; (debug-print object)
|
||||
(case object.type
|
||||
"info_pad" (pool:queue (info-pad object))
|
||||
))
|
||||
)
|
||||
|
||||
(lambda collider-manager [bump-world]
|
||||
(local collider-manager {})
|
||||
(lambda collider-manager.addToGroup [self group-name entity]
|
||||
(when (= group-name :hitbox)
|
||||
; (utils.debug-print {:adding entity})
|
||||
(bump-world:add entity (unpack entity.hitbox))))
|
||||
collider-manager)
|
||||
|
||||
; (lambda register-notifications []
|
||||
; (beholder.observe "PLAYER.HIT" (lambda [other]
|
||||
; (when (= other.object-type :info-pad)
|
||||
; (beholder.trigger "NOTIFICATION" "[a] is left wheel forward [d] is right wheel forward"))
|
||||
; (utils.debug-print {: other}))))
|
||||
; (beholder.observe "PLAYER.HIT" :info-pad (lambda [info-pad-name]
|
||||
; (case info-pad-name
|
||||
; :controls (beholder.trigger "NOTIFICATION" "[a] is left wheel forward [d] is right wheel forward")
|
||||
; ))))
|
||||
|
||||
|
||||
(fn load [screen]
|
||||
; (register-notifications)
|
||||
(let [
|
||||
canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)
|
||||
level-key :dev
|
||||
bump-world (bump.newWorld 25)
|
||||
pool (nata.new {
|
||||
:data {
|
||||
: screen
|
||||
: canvas
|
||||
: bump-world
|
||||
: level-key
|
||||
}
|
||||
:groups {
|
||||
; gravity = {filter = {'gravity'}},
|
||||
:hitbox {:filter [:hitbox]}
|
||||
; :post-draw {:filter [:post-draw]}
|
||||
}
|
||||
:systems [
|
||||
(nata:oop)
|
||||
(collider-manager bump-world)
|
||||
camera
|
||||
walls
|
||||
(hud {: screen})
|
||||
]
|
||||
})]
|
||||
(pool:queue (player {: bump-world : level-key}))
|
||||
(load-objects level-key pool)
|
||||
pool))
|
||||
|
||||
{
|
||||
: load
|
||||
}
|
||||
71
game/src/levels/tutorial.fnl
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
(local bump (require "libs/bump"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
(local nata (require "libs/nata"))
|
||||
(local utils (require "src/utils.fnl"))
|
||||
(local camera (require "src/systems/camera.fnl"))
|
||||
(local beholder (require "libs/beholder"))
|
||||
(local player (require "src/entities/player.fnl"))
|
||||
(local walls (require "src/systems/walls.fnl"))
|
||||
(local hud (require "src/systems/hud.fnl"))
|
||||
(local levels (require "levels.fnl"))
|
||||
(local info-pad (require "src/entities/info-pad.fnl"))
|
||||
|
||||
(lambda load-objects [level-key pool]
|
||||
(each [_ object (pairs (. levels :levels level-key :objects))]
|
||||
; (debug-print object)
|
||||
(case object.type
|
||||
"info_pad" (pool:queue (info-pad object.x object.y object.name))
|
||||
))
|
||||
)
|
||||
|
||||
(lambda collider-manager [bump-world]
|
||||
(local collider-manager {})
|
||||
(lambda collider-manager.addToGroup [self group-name entity]
|
||||
(when (= group-name :hitbox)
|
||||
; (utils.debug-print {:adding entity})
|
||||
(bump-world:add entity (unpack entity.hitbox))))
|
||||
collider-manager)
|
||||
|
||||
(lambda register-notifications []
|
||||
(beholder.observe "PLAYER.HIT" (lambda [other]
|
||||
(when (= other.object-type :info-pad)
|
||||
(beholder.trigger "NOTIFICATION" "[a] is left wheel forward [d] is right wheel forward"))
|
||||
(utils.debug-print {: other}))))
|
||||
; (beholder.observe "PLAYER.HIT" :info-pad (lambda [info-pad-name]
|
||||
; (case info-pad-name
|
||||
; :controls (beholder.trigger "NOTIFICATION" "[a] is left wheel forward [d] is right wheel forward")
|
||||
; ))))
|
||||
|
||||
|
||||
(fn load [screen]
|
||||
(register-notifications)
|
||||
(let [
|
||||
canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h)
|
||||
bump-world (bump.newWorld 25)
|
||||
pool (nata.new {
|
||||
:data {
|
||||
: screen
|
||||
: canvas
|
||||
: bump-world
|
||||
:level-key :tutorial
|
||||
}
|
||||
:groups {
|
||||
; gravity = {filter = {'gravity'}},
|
||||
:hitbox {:filter [:hitbox]}
|
||||
; :post-draw {:filter [:post-draw]}
|
||||
}
|
||||
:systems [
|
||||
(nata:oop)
|
||||
(collider-manager bump-world)
|
||||
camera
|
||||
walls
|
||||
(hud {: screen})
|
||||
]
|
||||
})]
|
||||
(pool:queue (player {:bump-world bump-world :level-key :tutorial}))
|
||||
(load-objects :tutorial pool)
|
||||
pool))
|
||||
|
||||
{
|
||||
: load
|
||||
}
|
||||
133
game/src/network.fnl
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
(local socket (require "socket"))
|
||||
|
||||
;; Guide: https://www.love2d.org/wiki/Tutorial:Networking_with_UDP
|
||||
|
||||
;; Configuration
|
||||
(local config {
|
||||
:host "localhost"
|
||||
:port 9999
|
||||
})
|
||||
|
||||
;; Network state
|
||||
(local state {
|
||||
:socket nil
|
||||
:connected false
|
||||
; :player-id nil
|
||||
; :last-error nil
|
||||
})
|
||||
|
||||
;; Callback functions for network events
|
||||
; (local callbacks {
|
||||
; :on-ready (fn [])
|
||||
; :on-wait (fn [])
|
||||
; :on-opponent-update (fn [data])
|
||||
; :on-error (fn [error])
|
||||
; })
|
||||
|
||||
(fn init []
|
||||
"Initialize UDP connection to server
|
||||
Returns true on success, false on error"
|
||||
(let [udp (socket.udp)]
|
||||
(udp:setpeername config.host config.port)
|
||||
(udp:settimeout 0) ; non-blocking
|
||||
|
||||
(set state.socket udp)
|
||||
(set state.connected true)))
|
||||
|
||||
(fn send-msg [dg]
|
||||
(state.socket:send dg))
|
||||
|
||||
(fn update [dt]
|
||||
(let [(data msg) (state.socket:receive)]
|
||||
(when data
|
||||
(print (fennel.view {: data : msg})))))
|
||||
|
||||
; (fn register []
|
||||
; "Send REGISTER message to server"
|
||||
; (when state.socket
|
||||
; (try
|
||||
; (state.socket:send "REGISTER")
|
||||
; (catch err
|
||||
; (set state.last-error err)
|
||||
; (when callbacks.on-error (callbacks.on-error err))))))
|
||||
|
||||
; (fn split-string [str delimiter]
|
||||
; "Split string by delimiter into table"
|
||||
; (local result [])
|
||||
; (local current "")
|
||||
; (each [i 1 (length str) 1]
|
||||
; (let [char (string.sub str i i)]
|
||||
; (if (= char delimiter)
|
||||
; (do
|
||||
; (table.insert result current)
|
||||
; (set current ""))
|
||||
; (set current (.. current char)))))
|
||||
; (table.insert result current)
|
||||
; result)
|
||||
|
||||
; (fn parse-message [msg]
|
||||
; "Parse message in format: COMMAND#arg1#arg2#..."
|
||||
; (split-string msg "#"))
|
||||
|
||||
; (fn send-update [x y rotation battery]
|
||||
; "Send player state to server in format: UPDATE#x#y#rotation#battery"
|
||||
; (when state.socket
|
||||
; (try
|
||||
; (let [msg (.. "UPDATE#" x "#" y "#" rotation "#" battery)]
|
||||
; (state.socket:send msg))
|
||||
; (catch err
|
||||
; (set state.last-error err)))))
|
||||
|
||||
; (fn update [dt]
|
||||
; "Call from love.update to process incoming messages"
|
||||
; (when state.socket
|
||||
; (let [(msg _src-ip _src-port) (state.socket:receivefrom)]
|
||||
; (when msg
|
||||
; (let [parts (parse-message msg)
|
||||
; command (. parts 1)]
|
||||
; (match command
|
||||
; "READY_TO_PLAY" (callbacks.on-ready)
|
||||
; "WAIT" (callbacks.on-wait)
|
||||
; "UPDATE" (callbacks.on-opponent-update {
|
||||
; :x (tonumber (. parts 2))
|
||||
; :y (tonumber (. parts 3))
|
||||
; :rotation (tonumber (. parts 4))
|
||||
; :battery (tonumber (. parts 5))
|
||||
; })
|
||||
; _ (print (.. "← Message: " msg))))))))
|
||||
|
||||
; (fn set-callback [event cb]
|
||||
; "Register callback for network events
|
||||
; Available events: :on-ready :on-wait :on-opponent-update :on-error"
|
||||
; (tset callbacks event cb))
|
||||
|
||||
(fn close []
|
||||
"Close network connection"
|
||||
(when state.socket
|
||||
(state.socket:close)
|
||||
(set state.socket nil)
|
||||
(set state.connected false)))
|
||||
|
||||
; (fn is-connected []
|
||||
; "Check if currently connected to server"
|
||||
; state.connected)
|
||||
|
||||
; (fn get-last-error []
|
||||
; "Get the last error that occurred"
|
||||
; state.last-error)
|
||||
|
||||
;; Export public API
|
||||
{
|
||||
: init
|
||||
: update
|
||||
: state
|
||||
: send-msg
|
||||
: close
|
||||
; :register register
|
||||
; :send-update send-update
|
||||
; :update update
|
||||
; :set-callback set-callback
|
||||
; :close close
|
||||
; :is-connected is-connected
|
||||
; :get-last-error get-last-error
|
||||
}
|
||||
37
game/src/systems/camera.fnl
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
(local beholder (require "libs/beholder"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
|
||||
;; Camera System
|
||||
|
||||
(local camera {:x 0 :y 0})
|
||||
|
||||
(fn camera.draw2 [self]
|
||||
(love.graphics.push)
|
||||
(love.graphics.translate (* -1 self.x) (* -1 self.y)))
|
||||
|
||||
(fn camera.load [self]
|
||||
(let [screen self.pool.data.screen]
|
||||
(beholder.observe "PLAYER.POS" (lambda [x y]
|
||||
;; Update camera to follow player (keep player centered on screen)
|
||||
(set self.x (- x (/ screen.canvas-w 2)))
|
||||
(set self.y (- y (/ screen.canvas-h 2)))))))
|
||||
|
||||
(fn camera.draw89 [self]
|
||||
(love.graphics.pop)) ; undo camera translation
|
||||
|
||||
(fn camera.draw99 [self]
|
||||
(let [screen self.pool.data.screen canvas self.pool.data.canvas]
|
||||
(love.graphics.setCanvas) ; reset to root canvas
|
||||
(color:reset-color)
|
||||
(love.graphics.draw canvas 0 0 0 screen.scale screen.scale)))
|
||||
|
||||
(fn camera.draw1 [self]
|
||||
(let [screen self.pool.data.screen canvas self.pool.data.canvas]
|
||||
; use canvas
|
||||
(love.graphics.setCanvas canvas)
|
||||
; clear the screen
|
||||
(love.graphics.clear)
|
||||
(color.set-color :cream)
|
||||
(love.graphics.rectangle "fill" 0 0 screen.canvas-w screen.canvas-h)))
|
||||
|
||||
camera
|
||||
88
game/src/systems/hud.fnl
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
(local beholder (require "libs/beholder"))
|
||||
(local assets (require "src/assets.fnl"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
|
||||
(var screen nil)
|
||||
(local hud {
|
||||
:player-battery 0
|
||||
:notification nil
|
||||
:notification-life 0
|
||||
})
|
||||
|
||||
(lambda draw-notification [self]
|
||||
(when (and self.notification (> self.notification-life 0))
|
||||
(love.graphics.push)
|
||||
(love.graphics.translate 0 (- 400 20))
|
||||
(color.set-color :cream)
|
||||
(love.graphics.rectangle "fill" 0 0 screen.canvas-w 20)
|
||||
(color.set-color :dark-purple)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.line 0 0 screen.canvas-w 0)
|
||||
(love.graphics.setFont self.font)
|
||||
(love.graphics.print self.notification 3 3)
|
||||
(love.graphics.pop)))
|
||||
|
||||
(fn hud.load [self]
|
||||
(set self.font (love.graphics.newFont 10))
|
||||
(set self.font-small (love.graphics.newFont 6))
|
||||
|
||||
(beholder.observe "PLAYER.BATTERY" (lambda [bat]
|
||||
(set self.player-battery bat)))
|
||||
|
||||
(beholder.observe "NOTIFICATION" (lambda [text]
|
||||
(set self.notification text)
|
||||
; set life of notification to 5 seconds
|
||||
(set self.notification-life 5))))
|
||||
|
||||
|
||||
(lambda hud.update [self dt]
|
||||
(when (> self.notification-life 0)
|
||||
(set self.notification-life (- self.notification-life dt))))
|
||||
|
||||
(lambda draw-header [self]
|
||||
(color.set-color :cream)
|
||||
(love.graphics.rectangle "fill" 0 0 screen.canvas-w 20)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.line 0 20 screen.canvas-w 20))
|
||||
|
||||
(lambda draw-side-walls [self]
|
||||
(color.set-color :cream)
|
||||
(love.graphics.rectangle "fill" 0 0 20 screen.canvas-h)
|
||||
(love.graphics.rectangle "fill" (- screen.canvas-w 20) 0 20 screen.canvas-h)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.line 20 0 20 screen.canvas-w)
|
||||
(love.graphics.line (- screen.canvas-w 20) 0 (- screen.canvas-w 20) screen.canvas-w))
|
||||
|
||||
(lambda draw-footer [self]
|
||||
(love.graphics.push)
|
||||
(love.graphics.translate 0 400)
|
||||
(color.set-color :cream)
|
||||
(love.graphics.rectangle "fill" 0 0 screen.canvas-w 100)
|
||||
(color.reset-color)
|
||||
(love.graphics.draw assets.battery-bar-sprite 78 8)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.setFont self.font)
|
||||
(love.graphics.print (.. "Battery: " (string.format "%d" self.player-battery) "%") 6 7)
|
||||
(color.set-color :light-blue)
|
||||
(love.graphics.rectangle "fill" 80 12 (* 152 (/ self.player-battery 100)) 4)
|
||||
(love.graphics.setFont self.font-small)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.print "100%" 229 2)
|
||||
(color.set-color :light-pink)
|
||||
(love.graphics.print "125%" 267 2)
|
||||
(love.graphics.print "125%" 306 2)
|
||||
(love.graphics.print "150%" 345 2)
|
||||
(love.graphics.print "200%" 382 2)
|
||||
(color.set-color :dark-purple)
|
||||
(love.graphics.line 0 0 screen.canvas-w 0)
|
||||
(love.graphics.pop))
|
||||
|
||||
(fn hud.draw90 [self]
|
||||
(draw-side-walls self)
|
||||
(draw-footer self)
|
||||
(draw-header self)
|
||||
(draw-notification self))
|
||||
|
||||
(lambda [{:screen s}]
|
||||
(set screen s)
|
||||
hud)
|
||||
58
game/src/systems/walls.fnl
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
(local assets (require "src/assets.fnl"))
|
||||
(local levels (require "levels.fnl"))
|
||||
(local color (require "src/colors.fnl"))
|
||||
|
||||
(local walls {
|
||||
:quads []
|
||||
:batch nil
|
||||
:collider-debug-boxes []
|
||||
})
|
||||
|
||||
(lambda mirror-collider [collider]
|
||||
"Mirror a collider box horizontally within a 25-unit tile (center is at 12.5)
|
||||
Transforms collider positions so they're reflected across the vertical center line."
|
||||
{
|
||||
:x (- 25 collider.x collider.width)
|
||||
:y collider.y
|
||||
:width collider.width
|
||||
:height collider.height
|
||||
})
|
||||
|
||||
(fn walls.load [self]
|
||||
(set self.batch (love.graphics.newSpriteBatch assets.walls-sprite 2500))
|
||||
;; load quads
|
||||
(let [(w h) (assets.walls-sprite:getDimensions)]
|
||||
(for [i 0 19 1]
|
||||
(table.insert self.quads (love.graphics.newQuad (* i 25) 0 25 25 w h))))
|
||||
;; fill batch
|
||||
(each [_ row (pairs (. levels :levels self.pool.data.level-key :tiles))]
|
||||
(each [_ tile (pairs row)]
|
||||
(let [
|
||||
x tile.x
|
||||
y tile.y
|
||||
id tile.tile-id
|
||||
colliders tile.colliders]
|
||||
(if (and (> id 0) (< id 21)) ;; 1-20 are wall tiles
|
||||
(do
|
||||
(self.batch:add (. self.quads id) (if tile.h-flip (+ x 25) x) y 0 (if tile.h-flip -1 1) 1)
|
||||
(each [_ collider (pairs colliders)]
|
||||
(let [
|
||||
mirrored-collider (if tile.h-flip (mirror-collider collider) collider)
|
||||
collider-x (+ x mirrored-collider.x)
|
||||
collider-y (+ y mirrored-collider.y)]
|
||||
(self.pool.data.bump-world:add {: x : y :name :wall :behavior :block} collider-x collider-y mirrored-collider.width mirrored-collider.height)
|
||||
(table.insert
|
||||
self.collider-debug-boxes
|
||||
{:x collider-x :y collider-y :width mirrored-collider.width :height mirrored-collider.height})))))))))
|
||||
|
||||
(fn walls.draw49 [self]
|
||||
(color.reset-color)
|
||||
(love.graphics.draw self.batch))
|
||||
|
||||
(fn walls.draw-debug [self]
|
||||
"draw collider debug boxes"
|
||||
(color.set-color :black)
|
||||
(each [_ collider (pairs self.collider-debug-boxes)]
|
||||
(love.graphics.rectangle "line" collider.x collider.y collider.width collider.height)))
|
||||
|
||||
walls
|
||||
6
game/src/utils.fnl
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(lambda debug-print [obj]
|
||||
(print (fennel.view obj)))
|
||||
|
||||
{
|
||||
: debug-print
|
||||
}
|
||||