/// import { ID, Item, PacketC, PacketS, Tile } from "./protocol.ts"; import { FALLBACK_TILE, TILES } from "./tiles.ts"; import { V2, add_v2, ceil_v2, floor_v2, length, lerp_exp_v2_mut, normalize } from "./util.ts"; let ctx: CanvasRenderingContext2D; let canvas: HTMLCanvasElement; let ws: WebSocket; document.addEventListener("DOMContentLoaded", () => { ws = new WebSocket(`${window.location.protocol.endsWith("s:") ? "wss" : "ws"}://${window.location.hostname}:27032/`) ws.onerror = console.error ws.onmessage = m => { packet(JSON.parse(m.data) as PacketC); } ws.onclose = () => console.log("close") ws.onopen = () => { console.log("open") send({ join: { name: "test" } }) } canvas = document.createElement("canvas"); document.body.append(canvas) ctx = canvas.getContext("2d")! resize() globalThis.addEventListener("resize", resize) draw() document.addEventListener("keydown", ev => keyboard(ev, true)) document.addEventListener("keyup", ev => keyboard(ev, false)) document.addEventListener("contextmenu", ev => ev.preventDefault()) setInterval(tick_update, 1000 / 25); }) interface PlayerData { x: number; y: number, name: string, rot: number, hand?: ID, facing: V2 } const players = new Map() interface ItemData { kind: Item, tile?: V2, player?: ID, tracking_player: boolean, x: number, y: number } const items = new Map() interface TileData { x: number; y: number, kind: Tile, items: ID[], active: boolean } const tiles = new Map() let my_id: number = -1 const camera: V2 = { x: 0, y: 0 } const interact_target_anim: V2 = { x: 0, y: 0 } let scale = 0 function send(p: PacketS) { ws.send(JSON.stringify(p)) } function packet(p: PacketC) { if (!("position" in p)) console.log(p); if ("joined" in p) { my_id = p.joined.id } else if ("add_player" in p) { if (p.add_player.hand) items.set(p.add_player.hand[0], { kind: p.add_player.hand[1], player: p.add_player.id, tracking_player: true, x: 0, y: 0 }) players.set(p.add_player.id, { x: 0, y: 0, name: p.add_player.name, rot: 0, hand: p.add_player.hand?.[0], facing: { x: 0, y: 1 } }) } else if ("remove_player" in p) { players.delete(p.remove_player.id) } else if ("position" in p) { if (p.position.player == my_id) return; // we know better where we are const pl = players.get(p.position.player)! pl.x = p.position.pos[0] pl.y = p.position.pos[1] pl.rot = p.position.rot } else if ("take_item" in p) { const item = items.get(p.take_item.item)! item.tracking_player = true item.player = p.take_item.player } else if ("put_item" in p) { const item = items.get(p.put_item.item)! item.tracking_player = false item.tile = { x: p.put_item.pos[0], y: p.put_item.pos[1] } } else if ("produce_item" in p) { items.set(p.produce_item.id, { kind: p.produce_item.kind, x: p.produce_item.pos[0] + 0.5, y: p.produce_item.pos[1] + 0.5, tracking_player: false, tile: { x: p.produce_item.pos[0], y: p.produce_item.pos[1] } }) tiles.get(p.produce_item.pos.toString())!.items.push(p.produce_item.id) } else if ("consume_item" in p) { // TODO } else if ("set_active" in p) { // TODO } else if ("update_map" in p) { tiles.set(p.update_map.pos.toString(), { x: p.update_map.pos[0], y: p.update_map.pos[1], kind: p.update_map.tile, active: false, items: [] }) } else console.warn("unknown packet", p); } const keys_down = new Set(); const HANDLED_KEYS = ["KeyW", "KeyA", "KeyS", "KeyD", "Space"] function keyboard(ev: KeyboardEvent, down: boolean) { if (HANDLED_KEYS.includes(ev.code)) ev.preventDefault() if (ev.code == "Space" && down) interact() if (down) keys_down.add(ev.code) else keys_down.delete(ev.code) } function get_interact_target(): V2 | undefined { const me = players.get(my_id) if (!me) return return { x: Math.floor(me.x + Math.sin(me.rot)), y: Math.floor(me.y + Math.cos(me.rot)) } } function interact() { const { x, y } = get_interact_target()!; send({ interact: { pos: [x, y] } }) } function tick_update() { const p = players.get(my_id) if (!p) return send({ position: { pos: [p.x, p.y], rot: p.rot } }) } function frame_update(dt: number) { const p = players.get(my_id) if (!p) return const input = normalize({ x: (+keys_down.has("KeyD") - +keys_down.has("KeyA")), y: (+keys_down.has("KeyS") - +keys_down.has("KeyW")) }) if (length(input) > 0.1) lerp_exp_v2_mut(p.facing, input, dt * 10.) p.rot = Math.atan2(p.facing.x, p.facing.y) p.x += input.x * dt * 5 p.y += input.y * dt * 5 for (const [_, i] of items) { lerp_exp_v2_mut(i, i.tracking_player ? players.get(i.player!)! : add_v2(i.tile!, 0.5), dt * 10.) } lerp_exp_v2_mut(interact_target_anim, get_interact_target() ?? { x: 0, y: 0 }, dt * 15.) lerp_exp_v2_mut(camera, p, dt * 10.) } function resize() { canvas.width = globalThis.innerWidth canvas.height = globalThis.innerHeight } let last_frame = Date.now() function draw() { const now = Date.now() frame_update((now - last_frame) / 1000) last_frame = now; if (ws.readyState == ws.CONNECTING) draw_wait("Connecting...") else if (ws.readyState == ws.CLOSING) draw_wait("Closing...") else if (ws.readyState == ws.CLOSED) draw_wait("Disconnected") else if (ws.readyState == ws.OPEN) draw_ingame() else throw new Error(`ws state invalid`); requestAnimationFrame(draw) } function draw_wait(text: string) { ctx.fillStyle = "#444" ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillStyle = "#555" ctx.font = "50px sans-serif" ctx.strokeStyle = "black" ctx.fillStyle = "white" ctx.lineWidth = 10 ctx.textAlign = "center" ctx.textBaseline = "middle" ctx.lineJoin = "round" ctx.lineCap = "round" ctx.strokeText(text, canvas.width / 2, canvas.height / 2) ctx.fillText(text, canvas.width / 2, canvas.height / 2) } function map_screen_to_world(screen: V2): V2 { return { x: ((screen.x - canvas.width / 2) / scale) + camera.x, y: ((screen.y - canvas.height / 2) / scale) + camera.y, } } function draw_ingame() { ctx.fillStyle = "#111" ctx.fillRect(0, 0, canvas.width, canvas.height) scale = Math.min(canvas.width, canvas.height) / 10; ctx.save() ctx.translate(canvas.width / 2, canvas.height / 2) ctx.scale(scale, scale) ctx.translate(-camera.x, -camera.y) draw_grid() for (const [_, tile] of tiles) { ctx.save() ctx.translate(tile.x, tile.y) const comps = TILES[tile.kind] ?? FALLBACK_TILE for (const c of comps) { c(ctx) } ctx.restore() } for (const [_, player] of players) { ctx.save() ctx.translate(player.x, player.y) ctx.rotate(-player.rot) ctx.fillStyle = "rgb(226, 176, 26)" const psize = 0.6; ctx.fillRect(-psize / 2, -psize / 2, psize, psize) ctx.restore() } for (const [_, item] of items) { ctx.save() ctx.translate(item.x, item.y) ctx.fillStyle = "rgb(252, 19, 19)" ctx.fillRect(-0.1, -0.1, 0.2, 0.2) ctx.restore() } draw_interact_target() ctx.restore() } function draw_interact_target() { ctx.save() ctx.translate(interact_target_anim.x, interact_target_anim.y) ctx.lineCap = "round" ctx.lineJoin = "round" ctx.lineWidth = 0.06 + 0.03 * Math.sin(Date.now() / 100) ctx.strokeStyle = "rgb(84, 122, 236)" ctx.strokeRect(0, 0, 1, 1) ctx.restore() } function draw_grid() { ctx.strokeStyle = "#333" ctx.lineWidth = 0.01 ctx.beginPath() const min = floor_v2(map_screen_to_world({ x: 0, y: 0 })) const max = ceil_v2(map_screen_to_world({ x: canvas.width, y: canvas.height })) for (let x = min.x; x < max.x; x++) { ctx.moveTo(x, min.y) ctx.lineTo(x, max.y) } for (let y = min.y; y < max.y; y++) { ctx.moveTo(min.x, y) ctx.lineTo(max.x, y) } ctx.stroke() }