diff options
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/customer.rs | 210 | ||||
-rw-r--r-- | server/src/data.rs | 19 | ||||
-rw-r--r-- | server/src/game.rs | 12 | ||||
-rw-r--r-- | server/src/lib.rs | 1 | ||||
-rw-r--r-- | server/src/main.rs | 3 | ||||
-rw-r--r-- | server/src/protocol.rs | 2 |
6 files changed, 238 insertions, 9 deletions
diff --git a/server/src/customer.rs b/server/src/customer.rs new file mode 100644 index 00000000..ea63f93a --- /dev/null +++ b/server/src/customer.rs @@ -0,0 +1,210 @@ +use crate::{ + data::Gamedata, + game::Game, + protocol::{PacketC, PacketS, PlayerID}, +}; +use glam::{IVec2, Vec2}; +use log::{error, info}; +use std::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet, VecDeque}, + sync::Arc, + time::Duration, +}; +use tokio::{ + sync::{broadcast, RwLock}, + time::interval, +}; + +struct DemandState { + data: Gamedata, + walkable: HashSet<IVec2>, + chairs: HashSet<IVec2>, + customers: Vec<Customer>, +} + +enum CustomerState { + WalkingToChair { target: Vec2 }, + Waiting, +} + +struct Customer { + id: PlayerID, + position: Vec2, + facing: Vec2, + vel: Vec2, + state: CustomerState, +} + +pub async fn customer(game: Arc<RwLock<Game>>, mut grx: broadcast::Receiver<PacketC>) { + let mut state = DemandState { + walkable: Default::default(), + chairs: Default::default(), + customers: Default::default(), + data: Gamedata::default(), + }; + let initial = game.write().await.prime_client(-1); + for p in initial { + match p { + PacketC::Init { data, .. } => { + state.data = data; + } + PacketC::UpdateMap { pos, tile } => { + let tilename = &state.data.tile_names[tile]; + if tilename == "floor" || tilename == "door" { + state.walkable.insert(pos); + } + if tilename == "chair" { + state.chairs.insert(pos); + } + } + _ => (), + } + } + + let mut interval = interval(Duration::from_millis(40)); + let mut packets_out = Vec::new(); + loop { + tokio::select! { + packet = grx.recv() => { + match packet.unwrap() { + // TODO handle map update + _ => () + } + } + _ = interval.tick() => { + state.tick(&mut packets_out, 0.04); + for (player,packet) in packets_out.drain(..) { + if let Err(e) = game.write().await.packet_in(player, packet) { + error!("customer misbehaved: {e}") + } + } + } + } + } +} + +impl DemandState {} +impl DemandState { + pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) { + if self.customers.is_empty() { + packets_out.push(( + -1, + PacketS::Join { + name: "George".to_string(), + character: 0, + }, + )); + self.customers.push(Customer { + id: -1, + position: self.data.customer_spawn, + facing: Vec2::X, + vel: Vec2::ZERO, + state: CustomerState::WalkingToChair { + target: Vec2::new(2., 2.), + }, + }); + } + + for p in &mut self.customers { + match p.state { + CustomerState::WalkingToChair { target } => { + packets_out.push(( + p.id, + move_player(p, &self.walkable, target - p.position, dt), + )); + if target.distance(p.position) < 0.5 { + p.state = CustomerState::Waiting; + } + } + CustomerState::Waiting => (), + } + } + } +} + +pub fn find_path(map: &HashSet<IVec2>, from: IVec2, to: IVec2) -> VecDeque<IVec2> { + #[derive(Debug, PartialEq, Eq)] + struct Open(i32, IVec2, IVec2); + impl PartialOrd for Open { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.0.partial_cmp(&other.0) + } + } + impl Ord for Open { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } + } + + let mut visited = HashMap::new(); + let mut open = BinaryHeap::new(); + open.push(Open(1, from, from)); + + while let Some(Open(_, p, f)) = open.pop() { + if !visited.contains_key(&p) { + continue; + } + visited.insert(p, f); + if p == to { + break; + } + for d in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { + open.push(Open(d.distance_squared(to), p + d, p)); + } + } + + let mut path = VecDeque::new(); + path.push_back(to); + + // loop { + // path. + // } + + path +} + +fn move_player(p: &mut Customer, map: &HashSet<IVec2>, direction: Vec2, dt: f32) -> PacketS { + let direction = direction.normalize(); + if direction.length() > 0.1 { + p.facing = direction + (p.facing - direction) * (-dt * 10.).exp(); + } + let rot = p.facing.x.atan2(p.facing.y); + p.vel += direction * dt * 0.5; + p.position += p.vel; + p.vel = p.vel * (-dt * 5.).exp(); + collide_player(p, map); + PacketS::Position { + pos: p.position, + rot, + } +} + +const PLAYER_SIZE: f32 = 0.4; +fn collide_player(p: &mut Customer, map: &HashSet<IVec2>) { + for xo in -1..=1 { + for yo in -1..=1 { + let tile = IVec2::new(xo, yo) + p.position.as_ivec2(); + if map.contains(&tile) { + continue; + } + let tile = tile.as_vec2(); + let d = aabb_circle_distance(tile, tile + Vec2::ONE, p.position); + if d > PLAYER_SIZE { + continue; + } + let h = 0.01; + let d_sample_x = + aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(h, 0.)); + let d_sample_y = + aabb_circle_distance(tile, tile + Vec2::ONE, p.position + Vec2::new(0., h)); + let grad = (Vec2::new(d_sample_x, d_sample_y) - d) / h; + + p.position += (PLAYER_SIZE - d) * grad; + p.vel -= grad * grad.dot(p.vel); + } + } +} +fn aabb_circle_distance(min: Vec2, max: Vec2, p: Vec2) -> f32 { + (p - p.clamp(min, max)).length() +} diff --git a/server/src/data.rs b/server/src/data.rs index 6affccb5..9f03b1ea 100644 --- a/server/src/data.rs +++ b/server/src/data.rs @@ -31,14 +31,15 @@ pub struct InitialMap { tiles: HashMap<String, String>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] 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 chef_spawn: Vec2, + pub customer_spawn: Vec2, } pub fn build_gamedata(recipes_in: Vec<RecipeDecl>, map_in: InitialMap) -> Gamedata { @@ -77,14 +78,19 @@ pub fn build_gamedata(recipes_in: Vec<RecipeDecl>, map_in: InitialMap) -> Gameda assert_eq!(outputs.next(), None, "{r2:?}"); } - let mut spawn = Vec2::new(0., 0.); + let mut chef_spawn = Vec2::new(0., 0.); + let mut customer_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(); + if tilename == "chef-spawn" { + chef_spawn = pos.as_vec2(); + tilename = "floor".to_owned(); + } + if tilename == "customer-spawn" { + customer_spawn = pos.as_vec2(); tilename = "floor".to_owned(); } let tile = register(&tile_names, tilename); @@ -97,7 +103,8 @@ pub fn build_gamedata(recipes_in: Vec<RecipeDecl>, map_in: InitialMap) -> Gameda initial_map, item_names: item_names.into_inner().unwrap(), tile_names: tile_names.into_inner().unwrap(), - spawn, + chef_spawn, + customer_spawn, } } diff --git a/server/src/game.rs b/server/src/game.rs index c9881e7c..9c1bdd29 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -59,6 +59,10 @@ impl Game { g } + pub fn tiles(&self) -> &HashMap<IVec2, Tile> { + &self.tiles + } + pub fn packet_out(&mut self) -> Option<PacketC> { self.packet_out.pop_front() } @@ -101,7 +105,11 @@ impl Game { Player { item: None, character, - position: self.data.spawn, + position: if player < 0 { + self.data.customer_spawn + } else { + self.data.chef_spawn + }, interacting: None, name: name.clone(), }, @@ -109,7 +117,7 @@ impl Game { self.packet_out.push_back(PacketC::AddPlayer { id: player, name, - position: self.data.spawn, + position: self.data.chef_spawn, character, item: None, }); diff --git a/server/src/lib.rs b/server/src/lib.rs index b40bcdd8..a326190d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -2,3 +2,4 @@ pub mod game; pub mod protocol; pub mod data; pub mod interaction; +pub mod customer; diff --git a/server/src/main.rs b/server/src/main.rs index 45ee39dd..e505195c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,6 +11,7 @@ use tokio::{ }; use tokio_tungstenite::tungstenite::Message; use undercooked::{ + customer::customer, data::build_gamedata, game::Game, protocol::{PacketC, PacketS}, @@ -53,6 +54,8 @@ async fn main() -> Result<()> { }); } + spawn(customer(game.clone(), rx.resubscribe())); + for id in 1.. { tokio::select! { r = raw_listener.accept() => { diff --git a/server/src/protocol.rs b/server/src/protocol.rs index 6fe8ccd8..34442263 100644 --- a/server/src/protocol.rs +++ b/server/src/protocol.rs @@ -2,7 +2,7 @@ use crate::data::Gamedata; use glam::{IVec2, Vec2}; use serde::{Deserialize, Serialize}; -pub type PlayerID = usize; +pub type PlayerID = i64; pub type ItemIndex = usize; pub type TileIndex = usize; pub type RecipeIndex = usize; |