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 2d39c97..12dc2e7 100644 --- a/src/activities/overworld/mod.rs +++ b/src/activities/overworld/mod.rs @@ -1,10 +1,13 @@ +use crate::{ + game::{Ghost, ZLayer}, + State +}; use comfy::{ - draw_circle, draw_rect_outline, draw_sprite, is_key_down, is_key_pressed, + draw_circle, draw_rect_outline, draw_sprite, error, is_key_down, is_key_pressed, main_camera_mut, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE }; -use log::info; - -use crate::game::{Ghost, ZLayer}; +use std::time::Instant; +use worldgen::MovementCost; pub mod worldgen; @@ -22,49 +25,143 @@ 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.as_vec2(), - 0.5, - RED, - ZLayer::Ghost.into() - ); + 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 { + 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()); + } + } + + 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()); + } + } + + 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()); + } + } + + 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()); + } + } + + // 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) => 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; - let mut ghost_pos = &mut state.ghost.overworld_pos; - // move player - if is_key_down(KeyCode::Up) { - ghost_pos.y += 1; - } - if is_key_down(KeyCode::Down) { - ghost_pos.y -= 1; - } - if is_key_down(KeyCode::Left) { - ghost_pos.x -= 1; - } - if is_key_down(KeyCode::Right) { - ghost_pos.x += 1; - } + 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( - *ghost_pos + IVec2::new(half_viewport.x, half_viewport.y) + rounded_ghost_pos + IVec2::new(half_viewport.x, half_viewport.y) ); state.overworld.get_or_generate_tile( - *ghost_pos + IVec2::new(half_viewport.x, -half_viewport.y) + rounded_ghost_pos + IVec2::new(half_viewport.x, -half_viewport.y) ); state.overworld.get_or_generate_tile( - *ghost_pos + IVec2::new(-half_viewport.x, half_viewport.y) + rounded_ghost_pos + IVec2::new(-half_viewport.x, half_viewport.y) ); state.overworld.get_or_generate_tile( - *ghost_pos + IVec2::new(-half_viewport.x, -half_viewport.y) + 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 3cfe2aa..809c7b6 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 { @@ -403,7 +402,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 +418,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); diff --git a/src/game.rs b/src/game.rs index aa9ac70..4215d64 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,16 +2,51 @@ use crate::{ activities::{house, overworld, Activity}, State }; -use comfy::{EngineContext, IVec2}; -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 + /// Max electric charge of the Ghost. pub max_charge: f32, - pub overworld_pos: IVec2 + + /// 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 * 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 { @@ -19,7 +54,11 @@ impl Default for Ghost { Self { charge: 1000.0, max_charge: 1000.0, - overworld_pos: IVec2::ZERO + + overworld_pos: Vec2::ZERO, + overworld_movement_pending: Vec2::ZERO, + overworld_movement_speed: 0.0, + overworld_pos_last_update: Instant::now() } } }