diff options
| -rw-r--r-- | data/map.yaml | 6 | ||||
| -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 | ||||
| -rw-r--r-- | test-client/main.ts | 3 | 
8 files changed, 245 insertions, 11 deletions
| diff --git a/data/map.yaml b/data/map.yaml index e85b6ba2..c3564eb8 100644 --- a/data/map.yaml +++ b/data/map.yaml @@ -9,9 +9,13 @@ map:      - "|tc......w..........F|"      - "|c.....ct|##ss#oopp#X|"      - "+---dd---+-----------+" +    - "......................" +    - "..................!..." +    - "......................"  tiles: -    "~": spawn +    "~": chef-spawn +    "!": customer-spawn      ".": floor      "+": wall      "-": wall 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; diff --git a/test-client/main.ts b/test-client/main.ts index 2a95d3c2..f208eb34 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -70,6 +70,7 @@ let data: Gamedata = { item_names: [], tile_names: [], spawn: [0, 0] }  let my_id: PlayerID = -1  const camera: V2 = { x: 0, y: 0 } +const camera_zoom = 0.05  const interact_target_anim: V2 = { x: 0, y: 0 }  let interacting: V2 | undefined;  let scale = 0 @@ -271,7 +272,7 @@ function draw_ingame() {      ctx.fillStyle = "#111"      ctx.fillRect(0, 0, canvas.width, canvas.height) -    scale = Math.min(canvas.width, canvas.height) / 10; +    scale = Math.min(canvas.width, canvas.height) * camera_zoom;      ctx.save()      ctx.translate(canvas.width / 2, canvas.height / 2)      ctx.scale(scale, scale) | 
