Overworld Movement (#9)
All checks were successful
Rust / rustfmt (push) Successful in 24s
Rust / clippy (push) Successful in 1m12s
Rust / build (push) Successful in 3m13s

Co-authored-by: luckyturtledev <git@lukas1818.de>
Reviewed-on: #9
Co-authored-by: Dominic <git@msrd0.de>
Co-committed-by: Dominic <git@msrd0.de>
This commit is contained in:
Dominic 2024-07-07 08:49:19 +00:00 committed by msrd0
parent c1474c51e5
commit ff743e97b8
5 changed files with 266 additions and 35 deletions

1
Cargo.lock generated
View file

@ -2129,7 +2129,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"comfy", "comfy",
"heck", "heck",
"log",
"resvg", "resvg",
] ]

View file

@ -16,7 +16,6 @@ opt-level = 3
[dependencies] [dependencies]
comfy = { version = "0.4.0", features = ["wayland"] } comfy = { version = "0.4.0", features = ["wayland"] }
log = "0.4.22"
[build-dependencies] [build-dependencies]
heck = "0.5" heck = "0.5"

View file

@ -1,9 +1,10 @@
use crate::{game::ZLayer, State};
use comfy::{ use comfy::{
draw_rect_outline, draw_sprite, main_camera_mut, EngineContext, IVec2, Vec2, RED, draw_circle, draw_rect_outline, draw_sprite, error, info, is_key_down,
WHITE main_camera_mut, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE
}; };
use std::time::Instant;
use crate::game::ZLayer; use worldgen::MovementCost;
pub mod worldgen; 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_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(); let mut camera = main_camera_mut();
camera.center = Vec2::ZERO; camera.center = Vec2::ZERO;
camera.zoom = 30.0; 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)
);
}
} }

View file

@ -2,8 +2,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::assets::ASSETS; use crate::assets::ASSETS;
use comfy::{IVec2, TextureHandle, UVec2}; use comfy::{info, IVec2, TextureHandle, UVec2};
use log::info;
use std::collections::HashMap; use std::collections::HashMap;
pub enum MovementCost { pub enum MovementCost {
@ -117,28 +116,61 @@ impl Tile {
} }
pub fn can_stand_inside(&self) -> bool { 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 { 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 { 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 { 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 { 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 { 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 /// The size of a chunk (both width and height). This value squared gives the amount of
/// tiles in the chunk. /// tiles in the chunk.
const CHUNK_SIZE: u32 = 100; const CHUNK_SIZE: u32 = 50;
/// Chunks /// Chunks
#[derive(Debug)] #[derive(Debug)]
@ -286,7 +318,10 @@ impl Chunk {
fence_vert, fence_vert,
grass, grass,
house(ASSETS.overworld.house_bottom_left), 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_window), house(ASSETS.overworld.house_bottom_window),
house(ASSETS.overworld.house_bottom_right), house(ASSETS.overworld.house_bottom_right),
@ -403,7 +438,15 @@ fn world_to_chunk_and_local_coords(world_coords: IVec2) -> (IVec2, UVec2) {
} }
impl Overworld { 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) = let (chunk_coords, local_chunk_coords) =
world_to_chunk_and_local_coords(world_coords); world_to_chunk_and_local_coords(world_coords);
@ -411,15 +454,15 @@ impl Overworld {
chunk.get_tile(local_chunk_coords) chunk.get_tile(local_chunk_coords)
} }
/// Return a [`Tile`] at the given world coordinates, or `None` if that tile has not /// Return a [`Tile`] at the given world coordinates. Generates tiles if necessary.
/// been generated yet. use engine/world cordinates. /// Uses engine/world cordinates.
pub fn get_or_generate_tile(&mut self, world_coords: IVec2) -> &Tile { pub fn get_or_generate_tile(&mut self, world_coords: IVec2) -> &Tile {
let mut coords = world_coords; let mut coords = world_coords;
coords.y *= -1; 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) = let (chunk_coords, local_chunk_coords) =
world_to_chunk_and_local_coords(world_coords); world_to_chunk_and_local_coords(world_coords);
@ -430,21 +473,21 @@ impl Overworld {
chunk.get_tile(local_chunk_coords).unwrap() 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 /// Iterate over all generated tiles in all generated chunks and their engine/world
/// been generated yet. /// cordinates.
pub fn iter_tiles(&self) -> impl Iterator<Item = (IVec2, &Tile)> { pub fn iter_tiles(&self) -> impl Iterator<Item = (IVec2, &Tile)> {
self.iter_tilese_private().map(|(coords, tile)| { self.iter_tiles_private().map(|(coords, tile)| {
let mut w_coords = coords; let mut w_coords = coords;
w_coords.y *= -1; w_coords.y *= -1;
(w_coords, tile) (w_coords, tile)
}) })
} }
/// iterate over all tiles and its global coords /// Iterate over all generated tiles in all generated chunks.
fn iter_tilese_private(&self) -> impl Iterator<Item = (IVec2, &Tile)> { fn iter_tiles_private(&self) -> impl Iterator<Item = (IVec2, &Tile)> {
self.chunks.iter().flat_map(|(chunk_coords, chunk)| { self.chunks.iter().flat_map(|(chunk_coords, chunk)| {
chunk.iter_tiles().map(|(local_coords, tile)| { 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(); let local_coords: IVec2 = local_coords.try_into().unwrap();
(local_coords + (*chunk_coords * CHUNK_SIZE as i32), tile) (local_coords + (*chunk_coords * CHUNK_SIZE as i32), tile)
}) })

View file

@ -2,22 +2,64 @@ use crate::{
activities::{house, overworld, Activity}, activities::{house, overworld, Activity},
State State
}; };
use comfy::EngineContext; use comfy::{EngineContext, Vec2};
use std::ops::Sub; use std::{ops::Sub, time::Instant};
#[derive(Debug)] #[derive(Debug)]
pub struct Ghost { pub struct Ghost {
/// current electric charge of the Ghost /// Current electric charge of the Ghost.
pub charge: f32, pub charge: f32,
/// max electric charge of the Ghost /// Max electric charge of the Ghost.
pub max_charge: f32 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 { impl Default for Ghost {
fn default() -> Self { fn default() -> Self {
Self { Self {
charge: 1000.0, 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()
} }
} }
} }