/* 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::{ helper::InterpolateExt, network::Network, render::{ misc::MiscTextures, sprite::{Sprite, SpriteDraw}, AtlasLayout, SpriteRenderer, }, tilemap::Tilemap, }; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, ClientGamedata, ItemIndex, ItemLocation, PacketC, PacketS, PlayerID, TileIndex, }; use log::{info, warn}; use sdl2::{ keyboard::{KeyboardState, Scancode}, rect::Rect, }; use std::collections::{HashMap, HashSet}; pub struct Game { network: Network, data: ClientGamedata, tiles: HashMap, tilemap: Tilemap, collision_map: HashSet, players: HashMap, items_removed: Vec, my_id: PlayerID, camera_center: Vec2, misc_textures: MiscTextures, item_sprites: Vec, movement_send_cooldown: f32, interacting: bool, score: Score, } #[derive(Debug, Default)] pub struct Score { points: i64, demands_failed: usize, demands_completed: usize, time_remaining: f32, } pub struct Tile { _kind: TileIndex, item: Option, } pub struct Player { movement: MovementBase, item: Option, _name: String, _character: i32, } pub struct Item { position: Vec2, parent_position: Vec2, kind: ItemIndex, alive: f32, progress: Option<(f32, bool)>, } impl Game { pub fn new(mut network: Network, layout: &AtlasLayout) -> Self { network .queue_out .push_back(hurrycurry_protocol::PacketS::Join { name: "light".to_string(), character: 0, }); Self { network, tiles: HashMap::new(), players: HashMap::new(), tilemap: Tilemap::default(), my_id: PlayerID(0), data: ClientGamedata::default(), collision_map: HashSet::new(), movement_send_cooldown: 0., misc_textures: MiscTextures::init(layout), item_sprites: Vec::new(), items_removed: Vec::new(), interacting: false, score: Score::default(), camera_center: Vec2::ZERO, } } pub fn tick(&mut self, dt: f32, keyboard: &KeyboardState, layout: &AtlasLayout) { self.network.poll(); // 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; self.score.time_remaining -= self.score.time_remaining.max(0.); if interact != self.interacting { if interact { self.network.queue_out.push_back(PacketS::Interact { pos: Some(self.players[&self.my_id].movement.get_interact_target()), }); } else { self.network .queue_out .push_back(PacketS::Interact { pos: None }); } self.interacting = interact; } if let Some(player) = self.players.get_mut(&self.my_id) { let movement_packet = player .movement .update(&self.collision_map, direction, boost, dt); if send_movement { self.network.queue_out.push_back(movement_packet); } self.camera_center.exp_to(player.movement.position, dt * 5.); } for (_pid, player) in &mut self.players { if let Some(item) = &mut player.item { item.parent_position = player.movement.position; item.tick(1., dt); } } for (_pos, tile) in &mut self.tiles { 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 }) } pub fn packet_in(&mut self, packet: PacketC, layout: &AtlasLayout) { match packet { PacketC::Init { 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.collision_map.remove(&tile); } else { self.collision_map.insert(tile); } } else { self.tiles.remove(&tile); self.collision_map.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 { _character: character, _name: name, item: None, movement: MovementBase { position, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, boosting: false, stamina: 0., }, }, ); } PacketC::RemovePlayer { id } => { info!("remove player {}", id.0); self.players.remove(&id); } PacketC::Position { player, pos, rot, boosting, } => { if player != self.my_id { if let Some(p) = self.players.get_mut(&player) { p.movement.position = pos; p.movement.rotation = rot; p.movement.boosting = boosting; } } } 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::Collide { player: _, force: _, } => (), PacketC::Communicate { .. } => { // TODO } PacketC::ServerMessage { text: _ } => { // TODO } PacketC::Score { points, demands_failed, demands_completed, time_remaining, } => { self.score.points = points; self.score.demands_completed = demands_completed; self.score.demands_failed = demands_failed; self.score.time_remaining = time_remaining.unwrap_or(-1.); } PacketC::SetIngame { state: _, lobby: _ } => { // TODO } 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 SpriteRenderer) { ctx.set_view(-self.camera_center + (ctx.size / ctx.get_scale() / 2.), 1.); self.tilemap.draw(ctx); if let Some(me) = self.players.get(&self.my_id) { let t = me.movement.get_interact_target(); ctx.draw_world( self.misc_textures .interact_target .at(t.as_vec2()) .tint(100, 100, 255), ) } for p in self.players.values() { ctx.draw_world(self.misc_textures.player.at(p.movement.position)); if let Some(item) = &p.item { item.draw(ctx, &self.item_sprites, &self.misc_textures) } } for tile in self.tiles.values() { if let Some(item) = &tile.item { item.draw(ctx, &self.item_sprites, &self.misc_textures) } } for item in &self.items_removed { item.draw(ctx, &self.item_sprites, &self.misc_textures) } } } 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 SpriteRenderer, item_sprites: &[Sprite], misc: &MiscTextures) { 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( misc.solid, self.position + Vec2::new(-0.5, -1.3), Vec2::new(1., 0.2), Some(bg), )); ctx.draw_world(SpriteDraw::overlay( misc.solid, self.position + Vec2::new(-0.5, -1.3), Vec2::new(progress, 0.2), Some(fg), )) } } }