/* Hurry Curry! - a game about cooking Copyright 2024 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use crate::{ config::Config, helper::InterpolateExt, render::{ misc::MiscTextures, sprite::{Sprite, SpriteDraw}, AtlasLayout, Renderer, }, tilemap::Tilemap, State, }; use hurrycurry_client_lib::{network::sync::Network, spatial_index::SpatialIndex}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, Gamedata, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC, PacketS, PlayerID, Score, TileIndex, }; use log::{info, warn}; use sdl2::{ keyboard::{KeyboardState, Scancode}, rect::Rect, }; use std::collections::{HashMap, HashSet}; pub struct Game { network: Network, data: Gamedata, tiles: HashMap, tilemap: Tilemap, walkable: HashSet, players: HashMap, players_spatial_index: SpatialIndex, items_removed: Vec, my_id: PlayerID, camera_center: Vec2, misc_textures: MiscTextures, item_sprites: Vec, movement_send_cooldown: f32, interacting: bool, score: Score, } pub struct Tile { _kind: TileIndex, item: Option, } pub struct Player { movement: MovementBase, item: Option, message_persist: Option<(Message, MessageTimeout)>, _name: String, _character: i32, interact_target_anim: Vec2, interact_target_anim_pressed: f32, } pub struct Item { position: Vec2, parent_position: Vec2, kind: ItemIndex, alive: f32, progress: Option<(f32, bool)>, } impl Game { pub fn new(mut network: Network, config: &Config, layout: &AtlasLayout) -> Self { network.queue_out.push_back(PacketS::Join { id: None, name: config.username.clone(), character: 0, }); Self { network, tiles: HashMap::new(), players: HashMap::new(), tilemap: Tilemap::default(), my_id: PlayerID(0), data: Gamedata::default(), walkable: HashSet::new(), movement_send_cooldown: 0., misc_textures: MiscTextures::init(layout), item_sprites: Vec::new(), items_removed: Vec::new(), interacting: false, score: Score::default(), players_spatial_index: SpatialIndex::default(), camera_center: Vec2::ZERO, } } pub fn tick( &mut self, dt: f32, keyboard: &KeyboardState, layout: &AtlasLayout, ) -> Option> { if let Err(e) = self.network.poll() { eprintln!("network error: {e}"); return Some(Box::new(State::Quit)); } // TODO perf for packet in self.network.queue_in.drain(..).collect::>() { self.packet_in(packet, layout); } let mut direction = IVec2::new( keyboard.is_scancode_pressed(Scancode::D) as i32 - keyboard.is_scancode_pressed(Scancode::A) as i32, keyboard.is_scancode_pressed(Scancode::S) as i32 - keyboard.is_scancode_pressed(Scancode::W) as i32, ) .as_vec2(); let boost = keyboard.is_scancode_pressed(Scancode::K); let interact = keyboard.is_scancode_pressed(Scancode::Space) | keyboard.is_scancode_pressed(Scancode::J); if interact { direction *= 0.; } self.movement_send_cooldown -= dt; let send_movement = self.movement_send_cooldown < 0.; if send_movement { self.movement_send_cooldown += 0.04 } self.score.time_remaining -= dt as f64; self.score.time_remaining -= self.score.time_remaining.max(0.); if interact != self.interacting { if interact { self.network.queue_out.push_back(PacketS::Interact { player: self.my_id, pos: Some(self.players[&self.my_id].movement.get_interact_target()), }); } else { self.network.queue_out.push_back(PacketS::Interact { player: self.my_id, pos: None, }); } self.interacting = interact; } if let Some(player) = self.players.get_mut(&self.my_id) { player.movement.input(direction, boost); if send_movement { self.network .queue_out .push_back(player.movement.movement_packet(self.my_id)); } player.interact_target_anim.exp_to( player.movement.get_interact_target().as_vec2() + Vec2::new(0., -0.4), dt * 20., ); player .interact_target_anim_pressed .exp_to(if interact { 1. } else { 0. }, dt * 10.); self.camera_center.exp_to(player.movement.position, dt * 5.); } for (&pid, player) in &mut self.players { player.movement.update(&self.walkable, dt); if let Some((_, timeout)) = &mut player.message_persist { timeout.remaining -= dt; if timeout.remaining < 0. { player.message_persist = None; } } self.players_spatial_index .update_entry(pid, player.movement.position); } self.players_spatial_index.all(|p1, pos1| { self.players_spatial_index.query(pos1, 2., |p2, _pos2| { if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) { a.movement.collide(&mut b.movement, dt) } }) }); for player in self.players.values_mut() { if let Some(item) = &mut player.item { item.parent_position = player.movement.position; item.tick(1., dt); } } for tile in self.tiles.values_mut() { if let Some(item) = &mut tile.item { item.tick(1., dt) } } self.items_removed.retain_mut(|i| { i.tick(0., dt); i.alive > 0.01 }); None } pub fn packet_in(&mut self, packet: PacketC, layout: &AtlasLayout) { match packet { PacketC::Joined { id } => self.my_id = id, PacketC::Data { data } => { self.tilemap.init(&data.tile_names, layout); self.item_sprites = data .item_names .iter() .map(|name| { Sprite::new( layout .get(&format!("{name}+a")) .copied() .unwrap_or_else(|| { warn!("no sprite for item {name:?}"); Rect::new(0, 0, 32, 24) }), Vec2::new(0., 0.0), 0.1, ) }) .collect(); self.data = data; } PacketC::UpdateMap { tile, kind, neighbors, } => { if let Some(kind) = kind { self.tiles.insert( tile, Tile { _kind: kind, item: None, }, ); if self.data.tile_collide[kind.0] { self.walkable.remove(&tile); } else { self.walkable.insert(tile); } } else { self.tiles.remove(&tile); self.walkable.remove(&tile); } self.tilemap.set(tile, kind, neighbors); } PacketC::AddPlayer { id, position, character, name, } => { info!("add player {} {name:?}", id.0); self.players.insert( id, Player { interact_target_anim: position, interact_target_anim_pressed: 0., _character: character, _name: name, message_persist: None, item: None, movement: MovementBase { position, input_direction: Vec2::ZERO, input_boost: false, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, boosting: false, stamina: 0., }, }, ); } PacketC::RemovePlayer { id } => { info!("remove player {}", id.0); self.players_spatial_index.remove_entry(id); self.players.remove(&id); } PacketC::Movement { player, pos, rot, boost, dir, } => { if player != self.my_id { if let Some(p) = self.players.get_mut(&player) { p.movement.position = pos; p.movement.rotation = rot; p.movement.input(dir, boost); } } } PacketC::MoveItem { from, to } => { let mut item = self.get_item(from).take(); if let Some(item) = &mut item { item.parent_position = self.get_location_position(to); } *self.get_item(to) = item; } PacketC::SetItem { location, item } => { let position = self.get_location_position(location); let slot = match location { ItemLocation::Tile(pos) => &mut self.tiles.get_mut(&pos).unwrap().item, ItemLocation::Player(pid) => &mut self.players.get_mut(&pid).unwrap().item, }; self.items_removed.extend(slot.take()); *slot = item.map(|kind| Item { kind, parent_position: position, alive: 0., position, progress: None, }) } PacketC::SetProgress { item, progress, warn, } => { self.get_item(item).as_mut().unwrap().progress = progress.map(|s| (s, warn)); } PacketC::ServerMessage { text: _ } => { // TODO } PacketC::Score(score) => { self.score = score; } PacketC::SetIngame { state: _, lobby: _ } => { // TODO } PacketC::Communicate { player, message, timeout, } => { if let Some(timeout) = timeout { if let Some(player) = self.players.get_mut(&player) { player.message_persist = message.map(|m| (m, timeout)); } } } PacketC::Error { message } => { warn!("server error: {message:?}") } _ => (), } } pub fn get_item(&mut self, location: ItemLocation) -> &mut Option { match location { ItemLocation::Tile(pos) => &mut self.tiles.get_mut(&pos).unwrap().item, ItemLocation::Player(pid) => &mut self.players.get_mut(&pid).unwrap().item, } } pub fn get_location_position(&self, location: ItemLocation) -> Vec2 { match location { ItemLocation::Tile(pos) => pos.as_vec2() + 0.5, ItemLocation::Player(p) => self.players[&p].movement.position, } } pub fn draw(&self, ctx: &mut Renderer) { ctx.set_world_view( -self.camera_center + (ctx.size / ctx.get_world_scale() / 2.), ctx.size.min_element() / 32. / 10., ); self.tilemap.draw(ctx); if let Some(me) = self.players.get(&self.my_id) { ctx.draw_world( self.misc_textures .interact_target .at(me.interact_target_anim) .tint( 100, 100 + (me.interact_target_anim_pressed * 150.) as u8, 100 + ((1. - me.interact_target_anim_pressed) * 150.) as u8, ), ) } for p in self.players.values() { p.draw(ctx, &self.item_sprites) } for tile in self.tiles.values() { if let Some(item) = &tile.item { item.draw(ctx, &self.item_sprites) } } for item in &self.items_removed { item.draw(ctx, &self.item_sprites) } } } impl Item { pub fn tick(&mut self, alive: f32, dt: f32) { self.position.exp_to(self.parent_position, dt * 20.); self.alive.exp_to(alive, dt * 20.) } pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) { ctx.draw_world( item_sprites[self.kind.0] .at(self.position) .alpha(self.alive), ); if let Some((progress, warn)) = self.progress { let (bg, fg) = if warn { ([100, 0, 0, 200], [255, 0, 0, 200]) } else { ([0, 100, 0, 200], [0, 255, 0, 200]) }; ctx.draw_world(SpriteDraw::overlay( ctx.misc_textures.solid, self.position + Vec2::new(-0.5, -1.3), Vec2::new(1., 0.2), Some(bg), )); ctx.draw_world(SpriteDraw::overlay( ctx.misc_textures.solid, self.position + Vec2::new(-0.5, -1.3), Vec2::new(progress, 0.2), Some(fg), )) } } } impl Player { pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) { ctx.draw_world( if self._character >= 0 { &ctx.misc_textures.chef } else { &ctx.misc_textures.customer } .at(self.movement.position), ); if let Some((message, _timeout)) = &self.message_persist { match message { Message::Text(_) => (), // TODO Message::Item(item) => { ctx.draw_world(ctx.misc_textures.itembubble.at(self.movement.position)); ctx.draw_world( item_sprites[item.0] .at(self.movement.position) .elevate(1.2) .scale(0.8), ); } _ => (), } } if let Some(item) = &self.item { item.draw(ctx, &item_sprites) } } }