Compare commits
6 commits
99f73315b3
...
ffe9d8226d
| Author | SHA1 | Date | |
|---|---|---|---|
| ffe9d8226d | |||
| e00250fd84 | |||
| a457893d14 | |||
| bbbe7bfe8a | |||
| 32ea10495e | |||
| 09bf48481a |
26 changed files with 740 additions and 264 deletions
1
backend/.cpcache/82615333.basis
Normal file
1
backend/.cpcache/82615333.basis
Normal file
File diff suppressed because one or more lines are too long
1
backend/.cpcache/82615333.cp
Normal file
1
backend/.cpcache/82615333.cp
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
23
backend/deps.edn
Normal 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
10
backend/dev.sh
Executable 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
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: love2d-backend-dev
|
||||
ports:
|
||||
- "8095:8095"
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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()
|
||||
}
|
||||
151
backend/src/game_server/main.clj
Normal file
151
backend/src/game_server/main.clj
Normal 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
77
backend/test-client.sh
Executable 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
61
backend/test-server.sh
Executable 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!"
|
||||
BIN
two_player_cleaning_game/assets/doors.aseprite
Normal file
BIN
two_player_cleaning_game/assets/doors.aseprite
Normal file
Binary file not shown.
|
|
@ -2,70 +2,103 @@
|
|||
(ns gen-levels
|
||||
(:require
|
||||
[babashka.fs :as fs]
|
||||
[clojure.pprint :as pprint]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.data.xml :as xml]
|
||||
[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")))
|
||||
(def wall-colliders
|
||||
(defn load-tiled-xml-file [path]
|
||||
(-> (slurp (io/reader path))
|
||||
(str/replace #">\s+" ">")
|
||||
(StringReader.)
|
||||
(xml/parse)))
|
||||
|
||||
(defn parse-tile-set [tsx]
|
||||
(->>
|
||||
(:content walls-tsx)
|
||||
(remove string?)
|
||||
(:content tsx)
|
||||
(filter #(= (:tag %) :tile))
|
||||
(reduce (fn [acc tile]
|
||||
(conj acc
|
||||
{(Integer/parseInt (get-in tile [:attrs :id]))
|
||||
(vec
|
||||
(->> (:content tile)
|
||||
(remove string?)
|
||||
(map :content)
|
||||
(flatten)
|
||||
(remove string?)
|
||||
(map :attrs)
|
||||
(map #(select-keys % [:x :y :height :width]))
|
||||
(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 level-001
|
||||
(let [tags (remove string? (:content level-001-tmx))
|
||||
tile-nums (-> (filter #(= (:tag %) :layer) tags)
|
||||
first
|
||||
:content
|
||||
second
|
||||
:content
|
||||
first
|
||||
str/split-lines
|
||||
(->>
|
||||
(remove empty?)
|
||||
(map #(str/split % #","))
|
||||
(map (fn [row] (map Integer/parseInt row)))))
|
||||
(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)))
|
||||
|
||||
tiles (vec (map-indexed
|
||||
(fn [y row]
|
||||
(vec (map-indexed
|
||||
(fn [x tile-id] {:x (* x 25) :y (* y 25) :tile-id tile-id})
|
||||
row))) tile-nums))]
|
||||
(def tutorial-map-tmx (load-tiled-xml-file "./tiled/tutorial.tmx"))
|
||||
(def level-001-map-tmx (load-tiled-xml-file "./tiled/level_001.tmx"))
|
||||
|
||||
(hash-map
|
||||
:tiles tiles
|
||||
:wall-colliders wall-colliders
|
||||
:spawns (vec (reduce (fn [acc tag]
|
||||
(if (= (:tag tag) :object)
|
||||
(conj acc
|
||||
(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))))))))
|
||||
(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 (dissoc (:attrs obj) :id)
|
||||
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)}
|
||||
: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 {:level01 level-001}) #",+" "")
|
||||
(str/replace (pr-str levels) #",+" "")
|
||||
")\n\n"
|
||||
"{ :levels levels }"])
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 549 B After Width: | Height: | Size: 649 B |
|
|
@ -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,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,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,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,
|
||||
|
|
@ -60,10 +60,10 @@
|
|||
</data>
|
||||
</layer>
|
||||
<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/>
|
||||
</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/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
|
|
|
|||
|
|
@ -17,4 +17,14 @@
|
|||
<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>
|
||||
|
|
|
|||
82
two_player_cleaning_game/assets/tiled/tutorial.tmx
Normal file
82
two_player_cleaning_game/assets/tiled/tutorial.tmx
Normal 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>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"activeFile": "level_001.tmx",
|
||||
"activeFile": "tutorial.tmx",
|
||||
"expandedProjectPaths": [
|
||||
"/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game",
|
||||
".",
|
||||
|
|
@ -7,11 +7,14 @@
|
|||
],
|
||||
"fileStates": {
|
||||
"level_001.tmx": {
|
||||
"scale": 2.4998,
|
||||
"selectedLayer": 0,
|
||||
"expandedObjectLayers": [
|
||||
2
|
||||
],
|
||||
"scale": 0.5497,
|
||||
"selectedLayer": 1,
|
||||
"viewCenter": {
|
||||
"x": 149.0119209536763,
|
||||
"y": 73.8059044723578
|
||||
"x": 624.886301619065,
|
||||
"y": 614.8808440967802
|
||||
}
|
||||
},
|
||||
"map_tileset.tsx": {
|
||||
|
|
@ -21,11 +24,22 @@
|
|||
},
|
||||
"objects.tsx": {
|
||||
"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": {
|
||||
"scaleInDock": 1.4397,
|
||||
"scaleInEditor": 8.2977
|
||||
"scaleInEditor": 5.9643
|
||||
}
|
||||
},
|
||||
"last.exportedFilePath": "/Users/she0001t/personal_projects/fennel_love2d_experiments/two_player_cleaning_game/assets/tiled",
|
||||
|
|
@ -39,13 +53,15 @@
|
|||
"openFiles": [
|
||||
"level_001.tmx",
|
||||
"walls.tsx",
|
||||
"objects.tsx"
|
||||
"objects.tsx",
|
||||
"tutorial.tmx"
|
||||
],
|
||||
"project": "untitled.tiled-project",
|
||||
"recentFiles": [
|
||||
"objects.tsx",
|
||||
"walls.tsx",
|
||||
"level_001.tmx",
|
||||
"walls.tsx",
|
||||
"objects.tsx",
|
||||
"tutorial.tmx",
|
||||
"map_tileset.tsx"
|
||||
],
|
||||
"tileset.lastUsedFormat": "tsx",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<?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">
|
||||
<image source="../walls.png" width="500" height="25"/>
|
||||
<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.23334" width="24.9767" height="17.4978"/>
|
||||
<object id="1" x="0.141111" y="4.02527" width="24.9767" height="17.7059"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
<tile id="1">
|
||||
|
|
@ -14,57 +14,63 @@
|
|||
</tile>
|
||||
<tile id="2">
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="-0.264752" 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="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="-0.264752" y="4.03746" width="15.157" height="17.8707"/>
|
||||
<object id="2" x="10.0606" y="20.1873" width="4.76553" height="4.96409"/>
|
||||
<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="1" x="10.1267" y="4.16984" width="14.7599" height="17.6722"/>
|
||||
<object id="2" x="10.0606" y="18.3341" width="4.83172" height="6.5526"/>
|
||||
<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="15.0247" height="17.8707"/>
|
||||
<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="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>
|
||||
</tile>
|
||||
<tile id="7">
|
||||
<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>
|
||||
</tile>
|
||||
<tile id="8">
|
||||
<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>
|
||||
</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="-0.132376" y="4.03746" width="25.019" height="17.7384"/>
|
||||
<object id="2" x="10.1929" y="13.9656" width="4.76553" height="11.1858"/>
|
||||
<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="0.0661879" y="4.23603" width="24.9528" height="17.3412"/>
|
||||
<object id="2" x="9.99437" y="0.0661879" width="4.89791" height="10.1267"/>
|
||||
<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">
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 559 B After Width: | Height: | Size: 419 B |
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,7 @@
|
|||
;; deps
|
||||
(local levels (. (require "levels.fnl") :levels))
|
||||
(local bump (require "bump"))
|
||||
(local network (require "network.fnl"))
|
||||
|
||||
;; colors
|
||||
(lambda color [full-r full-g full-b]
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
(love.graphics.setColor (unpack (. color-pallet color))))
|
||||
|
||||
;; global vars
|
||||
(var level-key "tutorial")
|
||||
(var player-art nil) ; 25x50 pixels each player is 25x25
|
||||
(var dust-sprite nil) ; 35x35 pixels
|
||||
(var battery-bar-sprite nil)
|
||||
|
|
@ -51,7 +53,7 @@
|
|||
:scale scale
|
||||
:canvas nil}))
|
||||
(local camera {:x 0 :y 0})
|
||||
(local debug false)
|
||||
(local debug true)
|
||||
(lambda debug-print [obj]
|
||||
(print (fennel.view obj)))
|
||||
|
||||
|
|
@ -59,34 +61,46 @@
|
|||
(lambda add-collider-debug-box [x y w 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 })
|
||||
|
||||
(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 []
|
||||
(set walls.sprite (love.graphics.newImage "assets/walls.png"))
|
||||
(set walls.batch (love.graphics.newSpriteBatch walls.sprite 2500))
|
||||
;; load quads
|
||||
(let [(w h) (walls.sprite:getDimensions)]
|
||||
(for [i 0 19 1]
|
||||
(table.insert walls.quads (love.graphics.newQuad (* i 25) 0 25 25 w h))))
|
||||
;; fill batch
|
||||
(each [_ row (pairs (. levels :level01 :tiles))]
|
||||
(each [_ row (pairs (. levels :levels level-key :tiles))]
|
||||
(each [_ tile (pairs row)]
|
||||
(let [
|
||||
x tile.x
|
||||
y tile.y
|
||||
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
|
||||
(do
|
||||
; (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)]
|
||||
(bump-world:add {: x : y :name :wall :behavior :block} (+ x collider.x) (+ y collider.y) collider.width collider.height)
|
||||
(table.insert
|
||||
collider-debug-boxes
|
||||
{:x (+ x collider.x) :y (+ y collider.y) :width collider.width :height 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
|
||||
collider-debug-boxes
|
||||
{:x collider-x :y collider-y :width mirrored-collider.width :height mirrored-collider.height})))))))))
|
||||
|
||||
(lambda create-charging-station [x y]
|
||||
(let [
|
||||
|
|
@ -122,33 +136,29 @@
|
|||
(table.insert objects.list station)))
|
||||
|
||||
(fn load-objects []
|
||||
(set objects.sprite (love.graphics.newImage "assets/objects.png"))
|
||||
(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.charging-station (love.graphics.newQuad 25 0 25 25 w h))))
|
||||
|
||||
(each [_ row (pairs (. levels :level01 :tiles))]
|
||||
(each [_ tile (pairs row)]
|
||||
(let [
|
||||
x tile.x
|
||||
y tile.y
|
||||
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)
|
||||
(fn load-player []
|
||||
(set player.battery 100)
|
||||
(set player.x (. levels :levels level-key :spawns :player-1 :x))
|
||||
(set player.y (. levels :levels level-key :spawns :player-1 :y))
|
||||
(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-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 player-art
|
||||
(let [
|
||||
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
|
||||
(: (love.thread.newThread "require('love.event')
|
||||
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))))
|
||||
|
||||
;; drawing
|
||||
|
||||
(fn draw-world []
|
||||
(reset-color)
|
||||
(love.graphics.draw walls.batch)
|
||||
|
|
@ -228,7 +246,15 @@ while 1 do love.event.push('stdin', io.read('*line')) end") :start))
|
|||
(let
|
||||
[(items len) (bump-world:queryRect player.x player.y 25 25 #(= $1.behavior "hover"))]
|
||||
(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 []
|
||||
(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))
|
||||
|
||||
(fn love.keypressed [key] nil )
|
||||
|
||||
(fn love.quit []
|
||||
"Clean up before game closes"
|
||||
(network.close)
|
||||
false)
|
||||
|
|
|
|||
133
two_player_cleaning_game/network.fnl
Normal file
133
two_player_cleaning_game/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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue