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:
+160
-2
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -94,10 +151,111 @@ impl Actor {
|
|||||||
placed = true;
|
placed = true;
|
||||||
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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user