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:
+163
-5
@@ -1,4 +1,6 @@
|
||||
use crate::grid::Grid;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
pub struct Actor {
|
||||
pub x: f32,
|
||||
@@ -8,15 +10,54 @@ pub struct Actor {
|
||||
pub vx: f32,
|
||||
pub vy: f32,
|
||||
pub color: [u8; 3],
|
||||
path: Vec<(i32, i32)>,
|
||||
path_index: usize,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
impl Actor {
|
||||
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) {
|
||||
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 = 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);
|
||||
|
||||
if self.vx != 0.0 && self.collides_at(grid, new_x, self.y) {
|
||||
self.vx = 0.0;
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
self.x = new_x;
|
||||
}
|
||||
@@ -93,11 +150,112 @@ impl Actor {
|
||||
grid.materials[idx] = 0;
|
||||
placed = true;
|
||||
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 {
|
||||
a.vx = dx * 150.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,
|
||||
move_actor,
|
||||
update_actor,
|
||||
actor_goto,
|
||||
} from "../pkg/atomic_engine.js";
|
||||
|
||||
export const M = {
|
||||
@@ -89,6 +90,7 @@ export interface Engine {
|
||||
actorY: () => number;
|
||||
moveActor: (dx: number, dy: number) => void;
|
||||
updateActor: (dt: number) => void;
|
||||
actorGoto: (x: number, y: number) => void;
|
||||
memory: WebAssembly.Memory;
|
||||
}
|
||||
|
||||
@@ -111,5 +113,6 @@ export async function createEngine(gridWidth: number, gridHeight: number): Promi
|
||||
actorY: () => actor_y(),
|
||||
moveActor: (dx, dy) => move_actor(dx, dy),
|
||||
updateActor: (dt) => update_actor(dt),
|
||||
actorGoto: (x, y) => actor_goto(x, y),
|
||||
};
|
||||
}
|
||||
|
||||
+10
-1
@@ -3,6 +3,7 @@ export class Input {
|
||||
mouseX = 0;
|
||||
mouseY = 0;
|
||||
mouseDown = false;
|
||||
mouseRight = false;
|
||||
wheel = 0;
|
||||
|
||||
constructor() {
|
||||
@@ -12,11 +13,19 @@ export class Input {
|
||||
this.mouseX = e.clientX;
|
||||
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("wheel", (e) => {
|
||||
this.wheel += e.deltaY > 0 ? -1 : 1;
|
||||
});
|
||||
window.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
isDown(key: string): boolean {
|
||||
|
||||
@@ -210,6 +210,11 @@ async function main() {
|
||||
const g = screenToGrid(input.mouseX, input.mouseY);
|
||||
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);
|
||||
engine.updateRender(
|
||||
|
||||
Reference in New Issue
Block a user