diff options
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | server/src/game.rs | 89 | ||||
| -rw-r--r-- | server/src/main.rs | 26 | ||||
| -rw-r--r-- | server/src/protocol.rs | 71 | ||||
| -rw-r--r-- | test-client/main.ts | 44 | ||||
| -rw-r--r-- | test-client/protocol.ts | 2 | ||||
| -rw-r--r-- | test-client/tiles.ts | 33 | 
7 files changed, 221 insertions, 45 deletions
| @@ -1,2 +1,3 @@  [workspace]  members = ["server"] +resolver = "2" diff --git a/server/src/game.rs b/server/src/game.rs index 458a796b..fbbab0a4 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -1,9 +1,10 @@ -use crate::protocol::{Item, PacketC, PacketS, ID}; +use crate::protocol::{Item, PacketC, PacketS, Tile, ID};  use anyhow::{anyhow, Result}; -use glam::UVec2; +use glam::IVec2;  use std::collections::{HashMap, VecDeque};  struct TileData { +    kind: Tile,      items: Vec<ID>,      active: bool,  } @@ -16,7 +17,7 @@ struct Player {  #[derive(Default)]  pub struct Game {      item_id_counter: ID, -    tiles: HashMap<UVec2, TileData>, +    tiles: HashMap<IVec2, TileData>,      items: HashMap<ID, Item>,      players: HashMap<ID, Player>,      packet_out: VecDeque<PacketC>, @@ -24,13 +25,72 @@ pub struct Game {  impl Game {      pub fn new() -> Self { -        Self::default() +        let mut g = Self::default(); +        for x in -5..5 { +            for y in -5..5 { +                g.tiles.insert(IVec2 { x, y }, Tile::Floor.into()); +            } +        } +        for x in -5..5 { +            g.tiles.insert(IVec2 { x, y: -5 }, Tile::Table.into()); +            g.tiles.insert(IVec2 { x, y: 4 }, Tile::Table.into()); +        } +        for y in -5..5 { +            g.tiles.insert(IVec2 { x: -5, y }, Tile::Table.into()); +            g.tiles.insert(IVec2 { x: 4, y }, Tile::Table.into()); +        } + +        g.tiles.extend( +            [ +                ([-5, 1], Tile::Pan), +                ([-5, 2], Tile::Pan), +                ([4, 3], Tile::Pan), +            ] +            .map(|(k, v)| { +                ( +                    IVec2::from_array(k), +                    TileData { +                        active: false, +                        items: vec![], +                        kind: v, +                    }, +                ) +            }), +        ); + +        g      }      pub fn packet_out(&mut self) -> Option<PacketC> {          self.packet_out.pop_front()      } +    pub fn prime_client(&self, id: ID) -> Vec<PacketC> { +        let mut out = Vec::new(); +        for (&id, player) in &self.players { +            out.push(PacketC::AddPlayer { +                id, +                name: player.name.clone(), +                hand: player.hand.map(|i| (i, self.items[&i])), +            }) +        } +        for (&pos, tdata) in &self.tiles { +            out.push(PacketC::UpdateMap { +                pos, +                tile: tdata.kind, +            }); +            for &id in &tdata.items { +                out.push(PacketC::ProduceItem { +                    id, +                    pos, +                    kind: self.items[&id], +                }) +            } +        } +        out.push(PacketC::Joined { id }); +        out +    } +      pub fn packet_in(&mut self, player: ID, packet: PacketS) -> Result<()> {          match packet {              PacketS::Join { name } => { @@ -41,16 +101,19 @@ impl Game {                          name: name.clone(),                      },                  ); -                self.packet_out -                    .push_back(PacketC::AddPlayer { id: player, name }); +                self.packet_out.push_back(PacketC::AddPlayer { +                    id: player, +                    name, +                    hand: None, +                });              }              PacketS::Leave => {                  let p = self                      .players                      .remove(&player)                      .ok_or(anyhow!("player does not exist"))?; -                if let Some(_i) = p.hand { -                    // TODO what now? +                if let Some(id) = p.hand { +                    self.items.remove(&id).expect("hand item lost");                  }                  self.packet_out                      .push_back(PacketC::RemovePlayer { id: player }) @@ -84,3 +147,13 @@ impl Game {          Ok(())      }  } + +impl From<Tile> for TileData { +    fn from(kind: Tile) -> Self { +        Self { +            kind, +            active: false, +            items: vec![], +        } +    } +} diff --git a/server/src/main.rs b/server/src/main.rs index 477595d0..7426e27e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -53,11 +53,14 @@ async fn main() -> Result<()> {                  let game = game.clone();                  let mut rx = rx.resubscribe();                  info!("{addr} connected"); +                let init = game.write().await.prime_client(id);                  spawn(async move { -                    write -                        .write_all(serde_json::to_string(&PacketC::Joined { id }).unwrap().as_bytes()) -                        .await?; -                    write.write_all(b"\n").await?; +                    for p in init { +                        write +                            .write_all(serde_json::to_string(&p).unwrap().as_bytes()) +                            .await?; +                        write.write_all(b"\n").await?; +                    }                      while let Ok(packet) = rx.recv().await {                          write                              .write_all(serde_json::to_string(&packet).unwrap().as_bytes()) @@ -78,6 +81,7 @@ async fn main() -> Result<()> {                              warn!("client error: {e}");                          }                      } +                    let _ = game.write().await.packet_in(id, PacketS::Leave);                  });              }              r = ws_listener.accept() => { @@ -87,12 +91,15 @@ async fn main() -> Result<()> {                  let game = game.clone();                  let mut rx = rx.resubscribe();                  info!("{addr} connected via ws"); +                let init = game.write().await.prime_client(id);                  spawn(async move { -                    if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text( -                        serde_json::to_string(&PacketC::Joined { id }).unwrap(), -                    )).await { -                        warn!("ws error on init: {e}"); -                        return; +                    for p in init { +                        if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text( +                            serde_json::to_string(&p).unwrap(), +                        )).await { +                            warn!("ws error on init: {e}"); +                            return; +                        }                      }                      while let Ok(packet) = rx.recv().await {                          if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text( @@ -120,6 +127,7 @@ async fn main() -> Result<()> {                              _ => (),                          }                      } +                    let _ = game.write().await.packet_in(id, PacketS::Leave);                  });              }          } diff --git a/server/src/protocol.rs b/server/src/protocol.rs index dc2af7d6..a8fd9979 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -1,13 +1,22 @@ -use glam::{UVec2, Vec2}; +use glam::{IVec2, Vec2};  use serde::{Deserialize, Serialize};  pub type ID = u32; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Item {} +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Item { +    Dough, +    Pancake, +} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Tile {} +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Tile { +    Floor, +    Table, +    Pan, +}  #[derive(Debug, Clone, Serialize, Deserialize)]  #[serde(rename_all = "snake_case")] @@ -15,20 +24,50 @@ pub enum PacketS {      Join { name: String },      Leave,      Position { pos: Vec2, rot: f32 }, -    Interact { pos: UVec2 }, +    Interact { pos: IVec2 },  }  #[derive(Debug, Clone, Serialize, Deserialize)]  #[serde(rename_all = "snake_case")]  pub enum PacketC { -    Joined { id: ID }, -    AddPlayer { id: ID, name: String }, -    RemovePlayer { id: ID }, -    Position { player: ID, pos: Vec2, rot: f32 }, -    TakeItem { item: ID, player: ID }, -    PutItem { item: ID, pos: UVec2 }, -    ProduceItem { id: ID, pos: UVec2, kind: Item }, -    ConsumeItem { id: ID, pos: UVec2 }, -    SetActive { tile: UVec2 }, -    UpdateMap { pos: UVec2, tile: Tile }, +    Joined { +        id: ID, +    }, +    AddPlayer { +        id: ID, +        name: String, +        hand: Option<(ID, Item)>, +    }, +    RemovePlayer { +        id: ID, +    }, +    Position { +        player: ID, +        pos: Vec2, +        rot: f32, +    }, +    TakeItem { +        item: ID, +        player: ID, +    }, +    PutItem { +        item: ID, +        pos: IVec2, +    }, +    ProduceItem { +        id: ID, +        pos: IVec2, +        kind: Item, +    }, +    ConsumeItem { +        id: ID, +        pos: IVec2, +    }, +    SetActive { +        tile: IVec2, +    }, +    UpdateMap { +        pos: IVec2, +        tile: Tile, +    },  } diff --git a/test-client/main.ts b/test-client/main.ts index 1780b5d7..3c1285a4 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -1,6 +1,7 @@  /// <reference lib="dom" /> -import { PacketC, PacketS } from "./protocol.ts"; +import { ID, Item, PacketC, PacketS, Tile } from "./protocol.ts"; +import { FALLBACK_TILE, TILES } from "./tiles.ts";  import { V2, ceil_v2, floor_v2, lerp_v2_mut, normalize } from "./util.ts";  let ctx: CanvasRenderingContext2D; @@ -27,15 +28,16 @@ document.addEventListener("DOMContentLoaded", () => {      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 Player { x: number; y: number, name: string, rot: number } -const players = new Map<number, Player>() -interface Item { x: number; y: number } -const items = new Map<number, Item>() -interface Tile { x: number; y: number } -const tiles = new Map<string, Tile>() +interface PlayerData { x: number; y: number, name: string, rot: number, hand?: ID } +const players = new Map<number, PlayerData>() +interface ItemData { x: number; y: number, kind: Item } +const items = new Map<number, ItemData>() +interface TileData { x: number; y: number, kind: Tile } +const tiles = new Map<string, TileData>()  let my_id: number = -1  const camera: V2 = { x: 0, y: 0 } @@ -43,24 +45,32 @@ let scale = 0  function send(p: PacketS) { ws.send(JSON.stringify(p)) }  function packet(p: PacketC) { -    console.log(p); +    if (!("position" in p)) console.log(p);      if ("joined" in p) {          my_id = p.joined.id      } else if ("add_player" in p) { -        players.set(p.add_player.id, { x: 0, y: 0, name: p.add_player.name, rot: 0 }) +        if (p.add_player.hand) items.set(p.add_player.hand[0], { kind: p.add_player.hand[1], 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] })      } 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) { +        // TODO      } else if ("put_item" in p) { +        // TODO      } else if ("produce_item" in p) { +        // TODO      } 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 })      } else console.warn("unknown packet", p);  } @@ -80,6 +90,8 @@ function interact() {  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) @@ -114,7 +126,7 @@ function draw() {      requestAnimationFrame(draw)  }  function draw_wait(text: string) { -    ctx.fillStyle = "gray" +    ctx.fillStyle = "#444"      ctx.fillRect(0, 0, canvas.width, canvas.height)      ctx.fillStyle = "#555"      ctx.font = "50px sans-serif" @@ -149,6 +161,16 @@ function draw_ingame() {      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) @@ -177,4 +199,4 @@ function draw_grid() {          ctx.lineTo(max.x, y)      }      ctx.stroke() -}
\ No newline at end of file +} diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 3b61ce94..717383d4 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -12,7 +12,7 @@ export type PacketS =  export type PacketC =      { joined: { id: ID } } -    | { add_player: { id: ID, name: string } } +    | { add_player: { id: ID, name: string, hand?: [number, Item] } }      | { remove_player: { id: ID } }      | { position: { player: ID, pos: Vec2, rot: number } }      | { take_item: { item: ID, player: ID } } diff --git a/test-client/tiles.ts b/test-client/tiles.ts new file mode 100644 index 00000000..3b15385c --- /dev/null +++ b/test-client/tiles.ts @@ -0,0 +1,33 @@ + +type Component = (ctx: CanvasRenderingContext2D) => void +function base(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 = "miter" +        c.lineCap = "square" +        c.fillRect(0, 0, 1, 1) +        if (stroke) c.strokeRect(c.lineWidth / 2, c.lineWidth / 2, 1 - c.lineWidth, 1 - c.lineWidth) +    } +} +function circle(radius: 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.beginPath() +        c.arc(0.5, 0.5, radius, 0, Math.PI * 2) +        if (stroke) c.stroke() +        c.fill() +    } +} + +const table = [base("rgb(133, 76, 38)")]; + +export const FALLBACK_TILE: Component[] = [base("#f0f")]; +export const TILES: { [key: string]: Component[] } = { +    "floor": [base("#333", "#222", 0.05)], +    "table": table, +    "pan": [...table, circle(0.4, "#444", "#999")], +} | 
