diff --git a/engine/src/actors.rs b/engine/src/actors.rs index e62f5f7..eca9a8d 100644 --- a/engine/src/actors.rs +++ b/engine/src/actors.rs @@ -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 { + 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() +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index a4eb02a..f0cc00d 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -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 +} diff --git a/web/src/engine.ts b/web/src/engine.ts index cbe9275..dadd7fe 100644 --- a/web/src/engine.ts +++ b/web/src/engine.ts @@ -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), }; } diff --git a/web/src/input.ts b/web/src/input.ts index 69db687..4fc3210 100644 --- a/web/src/input.ts +++ b/web/src/input.ts @@ -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 { diff --git a/web/src/main.ts b/web/src/main.ts index 4ee2b87..be9de96 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -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(