diff options
author | metamuffin <metamuffin@disroot.org> | 2024-06-18 19:36:36 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-06-23 19:21:22 +0200 |
commit | 48934ff63ee14d4759eda36512af87361dd915dd (patch) | |
tree | f7a80115eacfee7b6871040a87f5fb0087098ea8 | |
parent | 6ec47d729509db83eaeb6a9d855ce2483d70f227 (diff) | |
download | hurrycurry-48934ff63ee14d4759eda36512af87361dd915dd.tar hurrycurry-48934ff63ee14d4759eda36512af87361dd915dd.tar.bz2 hurrycurry-48934ff63ee14d4759eda36512af87361dd915dd.tar.zst |
remodel game.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | data/map.yaml | 12 | ||||
-rw-r--r-- | data/recipes.yaml | 30 | ||||
-rw-r--r-- | server/src/data.rs | 127 | ||||
-rw-r--r-- | server/src/game.rs | 224 | ||||
-rw-r--r-- | server/src/interaction.rs | 302 | ||||
-rw-r--r-- | server/src/lib.rs | 2 | ||||
-rw-r--r-- | server/src/main.rs | 2 | ||||
-rw-r--r-- | server/src/protocol.rs | 24 | ||||
-rw-r--r-- | server/src/recipes.rs | 104 | ||||
-rw-r--r-- | specs/specs_book.html | 131 | ||||
-rw-r--r-- | test-client/main.ts | 161 | ||||
-rw-r--r-- | test-client/protocol.ts | 13 | ||||
-rw-r--r-- | test-client/tiles.ts | 25 | ||||
-rw-r--r-- | test-client/util.ts | 13 |
15 files changed, 602 insertions, 569 deletions
@@ -1,3 +1,4 @@ /target .vscode/ /test-client/*.js +/specs/*.html diff --git a/data/map.yaml b/data/map.yaml index 096bb945..ea376bf1 100644 --- a/data/map.yaml +++ b/data/map.yaml @@ -4,13 +4,14 @@ map: - "|.....c..............|" - "|c...c...+--www---dd-+" - "|tc.ctc..|##...##W..#|" - - "|c...c...w..........S|" + - "|c...c...w........~.S|" - "|c.......w..######..T|" - "|tc......w..........F|" - - "|c.....ct|##ss##ooo##|" + - "|c.....ct|##ss#oopp##|" - "+---dd---+-----------+" tiles: + "~": spawn ".": floor "+": wall "-": wall @@ -22,7 +23,8 @@ tiles: "w": window "s": sink "o": oven + "p": pan "W": watercooler - "S": raw-steak-spawn - "T": tomato-spawn - "F": flour-spawn + "S": raw-steak-crate + "T": tomato-crate + "F": flour-crate diff --git a/data/recipes.yaml b/data/recipes.yaml index e95c6e03..713509df 100644 --- a/data/recipes.yaml +++ b/data/recipes.yaml @@ -1,23 +1,23 @@ -- { tile: floor, action: !never } +# - { tile: floor, action: !never } -- { tile: window, inputs: [steak-meal, void] } -- { tile: window, inputs: [sliced-tomato-meal, void] } -- { tile: window, inputs: [bread-meal, void] } -- { tile: window, inputs: [burger-meal, void] } -- { tile: window, inputs: [tomatosteak-meal, void] } -- { tile: window, inputs: [tomatoburger-meal, void] } +# - { tile: window, inputs: [steak-meal, void] } +# - { tile: window, inputs: [sliced-tomato-meal, void] } +# - { tile: window, inputs: [bread-meal, void] } +# - { tile: window, inputs: [burger-meal, void] } +# - { tile: window, inputs: [tomatosteak-meal, void] } +# - { tile: window, inputs: [tomatoburger-meal, void] } - { tile: trash, action: !instant , inputs: [raw-steak] } - { tile: trash, action: !instant , inputs: [steak] } - { tile: trash, action: !instant , inputs: [flour] } - { tile: trash, action: !instant , inputs: [dough] } - { tile: trash, action: !instant , inputs: [steak-meal] } -- { tile: counter, inputs: [raw-steak, void] } -- { tile: counter, inputs: [sliced-tomato, void] } -- { tile: counter, inputs: [dough, void] } -- { tile: counter, inputs: [bread, void] } +# - { tile: counter, inputs: [raw-steak, void] } +# - { tile: counter, inputs: [sliced-tomato, void] } +# - { tile: counter, inputs: [dough, void] } +# - { tile: counter, inputs: [bread, void] } -- tile: tomato-spawn # Tomato pipeline +- tile: tomato-crate # Tomato pipeline outputs: [tomato] action: !instant - tile: counter @@ -29,7 +29,7 @@ outputs: [sliced-tomato-meal] action: !instant -- tile: flour-spawn # Bread pipeline +- tile: flour-crate # Bread pipeline outputs: [flour] action: !instant - tile: counter @@ -45,7 +45,7 @@ outputs: [bread-meal] action: !instant -- tile: raw-steak-spawn # Steak pipeline +- tile: raw-steak-crate # Steak pipeline action: !instant outputs: [raw-steak] - tile: pan @@ -83,7 +83,7 @@ outputs: [glass] action: !instant -- tile: dirty-plate-spawn # Cleaning +- tile: dirty-plate-crate # Cleaning outputs: [dirty-plate] action: !instant - tile: sink diff --git a/server/src/data.rs b/server/src/data.rs new file mode 100644 index 00000000..6affccb5 --- /dev/null +++ b/server/src/data.rs @@ -0,0 +1,127 @@ +use crate::{interaction::Recipe, protocol::TileIndex}; +use glam::{IVec2, Vec2}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::RwLock}; + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] +#[serde(rename_all = "snake_case")] +pub enum Action { + #[default] + Never, + Passive(f32), + Active(f32), + Instant, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RecipeDecl { + #[serde(default)] + pub tile: Option<String>, + #[serde(default)] + pub inputs: Vec<String>, + #[serde(default)] + pub outputs: Vec<String>, + #[serde(default)] + pub action: Action, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct InitialMap { + map: Vec<String>, + tiles: HashMap<String, String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Gamedata { + pub recipes: Vec<Recipe>, + pub item_names: Vec<String>, + pub tile_names: Vec<String>, + #[serde(skip)] + pub initial_map: HashMap<IVec2, TileIndex>, + pub spawn: Vec2, +} + +pub fn build_gamedata(recipes_in: Vec<RecipeDecl>, map_in: InitialMap) -> Gamedata { + let item_names = RwLock::new(Vec::new()); + let tile_names = RwLock::new(Vec::new()); + let mut recipes = Vec::new(); + + for r in recipes_in { + let r2 = r.clone(); + let mut inputs = r.inputs.into_iter().map(|i| register(&item_names, i)); + let mut outputs = r.outputs.into_iter().map(|o| register(&item_names, o)); + let tile = r.tile.map(|t| register(&tile_names, t)); + match r.action { + Action::Never => {} + Action::Passive(duration) => recipes.push(Recipe::Passive { + duration, + tile, + input: inputs.next().expect("passive recipe without input"), + output: outputs.next(), + }), + Action::Active(duration) => recipes.push(Recipe::Active { + duration, + tile, + input: inputs.next().expect("active recipe without input"), + outputs: [outputs.next(), outputs.next()], + }), + Action::Instant => { + recipes.push(Recipe::Instant { + tile, + inputs: [inputs.next(), inputs.next()], + outputs: [outputs.next(), outputs.next()], + }); + } + } + assert_eq!(inputs.next(), None, "{r2:?}"); + assert_eq!(outputs.next(), None, "{r2:?}"); + } + + let mut spawn = Vec2::new(0., 0.); + let mut initial_map = HashMap::new(); + for (y, line) in map_in.map.iter().enumerate() { + for (x, tile) in line.trim().char_indices() { + let pos = IVec2::new(x as i32, y as i32); + let mut tilename = map_in.tiles[&tile.to_string()].clone(); + if tilename == "spawn" { + spawn = pos.as_vec2(); + tilename = "floor".to_owned(); + } + let tile = register(&tile_names, tilename); + initial_map.insert(pos, tile); + } + } + + Gamedata { + recipes, + initial_map, + item_names: item_names.into_inner().unwrap(), + tile_names: tile_names.into_inner().unwrap(), + spawn, + } +} + +fn register(db: &RwLock<Vec<String>>, name: String) -> usize { + let mut db = db.write().unwrap(); + if let Some(index) = db.iter().position(|e| e == &name) { + index + } else { + let index = db.len(); + db.push(name); + index + } +} + +impl Gamedata { + pub fn get_tile(&self, name: &str) -> Option<TileIndex> { + self.tile_names.iter().position(|t| t == name) + } +} +impl Action { + pub fn duration(&self) -> f32 { + match self { + Action::Instant | Action::Never => 0., + Action::Passive(x) | Action::Active(x) => *x, + } + } +} diff --git a/server/src/game.rs b/server/src/game.rs index 5cb155f1..d481dc4f 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -1,10 +1,10 @@ use crate::{ - interaction::{interact, tick_tile, Out}, - protocol::{ItemID, ItemIndex, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, - recipes::Gamedata, + data::Gamedata, + interaction::interact, + protocol::{ItemIndex, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Result}; -use glam::IVec2; +use glam::{IVec2, Vec2}; use log::info; use std::{ collections::{HashMap, VecDeque}, @@ -12,29 +12,32 @@ use std::{ sync::Arc, }; -pub struct ActiveRecipe { +pub struct Involvement { pub recipe: RecipeIndex, pub progress: f32, pub working: usize, } +pub struct Item { + pub kind: ItemIndex, + pub active: Option<Involvement>, +} + pub struct Tile { - kind: TileIndex, - items: Vec<ItemID>, - active: Option<ActiveRecipe>, + pub kind: TileIndex, + pub item: Option<Item>, } -struct Player { - name: String, - interacting: bool, - hand: Option<ItemID>, +pub struct Player { + pub name: String, + pub position: Vec2, + pub interacting: bool, + pub item: Option<Item>, } pub struct Game { data: Arc<Gamedata>, - item_id_counter: ItemID, tiles: HashMap<IVec2, Tile>, - items: HashMap<ItemID, ItemIndex>, players: HashMap<PlayerID, Player>, packet_out: VecDeque<PacketC>, } @@ -43,8 +46,6 @@ impl Game { pub fn new(gamedata: Arc<Gamedata>) -> Self { let mut g = Self { data: gamedata.clone(), - item_id_counter: 0, - items: Default::default(), packet_out: Default::default(), players: Default::default(), tiles: Default::default(), @@ -69,19 +70,18 @@ impl Game { out.push(PacketC::AddPlayer { id, name: player.name.clone(), - hand: player.hand.map(|i| (i, self.items[&i].clone())), + hand: player.item.as_ref().map(|i| i.kind), }) } - for (&pos, tdata) in &self.tiles { + for (&tile, tdata) in &self.tiles { out.push(PacketC::UpdateMap { - pos, + pos: tile, tile: tdata.kind.clone(), }); - for &id in &tdata.items { + if let Some(item) = &tdata.item { out.push(PacketC::ProduceItem { - id, - pos, - kind: self.items[&id].clone(), + item: item.kind, + tile, }) } } @@ -94,7 +94,8 @@ impl Game { self.players.insert( player, Player { - hand: None, + item: None, + position: self.data.spawn, interacting: false, name: name.clone(), }, @@ -110,8 +111,8 @@ impl Game { .players .remove(&player) .ok_or(anyhow!("player does not exist"))?; - if let Some(id) = p.hand { - self.items.remove(&id).expect("hand item lost"); + if let Some(id) = p.item { + // TODO place on ground } self.packet_out .push_back(PacketC::RemovePlayer { id: player }) @@ -137,108 +138,105 @@ impl Game { bail!("already (not) interacting") } - let items = tile.items.iter().map(|e| self.items[e]).collect::<Vec<_>>(); - let tilekind = tile.kind; - let hand = player.hand.map(|e| self.items[&e]); + let tile_had_item = tile.item.is_some(); - interact( - &self.data, - edge, - tilekind, - &mut tile.active, - items, - hand, - |out| match out { - Out::Take(index) => { - info!("take"); - let item = tile.items.remove(index); - player.hand = Some(item); - self.packet_out - .push_back(PacketC::TakeItem { item, player: pid }) - } - Out::Put => { - info!("put"); - let hand = player.hand.take().unwrap(); - tile.items.push(hand); - self.packet_out - .push_back(PacketC::PutItem { item: hand, pos }) - } - Out::Produce(kind) => { - info!("produce"); - let id = self.item_id_counter; - self.item_id_counter += 1; - self.items.insert(id, kind); - tile.items.push(id); - self.packet_out - .push_back(PacketC::ProduceItem { id, pos, kind }); - } - Out::Consume(index) => { - info!("consume"); - let id = tile.items.remove(index); - info!("left {:?}", tile.items); - self.packet_out.push_back(PacketC::ConsumeItem { id, pos }); - } - Out::SetActive(progress) => { - self.packet_out.push_back(PacketC::SetActive { - tile: pos, - progress, - }); - } - }, - ); + interact(&self.data, edge, tile, player); + + // interact( + // &self.data, + // edge, + // tilekind, + // &mut tile.active, + // item, + // hand, + // |out| match out { + // Out::Take(index) => { + // info!("take"); + // let item = tile.items.remove(index); + // player.hand = Some(item); + // self.packet_out + // .push_back(PacketC::TakeItem { item, player: pid }) + // } + // Out::Put => { + // info!("put"); + // let hand = player.hand.take().unwrap(); + // tile.items.push(hand); + // self.packet_out + // .push_back(PacketC::PutItem { item: hand, pos }) + // } + // Out::Produce(kind) => { + // info!("produce"); + // let id = self.item_id_counter; + // self.item_id_counter += 1; + // self.items.insert(id, kind); + // tile.items.push(id); + // self.packet_out + // .push_back(PacketC::ProduceItem { id, pos, kind }); + // } + // Out::Consume(index) => { + // info!("consume"); + // let id = tile.items.remove(index); + // info!("left {:?}", tile.items); + // self.packet_out.push_back(PacketC::ConsumeItem { id, pos }); + // } + // Out::SetActive(progress) => { + // self.packet_out.push_back(PacketC::SetActive { + // tile: pos, + // progress, + // }); + // } + // }, + // ); player.interacting = edge; } + PacketS::Collide { player, force } => {} } Ok(()) } pub fn tick(&mut self, dt: f32) { for (&pos, tile) in &mut self.tiles { - let items = tile.items.iter().map(|e| self.items[e]).collect::<Vec<_>>(); - tick_tile( - dt, - &self.data, - tile.kind, - &mut tile.active, - items, - |out| match out { - Out::Take(_) | Out::Put => { - unreachable!() - } - Out::Produce(kind) => { - info!("produce"); - let id = self.item_id_counter; - self.item_id_counter += 1; - self.items.insert(id, kind); - tile.items.push(id); - self.packet_out - .push_back(PacketC::ProduceItem { id, pos, kind }); - } - Out::Consume(index) => { - info!("consume"); - let id = tile.items.remove(index); - info!("left {:?}", tile.items); - self.packet_out.push_back(PacketC::ConsumeItem { id, pos }); - } - Out::SetActive(progress) => { - self.packet_out.push_back(PacketC::SetActive { - tile: pos, - progress, - }); - } - }, - ); + // let items = tile.items.iter().map(|e| self.items[e]).collect::<Vec<_>>(); + // tick_tile( + // dt, + // &self.data, + // tile.kind, + // &mut tile.active, + // items, + // |out| match out { + // Out::Take(_) | Out::Put => { + // unreachable!() + // } + // Out::Produce(kind) => { + // info!("produce"); + // let id = self.item_id_counter; + // self.item_id_counter += 1; + // self.items.insert(id, kind); + // tile.items.push(id); + // self.packet_out + // .push_back(PacketC::ProduceItem { id, pos, kind }); + // } + // Out::Consume(index) => { + // info!("consume"); + // let id = tile.items.remove(index); + // info!("left {:?}", tile.items); + // self.packet_out.push_back(PacketC::ConsumeItem { id, pos }); + // } + // Out::SetActive(progress) => { + // self.packet_out.push_back(PacketC::SetActive { + // tile: pos, + // progress, + // }); + // } + // }, + // ); } } } impl From<TileIndex> for Tile { fn from(kind: TileIndex) -> Self { - Self { - kind, - items: vec![], - active: None, - } + Self { kind, item: None } } } diff --git a/server/src/interaction.rs b/server/src/interaction.rs index 11409b10..aa216410 100644 --- a/server/src/interaction.rs +++ b/server/src/interaction.rs @@ -1,150 +1,196 @@ use crate::{ - game::ActiveRecipe, + data::{Action, Gamedata}, + game::{Involvement, Item, Player, Tile}, protocol::{ItemIndex, TileIndex}, - recipes::{Action, Gamedata}, }; use log::{debug, info}; +use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; -use Out::*; -pub enum Out { - Take(usize), - Put, - Produce(ItemIndex), - Consume(usize), - SetActive(Option<f32>), +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Recipe { + Passive { + duration: f32, + tile: Option<TileIndex>, + input: ItemIndex, + output: Option<ItemIndex>, + }, + Active { + duration: f32, + tile: Option<TileIndex>, + input: ItemIndex, + outputs: [Option<ItemIndex>; 2], + }, + Instant { + tile: Option<TileIndex>, + inputs: [Option<ItemIndex>; 2], + outputs: [Option<ItemIndex>; 2], + }, } -pub fn interact( - data: &Gamedata, - edge: bool, - tile: TileIndex, - active: &mut Option<ActiveRecipe>, - mut items: Vec<ItemIndex>, - mut hand: Option<ItemIndex>, - mut out: impl FnMut(Out), -) { - let mut allowed = BTreeSet::new(); - for r in &data.recipes { - if r.tile == tile { - allowed.extend(r.inputs.clone()) - } - } - if !edge { - debug!("falling edge"); - if let Some(ac) = active { - if matches!(data.recipes[ac.recipe].action, Action::Active(_)) { - debug!("workers--"); - ac.working -= 1; - } - } - return; - } - - if hand.is_none() { - debug!("rising edge"); - if let Some(active) = active { - if matches!(data.recipes[active.recipe].action, Action::Active(_)) { - debug!("workers++"); - active.working += 1; - } +impl Recipe { + pub fn tile(&self) -> Option<TileIndex> { + match self { + Recipe::Passive { tile, .. } => *tile, + Recipe::Active { tile, .. } => *tile, + Recipe::Instant { tile, .. } => *tile, } } +} - if active.is_none() && !items.is_empty() && hand.is_none() { - out(Take(items.len() - 1)); +pub fn interact(data: &Gamedata, edge: bool, tile: &mut Tile, player: &mut Player) { + if !edge { return; } - if active.is_none() { - if let Some(hi) = hand { - if allowed.contains(&hi) { - out(Put); - items.push(hi); - hand = None; - } - } - } - - if hand.is_none() && active.is_none() { - 'rloop: for (ri, r) in data.recipes.iter().enumerate() { - if tile != r.tile { - continue; - } - let mut inputs = r.inputs.clone(); - for i in &items { - debug!("take {i:?} {inputs:?}"); - let Some(pos) = inputs.iter().position(|e| e == i) else { - continue 'rloop; - }; - inputs.remove(pos); - } - debug!("end {inputs:?}"); - if !inputs.is_empty() { + for recipe in &data.recipes { + if let Some(tile_constraint) = recipe.tile() { + if tile.kind != tile_constraint { continue; } - - match r.action { - Action::Passive(_) => { - info!("use passive recipe {ri}"); - *active = Some(ActiveRecipe { - recipe: ri, - progress: 0., - working: 1, - }); - break 'rloop; - } - Action::Active(_) => { - info!("use active recipe {ri}"); - *active = Some(ActiveRecipe { - recipe: ri, - progress: 0., - working: 1, - }); - break 'rloop; - } - Action::Instant => { - info!("use instant recipe {ri}"); - for _ in 0..items.len() { - out(Consume(0)) - } - for i in &r.outputs { - out(Produce(*i)); - } - if !r.outputs.is_empty() { - out(Take(r.outputs.len() - 1)); - } - items.clear(); - break 'rloop; + } + match recipe { + Recipe::Passive { + duration, + input, + output, + .. + } => todo!(), + Recipe::Active { + duration, + input: inputs, + outputs, + .. + } => todo!(), + Recipe::Instant { + inputs, outputs, .. + } => { + let on_tile = tile.item.as_ref().map(|i| i.kind); + let in_hand = player.item.as_ref().map(|i| i.kind); + let ok = (inputs[0] == on_tile && inputs[1] == in_hand) + || (inputs[1] == on_tile && inputs[0] == in_hand); + if ok { + player.item = outputs[0].map(|kind| Item { kind, active: None }); + tile.item = outputs[1].map(|kind| Item { kind, active: None }); } - Action::Never => (), } } } } +// if !edge { +// debug!("falling edge"); +// if let Some(ac) = active { +// if matches!(data.recipes[ac.recipe].action, Action::Active(_)) { +// debug!("workers--"); +// ac.working -= 1; +// } +// } +// return; +// } -pub fn tick_tile( - dt: f32, - data: &Gamedata, - _tile: TileIndex, - active: &mut Option<ActiveRecipe>, - items: Vec<ItemIndex>, - mut out: impl FnMut(Out), -) { - if let Some(a) = active { - let r = &data.recipes[a.recipe]; - a.progress += a.working as f32 * dt / r.action.duration(); - if a.progress >= 1. { - for _ in 0..items.len() { - out(Consume(0)) - } - for i in &r.outputs { - out(Produce(*i)); - } - out(SetActive(None)); - active.take(); - } else { - out(SetActive(Some(a.progress))); - } - } -} +// if hand.is_none() { +// debug!("rising edge"); +// if let Some(active) = active { +// if matches!(data.recipes[active.recipe].action, Action::Active(_)) { +// debug!("workers++"); +// active.working += 1; +// } +// } +// } + +// if active.is_none() && !items.is_empty() && hand.is_none() { +// out(Take(items.len() - 1)); +// return; +// } + +// if active.is_none() { +// if let Some(hi) = hand { +// if allowed.contains(&hi) { +// out(Put); +// items.push(hi); +// hand = None; +// } +// } +// } + +// if hand.is_none() && active.is_none() { +// 'rloop: for (ri, r) in data.recipes.iter().enumerate() { +// if tile != r.tile { +// continue; +// } +// let mut inputs = r.inputs.clone(); +// for i in &items { +// debug!("take {i:?} {inputs:?}"); +// let Some(pos) = inputs.iter().position(|e| e == i) else { +// continue 'rloop; +// }; +// inputs.remove(pos); +// } +// debug!("end {inputs:?}"); +// if !inputs.is_empty() { +// continue; +// } + +// match r.action { +// Action::Passive(_) => { +// info!("use passive recipe {ri}"); +// *active = Some(Involvement { +// recipe: ri, +// progress: 0., +// working: 1, +// }); +// break 'rloop; +// } +// Action::Active(_) => { +// info!("use active recipe {ri}"); +// *active = Some(Involvement { +// recipe: ri, +// progress: 0., +// working: 1, +// }); +// break 'rloop; +// } +// Action::Instant => { +// info!("use instant recipe {ri}"); +// for _ in 0..items.len() { +// out(Consume(0)) +// } +// for i in &r.outputs { +// out(Produce(*i)); +// } +// if !r.outputs.is_empty() { +// out(Take(r.outputs.len() - 1)); +// } +// items.clear(); +// break 'rloop; +// } +// Action::Never => (), +// } +// } +// } + +// pub fn tick_tile( +// dt: f32, +// data: &Gamedata, +// _tile: TileIndex, +// active: &mut Option<Involvement>, +// items: Vec<ItemIndex>, +// mut out: impl FnMut(Out), +// ) { +// if let Some(a) = active { +// let r = &data.recipes[a.recipe]; +// a.progress += a.working as f32 * dt / r.action.duration(); +// if a.progress >= 1. { +// for _ in 0..items.len() { +// out(Consume(0)) +// } +// for i in &r.outputs { +// out(Produce(*i)); +// } +// out(SetActive(None)); +// active.take(); +// } else { +// out(SetActive(Some(a.progress))); +// } +// } +// } diff --git a/server/src/lib.rs b/server/src/lib.rs index 5bb09e41..b40bcdd8 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,4 +1,4 @@ pub mod game; pub mod protocol; -pub mod recipes; +pub mod data; pub mod interaction; diff --git a/server/src/main.rs b/server/src/main.rs index 5414e9fd..6cb65141 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -13,7 +13,7 @@ use tokio_tungstenite::tungstenite::Message; use undercooked::{ game::Game, protocol::{PacketC, PacketS}, - recipes::build_gamedata, + data::build_gamedata, }; #[tokio::main] diff --git a/server/src/protocol.rs b/server/src/protocol.rs index 9e6717a3..4f04793f 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -1,9 +1,8 @@ -use crate::recipes::Gamedata; +use crate::data::Gamedata; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; pub type PlayerID = usize; -pub type ItemID = usize; pub type ItemIndex = usize; pub type TileIndex = usize; pub type RecipeIndex = usize; @@ -15,6 +14,7 @@ pub enum PacketS { Leave, Position { pos: Vec2, rot: f32 }, Interact { pos: IVec2, edge: bool }, + Collide { player: PlayerID, force: Vec2 }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,7 +27,7 @@ pub enum PacketC { AddPlayer { id: PlayerID, name: String, - hand: Option<(ItemID, ItemIndex)>, + hand: Option<ItemIndex>, }, RemovePlayer { id: PlayerID, @@ -38,21 +38,19 @@ pub enum PacketC { rot: f32, }, TakeItem { - item: ItemID, + tile: IVec2, player: PlayerID, }, PutItem { - item: ItemID, - pos: IVec2, + player: PlayerID, + tile: IVec2, }, ProduceItem { - id: ItemID, - pos: IVec2, - kind: ItemIndex, + tile: IVec2, + item: ItemIndex, }, ConsumeItem { - id: ItemID, - pos: IVec2, + tile: IVec2, }, SetActive { tile: IVec2, @@ -62,4 +60,8 @@ pub enum PacketC { pos: IVec2, tile: TileIndex, }, + Collide { + player: PlayerID, + force: Vec2, + }, } diff --git a/server/src/recipes.rs b/server/src/recipes.rs deleted file mode 100644 index 2dcb215c..00000000 --- a/server/src/recipes.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::protocol::{ItemIndex, TileIndex}; -use glam::IVec2; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] -#[serde(rename_all = "snake_case")] -pub enum Action { - #[default] - Never, - Passive(f32), - Active(f32), - Instant, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Recipe<T = TileIndex, I = ItemIndex> { - pub tile: T, - #[serde(default)] - pub inputs: Vec<I>, - #[serde(default)] - pub outputs: Vec<I>, - #[serde(default)] - pub action: Action, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct InitialMap { - map: Vec<String>, - tiles: HashMap<String, String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Gamedata { - pub recipes: Vec<Recipe>, - pub item_names: Vec<String>, - pub tile_names: Vec<String>, - #[serde(skip)] - pub initial_map: HashMap<IVec2, TileIndex>, -} -pub fn build_gamedata(recipes_in: Vec<Recipe<String, String>>, map_in: InitialMap) -> Gamedata { - let mut item_names = Vec::new(); - let mut tile_names = Vec::new(); - let mut recipes = Vec::new(); - - for r in recipes_in { - recipes.push(Recipe { - action: r.action, - tile: register(&mut tile_names, r.tile.clone()), - inputs: r - .inputs - .clone() - .into_iter() - .map(|e| register(&mut item_names, e)) - .collect(), - outputs: r - .outputs - .clone() - .into_iter() - .map(|e| register(&mut item_names, e)) - .collect(), - }) - } - - let mut initial_map = HashMap::new(); - for (y, line) in map_in.map.iter().enumerate() { - for (x, tile) in line.trim().char_indices() { - debug!("{tile:?}"); - let tile = register(&mut tile_names, map_in.tiles[&tile.to_string()].clone()); - initial_map.insert(IVec2::new(x as i32, y as i32), tile); - } - } - - Gamedata { - recipes, - initial_map, - item_names, - tile_names, - } -} -fn register(db: &mut Vec<String>, name: String) -> usize { - if let Some(index) = db.iter().position(|e| e == &name) { - index - } else { - let index = db.len(); - db.push(name); - index - } -} - -impl Gamedata { - pub fn get_tile(&self, name: &str) -> Option<TileIndex> { - self.tile_names.iter().position(|t| t == name) - } -} -impl Action { - pub fn duration(&self) -> f32 { - match self { - Action::Instant | Action::Never => 0., - Action::Passive(x) | Action::Active(x) => *x, - } - } -} diff --git a/specs/specs_book.html b/specs/specs_book.html deleted file mode 100644 index becff66e..00000000 --- a/specs/specs_book.html +++ /dev/null @@ -1,131 +0,0 @@ -<h1 id="undercooked">Undercooked</h1> -<p>A cooperative fast-paced game about running a restaurant together.</p> -<h1 id="alt-names">Alt Names</h1> -<ul> -<li>Undercooked</li> -<li>Flussigstätte</li> -<li>Das Geölterestaurant</li> -</ul> -<h1 id="game-loop-summary">Game Loop Summary</h1> -<p>A game starts, you and friends are given basic starting supplies with sufficent amount for a small first day.</p> -<p>Then you go to selecting your opening hours for the first day. Longer opening times means more profit IF it can be met. For a first day, it is recommended you stick to a short 11:00 till 15:00 opening hours (Each in game hour is 2 minutes irl). (Keep in mind the rush hours of 13:00 & 19:00, and the downtime of 15:00) Then the first day(round) starts.</p> -<p>During the course of the day, customers will start to come in. You & your friends’ job is to: 1. wait for them to decide on an order 1. take their order 1. prepare the dish 1. serve the dish in time 1. take & clean dirty dishes when they finish During the day, there’ll be many orders going simultaneously; the challenge is handling them in parallel.</p> -<p>By the end of the day, at closing time, the restaurant will close (ie no longer accept new customers), but you’ll get an extra hour post closing to finish up the last few orders.</p> -<p>After that 1 hour grace closing period, the day(round) ends, and you’ll be meet with a summary sheet of your performance. Then you’ll be met with the upgrades screen, where you can purchase upgrades & unlock new recipes for your restaurant. Then you’ll be met with the supply screen, where you’ll see how much of each raw items you have in stock, and where you can buy extra for tomorrow(next round). (Keep in mind, that any net profits will be taxed 10% by the goverment. In case of no net profit (or loss), there’ll be no tax.)</p> -<p>Rinse & repeat. But starting from day 2, before you decide when to open, there’ll be a summary screen of the previous day’s(round’s) customer’s arrival times. (expect today’s rush & peak hours and number of customer to not be too drastically different from yesterday.)</p> -<p>Goodluck Cooking!</p> -<h1 id="sections">Sections</h1> -<h3 id="introduction">00. Introduction</h3> -<p>Short summary of how the game works from the player’s prespective.</p> -<h3 id="game-loop-summary-1">01. Game Loop Summary</h3> -<p>Short summary of how the game works from the player’s prespective.</p> -<h3 id="sections-1">02. Sections</h3> -<p>Short summary of the different sections of this design document. (You are here)</p> -<h3 id="mechanics">03. Mechanics</h3> -<p>Short summary of each of the different mechanics of the game.</p> -<h3 id="game-setup">04. Game Setup</h3> -<p>Intial game setup at the start of a new game.</p> -<h3 id="game-loop-todo">05. Game Loop (!TODO)</h3> -<p>The game loop of the game.</p> -<h3 id="a.-pre-round-todo">05.a. Pre Round (!TODO)</h3> -<p>Everything that happens before opening.</p> -<h3 id="b.-round-todo">05.b. Round (!TODO)</h3> -<p>Everything that happens during a day.</p> -<h3 id="c.-post-round-todo">05.c. Post Round (!TODO)</h3> -<p>Everything that happens after closing.</p> -<h3 id="demand">06. Demand</h3> -<p>Details of the computation of the customers’ group traffic & size and demand scaling.</p> -<h3 id="service-todo">07. Service (!TODO)</h3> -<p>Details of how customers behave.</p> -<h3 id="items-todo">08. Items (!TODO)</h3> -<p>Details of all items: raw, meals, inbetweens, & their pipelines with flow graphs.</p> -<h3 id="unlocksupgrades-todo">09. Unlocks/Upgrades (!TODO)</h3> -<p>Details of all the stuff that is locked and can be unlocked during the course of a game.</p> -<h3 id="hud-todo">10. HUD (!TODO)</h3> -<p>Visual elements of the screen.</p> -<h1 id="mechanics-1">Mechanics</h1> -<h2 id="grid">01. Grid</h2> -<p>The restaurant(game area) is split into grid cells called tiles. A tile may be walkable if it has no furniture or appliances.</p> -<h2 id="items">02. Items</h2> -<p>An item is anything that can be carried by the player. Such items include plates, meals(eg burger), and raw foods(eg flour). Only one item may be carried at a time.</p> -<h2 id="recipes">03. Recipes</h2> -<p>Recipes are how items can be made into other items alone or by combining. Some recipes take raw foods(eg tomatoes->sliced tomatoes), but not all(eg dough->bread). Some result in servicable meals(eg bread + cooked steak -> burger), but not all(eg flour->dough). Some combine instantly(eg sliced tomatoes + plate -> tomato meal), but some require waiting(eg dough->bread in oven), others require active cooking(by holding down the action button for some duration)(eg flour->dough).</p> -<h2 id="clock">04. Clock</h2> -<p>Usual opening hours 12:00-22:00 (10 hours) last 20 irl minutes. This can be shortened or elongated (min 1 hour, max 24 hours from 00:00 to 24:00). Not all opening times are equal, some will result in more or fewer customers. But even when they’ll result in more or less customers will shift from day to day. But consecutive days will usually result in similar traffic graphs.</p> -<h2 id="activepassive-time-sink-actions">05. Active/Passive Time Sink Actions</h2> -<p>As stated <a href="#03-recipes">above</a>, some actions will not be instant. As an example, kneading flour into dough will require the player to hold down the action button for 5 seconds. But some are done passively, for example, baking dough into bread requires the player to put the dough into the oven, it will then start to be bake, and the player can return in 20 seconds to grab their baked bread.</p> -<h2 id="customers">06. Customers</h2> -<p>Customers come during opening hours, usually alone or in groups of 2. Customers will find a seat, and then will usually take 0:20 (ie 10 seconds) to decide what to order. Customers will always order 1 meal, once they decided on their order, most will only wait for their order to be fuffilled in 0:40 (ie 20 seconds). If not serviced in 0:40, they’ll get angry, leave and decrease your star rating. (also they won’t pay; also the entire group will leave) If serviced, you’ll get money from 1€ to 10€. If the entire group is serviced, you’ll also increase your star rating, and they’ll leave leaving dirty dishes behind, making room for other customers.</p> -<h2 id="money">07. Money</h2> -<p>Money is in €, it is gained by fuffiling orders. It can be used to buy raw supplies and unlocks.</p> -<h2 id="raw-supply">08. Raw Supply</h2> -<p>Can be ordered for the next day after closing and finishing off a day. Each raw item has a seperate expiry duration, some are short(eg steak must be used in 2 days), some are essentially infinite(eg water has a duration of 1000 days). When used during the day, items closer to the expiry date are used first.</p> -<h2 id="star-rating">09. Star Rating</h2> -<p>Goes from 0 stars to 5. Gained by fuffiling orders of an entire customer table (+0.03 stars). Lost by not doing so (-0.12 stars). Does nothing.</p> -<h2 id="unlocksupgrades">10. Unlocks/Upgrades</h2> -<p>Not all items, raw supplies, & recipes are unlocked at the start of the game. But they can be purchased at the end of a day. When an unlock(eg bread) is purchased, all of its assoicated items(ie flour, dough, & bread), raw item supplies(ie flour supply), and recipes(ie flour->dough and dough->bread, and if steak is also unlocked then eg burger will also be unlocked).</p> -<h1 id="game-setup-1">Game Setup</h1> -<h2 id="restaurant">Restaurant</h2> -<p>The game intially starts with 0€, and only tomatoes unlocked. You start out with an intial supply of 10 tomatoes in the tomato bag.</p> -<p>With the restaurant in this form</p> -<pre><code>+--------------------+ -|ctc.ctc.ctc.ctc.ctc.| -|.....c..............| -|c...c...+--www---dd-+ -|tc.ctc..|##...##W..#| -|c...c...w..........S| -|c.......w..######..T| -|tc......w..........F| -|c.....ct|##ss##ooo##| -+---dd---+-----------+</code></pre> -<ul> -<li><code>.</code>: nothing</li> -<li><code>+</code>,<code>-</code>,<code>|</code> : walls</li> -<li><code>d</code>: door</li> -<li><code>t</code>: table</li> -<li><code>c</code>: chair</li> -<li><code>#</code>: counter</li> -<li><code>w</code>: counter(window)</li> -<li><code>s</code>: sink</li> -<li><code>o</code>: oven</li> -<li><code>W</code>: watercooler</li> -<li><code>S</code>: [steak] freezer</li> -<li><code>T</code>: tomoto bag</li> -<li><code>F</code>: flour bag</li> -</ul> -<h2 id="demand-1">Demand</h2> -<p>Check the <a href="06.Demand.md">Demand section</a> for more info on what this means.</p> -<p>Demand scale starts at 0%. (from -50% to +50%)</p> -<p>Demand graph starts at offset 9 hours. <br /><img style="vertical-align:middle" src="https://latex.codecogs.com/png.latex?%5Cdisplaystyle%20%5Csin%5Cleft%283%5Csin%5Cfrac%7B%5Cleft%28hour%2Boffset%5Cright%29%7D%7B2%7D%5Cright%29%2B1" alt="\sin\left(3\sin\frac{\left(hour+offset\right)}{2}\right)+1" title="\sin\left(3\sin\frac{\left(hour+offset\right)}{2}\right)+1" /><br /></p> -<div class="sourceCode" id="cb2"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true"></a>demand = sin(<span class="dv">3</span>*sin((hour+offset)/<span class="dv">2</span>))+<span class="dv">1</span></span></code></pre></div> -<h1 id="pre-round">Pre Round</h1> -<p>!TODO</p> -<h1 id="round">Round</h1> -<p>!TODO</p> -<h1 id="post-round">Post Round</h1> -<p>!TODO</p> -<h1 id="demand-2">Demand</h1> -<p>Every tick(?TODO: specify further), a probability is sampled from <a href="#the-demand-equation">the demand equation</a>, that number is then multiplied by the <a href="#traffic-coefficent">traffic coefficent</a>, that number is then multiplied by the <a href="#the-demand-bias">demand bias</a>, that will give a probability on whether a customer will be spawned on this tick.</p> -<h3 id="the-demand-equation">The Demand Equation</h3> -<p><br /><img style="vertical-align:middle" src="https://latex.codecogs.com/png.latex?%5Cdisplaystyle%20%5Csin%5Cleft%283%5Csin%5Cfrac%7B%5Cleft%28hour%2Boffset%5Cright%29%7D%7B2%7D%5Cright%29%2B1" alt="\sin\left(3\sin\frac{\left(hour+offset\right)}{2}\right)+1" title="\sin\left(3\sin\frac{\left(hour+offset\right)}{2}\right)+1" /><br /></p> -<div class="sourceCode" id="cb3"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true"></a>demand = sin(<span class="dv">3</span>*sin((hour+offset)/<span class="dv">2</span>))+<span class="dv">1</span></span></code></pre></div> -<p>(always nonnegative below 2)</p> -<p>every day the <code>offset</code> will be offset by <code>[-1,1]</code> multiplied by the <a href="#sway-coefficent">sway coefficent</a>.</p> -<div class="sourceCode" id="cb4"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true"></a>offset += rand(-<span class="dv">1</span>,<span class="dv">1</span>) * sway</span></code></pre></div> -<h3 id="the-demand-bias">The Demand Bias</h3> -<p>Demand scale starts at 0%. (from -50% to +50%)</p> -<p>every day the <code>bias</code> will be offset by <code>[-10,10]</code> multiplied by the <a href="#sway-coefficent">sway coefficent</a>. (clamped to -50% & 50%).</p> -<div class="sourceCode" id="cb5"><pre class="sourceCode c"><code class="sourceCode c"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true"></a>bias += rand(-<span class="dv">10</span>,<span class="dv">10</span>) * sway</span> -<span id="cb5-2"><a href="#cb5-2" aria-hidden="true"></a>bias = clamp(bias,-<span class="dv">50</span>,<span class="dv">50</span>)</span></code></pre></div> -<h3 id="traffic-coefficent">Traffic Coefficent</h3> -<p>Usually 1% can be changed by the host.</p> -<h3 id="sway-coefficent">Sway Coefficent</h3> -<p>Usually 50% can be changed by the host.</p> -<h1 id="service">Service</h1> -<p>!TODO</p> -<h1 id="items-1">Items</h1> -<p>!TODO</p> -<h1 id="unlocksupgrades-1">Unlocks/Upgrades</h1> -<p>!TODO</p> -<h1 id="hud">HUD</h1> -<p>!TODO</p> diff --git a/test-client/main.ts b/test-client/main.ts index e0cc9b3c..4c755e55 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -1,9 +1,11 @@ /// <reference lib="dom" /> -import { Gamedata, ItemID, ItemIndex, PacketC, PacketS, PlayerID, TileIndex } from "./protocol.ts"; +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 } from "./util.ts"; +import { V2, add_v2, ceil_v2, floor_v2, length, lerp_exp_v2_mut, normalize, aabb_circle_distance } from "./util.ts"; + +const PLAYER_SIZE = 0.4; let ctx: CanvasRenderingContext2D; let canvas: HTMLCanvasElement; @@ -33,14 +35,31 @@ document.addEventListener("DOMContentLoaded", () => { setInterval(tick_update, 1000 / 25); }) -interface PlayerData { x: number; y: number, name: string, rot: number, hand?: ItemID, facing: V2 } +interface ItemData { + kind: ItemIndex, + x: number, + y: number, + progress?: number +} +interface PlayerData { + x: number, + y: number, + name: string, + rot: number, + item?: ItemData, + facing: V2, + vel: { x: number, y: number } +} +interface TileData { + x: number + y: number + kind: TileIndex + item?: ItemData +} const players = new Map<PlayerID, PlayerData>() -interface ItemData { kind: ItemIndex, tile?: V2, player?: PlayerID, tracking_player: boolean, x: number, y: number } -const items = new Map<ItemID, ItemData>() -interface TileData { x: number; y: number, kind: TileIndex, items: ItemID[], active_progress?: number } const tiles = new Map<string, TileData>() -let data: Gamedata = { item_names: [], tile_names: [] } +let data: Gamedata = { item_names: [], tile_names: [], spawn: [0, 0] } let my_id: PlayerID = -1 const camera: V2 = { x: 0, y: 0 } @@ -55,10 +74,12 @@ function packet(p: PacketC) { my_id = p.id data = p.data break; - case "add_player": - if (p.hand) items.set(p.hand[0], { kind: p.hand[1], player: p.id, tracking_player: true, x: 0, y: 0 }) - players.set(p.id, { x: 0, y: 0, name: p.name, rot: 0, hand: p.hand?.[0], facing: { x: 0, y: 1 } }) + case "add_player": { + let item = undefined + if (p.item) item = { kind: p.item, x: 0, y: 0 }; + players.set(p.id, { x: data.spawn[0], y: data.spawn[1], name: p.name, rot: 0, item, facing: { x: 0, y: 1 }, vel: { x: 0, y: 0 } }) break; + } case "remove_player": players.delete(p.id) break; @@ -71,33 +92,34 @@ function packet(p: PacketC) { break; } case "take_item": { - const item = items.get(p.item)! - item.tracking_player = true - item.player = p.player + const player = players.get(p.player)! + const tile = tiles.get(p.tile.toString())! + player.item = tile.item; + tile.item = undefined break; } case "put_item": { - const item = items.get(p.item)! - item.tracking_player = false - item.tile = { x: p.pos[0], y: p.pos[1] } + const player = players.get(p.player)! + const tile = tiles.get(p.tile.toString())! + tile.item = player.item; + player.item = undefined break; } - case "produce_item": - items.set(p.id, { kind: p.kind, x: p.pos[0] + 0.5, y: p.pos[1] + 0.5, tracking_player: false, tile: { x: p.pos[0], y: p.pos[1] } }) - tiles.get(p.pos.toString())!.items.push(p.id) + case "produce_item": { + const item = { kind: p.item, x: p.tile[0] + 0.5, y: p.tile[1] + 0.5 } + tiles.get(p.tile.toString())!.item = item break; + } case "consume_item": { - const t = tiles.get(p.pos.toString())! - t.items.splice(t.items.indexOf(p.id)) - items.delete(p.id) + tiles.get(p.tile.toString())!.item = undefined break; } case "set_active": { - tiles.get(p.tile.toString())!.active_progress = p.progress + tiles.get(p.tile.toString())!.item!.progress = p.progress break; } case "update_map": - tiles.set(p.pos.toString(), { x: p.pos[0], y: p.pos[1], kind: p.tile, items: [] }) + tiles.set(p.pos.toString(), { x: p.pos[0], y: p.pos[1], kind: p.tile }) break; default: console.warn("unknown packet", p); @@ -136,6 +158,7 @@ function tick_update() { send({ type: "position", pos: [p.x, p.y], rot: p.rot }) } + function frame_update(dt: number) { const p = players.get(my_id) if (!p) return @@ -147,26 +170,37 @@ function frame_update(dt: number) { 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 + p.vel.x += input.x * dt * 0.5 + p.vel.y += input.y * dt * 0.5 + p.x += p.vel.x + p.y += p.vel.y + collide_player(p) + lerp_exp_v2_mut(p.vel, { x: 0, y: 0 }, 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.) + const update_item = (item: ItemData, parent: V2) => { + lerp_exp_v2_mut(item, parent, dt * 10.) + } + for (const [_, player] of players) { + if (player.item) update_item(player.item, player) } + for (const [_, tile] of tiles) { + if (tile.item) update_item(tile.item, add_v2(tile, 0.5)) + } + 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() +let last_frame = performance.now() function draw() { - const now = Date.now() + const now = performance.now() frame_update((now - last_frame) / 1000) last_frame = now; if (ws.readyState == ws.CONNECTING) draw_wait("Connecting...") @@ -226,29 +260,26 @@ function draw_ingame() { 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() - } + ctx.beginPath() + ctx.arc(0, 0, PLAYER_SIZE, 0, Math.PI * 2) + ctx.fill() + + ctx.fillStyle = "rgb(103, 79, 7)" + ctx.beginPath() + ctx.arc(0, -0.2, PLAYER_SIZE, 0, Math.PI * 2) + ctx.fill() - for (const [_, item] of items) { - ctx.save() - ctx.translate(item.x, item.y) - const comps = ITEMS[data.item_names[item.kind]] ?? FALLBACK_ITEM - for (const c of comps) { - c(ctx) - } ctx.restore() + + if (player.item) draw_item(player.item) } for (const [_, tile] of tiles) { ctx.save() ctx.translate(tile.x, tile.y) - if (tile.active_progress !== null && tile.active_progress !== undefined) { - ctx.fillStyle = "rgba(115, 230, 58, 0.66)" - ctx.fillRect(0, 0, 1, tile.active_progress) - } + if (tile.item) draw_item(tile.item) ctx.restore() } @@ -257,6 +288,20 @@ function draw_ingame() { ctx.restore() } +function draw_item(item: ItemData) { + ctx.save() + ctx.translate(item.x, item.y) + 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, 0, 1, item.progress) + } + ctx.restore() +} + function draw_interact_target() { ctx.save() ctx.translate(interact_target_anim.x, interact_target_anim.y) @@ -286,3 +331,27 @@ function draw_grid() { } ctx.stroke() } + +function collide_player(p: PlayerData) { + const tiles_ignored = ["floor", "door", "chair"].map(t => data.tile_names.indexOf(t)) + for (const [_, tile] of tiles) { + if (tiles_ignored.includes(tile.kind)) continue + const d = aabb_circle_distance(tile.x, tile.y, tile.x + 1, tile.y + 1, p.x, p.y) + if (d > PLAYER_SIZE) continue + + const h = 0.01 + const d_sample_x = aabb_circle_distance(tile.x, tile.y, tile.x + 1, tile.y + 1, p.x + h, p.y) + const d_sample_y = aabb_circle_distance(tile.x, tile.y, tile.x + 1, tile.y + 1, p.x, p.y + h) + const grad_x = (d_sample_x - d) / h + const grad_y = (d_sample_y - d) / h + + p.x += (PLAYER_SIZE - d) * grad_x + p.y += (PLAYER_SIZE - d) * grad_y + + const vdotn = (grad_x * p.vel.x) + (grad_y * p.vel.y) + p.vel.x -= grad_x * vdotn + p.vel.y -= grad_y * vdotn + + + } +} diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 96657db7..98fcbf39 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -1,12 +1,12 @@ export type Vec2 = [number, number] // x, y export type PlayerID = number -export type ItemID = number export type ItemIndex = number export type TileIndex = number export interface Gamedata { item_names: string[], // Look-up table for ItemIndex tile_names: string[], // Look-up table for TileIndex + spawn: Vec2, // Where players spawn when they join. } export type PacketS = @@ -17,13 +17,12 @@ export type PacketS = export type PacketC = { type: "init", id: PlayerID, data: Gamedata } // You joined - | { type: "add_player", id: PlayerID, name: string, hand?: [ItemID, ItemIndex] } // Somebody else joined (or was already in the game) + | { type: "add_player", id: PlayerID, name: string, item?: ItemIndex } // Somebody else joined (or was already in the game) | { type: "remove_player", id: PlayerID } // Somebody left | { type: "position", player: PlayerID, pos: Vec2, rot: number } // Update the position of a players (your own position is included here) - | { type: "take_item", item: ItemID, player: PlayerID } // An item was taken from a tile - | { type: "put_item", item: ItemID, pos: Vec2 } // An item was put on a tile - | { type: "produce_item", id: ItemID, pos: Vec2, kind: ItemIndex } // A tile generated a new item - | { type: "consume_item", id: ItemID, pos: Vec2 } // A tile removed an item + | { type: "take_item", tile: Vec2, player: PlayerID } // An item was taken from a tile + | { type: "put_item", tile: Vec2, player: PlayerID } // An item was put on a tile + | { 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 } // A map tile was changed - diff --git a/test-client/tiles.ts b/test-client/tiles.ts index 57d96c0f..5eee2b32 100644 --- a/test-client/tiles.ts +++ b/test-client/tiles.ts @@ -55,6 +55,11 @@ function arrange_items(...items: string[]): Component[] { }) } +const door: Component = c => { + c.fillStyle = "#ff9843" + c.fillRect(-0.5, -0.1, 1, 0.2) +} + const plate = [circle(0.4, "#b6b6b6", "#f7f7f7", 0.02)]; export const FALLBACK_ITEM: Component[] = [circle(0.3, "#f0f")]; @@ -76,19 +81,25 @@ export const ITEMS: { [key: string]: Component[] } = { const table = [base("rgb(133, 76, 38)")]; const floor = [base("#333", "#222", 0.05)]; -const spawn = (i: string) => [base("#60701e", "#b9da37", 0.05), ...ITEMS[i]]; +const counter = [base("rgb(182, 172, 164)")]; +const crate = (i: string) => [base("#60701e", "#b9da37", 0.05), ...ITEMS[i]]; export const FALLBACK_TILE: Component[] = [base("#f0f")]; export const TILES: { [key: string]: Component[] } = { "floor": floor, "table": table, - "counter": [base("rgb(182, 172, 164)")], + "counter": counter, + "door": [...floor, door], + "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)")], "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)], - "pan": [...table, circle(0.4, "#444", "#999")], - "flour-spawn": spawn("flour"), - "dirty-plate-spawn": spawn("dirty-plate"), - "raw-steak-spawn": spawn("raw-steak"), - "tomato-spawn": spawn("tomato"), + "pan": [...counter, circle(0.4, "#444", "#999")], + "flour-crate": crate("flour"), + "dirty-plate-crate": crate("dirty-plate"), + "raw-steak-crate": crate("raw-steak"), + "tomato-crate": crate("tomato"), } diff --git a/test-client/util.ts b/test-client/util.ts index baebc61e..23679974 100644 --- a/test-client/util.ts +++ b/test-client/util.ts @@ -21,3 +21,16 @@ export function add_v2(p: V2, o: V2 | number) { if (typeof o == "number") return { x: p.x + o, y: p.y + o } else return { x: p.x + o.x, y: p.y + o.y } } + +export function aabb_circle_distance( + min_x: number, + min_y: number, + max_x: number, + max_y: number, + px: number, + py: number +): number { + const dx = px - Math.max(min_x, Math.min(max_x, px)) + const dy = py - Math.max(min_y, Math.min(max_y, py)) + return Math.sqrt(dx * dx + dy * dy) +} |