Overworld Movement #9
5 changed files with 185 additions and 44 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2129,7 +2129,6 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comfy",
|
"comfy",
|
||||||
"heck",
|
"heck",
|
||||||
"log",
|
|
||||||
"resvg",
|
"resvg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
use crate::{
|
||||||
|
game::{Ghost, ZLayer},
|
||||||
|
State
|
||||||
|
};
|
||||||
use comfy::{
|
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
|
main_camera_mut, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE
|
||||||
};
|
};
|
||||||
use log::info;
|
use std::time::Instant;
|
||||||
|
use worldgen::MovementCost;
|
||||||
use crate::game::{Ghost, ZLayer};
|
|
||||||
|
|
||||||
pub mod worldgen;
|
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_rect_outline(coords.as_vec2(), Vec2::ONE, 0.1, RED, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draw_circle(
|
draw_circle(state.ghost.overworld_pos, 0.5, RED, ZLayer::Ghost.into());
|
||||||
state.ghost.overworld_pos.as_vec2(),
|
|
||||||
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();
|
let mut camera = main_camera_mut();
|
||||||
camera.center = Vec2::ZERO;
|
camera.center = Vec2::ZERO;
|
||||||
camera.zoom = 30.0;
|
camera.zoom = 30.0;
|
||||||
|
|
||||||
let mut ghost_pos = &mut state.ghost.overworld_pos;
|
|
||||||
|
|
||||||
// move player
|
// move player
|
||||||
if is_key_down(KeyCode::Up) {
|
update_move_player(state);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate more chunks if needed
|
// generate more chunks if needed
|
||||||
{
|
{
|
||||||
let half_viewport = (camera.world_viewport() * 0.5 + 3.0).as_ivec2();
|
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(
|
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(
|
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(
|
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(
|
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -403,7 +402,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 +418,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);
|
||||||
|
|
||||||
|
|
51
src/game.rs
51
src/game.rs
|
@ -2,16 +2,51 @@ use crate::{
|
||||||
activities::{house, overworld, Activity},
|
activities::{house, overworld, Activity},
|
||||||
State
|
State
|
||||||
};
|
};
|
||||||
use comfy::{EngineContext, IVec2};
|
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,
|
||||||
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 {
|
impl Default for Ghost {
|
||||||
|
@ -19,7 +54,11 @@ impl Default for Ghost {
|
||||||
Self {
|
Self {
|
||||||
charge: 1000.0,
|
charge: 1000.0,
|
||||||
max_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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue