v0.7: A* pathfinding, mouse control, step-up, jump limits

- A* pathfinding for actor (find_path in actors.rs)
- Right-click sets target (actor_goto), finds ground below air clicks
- Actor follows path with horizontal velocity + jumping
- Step-up: actor climbs 1-4px small edges/obstacles
- Jump height limited to 40px in pathfinding (avoids unreachable targets)
- Powder displacement when walking through sand/dirt
- Actor collision: solids block, powders pass-through
- Fallback: direct movement if no path found
This commit is contained in:
2026-07-10 19:01:51 +02:00
parent f5d16ffe64
commit 1d467429da
5 changed files with 204 additions and 7 deletions
+160 -2
View File
@@ -1,4 +1,6 @@
use crate::grid::Grid; use crate::grid::Grid;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
pub struct Actor { pub struct Actor {
pub x: f32, pub x: f32,
@@ -8,15 +10,54 @@ pub struct Actor {
pub vx: f32, pub vx: f32,
pub vy: f32, pub vy: f32,
pub color: [u8; 3], pub color: [u8; 3],
path: Vec<(i32, i32)>,
path_index: usize,
speed: f32,
} }
impl Actor { impl Actor {
pub fn new(x: f32, y: f32, w: u32, h: u32, color: [u8; 3]) -> Self { pub fn new(x: f32, y: f32, w: u32, h: u32, color: [u8; 3]) -> Self {
Self { x, y, w, h, vx: 0.0, vy: 0.0, color } Self { x, y, w, h, vx: 0.0, vy: 0.0, color, path: Vec::new(), path_index: 0, speed: 120.0 }
}
pub fn goto(&mut self, grid: &Grid, target_x: f32, target_y: f32) {
let sx = self.x.round() as i32;
let sy = (self.y + self.h as f32 / 2.0).round() as i32;
let tx = target_x.round() as i32;
let ty = target_y.round() as i32;
self.path = find_path(grid, self.w, self.h, sx, sy, tx, ty);
if self.path.is_empty() {
self.path = vec![(tx, ty)];
}
self.path_index = 0;
} }
pub fn update(&mut self, grid: &Grid, dt: f32) { pub fn update(&mut self, grid: &Grid, dt: f32) {
const GRAVITY: f32 = 500.0; const GRAVITY: f32 = 500.0;
if !self.path.is_empty() && self.path_index < self.path.len() {
let (wx, wy) = self.path[self.path_index];
let cx = self.x + self.w as f32 / 2.0;
let cy = self.y + self.h as f32 / 2.0;
let dx = wx as f32 - cx;
let dy = wy as f32 - cy;
let dist = (dx * dx + dy * dy).sqrt();
if dist < 3.0 {
self.path_index += 1;
if self.path_index >= self.path.len() {
self.vx = 0.0;
self.path.clear();
}
} else {
let spd = self.speed.min(dist * 10.0);
self.vx = dx / dist * spd;
if dy < -10.0 && self.vy == 0.0 {
self.vy = -280.0;
}
}
}
self.vy += GRAVITY * dt; self.vy += GRAVITY * dt;
self.vy = self.vy.clamp(-600.0, 600.0); self.vy = self.vy.clamp(-600.0, 600.0);
@@ -24,7 +65,23 @@ impl Actor {
let new_y = (self.y + self.vy * dt).clamp(0.0, grid.height as f32 - self.h as f32); let new_y = (self.y + self.vy * dt).clamp(0.0, grid.height as f32 - self.h as f32);
if self.vx != 0.0 && self.collides_at(grid, new_x, self.y) { if self.vx != 0.0 && self.collides_at(grid, new_x, self.y) {
let mut stepped = false;
for step in 1u32..=4 {
let try_y = self.y - step as f32;
if try_y >= 0.0 && !self.collides_at(grid, new_x, try_y) {
self.x = new_x;
self.y = try_y;
self.vy = 0.0;
stepped = true;
break;
}
}
if stepped {
return;
}
if !stepped {
self.vx = 0.0; self.vx = 0.0;
}
} else { } else {
self.x = new_x; self.x = new_x;
} }
@@ -95,9 +152,110 @@ impl Actor {
break; break;
} }
} }
}
if placed { break; } if placed { break; }
} }
} }
} }
} }
}
#[derive(Clone, Eq, PartialEq)]
struct Node {
x: i32,
y: i32,
g: i32,
f: i32,
}
impl Ord for Node {
fn cmp(&self, other: &Self) -> Ordering {
other.f.cmp(&self.f)
}
}
impl PartialOrd for Node {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn walkable(grid: &Grid, aw: u32, ah: u32, x: i32, y: i32) -> bool {
for dy in 0i32..ah as i32 {
for dx in 0i32..aw as i32 {
let gx = x + dx;
let gy = y + dy;
if gx < 0 || gy < 0 || gx >= grid.width as i32 || gy >= grid.height as i32 {
return false;
}
let mat = grid.get(gx as u32, gy as u32);
let props = &crate::material::PROPS[mat as usize];
if props.is_solid && !props.is_powder {
return false;
}
}
}
true
}
fn find_path(grid: &Grid, aw: u32, ah: u32, sx: i32, sy: i32, tx: i32, ty: i32) -> Vec<(i32, i32)> {
if !walkable(grid, aw, ah, tx, ty) {
return Vec::new();
}
let w = grid.width as i32;
let h = grid.height as i32;
let mut open = BinaryHeap::new();
let mut g_scores = vec![i32::MAX; (w * h) as usize];
let mut came_from = vec![-1i32; (w * h) as usize];
let start_idx = (sy * w + sx) as usize;
g_scores[start_idx] = 0;
let h_start = (tx - sx).abs() + (ty - sy).abs();
open.push(Node { x: sx, y: sy, g: 0, f: h_start });
let dirs = [(0, -1), (1, 0), (0, 1), (-1, 0), (-1, -1), (1, -1), (1, 1), (-1, 1)];
while let Some(current) = open.pop() {
if current.x == tx && current.y == ty {
let mut path = Vec::new();
let mut cx = tx;
let mut cy = ty;
path.push((cx, cy));
let mut idx = (cy * w + cx) as usize;
while came_from[idx] != -1 {
let prev = came_from[idx];
cx = prev % w;
cy = prev / w;
path.push((cx, cy));
idx = (cy * w + cx) as usize;
}
path.reverse();
if path.len() > 1 {
path.remove(0);
}
return path;
}
for &(dx, dy) in &dirs {
let nx = current.x + dx;
let ny = current.y + dy;
if nx < 0 || ny < 0 || nx >= w || ny >= h { continue; }
if !walkable(grid, aw, ah, nx, ny) { continue; }
if ny < current.y && (current.y - ny) > 40 { continue; }
let cost = if dx != 0 && dy != 0 { 14 } else { 10 };
let tentative_g = current.g + cost;
let nidx = (ny * w + nx) as usize;
if tentative_g < g_scores[nidx] {
g_scores[nidx] = tentative_g;
came_from[nidx] = (current.y * w + current.x) as i32;
let h = (tx - nx).abs() + (ty - ny).abs();
open.push(Node { x: nx, y: ny, g: tentative_g, f: tentative_g + h });
}
}
}
Vec::new()
}
+23 -1
View File
@@ -163,7 +163,7 @@ pub fn move_actor(dx: f32, dy: f32) {
if let Some(ref mut a) = e.actor { if let Some(ref mut a) = e.actor {
a.vx = dx * 150.0; a.vx = dx * 150.0;
if dy < 0.0 && a.vy == 0.0 { if dy < 0.0 && a.vy == 0.0 {
a.vy = -400.0; a.vy = -280.0;
} }
} }
}); });
@@ -179,3 +179,25 @@ pub fn update_actor(dt: f32) {
} }
}); });
} }
#[wasm_bindgen]
pub fn actor_goto(x: f32, y: f32) {
with_engine(|e| {
if let Some(ref mut a) = e.actor {
let ground_y = find_ground(&e.grid, x as i32, y as i32);
a.goto(&e.grid, x, ground_y as f32);
}
});
}
fn find_ground(grid: &Grid, x: i32, mut y: i32) -> i32 {
while y < grid.height as i32 {
let mat = grid.get(x as u32, y as u32);
let props = &crate::material::PROPS[mat as usize];
if props.is_solid && !props.is_powder {
return y - 1;
}
y += 1;
}
y
}
+3
View File
@@ -13,6 +13,7 @@ import __wbg_init, {
actor_y, actor_y,
move_actor, move_actor,
update_actor, update_actor,
actor_goto,
} from "../pkg/atomic_engine.js"; } from "../pkg/atomic_engine.js";
export const M = { export const M = {
@@ -89,6 +90,7 @@ export interface Engine {
actorY: () => number; actorY: () => number;
moveActor: (dx: number, dy: number) => void; moveActor: (dx: number, dy: number) => void;
updateActor: (dt: number) => void; updateActor: (dt: number) => void;
actorGoto: (x: number, y: number) => void;
memory: WebAssembly.Memory; memory: WebAssembly.Memory;
} }
@@ -111,5 +113,6 @@ export async function createEngine(gridWidth: number, gridHeight: number): Promi
actorY: () => actor_y(), actorY: () => actor_y(),
moveActor: (dx, dy) => move_actor(dx, dy), moveActor: (dx, dy) => move_actor(dx, dy),
updateActor: (dt) => update_actor(dt), updateActor: (dt) => update_actor(dt),
actorGoto: (x, y) => actor_goto(x, y),
}; };
} }
+10 -1
View File
@@ -3,6 +3,7 @@ export class Input {
mouseX = 0; mouseX = 0;
mouseY = 0; mouseY = 0;
mouseDown = false; mouseDown = false;
mouseRight = false;
wheel = 0; wheel = 0;
constructor() { constructor() {
@@ -12,11 +13,19 @@ export class Input {
this.mouseX = e.clientX; this.mouseX = e.clientX;
this.mouseY = e.clientY; this.mouseY = e.clientY;
}); });
window.addEventListener("mousedown", () => (this.mouseDown = true)); window.addEventListener("mousedown", (e) => {
if (e.button === 2) {
this.mouseRight = true;
e.preventDefault();
} else {
this.mouseDown = true;
}
});
window.addEventListener("mouseup", () => (this.mouseDown = false)); window.addEventListener("mouseup", () => (this.mouseDown = false));
window.addEventListener("wheel", (e) => { window.addEventListener("wheel", (e) => {
this.wheel += e.deltaY > 0 ? -1 : 1; this.wheel += e.deltaY > 0 ? -1 : 1;
}); });
window.addEventListener("contextmenu", (e) => e.preventDefault());
} }
isDown(key: string): boolean { isDown(key: string): boolean {
+5
View File
@@ -210,6 +210,11 @@ async function main() {
const g = screenToGrid(input.mouseX, input.mouseY); const g = screenToGrid(input.mouseX, input.mouseY);
engine.fillCircle(g.x, g.y, 6, paintMat); engine.fillCircle(g.x, g.y, 6, paintMat);
} }
if (input.mouseRight) {
const g = screenToGrid(input.mouseX, input.mouseY);
engine.actorGoto(g.x, g.y);
input.mouseRight = false;
}
camera.update(dt); camera.update(dt);
engine.updateRender( engine.updateRender(