From ff743e97b8b56e3448e393c0933d3ffaf9451c33 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 7 Jul 2024 08:49:19 +0000 Subject: [PATCH] Overworld Movement (#9) Co-authored-by: luckyturtledev Reviewed-on: https://msrd0.dev/spielemarmelade/turtlegame/pulls/9 Co-authored-by: Dominic Co-committed-by: Dominic --- Cargo.lock | 1 - Cargo.toml | 1 - src/activities/overworld/mod.rs | 160 ++++++++++++++++++++++++++- src/activities/overworld/worldgen.rs | 85 ++++++++++---- src/game.rs | 54 ++++++++- 5 files changed, 266 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e71edcb..b45ae10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2129,7 +2129,6 @@ version = "0.1.0" dependencies = [ "comfy", "heck", - "log", "resvg", ] diff --git a/Cargo.toml b/Cargo.toml index 3dfe2bc..38a2d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ opt-level = 3 [dependencies] comfy = { version = "0.4.0", features = ["wayland"] } -log = "0.4.22" [build-dependencies] heck = "0.5" diff --git a/src/activities/overworld/mod.rs b/src/activities/overworld/mod.rs index f8bee45..f22d84b 100644 --- a/src/activities/overworld/mod.rs +++ b/src/activities/overworld/mod.rs @@ -1,9 +1,10 @@ +use crate::{game::ZLayer, State}; use comfy::{ - draw_rect_outline, draw_sprite, main_camera_mut, EngineContext, IVec2, Vec2, RED, - WHITE + draw_circle, draw_rect_outline, draw_sprite, error, info, is_key_down, + main_camera_mut, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE }; - -use crate::game::ZLayer; +use std::time::Instant; +use worldgen::MovementCost; pub mod worldgen; @@ -21,11 +22,158 @@ pub fn draw(state: &crate::State, _engine: &comfy::EngineContext<'_>) { draw_rect_outline(coords.as_vec2(), Vec2::ONE, 0.1, RED, 10); } } + draw_circle(state.ghost.overworld_pos, 0.5, RED, ZLayer::Ghost.into()); } -pub fn update(state: &mut crate::State, _engine: &mut EngineContext<'_>) { +fn update_move_player(state: &mut State) { + let now = Instant::now(); + + // Are there any pending position updates? If so, we ignore all user input and execute + // the pending updates. + if state.ghost.overworld_movement_pending != Vec2::ZERO { + info!( + "Pending Movement: {:?}", + state.ghost.overworld_movement_pending + ); + state.ghost.update_overworld_pos(now); + return; + } + + // Otherwise, we check for user inputs, and update the pending movement accordingly. + let tile_pos = IVec2 { + x: state.ghost.overworld_pos.x.round() as _, + y: state.ghost.overworld_pos.y.round() as _ + }; + let Some(tile) = state.overworld.get_tile(tile_pos).copied() else { + error!("How can we be standing inside a non-generated tile?"); + return; + }; + + let mut requested_pos = None; + let mut requested_pos_diff = None; + let mut requested_cost_curr = None; + let mut requested_cost_new = None; + + if is_key_down(KeyCode::Up) { + let diff = IVec2 { x: 0, y: 1 }; + let new_pos = tile_pos + diff; + let new_tile = state.overworld.get_or_generate_tile(new_pos); + if new_tile.can_stand_inside() { + requested_pos = Some(new_pos); + requested_pos_diff = Some(diff); + requested_cost_curr = Some(tile.movement_cost_up()); + requested_cost_new = Some(new_tile.movement_cost_down()); + } else { + info!("Rejecting movement - cannot stand in the requested tile."); + } + } + + if is_key_down(KeyCode::Down) { + let diff = IVec2 { x: 0, y: -1 }; + let new_pos = tile_pos + diff; + let new_tile = state.overworld.get_or_generate_tile(new_pos); + if new_tile.can_stand_inside() { + requested_pos = Some(new_pos); + requested_pos_diff = Some(diff); + requested_cost_curr = Some(tile.movement_cost_down()); + requested_cost_new = Some(new_tile.movement_cost_up()); + } else { + info!("Rejecting movement - cannot stand in the requested tile."); + } + } + + if is_key_down(KeyCode::Left) { + let diff = IVec2 { x: -1, y: 0 }; + let new_pos = tile_pos + diff; + let new_tile = state.overworld.get_or_generate_tile(new_pos); + if new_tile.can_stand_inside() { + requested_pos = Some(new_pos); + requested_pos_diff = Some(diff); + requested_cost_curr = Some(tile.movement_cost_left()); + requested_cost_new = Some(new_tile.movement_cost_right()); + } else { + info!("Rejecting movement - cannot stand in the requested tile."); + } + } + + if is_key_down(KeyCode::Right) { + let diff = IVec2 { x: 1, y: 0 }; + let new_pos = tile_pos + diff; + let new_tile = state.overworld.get_or_generate_tile(new_pos); + if new_tile.can_stand_inside() { + requested_pos = Some(new_pos); + requested_pos_diff = Some(diff); + requested_cost_curr = Some(tile.movement_cost_right()); + requested_cost_new = Some(new_tile.movement_cost_left()); + } else { + info!("Rejecting movement - cannot stand in the requested tile."); + } + } + + // only continue if some movement was requested + let Some(_requested_pos) = requested_pos else { + return; + }; + let Some(requested_pos_diff) = requested_pos_diff else { + return; + }; + let Some(requested_cost_curr) = requested_cost_curr else { + return; + }; + let Some(requested_cost_new) = requested_cost_new else { + return; + }; + + state.ghost.overworld_movement_speed = match (requested_cost_curr, requested_cost_new) + { + // movement in this direction not possible + (MovementCost::Infinite, _) | (_, MovementCost::Infinite) => { + info!("Rejecting movement - movement cost is infinite"); + return; + }, + + // we are walking on a path + (MovementCost::Path, MovementCost::Path) => 10.0, + + // we are walking across an obstacle + (MovementCost::Obstacle, _) | (_, MovementCost::Obstacle) => 1.0, + + // we are walking on grass + _ => 5.0 + }; + state.ghost.overworld_movement_pending = Vec2 { + x: requested_pos_diff.x as _, + y: requested_pos_diff.y as _ + }; + state.ghost.overworld_pos_last_update = now; +} + +pub fn update(state: &mut State, _ctx: &mut EngineContext<'_>) { let mut camera = main_camera_mut(); camera.center = Vec2::ZERO; camera.zoom = 30.0; - state.overworld.get_or_generate_tile(IVec2::ZERO); + + // move player + update_move_player(state); + + // generate more chunks if needed + { + let half_viewport = (camera.world_viewport() * 0.5 + 3.0).as_ivec2(); + let rounded_ghost_pos = IVec2 { + x: state.ghost.overworld_pos.x.round() as _, + y: state.ghost.overworld_pos.y.round() as _ + }; + state.overworld.get_or_generate_tile( + rounded_ghost_pos + IVec2::new(half_viewport.x, half_viewport.y) + ); + state.overworld.get_or_generate_tile( + rounded_ghost_pos + IVec2::new(half_viewport.x, -half_viewport.y) + ); + state.overworld.get_or_generate_tile( + rounded_ghost_pos + IVec2::new(-half_viewport.x, half_viewport.y) + ); + state.overworld.get_or_generate_tile( + rounded_ghost_pos + IVec2::new(-half_viewport.x, -half_viewport.y) + ); + } } diff --git a/src/activities/overworld/worldgen.rs b/src/activities/overworld/worldgen.rs index 5af1dcd..83e9067 100644 --- a/src/activities/overworld/worldgen.rs +++ b/src/activities/overworld/worldgen.rs @@ -2,8 +2,7 @@ #![allow(dead_code)] use crate::assets::ASSETS; -use comfy::{IVec2, TextureHandle, UVec2}; -use log::info; +use comfy::{info, IVec2, TextureHandle, UVec2}; use std::collections::HashMap; pub enum MovementCost { @@ -117,28 +116,61 @@ impl Tile { } pub fn can_stand_inside(&self) -> bool { - unimplemented!() + #[allow(clippy::match_like_matches_macro)] // I believe this is better readable + match self { + Self::House { door: false, .. } => false, + _ => true + } } pub fn movement_cost_left(&self) -> MovementCost { - unimplemented!() + match self { + Self::Grass => MovementCost::Default, + Self::Path { left: true, .. } => MovementCost::Path, + Self::Path { left: false, .. } => MovementCost::Default, + Self::Fence { left: false, .. } => MovementCost::Obstacle, + _ => MovementCost::Infinite + } } pub fn movement_cost_right(&self) -> MovementCost { - unimplemented!() + match self { + Self::Grass => MovementCost::Default, + Self::Path { right: true, .. } => MovementCost::Path, + Self::Path { right: false, .. } => MovementCost::Default, + Self::Fence { right: false, .. } => MovementCost::Obstacle, + _ => MovementCost::Infinite + } } pub fn movement_cost_up(&self) -> MovementCost { - unimplemented!() + match self { + Self::Grass => MovementCost::Default, + Self::Path { top: true, .. } => MovementCost::Path, + Self::Path { top: false, .. } => MovementCost::Default, + Self::Fence { top: false, .. } => MovementCost::Obstacle, + _ => MovementCost::Infinite + } } pub fn movement_cost_down(&self) -> MovementCost { - unimplemented!() + match self { + Self::Grass => MovementCost::Default, + Self::Path { bottom: true, .. } => MovementCost::Path, + Self::Path { bottom: false, .. } => MovementCost::Default, + Self::Fence { bottom: false, .. } => MovementCost::Obstacle, + Self::House { door: true, .. } => MovementCost::Path, + _ => MovementCost::Infinite + } } pub fn can_enter_house(&self) -> bool { - unimplemented!() + #[allow(clippy::match_like_matches_macro)] // I believe this is better readable + match self { + Self::House { door: true, .. } => true, + _ => false + } } } /// The size of a chunk (both width and height). This value squared gives the amount of /// tiles in the chunk. -const CHUNK_SIZE: u32 = 100; +const CHUNK_SIZE: u32 = 50; /// Chunks #[derive(Debug)] @@ -286,7 +318,10 @@ impl Chunk { fence_vert, grass, house(ASSETS.overworld.house_bottom_left), - house(ASSETS.overworld.house_bottom_door), + Tile::House { + door: true, + texture: ASSETS.overworld.house_bottom_door + }, house(ASSETS.overworld.house_bottom_window), house(ASSETS.overworld.house_bottom_window), house(ASSETS.overworld.house_bottom_right), @@ -403,7 +438,15 @@ fn world_to_chunk_and_local_coords(world_coords: IVec2) -> (IVec2, UVec2) { } impl Overworld { - fn get_tile(&self, world_coords: IVec2) -> Option<&Tile> { + /// Return a [`Tile`] at the given world coordinates, or `None` if that tile has not + /// been generated yet. Uses engine/world cordinates. + pub fn get_tile(&self, world_coords: IVec2) -> Option<&Tile> { + let mut coords = world_coords; + coords.y *= -1; + self.get_tile_private(coords) + } + + fn get_tile_private(&self, world_coords: IVec2) -> Option<&Tile> { let (chunk_coords, local_chunk_coords) = world_to_chunk_and_local_coords(world_coords); @@ -411,15 +454,15 @@ impl Overworld { chunk.get_tile(local_chunk_coords) } - /// Return a [`Tile`] at the given world coordinates, or `None` if that tile has not - /// been generated yet. use engine/world cordinates. + /// Return a [`Tile`] at the given world coordinates. Generates tiles if necessary. + /// Uses engine/world cordinates. pub fn get_or_generate_tile(&mut self, world_coords: IVec2) -> &Tile { let mut coords = world_coords; coords.y *= -1; - self.get_or_generate_tiles_private(coords) + self.get_or_generate_tile_private(coords) } - fn get_or_generate_tiles_private(&mut self, world_coords: IVec2) -> &Tile { + fn get_or_generate_tile_private(&mut self, world_coords: IVec2) -> &Tile { let (chunk_coords, local_chunk_coords) = world_to_chunk_and_local_coords(world_coords); @@ -430,21 +473,21 @@ impl Overworld { chunk.get_tile(local_chunk_coords).unwrap() } - /// Iterate over all generated tiles and its engine/world cordinates. using a [`Tile`] at the given world coordinates, or `None` if that tile has not - /// been generated yet. + /// Iterate over all generated tiles in all generated chunks and their engine/world + /// cordinates. pub fn iter_tiles(&self) -> impl Iterator { - self.iter_tilese_private().map(|(coords, tile)| { + self.iter_tiles_private().map(|(coords, tile)| { let mut w_coords = coords; w_coords.y *= -1; (w_coords, tile) }) } - /// iterate over all tiles and its global coords - fn iter_tilese_private(&self) -> impl Iterator { + /// Iterate over all generated tiles in all generated chunks. + fn iter_tiles_private(&self) -> impl Iterator { self.chunks.iter().flat_map(|(chunk_coords, chunk)| { chunk.iter_tiles().map(|(local_coords, tile)| { - // never fail because chunksize fits alswas into i32 + // never fail because chunksize fits always into i32 let local_coords: IVec2 = local_coords.try_into().unwrap(); (local_coords + (*chunk_coords * CHUNK_SIZE as i32), tile) }) diff --git a/src/game.rs b/src/game.rs index 2a2abf2..936373e 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,22 +2,64 @@ use crate::{ activities::{house, overworld, Activity}, State }; -use comfy::EngineContext; -use std::ops::Sub; +use comfy::{EngineContext, Vec2}; +use std::{ops::Sub, time::Instant}; #[derive(Debug)] pub struct Ghost { - /// current electric charge of the Ghost + /// Current electric charge of the Ghost. pub charge: f32, - /// max electric charge of the Ghost - pub max_charge: f32 + /// Max electric charge of the Ghost. + pub max_charge: f32, + + /// The position of the ghost in the overworld. Expressed in tile coordinates, but + /// as a float as a ghost takes more than one tick to move. + pub overworld_pos: Vec2, + /// Pending movement of the ghost in the overworld. + pub overworld_movement_pending: Vec2, + /// The current movement speed of the ghost in tiles/sec. + pub overworld_movement_speed: f32, + /// The timestamp of the last overworld position update. + pub overworld_pos_last_update: Instant +} + +impl Ghost { + pub fn update_overworld_pos(&mut self, now: Instant) { + // This calculation is extremely simplistic. It will not work properly if both + // x and y of movement_pending are non-zero. But we only move left,right,up,down + // so that should never happen! + let secs = now + .duration_since(self.overworld_pos_last_update) + .as_secs_f32(); + let mut movement = self.overworld_movement_pending.signum() + * self.overworld_movement_speed + * secs; + + // limit the movement to the remaining movement + if self.overworld_movement_pending.x.abs() < movement.x.abs() { + movement.x = self.overworld_movement_pending.x; + } + if self.overworld_movement_pending.y.abs() < movement.y.abs() { + movement.y = self.overworld_movement_pending.y; + } + + // execute the movement + self.overworld_pos += movement; + self.overworld_movement_pending -= movement; + self.overworld_pos_last_update = now; + } } impl Default for Ghost { fn default() -> Self { Self { charge: 1000.0, - max_charge: 1000.0 + max_charge: 1000.0, + + overworld_pos: Vec2::ZERO, + overworld_movement_pending: Vec2::ZERO, + overworld_movement_speed: 0.0, + overworld_pos_last_update: Instant::now() } } }