This commit is contained in:
2026-02-24 21:02:37 +01:00
parent 1a43e5192b
commit 0aca1d58e6
4 changed files with 188 additions and 92 deletions

3
.prettierrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"semi": false
}

View File

@@ -1,33 +1,33 @@
import { crew } from "@kaplayjs/crew"; import { crew } from "@kaplayjs/crew"
import kaplay from "kaplay"; import kaplay from "kaplay"
import { createCards } from "./card"; import { createCards } from "./card"
import { WOBBLE_ANGLE, SPEED } from "./constants"; import { WOBBLE_ANGLE, SPEED } from "./constants"
import "kaplay/global"; // uncomment if you want to use without the prefix import "kaplay/global" // uncomment if you want to use without the prefix
import { initWebRTC } from "./multiplayer"; import { initWebRTC } from "./multiplayer"
export const k = kaplay({ export const k = kaplay({
plugins: [crew], plugins: [crew],
background: "#4e495f", background: "#4e495f",
scale: 2, scale: 2,
debugKey: "r", debugKey: "r",
}); })
loadRoot("./"); // A good idea for Itch.io publishing later loadRoot("./") // A good idea for Itch.io publishing later
loadSprite("card", "sprites/card.png"); loadSprite("card", "sprites/card.png")
loadSprite("octo", "sprites/octo.png"); loadSprite("octo", "sprites/octo.png")
loadSprite("dead", "sprites/dead.png"); loadSprite("dead", "sprites/dead.png")
loadSprite("triboi", "sprites/triboi.png"); loadSprite("triboi", "sprites/triboi.png")
loadSprite("wolfi", "sprites/wolfi.png"); loadSprite("wolfi", "sprites/wolfi.png")
loadSprite("bubble", "sprites/bubble.png"); loadSprite("bubble", "sprites/bubble.png")
k.loadCrew("sprite", "cursor"); k.loadCrew("sprite", "cursor")
k.loadCrew("sprite", "pointer"); k.loadCrew("sprite", "pointer")
k.loadCrew("sprite", "kat"); k.loadCrew("sprite", "kat")
k.loadCrew("sprite", "pineapple"); k.loadCrew("sprite", "pineapple")
k.loadCrew("sprite", "been"); k.loadCrew("sprite", "been")
k.loadCrew("font", "happy"); k.loadCrew("font", "happy")
setLayers(["game", "hover", "ui"], "game"); setLayers(["game", "hover", "ui"], "game")
const createPlayer = (scravatar: string) => { const createPlayer = (scravatar: string) => {
const player = add([ const player = add([
@@ -41,48 +41,48 @@ const createPlayer = (scravatar: string) => {
animate(), animate(),
named("player"), named("player"),
"player", "player",
]); ])
player.animate("angle", [WOBBLE_ANGLE, -WOBBLE_ANGLE], { player.animate("angle", [WOBBLE_ANGLE, -WOBBLE_ANGLE], {
easing: easings.easeInOutCubic, easing: easings.easeInOutCubic,
direction: "ping-pong", direction: "ping-pong",
duration: 1, duration: 1,
}); })
const bubble = player.add([sprite("bubble"), anchor("center"), pos(30, -50)]); const bubble = player.add([sprite("bubble"), anchor("center"), pos(30, -50)])
const bubbleText = bubble.add([ const bubbleText = bubble.add([
text("9", { font: "happy", size: 24 }), text("9", { font: "happy", size: 24 }),
pos(-5, -24), pos(-5, -24),
color(Color.BLACK), color(Color.BLACK),
timer(), timer(),
]); ])
bubble.hidden = true; bubble.hidden = true
player.on("countdown", () => { player.on("countdown", () => {
bubble.hidden = false; bubble.hidden = false
let time = 9; let time = 9
bubbleText.loop( bubbleText.loop(
1, 1,
() => { () => {
if (time === -1) { if (time === -1) {
bubble.hidden = true; bubble.hidden = true
} }
bubbleText.text = time.toString(); bubbleText.text = time.toString()
time--; time--
}, },
11, 11,
); )
}); })
return player; return player
}; }
scene("menu", () => { scene("menu", () => {
add([text("Choose your Scravatar", { font: "happy" })]); add([text("Choose your Scravatar", { font: "happy" })])
["wolfi", "octo", "dead", "triboi"].forEach((image, i) => { ;["wolfi", "octo", "dead", "triboi"].forEach((image, i) => {
const selection = add([ const selection = add([
pos(88 * i + 100, 100), pos(88 * i + 100, 100),
anchor("center"), anchor("center"),
@@ -90,74 +90,74 @@ scene("menu", () => {
opacity(0.1), opacity(0.1),
z(-1), z(-1),
area(), area(),
]); ])
selection.add([ selection.add([
text(image.toUpperCase(), { font: "happy", size: 14 }), text(image.toUpperCase(), { font: "happy", size: 14 }),
anchor("center"), anchor("center"),
pos(0, 40), pos(0, 40),
]); ])
const avatar = selection.add([ const avatar = selection.add([
sprite(image), sprite(image),
rotate(), rotate(),
animate(), animate(),
anchor("center"), anchor("center"),
]); ])
selection.onHover(() => { selection.onHover(() => {
selection.opacity = 1; selection.opacity = 1
setCursor("pointer"); setCursor("pointer")
}); })
selection.onHoverEnd(() => { selection.onHoverEnd(() => {
selection.opacity = 0.1; selection.opacity = 0.1
setCursor("default"); setCursor("default")
}); })
selection.onClick(() => { selection.onClick(() => {
pushScene("game", image); pushScene("game", image)
}, "left"); }, "left")
avatar.animate("angle", [WOBBLE_ANGLE, -WOBBLE_ANGLE], { avatar.animate("angle", [WOBBLE_ANGLE, -WOBBLE_ANGLE], {
easing: easings.easeInOutCubic, easing: easings.easeInOutCubic,
direction: "ping-pong", direction: "ping-pong",
duration: 1, duration: 1,
}); })
}); })
const peerIDToConnect = location.pathname.replace("/", ""); const peerIDToConnect = location.pathname.replace("/", "")
initWebRTC(peerIDToConnect); initWebRTC((gameState) => {}, peerIDToConnect)
}); })
scene("game", (scravatar: string) => { scene("game", (scravatar: string) => {
createCards(); createCards()
const player = createPlayer(scravatar); const player = createPlayer(scravatar)
onUpdate(() => { onUpdate(() => {
setCamPos(lerp(getCamPos(), player.pos, 0.1)); setCamPos(lerp(getCamPos(), player.pos, 0.1))
}); })
onKeyDown(["w", "up"], () => { onKeyDown(["w", "up"], () => {
player.moveBy(0, -SPEED); player.moveBy(0, -SPEED)
}); })
onKeyDown(["a", "left"], () => { onKeyDown(["a", "left"], () => {
player.moveBy(-SPEED, 0); player.moveBy(-SPEED, 0)
}); })
onKeyDown(["s", "down"], () => { onKeyDown(["s", "down"], () => {
player.moveBy(0, SPEED); player.moveBy(0, SPEED)
}); })
onKeyDown(["d", "right"], () => { onKeyDown(["d", "right"], () => {
player.moveBy(SPEED, 0); player.moveBy(SPEED, 0)
}); })
onKeyDown("u", () => { onKeyDown("u", () => {
player.trigger("countdown"); player.trigger("countdown")
}); })
}); })
pushScene("menu"); pushScene("menu")
// pushScene("game", "wolfi"); // pushScene("game", "wolfi");

View File

@@ -1,4 +1,6 @@
import { Peer } from "peerjs"; import { DataConnection, DataConnectionErrorType, Peer } from "peerjs"
import { GameState, Message } from "./types"
const configuration: RTCConfiguration = { const configuration: RTCConfiguration = {
iceServers: [ iceServers: [
{ {
@@ -25,43 +27,105 @@ const configuration: RTCConfiguration = {
credential: "N3Bhe64kpGzogdjY", credential: "N3Bhe64kpGzogdjY",
}, },
], ],
}; }
type Message = { const peers: Record<string, { conn: DataConnection; lastHeartBeat: string }>[] =
type: "HeartBeat" | "GameState" | "UpdatePosition"; []
};
export async function initWebRTC(peerId?: string) { const gameState: GameState = {
players: [],
}
export async function initWebRTC(
render: (gameState: GameState) => void,
peerId?: string,
) {
const peer = new Peer({ const peer = new Peer({
config: configuration, config: configuration,
}); })
// Host Part
peer.on("connection", (conn) => { peer.on("connection", (conn) => {
console.log(`Connection received from: ${conn.peer}`); console.log(`Connection received from: ${conn.peer}`)
peers[conn.peer] = {
lastHeartBeat: new Date().toISOString(),
conn,
}
conn.on("open", () => { conn.on("open", () => {
conn.on("data", (data: Message) => { conn.on("data", (data: Message) => {
console.log(`Data: ${data}`); switch (data.type) {
}); case "HeartBeat":
peers[conn.peer].lastHeartBeat = new Date().toISOString()
conn.send("Hello from HOST?!"); console.log("Updating peers", peers)
}); break
}); case "GameState":
console.error("Clients shouldn't send GameState messages")
break
case "UpdatePosition":
gameState.players.map((p) => {
if (p.peerId == conn.peer) {
return { ...p, position: data.pos }
}
return p
})
break
case "Register":
console.log()
gameState.players.push(data.player)
break
}
})
})
})
peer.on("open", (id) => { peer.on("open", (id) => {
console.log(`My peer ID is: ${id}`);
if (peerId) { if (peerId) {
console.log(`Connecting to: ${peerId}`); // Client Part
const conn = peer.connect(peerId); console.log(`Connecting to: ${peerId}`)
const conn = peer.connect(peerId)
conn.on("open", () => { conn.on("open", () => {
conn.on("data", (data: Message) => { conn.on("data", (data: Message) => {
console.log(`Data: ${data}`); if (data.type === "GameState") {
}); render(data.state)
}
})
conn.send("Hello from CLIENT!"); const registerMsg: Message = {
}); type: "Register",
player: {
peerId: id,
name: "peter",
position: vec2(0),
scravatar: "wolfi",
},
} }
}); conn.send(registerMsg)
setInterval(() => {
console.log("Sending HeartBeat")
const msg: Message = {
type: "HeartBeat",
}
conn.send(msg)
}, 500)
})
} else {
console.log(`${window.location}${id}`)
// Host Part
setInterval(() => {
const msg: Message = {
type: "GameState",
state: gameState,
}
peers.forEach((r) => {
r.value.conn.send(msg)
})
}, 60)
}
})
} }

29
src/types.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Vec2 } from "kaplay"
export type Player = {
peerId: string
name: string
scravatar: string
position: Vec2
}
export type GameState = {
players: Player[]
}
export type Message =
| {
type: "HeartBeat"
}
| {
type: "GameState"
state: GameState
}
| {
type: "UpdatePosition"
pos: Vec2
}
| {
type: "Register"
player: Player
}