Compare commits

...

6 commits

Author SHA1 Message Date
ffe9d8226d
mirror collider box 2026-04-22 23:05:05 +02:00
e00250fd84
add objects as seprate key in level 2026-04-20 16:01:26 +02:00
a457893d14
flip tiles according to h-flip 2026-04-19 21:34:20 +02:00
bbbe7bfe8a
move objects out of bg tiles
"
Press any key to quit" 10 10))
2026-04-19 21:25:08 +02:00
32ea10495e
get join-game logic working 2026-04-16 22:59:55 +02:00
09bf48481a
switch backend to clojure 2026-04-16 22:30:31 +02:00
26 changed files with 740 additions and 264 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,44 +0,0 @@
# Build stage
FROM golang:1.25-bookworm AS builder
WORKDIR /app
# Install dependencies including libenet-dev from Debian repos
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
gcc \
pkg-config \
libenet-dev \
sqlite3 \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
COPY ./*.go .
# Enable CGO for go-sqlite3 and go-enet
ENV CGO_ENABLED=1
RUN go build -o main .
# Runtime stage
FROM debian:bookworm-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libenet7 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/main .
EXPOSE 8095
CMD ["./main"]

23
backend/deps.edn Normal file
View file

@ -0,0 +1,23 @@
{:paths ["src"]
:deps { ;; db
com.datomic/local {:mvn/version "1.0.291"}
;; logging
com.taoensso/telemere {:mvn/version "1.0.0"}
;; environment variables
environ/environ {:mvn/version "1.2.0"}
;; json / schema validation
metosin/malli {:mvn/version "0.18.0"}
metosin/muuntaja {:mvn/version "0.6.11"}
;; networking - UDP/TCP/HTTP streaming
aleph/aleph {:mvn/version "0.9.7"}
org.clj-commons/byte-streams {:mvn/version "0.3.4"}
org.clojure/clojure {:mvn/version "1.12.1"}}
:aliases
{;; Run with clj -T:build function-in-build
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
:ns-default build}}}

10
backend/dev.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/sh
# Watch for changes to .clj files and auto-restart the server
# fd -e clj: Find all files ending in .clj
# entr -r: Run the command when files change, -r flag restarts the command on re-trigger
# clojure: Clojure CLI
# -M: Use the main alias from deps.edn (if defined)
# -m game-server.main: Run the -main function from game-server.main namespace
fd -e clj | entr -r clojure -M -m game-server.main

View file

@ -1,8 +0,0 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile
container_name: love2d-backend-dev
ports:
- "8095:8095"

View file

@ -1,16 +0,0 @@
module love2d_backend
go 1.25.0
require (
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05
github.com/mattn/go-sqlite3 v1.14.32
)
require (
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138
github.com/fatih/color v1.19.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.42.0 // indirect
)

View file

@ -1,15 +0,0 @@
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138 h1:qQaqly9oVi84i4nLWmM551z7kcFZIy5koCLoPwCarGY=
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138/go.mod h1:sXkhNvv8T1d/aPEwipUzk7rGWwGJ3uJST2P/Z0zE0L4=
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05 h1:JSfDXHJvrIpQ8Agy//yoIlGpfIprTCDUytmf68fd/Lc=
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05/go.mod h1:xJW98cHEb+Kbuu0qmoKzExh3blthZqojIYOFo27VgvE=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=

View file

@ -1,76 +0,0 @@
package main
import (
_ "embed"
"log/slog"
"os"
enet "github.com/codecat/go-enet"
_ "github.com/mattn/go-sqlite3"
)
func main() {
port := uint16(8095)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
// Initialize enet
slog.Info("Initializing E-Net Server", "port", port)
enet.Initialize()
// Create a host listening on 0.0.0.0:8095
host, err := enet.NewHost(enet.NewListenAddress(port), 32, 1, 0, 0)
if err != nil {
slog.Error("Couldn't create host", "error", err)
return
}
slog.Info("Server started", "port", port)
// The event loop
for true {
// Wait until the next event
ev := host.Service(1000)
// Do nothing if we didn't get any event
if ev.GetType() == enet.EventNone {
continue
}
switch ev.GetType() {
case enet.EventConnect: // A new peer has connected
slog.Info("New peer connected", "address", ev.GetPeer().GetAddress())
case enet.EventDisconnect: // A connected peer has disconnected
slog.Info("Peer disconnected", "address", ev.GetPeer().GetAddress())
case enet.EventReceive: // A peer sent us some data
// Get the packet
packet := ev.GetPacket()
// We must destroy the packet when we're done with it
defer packet.Destroy()
// Get the bytes in the packet
packetBytes := packet.GetData()
// Respond "pong" to "ping"
if string(packetBytes) == "ping" {
ev.GetPeer().SendString("pong", ev.GetChannelID(), enet.PacketFlagReliable)
continue
}
// Disconnect the peer if they say "bye"
if string(packetBytes) == "bye" {
slog.Info("Peer sent bye, disconnecting")
ev.GetPeer().Disconnect(0)
continue
}
}
}
// Destroy the host when we're done with it
host.Destroy()
// Uninitialize enet
enet.Deinitialize()
}

View file

@ -0,0 +1,151 @@
(ns game-server.main
(:require
[aleph.udp :as udp]
[manifold.stream :as s]
[clj-commons.byte-streams :as bs]
[taoensso.telemere :as t]
[clojure.pprint :refer [pprint]]
[clojure.string :as str])
(:import (java.security MessageDigest))
(:gen-class))
;; UDP examples
;; - https://gist.github.com/crosstyan/8a87ebdb0c23e549b1c75e9e4013ffa5
(def port 9999)
(def players (atom {})) ; client registry: {player-id {:host ... :port ...}}
(def games (atom {:by-id {} :all []}))
;; Global socket reference for sending
(def server-socket (atom nil))
;; (defn parse-position-msg
;; "Parse: 'player-id:x:y' format"
;; [msg-str]
;; (try
;; (let [[player-id x y] (str/split msg-str #":")]
;; {:player-id (Integer/parseInt player-id)
;; :x (Double/parseDouble x)
;; :y (Double/parseDouble y)})
;; (catch Exception e
;; (t/log! {:level :warn :data {:msg msg-str :error (str e)}} "Failed to parse message")
;; nil)))
;;
;; (if waiting-game
;; (swap! games (fn [state]
;; (let )
;; :by-id (assoc (:by-id state) player-id {:player-1 }
;; (-> (:by-id state)
;; (assoc player-id waiting-game))
;; :waiting (disj (:waiting state) waiting-game)
;; :all (conj (:all state) waiting-game)
;; })))
;; (defn get-game-by-player-id [player-id]
;; (first (filter (fn [game] (or (= (:player-1 game) player-id) (= (:player-2 game) player-id))) @games)))
;; (defn get-waiting-game []
;; (first (filter (fn [game] (nil? (:player-2 game))) @games)))
;; (defn join-waiting-game! [player-id other-player-id]
;; (if-let [waiting-game (get-game-by-player-id other-player-id)]
;; (swap! games update-in [(.indexOf @games waiting-game)] assoc :player-2 player-id)
;; (swap! games conj {:player-1 player-id :player-2 nil})))
(defn md5 [s]
(let [md (MessageDigest/getInstance "MD5")
digest (.digest md (.getBytes s))]
(apply str (map #(format "%02x" %) digest))))
(defn send-to-player
"Send a UDP packet to a player"
[player-id message]
(if-let [player (get @players player-id)]
(try
(t/log! {:level :info :data {:player-id player-id :message message}} "Sending to player")
(s/put! @server-socket
{:host (:host player)
:port (:port player)
:message message})
(catch Exception e
(t/log! {:level :warn :data {:player-id player-id :error (str e)}} "Failed to send to player")))
(t/log! {:level :warn :data {:player-id player-id}} "Player not found")))
(defn join-game! [player-id]
(let [existing-game (get-in @games [:by-id player-id])
waiting-game
(some-> (first (filter (fn [game] (nil? (:player-2 game))) (@games :all)))
(assoc :player-2 player-id))
other-player-id (:player-1 waiting-game)
new-game {:player-1 player-id :player-2 nil}]
(cond
existing-game
(if (nil? (:player-2 existing-game))
(send-to-player player-id "WAIT")
(send-to-player player-id "READY_TO_PLAY"))
waiting-game (do
(swap! games (fn [state]
{:by-id
(-> (:by-id state)
(assoc player-id waiting-game)
(assoc other-player-id waiting-game))
:all (map (fn [game] (if (= (:player-1 game) other-player-id)
waiting-game
game)) (:all state))}))
(send-to-player other-player-id "READY_TO_PLAY")
(send-to-player player-id "READY_TO_PLAY"))
:else (do
(swap! games (fn [state]
{:by-id (assoc (:by-id state) player-id new-game)
:all (conj (:all state) new-game)}))
(send-to-player player-id "WAIT")))))
(defn register-player! [host port]
(let [player-id (md5 (str host port))]
(swap! players assoc player-id {:host host :port port})
(t/log! {:level :info :data {:player-id player-id}} "Player registered")
(join-game! player-id)
(pprint {:games @games :players @players})))
;; (defn broadcast-to-others [from-player-id position-data]
;; "Send position to all other players"
;; (let [other-players (dissoc @players from-player-id)
;; msg (str from-player-id ":" (:x position-data) ":" (:y position-data))]
;; (doseq [[pid _] other-players]
;; (send-to-player pid msg))))
(defn parse-packet [packet]
(let [host (-> packet (:sender) (bean) (:address) (bean) (:hostAddress))
port (-> packet (:sender) (bean) (:port))
vector-msg (-> packet (:message) (vec))
string-msg (bs/to-string (:message packet))
msg-parts (str/split string-msg #"#")]
{:host host
:port port
:msg (:message packet)
:msg-vec vector-msg
:msg-string string-msg
:msg-parts msg-parts}))
(defn packet-consumer [packet]
(t/log! {:level :info :data {:packet packet}} "Consuming packet")
(case (first (:msg-parts packet))
"REGISTER" (register-player! (:host packet) (:port packet))))
(defn start-server [port]
(let [socket @(udp/socket {:port port})]
(reset! server-socket socket)
(t/log! {:level :info :data {:port port}} "UDP server listening")
(->> socket
(s/map parse-packet)
(s/consume packet-consumer))
socket))
(defn -main []
(t/log! {:level :info :data {:port port}} "Game server starting")
(start-server port)
@(promise)) ;; keep server running

77
backend/test-client.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/bash
# UDP test client with interactive key controls
# Connects to game server and displays responses
HOST="localhost"
SERVER_PORT=9999
LISTEN_PORT=12345
echo "=========================================="
echo " Game Test Client"
echo "=========================================="
echo ""
echo "Controls:"
echo " 1 = REGISTER (join game)"
echo " 2 = Send position update (100, 200)"
echo " 3 = Send position update (300, 150)"
echo " q = quit"
echo ""
echo "Listening for server responses on port $LISTEN_PORT..."
echo "=========================================="
echo ""
# Create named pipe for responses
RESPONSE_PIPE=$(mktemp -d)/response
mkfifo "$RESPONSE_PIPE"
# Start UDP listener in background, output to FIFO
nc -u -l 127.0.0.1 $LISTEN_PORT > "$RESPONSE_PIPE" 2>/dev/null &
LISTENER_PID=$!
# Read responses in background and display them
(while IFS= read -r line; do
echo "[SERVER] $line"
done < "$RESPONSE_PIPE") &
READER_PID=$!
# Cleanup on exit
cleanup() {
kill $LISTENER_PID $READER_PID 2>/dev/null
rm -rf "$(dirname "$RESPONSE_PIPE")"
echo ""
echo "Disconnected."
}
trap cleanup EXIT
# Interactive input loop
echo -n "> "
while IFS= read -rsn1 key; do
case "$key" in
1)
echo ""
echo "[CLIENT] Sending: REGISTER"
echo -n "REGISTER" | nc -u -w1 -p $LISTEN_PORT "$HOST" "$SERVER_PORT" 2>/dev/null
echo -n "> "
;;
2)
echo ""
echo "[CLIENT] Sending: POS#100#200"
echo -n "POS#100#200" | nc -u -w1 -p $LISTEN_PORT "$HOST" "$SERVER_PORT" 2>/dev/null
echo -n "> "
;;
3)
echo ""
echo "[CLIENT] Sending: POS#300#150"
echo -n "POS#300#150" | nc -u -w1 -p $LISTEN_PORT "$HOST" "$SERVER_PORT" 2>/dev/null
echo -n "> "
;;
q|Q)
echo ""
break
;;
*)
# Don't print anything for invalid keys, just show prompt again
;;
esac
done

61
backend/test-server.sh Executable file
View file

@ -0,0 +1,61 @@
#!/bin/bash
# Simple UDP test script for the game server
# Sends registration from two players on specific ports and receives responses
HOST="localhost"
SERVER_PORT=9999
echo "Testing game server at $HOST:$SERVER_PORT"
RESPONSE1=$(mktemp)
# RESPONSE2=$(mktemp)
echo "=== Listener on port 12345 ==="
nc -u -l 127.0.0.1 12345 > "$RESPONSE1" &
PID1=$!
sleep 3
trap "rm -f $RESPONSE1 $RESPONSE2; jobs -p | xargs kill -9 2>/dev/null" EXIT
# echo ""
# # Create temp files for responses
# RESPONSE1=$(mktemp)
# RESPONSE2=$(mktemp)
# # Cleanup on exit
# trap "rm -f $RESPONSE1 $RESPONSE2; jobs -p | xargs kill -9 2>/dev/null" EXIT
# echo "=== Starting Player 1 listener on port 12345 ==="
# nc -u -l 127.0.0.1 12345 > "$RESPONSE1" 2>/dev/null &
# PID1=$!
# sleep 0.3
# echo "=== Starting Player 2 listener on port 12346 ==="
# nc -u -l 127.0.0.1 12346 > "$RESPONSE2" 2>/dev/null &
# PID2=$!
# sleep 0.3
# echo ""
# echo "=== Sending REGISTER from Player 1 (port 12345) ==="
# echo -n "REGISTER" | nc -u -w1 -p 12345 "$HOST" "$SERVER_PORT"
# sleep 0.5
# echo "=== Sending REGISTER from Player 2 (port 12346) ==="
# echo -n "REGISTER" | nc -u -w1 -p 12346 "$HOST" "$SERVER_PORT"
# sleep 1
# echo ""
# echo "=== Stopping listeners ==="
# kill $PID1 $PID2 2>/dev/null
# sleep 0.2
# echo ""
# echo "=== Results ==="
# echo "Player 1 response:"
# cat "$RESPONSE1"
# echo ""
# echo "Player 2 response:"
# cat "$RESPONSE2"
# echo ""
echo "Done!"

Binary file not shown.

View file

@ -2,70 +2,103 @@
(ns gen-levels (ns gen-levels
(:require (:require
[babashka.fs :as fs] [babashka.fs :as fs]
[clojure.pprint :as pprint] [clojure.pprint :refer [pprint]]
[clojure.data.xml :as xml] [clojure.data.xml :as xml]
[clojure.string :as str] [clojure.string :as str]
[clojure.java.io :as io])) [clojure.java.io :as io])
(:import
'java.io.StringReader))
(def walls-tsx (xml/parse (io/reader "./tiled/walls.tsx"))) (defn load-tiled-xml-file [path]
(def wall-colliders (-> (slurp (io/reader path))
(str/replace #">\s+" ">")
(StringReader.)
(xml/parse)))
(defn parse-tile-set [tsx]
(->> (->>
(:content walls-tsx) (:content tsx)
(remove string?)
(filter #(= (:tag %) :tile)) (filter #(= (:tag %) :tile))
(reduce (fn [acc tile] (reduce (fn [acc tile]
(conj acc (conj acc
{(Integer/parseInt (get-in tile [:attrs :id])) {(Integer/parseInt (get-in tile [:attrs :id]))
(vec (vec
(->> (:content tile) (->> (:content tile)
(remove string?)
(map :content) (map :content)
(flatten) (flatten)
(remove string?)
(map :attrs) (map :attrs)
(map #(select-keys % [:x :y :height :width])) (map #(select-keys % [:x :y :height :width]))
(map #(update-vals % (fn [v] (Math/round (Double/parseDouble v)))))))})) (map #(update-vals % (fn [v] (Math/round (Double/parseDouble v)))))))}))
{}))) {})))
(pprint/pprint wall-colliders)
(def level-001-tmx (xml/parse (io/reader "./tiled/level_001.tmx"))) (def walls-tsx (load-tiled-xml-file "./tiled/walls.tsx"))
(def level-001 (def objects-tsx (load-tiled-xml-file "./tiled/objects.tsx"))
(let [tags (remove string? (:content level-001-tmx)) ;; (def colliders {:walls (parse-tile-set walls-tsx) :objects (parse-tile-set objects-tsx)})
tile-nums (-> (filter #(= (:tag %) :layer) tags) ;; (into {} (map (fn [[k v]]
first ;; [(transform-key k) v])
:content ;; your-map))
second (def colliders
:content "collider boxes by tile GID"
first (let [walls (into {} (map (fn [[k v]] [(+ k 1) v]) (parse-tile-set walls-tsx)))
str/split-lines 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"))
(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
(->> (->>
(remove empty?) (mapv #(str/split % #","))
(map #(str/split % #",")) (mapv (fn [row] (mapv parse-gid row)))))]
(map (fn [row] (map Integer/parseInt row))))) {:tiles
(vec (map-indexed
tiles (vec (map-indexed
(fn [y row] (fn [y row]
(vec (map-indexed (vec (map-indexed
(fn [x tile-id] {:x (* x 25) :y (* y 25) :tile-id tile-id}) (fn [x tile] (conj {:x (* x 25) :y (* y 25)} tile))
row))) tile-nums))] row))) tiles))
(hash-map :objects
:tiles tiles (let [objects (:content (first (filter #(= (get-in % [:attrs :name]) "objects") (:content map-tmx))))]
:wall-colliders wall-colliders (vec (map (fn [obj]
:spawns (vec (reduce (fn [acc tag] (let [attrs (dissoc (:attrs obj) :id)
(if (= (:tag tag) :object) tags (flatten (map :content (:content obj)))
(conj acc properties (map #(hash-map (keyword (get-in % [:attrs :name])) (get-in % [:attrs :value])) tags)]
(hash-map
:name (get-in tag [:attrs :name])
:x (Math/round (Double/parseDouble (get-in tag [:attrs :x])))
:y (Math/round (Double/parseDouble (get-in tag [:attrs :y])))))
acc)) '() (:content (first (filter #(= (get-in % [:attrs :name]) "spawns") 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)}
:colliders colliders})
(pprint (get-in levels [:levels :tutorial :objects]))
;; (pprint/pprint {:row (first (:tiles level-001))}) ;; (pprint/pprint {:row (first (:tiles level-001))})
;; (pprint/pprint (pr-str level-001)) ;; (pprint/pprint (pr-str level-001))
(fs/write-lines "../levels.fnl" ["(local levels" (fs/write-lines "../levels.fnl" ["(local levels"
(str/replace (pr-str {:level01 level-001}) #",+" "") (str/replace (pr-str levels) #",+" "")
")\n\n" ")\n\n"
"{ :levels levels }"]) "{ :levels levels }"])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 B

After

Width:  |  Height:  |  Size: 649 B

Before After
Before After

View file

@ -31,7 +31,7 @@
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,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,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,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,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,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,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,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,
@ -60,10 +60,10 @@
</data> </data>
</layer> </layer>
<objectgroup id="2" name="spawns"> <objectgroup id="2" name="spawns">
<object id="1" name="spawn1" x="569.372" y="697.644"> <object id="1" name="player_1" x="569.372" y="697.644">
<point/> <point/>
</object> </object>
<object id="3" name="spawn2" x="679.319" y="590.314"> <object id="3" name="player_2" x="679.319" y="590.314">
<point/> <point/>
</object> </object>
</objectgroup> </objectgroup>

View file

@ -17,4 +17,14 @@
<object id="1" x="1.12811" y="2.08266" width="22.8225" height="20.6531"/> <object id="1" x="1.12811" y="2.08266" width="22.8225" height="20.6531"/>
</objectgroup> </objectgroup>
</tile> </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> </tileset>

View file

@ -0,0 +1,82 @@
<?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>
<objectgroup id="4" name="Layer via Copy">
<object id="18" name="waiting_for_player2" type="info_pad" x="675" y="1025" width="25" height="25"/>
</objectgroup>
</map>

View file

@ -1,5 +1,5 @@
{ {
"activeFile": "level_001.tmx", "activeFile": "tutorial.tmx",
"expandedProjectPaths": [ "expandedProjectPaths": [
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game", "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game",
".", ".",
@ -7,11 +7,14 @@
], ],
"fileStates": { "fileStates": {
"level_001.tmx": { "level_001.tmx": {
"scale": 2.4998, "expandedObjectLayers": [
"selectedLayer": 0, 2
],
"scale": 0.5497,
"selectedLayer": 1,
"viewCenter": { "viewCenter": {
"x": 149.0119209536763, "x": 624.886301619065,
"y": 73.8059044723578 "y": 614.8808440967802
} }
}, },
"map_tileset.tsx": { "map_tileset.tsx": {
@ -21,11 +24,22 @@
}, },
"objects.tsx": { "objects.tsx": {
"scaleInDock": 1, "scaleInDock": 1,
"scaleInEditor": 6.3771 "scaleInEditor": 2.8063
},
"tutorial.tmx": {
"expandedObjectLayers": [
2
],
"scale": 1.1429,
"selectedLayer": 0,
"viewCenter": {
"x": 657.5378423309126,
"y": 883.7168606177268
}
}, },
"walls.tsx": { "walls.tsx": {
"scaleInDock": 1.4397, "scaleInDock": 1.4397,
"scaleInEditor": 8.2977 "scaleInEditor": 5.9643
} }
}, },
"last.exportedFilePath": "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets/tiled", "last.exportedFilePath": "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets/tiled",
@ -39,13 +53,15 @@
"openFiles": [ "openFiles": [
"level_001.tmx", "level_001.tmx",
"walls.tsx", "walls.tsx",
"objects.tsx" "objects.tsx",
"tutorial.tmx"
], ],
"project": "untitled.tiled-project", "project": "untitled.tiled-project",
"recentFiles": [ "recentFiles": [
"objects.tsx",
"walls.tsx",
"level_001.tmx", "level_001.tmx",
"walls.tsx",
"objects.tsx",
"tutorial.tmx",
"map_tileset.tsx" "map_tileset.tsx"
], ],
"tileset.lastUsedFormat": "tsx", "tileset.lastUsedFormat": "tsx",

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.12.1" name="walls" tilewidth="25" tileheight="25" tilecount="20" columns="20"> <tileset version="1.10" tiledversion="1.12.1" name="walls" tilewidth="25" tileheight="25" tilecount="20" columns="13">
<image source="../walls.png" width="500" height="25"/> <image source="../walls.png" width="325" height="25"/>
<tile id="0"> <tile id="0">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="0.141111" y="4.23334" width="24.9767" height="17.4978"/> <object id="1" x="0.141111" y="4.02527" width="24.9767" height="17.7059"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="1"> <tile id="1">
@ -14,57 +14,63 @@
</tile> </tile>
<tile id="2"> <tile id="2">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="-0.264752" y="4.10365" width="15.0247" height="17.8045"/> <object id="1" x="10.0692" y="4.10365" width="15.0247" height="17.8045"/>
<object id="2" x="10.0606" y="-0.0661879" width="4.83172" height="4.30221"/> <object id="2" x="10.13" y="20.5323" width="4.83172" height="4.30221"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="3"> <tile id="3">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="-0.264752" y="4.03746" width="15.157" height="17.8707"/> <object id="1" x="9.99984" y="3.9681" width="15.157" height="17.8707"/>
<object id="2" x="10.0606" y="20.1873" width="4.76553" height="4.96409"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="4"> <tile id="4">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="10.1267" y="4.16984" width="14.7599" height="17.6722"/> <object id="2" x="10.13" y="0.0242897" width="4.83172" height="21.8801"/>
<object id="2" x="10.0606" y="18.3341" width="4.83172" height="6.5526"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="5"> <tile id="5">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="9.99437" y="4.10365" width="15.0247" height="17.8707"/> <object id="1" x="9.99437" y="4.10365" width="4.89882" height="20.853"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="6"> <tile id="6">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="1.77636e-15" y="4.10365" width="14.8261" height="18.0031"/> <object id="1" x="10.1259" y="0.0116848" width="4.70022" height="24.9386"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="7"> <tile id="7">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="10.1267" y="0.0661879" width="4.89791" height="21.7758"/> <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> </objectgroup>
</tile> </tile>
<tile id="8"> <tile id="8">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="10.0606" y="4.10365" width="4.89791" height="20.8492"/> <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> </objectgroup>
</tile> </tile>
<tile id="9"> <tile id="9">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="9.99437" y="-0.0661879" width="4.96409" height="24.8867"/> <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> </objectgroup>
</tile> </tile>
<tile id="11"> <tile id="11">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="-0.132376" y="4.03746" width="25.019" height="17.7384"/> <object id="1" x="2.98861" y="4.03746" width="21.898" height="17.7384"/>
<object id="2" x="10.1929" y="13.9656" width="4.76553" height="11.1858"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="12"> <tile id="12">
<objectgroup draworder="index" id="2"> <objectgroup draworder="index" id="2">
<object id="1" x="0.0661879" y="4.23603" width="24.9528" height="17.3412"/> <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="10.1267"/> <object id="2" x="9.99437" y="0.0661879" width="4.89791" height="24.83"/>
</objectgroup> </objectgroup>
</tile> </tile>
<tile id="13"> <tile id="13">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 419 B

Before After
Before After

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
;; deps ;; deps
(local levels (. (require "levels.fnl") :levels)) (local levels (. (require "levels.fnl") :levels))
(local bump (require "bump")) (local bump (require "bump"))
(local network (require "network.fnl"))
;; colors ;; colors
(lambda color [full-r full-g full-b] (lambda color [full-r full-g full-b]
@ -28,6 +29,7 @@
(love.graphics.setColor (unpack (. color-pallet color)))) (love.graphics.setColor (unpack (. color-pallet color))))
;; global vars ;; global vars
(var level-key "tutorial")
(var player-art nil) ; 25x50 pixels each player is 25x25 (var player-art nil) ; 25x50 pixels each player is 25x25
(var dust-sprite nil) ; 35x35 pixels (var dust-sprite nil) ; 35x35 pixels
(var battery-bar-sprite nil) (var battery-bar-sprite nil)
@ -51,7 +53,7 @@
:scale scale :scale scale
:canvas nil})) :canvas nil}))
(local camera {:x 0 :y 0}) (local camera {:x 0 :y 0})
(local debug false) (local debug true)
(lambda debug-print [obj] (lambda debug-print [obj]
(print (fennel.view obj))) (print (fennel.view obj)))
@ -59,34 +61,46 @@
(lambda add-collider-debug-box [x y w h] (lambda add-collider-debug-box [x y w h]
(table.insert collider-debug-boxes {: x : y :width w :height h})) (table.insert collider-debug-boxes {: x : y :width w :height h}))
(local bump-world (bump.newWorld 25)) (var bump-world nil)
(local player { :x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0 }) (local player { :x 50 :y 50 :w 25 :h 25 :speed 80 :battery 100 :rot 0 })
(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 load-walls [] (fn load-walls []
(set walls.sprite (love.graphics.newImage "assets/walls.png"))
(set walls.batch (love.graphics.newSpriteBatch walls.sprite 2500)) (set walls.batch (love.graphics.newSpriteBatch walls.sprite 2500))
;; load quads ;; load quads
(let [(w h) (walls.sprite:getDimensions)] (let [(w h) (walls.sprite:getDimensions)]
(for [i 0 19 1] (for [i 0 19 1]
(table.insert walls.quads (love.graphics.newQuad (* i 25) 0 25 25 w h)))) (table.insert walls.quads (love.graphics.newQuad (* i 25) 0 25 25 w h))))
;; fill batch ;; fill batch
(each [_ row (pairs (. levels :level01 :tiles))] (each [_ row (pairs (. levels :levels level-key :tiles))]
(each [_ tile (pairs row)] (each [_ tile (pairs row)]
(let [ (let [
x tile.x x tile.x
y tile.y y tile.y
id tile.tile-id id tile.tile-id
colliders (or (. levels.level01.wall-colliders (- id 1)) [])] colliders tile.colliders]
(if (and (> id 0) (< id 21)) ;; 1-20 are wall tiles (if (and (> id 0) (< id 21)) ;; 1-20 are wall tiles
(do (do
; (print (fennel.view {:quad (. walls.quads id) : x : y : id})) ; (print (fennel.view {:quad (. walls.quads id) : x : y : id}))
(walls.batch:add (. walls.quads id) x y) (walls.batch:add (. walls.quads id) (if tile.h-flip (+ x 25) x) y 0 (if tile.h-flip -1 1) 1)
(each [_ collider (pairs colliders)] (each [_ collider (pairs colliders)]
(bump-world:add {: x : y :name :wall :behavior :block} (+ x collider.x) (+ y collider.y) collider.width collider.height) (let [
mirrored-collider (if tile.h-flip (mirror-collider collider) collider)
collider-x (+ x mirrored-collider.x)
collider-y (+ y mirrored-collider.y)]
(bump-world:add {: x : y :name :wall :behavior :block} collider-x collider-y mirrored-collider.width mirrored-collider.height)
(table.insert (table.insert
collider-debug-boxes collider-debug-boxes
{:x (+ x collider.x) :y (+ y collider.y) :width collider.width :height collider.height})) {:x collider-x :y collider-y :width mirrored-collider.width :height mirrored-collider.height})))))))))
))))))
(lambda create-charging-station [x y] (lambda create-charging-station [x y]
(let [ (let [
@ -122,33 +136,29 @@
(table.insert objects.list station))) (table.insert objects.list station)))
(fn load-objects [] (fn load-objects []
(set objects.sprite (love.graphics.newImage "assets/objects.png"))
(let [(w h) (objects.sprite:getDimensions)] (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 (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-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.charging-station (love.graphics.newQuad 25 0 25 25 w h))))
(each [_ row (pairs (. levels :level01 :tiles))] (fn load-player []
(each [_ tile (pairs row)] (set player.battery 100)
(let [ (set player.x (. levels :levels level-key :spawns :player-1 :x))
x tile.x (set player.y (. levels :levels level-key :spawns :player-1 :y))
y tile.y (bump-world:add player player.x player.y player.w player.h))
id tile.tile-id]
(when (> id 20) ;; 21+ are object tiles
(when (= id 22) (create-charging-station x y)))))))
(fn love.load []
(love.window.setMode screen.screen-w screen.screen-h)
(tset screen :canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h))
(bump-world:add player player.x player.y player.w player.h)
(lambda load-level [lvl-name]
(set bump-world (bump.newWorld 25))
(set level-key :tutorial)
;; set player 1 location
(load-player)
(load-walls) (load-walls)
(load-objects) (load-objects))
;; load world images (fn load-assets []
(set objects.sprite (love.graphics.newImage "assets/objects.png"))
(set walls.sprite (love.graphics.newImage "assets/walls.png"))
(set battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png")) (set battery-bar-sprite (love.graphics.newImage "assets/battery_bar.png"))
(set player-art (set player-art
(let [ (let [
player-sprite (love.graphics.newImage "assets/player.png") player-sprite (love.graphics.newImage "assets/player.png")
@ -169,9 +179,18 @@
} }
} }
)) ))
)
(fn love.load []
(love.window.setMode screen.screen-w screen.screen-h)
(tset screen :canvas (love.graphics.newCanvas screen.canvas-w screen.canvas-h))
(load-assets)
(load-level :tutorial)
;; Initialize network
(network.init)
(network.send-msg "REGISTER")
(set dust-sprite (love.graphics.newImage "assets/dust_001.png"))
; (print (fennel.view game-state))
;; start a thread listening on stdin ;; start a thread listening on stdin
(: (love.thread.newThread "require('love.event') (: (love.thread.newThread "require('love.event')
while 1 do love.event.push('stdin', io.read('*line')) end") :start)) while 1 do love.event.push('stdin', io.read('*line')) end") :start))
@ -182,7 +201,6 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start))
(print (if ok (fennel.view val) val)))) (print (if ok (fennel.view val) val))))
;; drawing ;; drawing
(fn draw-world [] (fn draw-world []
(reset-color) (reset-color)
(love.graphics.draw walls.batch) (love.graphics.draw walls.batch)
@ -228,7 +246,15 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start))
(let (let
[(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))] [(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))]
(each [_ item (pairs items)] (each [_ item (pairs items)]
(item:hover-cb dt)))) (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 draw-ui [] (fn draw-ui []
(love.graphics.push) (love.graphics.push)
@ -313,3 +339,8 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start))
; (love.graphics.print "Hello from Fennel!\nPress any key to quit" 10 10)) ; (love.graphics.print "Hello from Fennel!\nPress any key to quit" 10 10))
(fn love.keypressed [key] nil ) (fn love.keypressed [key] nil )
(fn love.quit []
"Clean up before game closes"
(network.close)
false)

View 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
}