/*
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, Involvement};
use hurrycurry_protocol::{
glam::{IVec2, Vec2},
movement::MovementBase,
Gamedata, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC, PacketS, PlayerID,
RecipeIndex, 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,
active: Option,
}
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));
}
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_s(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,
active: None,
})
}
PacketC::ClearProgress { item } => self.get_item(item).as_mut().unwrap().active = None,
PacketC::SetProgress {
item,
position,
speed,
player,
warn,
} => {
self.get_item(item).as_mut().unwrap().active = Some(Involvement {
position,
speed,
player,
warn,
recipe: RecipeIndex(0),
});
}
PacketC::ServerMessage { .. } => {
// TODO
}
PacketC::Score(score) => {
self.score = score;
}
PacketC::SetIngame { state: _, lobby: _ } => {
// TODO
}
PacketC::Communicate {
player,
message,
timeout: Some(timeout),
} => {
if let Some(player) = self.players.get_mut(&player) {
player.message_persist = message.map(|m| (m, timeout));
}
}
_ => (),
}
}
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.);
if let Some(active) = &mut self.active {
active.position += active.speed * dt;
}
}
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(Involvement { position, warn, .. }) = self.active {
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(position, 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)
}
}
}