diff options
-rw-r--r-- | data/demands.yaml | 1 | ||||
-rw-r--r-- | data/map.yaml | 4 | ||||
-rw-r--r-- | data/recipes.yaml | 2 | ||||
-rw-r--r-- | server/src/customer.rs | 8 | ||||
-rw-r--r-- | server/src/game.rs | 27 | ||||
-rw-r--r-- | server/src/protocol.rs | 14 | ||||
-rw-r--r-- | test-client/main.ts | 187 | ||||
-rw-r--r-- | test-client/protocol.ts | 7 | ||||
-rw-r--r-- | test-client/tiles.ts | 37 | ||||
-rw-r--r-- | test-client/visual.ts | 172 |
10 files changed, 287 insertions, 172 deletions
diff --git a/data/demands.yaml b/data/demands.yaml new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/data/demands.yaml @@ -0,0 +1 @@ + diff --git a/data/map.yaml b/data/map.yaml index 4e9c2a11..aa065a47 100644 --- a/data/map.yaml +++ b/data/map.yaml @@ -3,7 +3,7 @@ map: - "|ctc.ctc.ctc.ctc.ctc.|" - "|.....c..............|" - "|c...c...+--www---dd-+" - - "|tc.ctc..|##...##W..D|" + - "|tc.ctc..|##...CC#..D|" - "|c...c...w........~.S|" - "|c.......w..######..T|" - "|tc......w..........F|" @@ -28,7 +28,7 @@ tiles: "s": sink "o": oven "p": pan - "W": watercooler + "C": cuttingboard "S": raw-steak-crate "T": tomato-crate "F": flour-crate diff --git a/data/recipes.yaml b/data/recipes.yaml index d462acbe..46210a7e 100644 --- a/data/recipes.yaml +++ b/data/recipes.yaml @@ -21,7 +21,7 @@ - tile: tomato-crate # Tomato pipeline outputs: [tomato] action: !instant -- tile: counter +- tile: cuttingboard inputs: [tomato] outputs: [sliced-tomato] action: !active 3 diff --git a/server/src/customer.rs b/server/src/customer.rs index b003c935..87b67f0e 100644 --- a/server/src/customer.rs +++ b/server/src/customer.rs @@ -1,7 +1,7 @@ use crate::{ data::Gamedata, game::Game, - protocol::{PacketC, PacketS, PlayerID}, + protocol::{Message, PacketC, PacketS, PlayerID}, }; use glam::{IVec2, Vec2}; use log::{debug, error}; @@ -118,6 +118,12 @@ impl DemandState { packets_out .push((p.id, move_player(p, &self.walkable, next - p.position, dt))); } else { + packets_out.push(( + p.id, + PacketS::Communicate { + message: Some(Message::Item(4)), + }, + )); p.state = CustomerState::Waiting { chair: *chair }; } } diff --git a/server/src/game.rs b/server/src/game.rs index 228d7048..225455bf 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -1,7 +1,7 @@ use crate::{ data::Gamedata, interaction::{interact, tick_tile, InteractEffect, TickEffect}, - protocol::{ItemIndex, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, + protocol::{ItemIndex, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Result}; use glam::{IVec2, Vec2}; @@ -36,6 +36,7 @@ pub struct Player { pub position: Vec2, pub interacting: Option<IVec2>, pub item: Option<Item>, + pub communicate: Option<Message>, } pub struct Game { @@ -80,12 +81,18 @@ impl Game { character: player.character, name: player.name.clone(), item: player.item.as_ref().map(|i| i.kind), - }) + }); + if let Some(c) = &player.communicate { + out.push(PacketC::Communicate { + player: id, + message: Some(c.to_owned()), + }) + } } for (&tile, tdata) in &self.tiles { out.push(PacketC::UpdateMap { pos: tile, - neighbours: [ + neighbors: [ self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), @@ -116,6 +123,7 @@ impl Game { } else { self.data.chef_spawn }, + communicate: None, interacting: None, name: name.clone(), }, @@ -151,6 +159,11 @@ impl Game { PacketS::Position { pos, rot } => { self.packet_out .push_back(PacketC::Position { player, pos, rot }); + let player = self + .players + .get_mut(&player) + .ok_or(anyhow!("player does not exist"))?; + player.position = pos; } PacketS::Collide { player, force } => { self.packet_out @@ -224,6 +237,14 @@ impl Game { player.interacting = if edge { Some(pos) } else { None }; } + PacketS::Communicate { message } => { + info!("{player} message {message:?}"); + if let Some(player) = self.players.get_mut(&player) { + player.communicate = message.clone() + } + self.packet_out + .push_back(PacketC::Communicate { player, message }) + } } Ok(()) } diff --git a/server/src/protocol.rs b/server/src/protocol.rs index 18b5f6fa..7eff2ba1 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -15,6 +15,14 @@ pub enum PacketS { Position { pos: Vec2, rot: f32 }, Interact { pos: IVec2, edge: bool }, Collide { player: PlayerID, force: Vec2 }, + Communicate { message: Option<Message> }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Message { + Text(String), + Item(ItemIndex), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -61,10 +69,14 @@ pub enum PacketC { UpdateMap { pos: IVec2, tile: TileIndex, - neighbours: [Option<TileIndex>; 4], + neighbors: [Option<TileIndex>; 4], }, Collide { player: PlayerID, force: Vec2, }, + Communicate { + player: PlayerID, + message: Option<Message>, + }, } diff --git a/test-client/main.ts b/test-client/main.ts index 807567c8..b34220df 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -1,14 +1,13 @@ /// <reference lib="dom" /> -import { Gamedata, ItemIndex, PacketC, PacketS, PlayerID, TileIndex } from "./protocol.ts"; -import { FALLBACK_ITEM } from "./tiles.ts"; -import { FALLBACK_TILE, ITEMS, TILES } from "./tiles.ts"; -import { V2, add_v2, ceil_v2, floor_v2, length, lerp_exp_v2_mut, normalize, aabb_circle_distance } from "./util.ts"; +import { Gamedata, ItemIndex, Message, PacketC, PacketS, PlayerID, TileIndex } from "./protocol.ts"; +import { V2, add_v2, length, lerp_exp_v2_mut, normalize, aabb_circle_distance } from "./util.ts"; +import { draw_ingame, draw_wait } from "./visual.ts"; -const PLAYER_SIZE = 0.4; +export const PLAYER_SIZE = 0.4; -let ctx: CanvasRenderingContext2D; -let canvas: HTMLCanvasElement; +export let ctx: CanvasRenderingContext2D; +export let canvas: HTMLCanvasElement; let ws: WebSocket; document.addEventListener("DOMContentLoaded", () => { const ws_uri = window.location.protocol.endsWith("s:") @@ -38,7 +37,7 @@ document.addEventListener("DOMContentLoaded", () => { setInterval(tick_update, 1000 / 25); }) -interface ItemData { +export interface ItemData { kind: ItemIndex, x: number, y: number, @@ -46,7 +45,7 @@ interface ItemData { progress?: number remove_anim?: number } -interface PlayerData { +export interface PlayerData { x: number, y: number, name: string, @@ -54,30 +53,31 @@ interface PlayerData { item?: ItemData, facing: V2, character: number, - vel: { x: number, y: number } + vel: { x: number, y: number }, + message?: Message, } -interface TileData { +export interface TileData { x: number y: number kind: TileIndex item?: ItemData } -const players = new Map<PlayerID, PlayerData>() -const tiles = new Map<string, TileData>() -const items_removed = new Set<ItemData>() -let data: Gamedata = { item_names: [], tile_names: [], spawn: [0, 0] } +export const players = new Map<PlayerID, PlayerData>() +export const tiles = new Map<string, TileData>() +export const items_removed = new Set<ItemData>() + +export let data: Gamedata = { item_names: [], tile_names: [], spawn: [0, 0] } + let my_id: PlayerID = -1 -const camera: V2 = { x: 0, y: 0 } -let camera_zoom = 0.1 -const interact_target_anim: V2 = { x: 0, y: 0 } +export const camera: V2 = { x: 0, y: 0 } +export const interact_target_anim: V2 = { x: 0, y: 0 } let interacting: V2 | undefined; -let scale = 0 function send(p: PacketS) { ws.send(JSON.stringify(p)) } function packet(p: PacketC) { - if (!["position", "set_active"].includes(p.type)) + if (!["position", "set_active", "update_map"].includes(p.type)) console.log(p); switch (p.type) { case "init": @@ -144,12 +144,17 @@ function packet(p: PacketC) { case "update_map": tiles.set(p.pos.toString(), { x: p.pos[0], y: p.pos[1], kind: p.tile }) break; + case "communicate": { + const player = players.get(p.player)! + player.message = p.message + break; + } default: console.warn("unknown packet", p); } } -const keys_down = new Set(); +export 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() @@ -157,7 +162,7 @@ function keyboard(ev: KeyboardEvent, down: boolean) { else keys_down.delete(ev.code) } -function get_interact_target(): V2 | undefined { +export function get_interact_target(): V2 | undefined { const me = players.get(my_id) if (!me) return return { @@ -244,144 +249,6 @@ function draw() { 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) * camera_zoom; - 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 + 0.5, tile.y + 0.5) - const comps = TILES[data.tile_names[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 = `hsl(${player.character}rad, 50%, 50%)` - ctx.beginPath() - ctx.arc(0, 0, PLAYER_SIZE, 0, Math.PI * 2) - ctx.fill() - - ctx.fillStyle = `hsl(${player.character}rad, 80%, 10%)` - ctx.beginPath() - ctx.arc(0, -0.2, PLAYER_SIZE, 0, Math.PI * 2) - ctx.fill() - - ctx.fillStyle = `hsl(${player.character}rad, 80%, 70%)` - ctx.beginPath() - ctx.moveTo(-0.04, 0.25) - ctx.lineTo(0.04, 0.25) - ctx.lineTo(0, 0.4) - ctx.fill() - - ctx.restore() - - if (player.item) draw_item(player.item) - } - - for (const item of items_removed) { - draw_item(item) - } - for (const [_, tile] of tiles) { - if (tile.item) draw_item(tile.item) - } - - draw_interact_target() - - ctx.restore() - - if (keys_down.has("KeyP")) { - camera_zoom = 0.05 - ctx.fillStyle = "white" - ctx.textAlign = "left" - ctx.textBaseline = "bottom" - ctx.font = "20px sans-serif" - ctx.fillText(`interact = ${JSON.stringify(get_interact_target())}`, 10, 30) - } else { camera_zoom = 0.1 } -} - -function draw_item(item: ItemData) { - ctx.save() - ctx.translate(item.x, item.y) - if (item.remove_anim) ctx.scale(1 - item.remove_anim, 1 - item.remove_anim) - const comps = ITEMS[data.item_names[item.kind]] ?? FALLBACK_ITEM - for (const c of comps) { - c(ctx) - } - if (item.progress !== null && item.progress !== undefined) { - ctx.fillStyle = "rgba(115, 230, 58, 0.66)" - ctx.fillRect(-0.5, -0.5, 1, item.progress) - } - 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() -} function collide_player(p: PlayerData) { const tiles_ignored = ["floor", "door", "chair"].map(t => data.tile_names.indexOf(t)) diff --git a/test-client/protocol.ts b/test-client/protocol.ts index aa3aa063..c20588ce 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -25,4 +25,9 @@ export type PacketC = | { type: "produce_item", tile: Vec2, item: ItemIndex } // A tile generated a new item | { type: "consume_item", tile: Vec2 } // A tile removed an item | { type: "set_active", tile: Vec2, progress?: number } // A tile is doing something. progress goes from 0 to 1, then null when finished - | { type: "update_map", pos: Vec2, tile: TileIndex, neighbours: [TileIndex | null] } // A map tile was changed + | { type: "update_map", pos: Vec2, tile: TileIndex, neighbors: [TileIndex | null] } // A map tile was changed + | { type: "communicate", player: PlayerID, message?: Message } // A map tile was changed + +export type Message = + { item: number } + | { text: string } diff --git a/test-client/tiles.ts b/test-client/tiles.ts index 5eee2b32..5a6769ed 100644 --- a/test-client/tiles.ts +++ b/test-client/tiles.ts @@ -7,8 +7,39 @@ function base(fill: string, stroke?: string, stroke_width?: number): Component { c.lineWidth = stroke_width ?? 0.05 c.lineJoin = "miter" c.lineCap = "square" - c.fillRect(-0.5, -0.5, 1, 1) - if (stroke) c.strokeRect(-0.5 + c.lineWidth / 2, -0.5 + c.lineWidth / 2, 1 - c.lineWidth, 1 - c.lineWidth) + c.fillRect( + -0.5, + -0.5, + 1, + 1 + ) + if (stroke) c.strokeRect( + -0.5 + c.lineWidth / 2, + -0.5 + c.lineWidth / 2, + 1 - c.lineWidth, + 1 - c.lineWidth + ) + } +} +function rect(inset: number, fill: string, stroke?: string, stroke_width?: number): Component { + return c => { + c.fillStyle = fill; + c.strokeStyle = stroke ?? "black"; + c.lineWidth = stroke_width ?? 0.05 + c.lineJoin = "round" + c.lineCap = "round" + c.fillRect( + -0.5 + inset, + -0.5 + inset, + 1 - inset * 2, + 1 - inset * 2 + ) + if (stroke) c.strokeRect( + -0.5 + inset, + -0.5 + inset, + 1 - inset * 2, + 1 - inset * 2 + ) } } function circle(radius: number, fill: string, stroke?: string, stroke_width?: number): Component { @@ -93,7 +124,7 @@ export const TILES: { [key: string]: Component[] } = { "chair": [...floor, circle(0.45, "rgb(136, 83, 41)")], "wall": [base("rgb(0, 14, 56)")], "window": [base("rgb(233, 233, 233)")], - "watercooler": [...floor, circle(0.4, "rgb(64, 226, 207)")], + "cuttingboard": [...counter, rect(0.3, "rgb(158, 236, 68)", "rgb(158, 236, 68)", 0.2)], "trash": [...floor, circle(0.4, "rgb(20, 20, 20)"), cross(0.3, "rgb(90, 36, 36)")], "sink": [base("rgb(131, 129, 161)", "rgb(177, 174, 226)", 0.2)], "oven": [base("rgb(241, 97, 61)", "rgb(109, 84, 84)", 0.3)], diff --git a/test-client/visual.ts b/test-client/visual.ts new file mode 100644 index 00000000..4bbfac6c --- /dev/null +++ b/test-client/visual.ts @@ -0,0 +1,172 @@ +import { ItemData, PLAYER_SIZE, camera, canvas, ctx, data, get_interact_target, interact_target_anim, items_removed, keys_down, players, tiles } from "./main.ts"; +import { Message } from "./protocol.ts"; +import { FALLBACK_TILE, ITEMS, TILES, FALLBACK_ITEM } from "./tiles.ts"; +import { V2, ceil_v2, floor_v2 } from "./util.ts"; + +let camera_zoom = 0.1 +let scale = 0 + +export 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) +} + + +export function draw_ingame() { + ctx.fillStyle = "#111" + ctx.fillRect(0, 0, canvas.width, canvas.height) + + scale = Math.min(canvas.width, canvas.height) * camera_zoom; + 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 + 0.5, tile.y + 0.5) + const comps = TILES[data.tile_names[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.save() + ctx.rotate(-player.rot) + draw_character(player.character) + ctx.restore() + } + if (player.message) draw_message(player.message) + ctx.restore() + } + if (player.item) draw_item(player.item) + } + + for (const item of items_removed) { + draw_item(item) + } + for (const [_, tile] of tiles) { + if (tile.item) draw_item(tile.item) + } + + draw_interact_target() + + ctx.restore() + + if (keys_down.has("KeyP")) { + camera_zoom = 0.05 + ctx.fillStyle = "white" + ctx.textAlign = "left" + ctx.textBaseline = "bottom" + ctx.font = "20px sans-serif" + ctx.fillText(`interact = ${JSON.stringify(get_interact_target())}`, 10, 30) + } else { camera_zoom = 0.1 } +} + +function draw_item(item: ItemData) { + ctx.save() + ctx.translate(item.x, item.y) + if (item.remove_anim) ctx.scale(1 - item.remove_anim, 1 - item.remove_anim) + const comps = ITEMS[data.item_names[item.kind]] ?? FALLBACK_ITEM + for (const c of comps) { + c(ctx) + } + if (item.progress !== null && item.progress !== undefined) { + ctx.fillStyle = "rgba(115, 230, 58, 0.66)" + ctx.fillRect(-0.5, -0.5, 1, item.progress) + } + 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() +} + +function draw_character(character: number) { + ctx.fillStyle = `hsl(${character}rad, 50%, 50%)` + ctx.beginPath() + ctx.arc(0, 0, PLAYER_SIZE, 0, Math.PI * 2) + ctx.fill() + + ctx.fillStyle = `hsl(${character}rad, 80%, 10%)` + ctx.beginPath() + ctx.arc(0, -0.2, PLAYER_SIZE, 0, Math.PI * 2) + ctx.fill() + + ctx.fillStyle = `hsl(${character}rad, 80%, 70%)` + ctx.beginPath() + ctx.moveTo(-0.04, 0.25) + ctx.lineTo(0.04, 0.25) + ctx.lineTo(0, 0.4) + ctx.fill() +} + +function draw_message(m: Message) { + ctx.save() + ctx.translate(0, -1) + if ("item" in m) { + ctx.fillStyle = "#fffa" + ctx.beginPath() + ctx.moveTo(0, 0.7) + ctx.arc(0, 0, 0.5, Math.PI / 4, Math.PI - Math.PI / 4, true) + ctx.closePath() + ctx.fill() + + const comps = ITEMS[data.item_names[m.item]] ?? FALLBACK_ITEM + for (const c of comps) c(ctx) + } + ctx.restore() +} + +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, + } +} + |