v0.6: Actor system (rigidbody) replaces pixel player
- New actors.rs: Rigidbody Actor with gravity, grid collision, powder displacement - Actor rendered as colored rectangle on pixel buffer (zoom-scaled) - WASM API: spawn_actor, move_actor, update_actor, actor_x/y - WASD/Arrow keys control actor, camera follows with soft lerp - displace_powders pushes sand/dirt out of actor area - Collision: stops at solids, walks through powders - Updated AGENTS.md with actor documentation
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
use crate::grid::Grid;
|
||||
|
||||
pub struct Actor {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
pub vx: f32,
|
||||
pub vy: f32,
|
||||
pub color: [u8; 3],
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
pub fn update(&mut self, grid: &Grid, dt: f32) {
|
||||
const GRAVITY: f32 = 500.0;
|
||||
self.vy += GRAVITY * dt;
|
||||
self.vy = self.vy.clamp(-600.0, 600.0);
|
||||
|
||||
let new_x = (self.x + self.vx * dt).clamp(0.0, grid.width as f32 - self.w 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) {
|
||||
self.vx = 0.0;
|
||||
} else {
|
||||
self.x = new_x;
|
||||
}
|
||||
|
||||
if self.collides_at(grid, self.x, new_y) {
|
||||
if self.vy > 0.0 {
|
||||
let mut sy = new_y;
|
||||
while sy <= new_y + self.h as f32 && self.collides_at(grid, self.x, sy) {
|
||||
sy -= 1.0;
|
||||
}
|
||||
self.y = sy;
|
||||
} else {
|
||||
let mut sy = new_y;
|
||||
while sy >= new_y - self.h as f32 && self.collides_at(grid, self.x, sy) {
|
||||
sy += 1.0;
|
||||
}
|
||||
self.y = sy;
|
||||
}
|
||||
self.vy = 0.0;
|
||||
} else {
|
||||
self.y = new_y;
|
||||
}
|
||||
}
|
||||
|
||||
fn collides_at(&self, grid: &Grid, ax: f32, ay: f32) -> bool {
|
||||
let x0 = ax.floor() as u32;
|
||||
let y0 = ay.floor() as u32;
|
||||
let x1 = ((ax + self.w as f32 - 0.01).ceil() as u32).min(grid.width);
|
||||
let y1 = ((ay + self.h as f32 - 0.01).ceil() as u32).min(grid.height);
|
||||
|
||||
for gy in y0..y1 {
|
||||
for gx in x0..x1 {
|
||||
let mat = grid.get(gx, gy);
|
||||
let props = &crate::material::PROPS[mat as usize];
|
||||
if props.is_solid && !props.is_powder {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn displace_powders(&self, grid: &mut Grid) {
|
||||
let x0 = self.x.floor() as u32;
|
||||
let y0 = self.y.floor() as u32;
|
||||
let x1 = ((self.x + self.w as f32).ceil() as u32).min(grid.width);
|
||||
let y1 = ((self.y + self.h as f32).ceil() as u32).min(grid.height);
|
||||
|
||||
for gy in y0..y1 {
|
||||
for gx in x0..x1 {
|
||||
let mat = grid.get(gx, gy);
|
||||
let props = &crate::material::PROPS[mat as usize];
|
||||
if !props.is_powder { continue; }
|
||||
let mut placed = false;
|
||||
for dy in 0i32..=4i32 {
|
||||
for dx in -4i32..=4i32 {
|
||||
let sx = gx as i32 + dx;
|
||||
let sy = gy as i32 + dy;
|
||||
if !grid.in_bounds(sx, sy) { continue; }
|
||||
let su = (sx as u32, sy as u32);
|
||||
if su.0 >= x0 && su.0 < x1 && su.1 >= y0 && su.1 < y1 { continue; }
|
||||
if grid.get(su.0, su.1) == 0 {
|
||||
grid.set(su.0, su.1, mat);
|
||||
let idx = grid.index(gx, gy);
|
||||
grid.materials[idx] = 0;
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if placed { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
-2
@@ -1,12 +1,14 @@
|
||||
pub mod actors;
|
||||
pub mod grid;
|
||||
pub mod material;
|
||||
pub mod physics;
|
||||
pub mod render_buffer;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use actors::Actor;
|
||||
use grid::Grid;
|
||||
use physics::Physics;
|
||||
use render_buffer::RenderBuffer;
|
||||
use render_buffer::{ActorRect, RenderBuffer};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
thread_local! {
|
||||
@@ -17,6 +19,7 @@ struct Engine {
|
||||
grid: Grid,
|
||||
physics: Physics,
|
||||
render_buffer: RenderBuffer,
|
||||
actor: Option<Actor>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
@@ -41,6 +44,7 @@ pub fn init(width: u32, height: u32) {
|
||||
grid: Grid::new(width, height),
|
||||
physics: Physics::new(),
|
||||
render_buffer: RenderBuffer::new(320, 180),
|
||||
actor: None,
|
||||
};
|
||||
ENGINE.with(|cell| {
|
||||
*cell.borrow_mut() = Some(engine);
|
||||
@@ -106,7 +110,19 @@ pub fn render_height() -> u32 {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn update_render(cam_x: i32, cam_y: i32, zoom: f32) {
|
||||
with_engine(|e| e.render_buffer.render(&e.grid, cam_x, cam_y, zoom));
|
||||
with_engine(|e| {
|
||||
let mut actors = Vec::new();
|
||||
if let Some(ref a) = e.actor {
|
||||
actors.push(ActorRect {
|
||||
x: a.x,
|
||||
y: a.y,
|
||||
w: a.w,
|
||||
h: a.h,
|
||||
color: (a.color[0], a.color[1], a.color[2]),
|
||||
});
|
||||
}
|
||||
e.render_buffer.render(&e.grid, cam_x, cam_y, zoom, &actors);
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -115,3 +131,51 @@ pub fn reset() {
|
||||
e.grid = Grid::new(e.grid.width, e.grid.height);
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn spawn_actor(x: f32, y: f32) {
|
||||
with_engine(|e| {
|
||||
e.actor = Some(Actor::new(x, y, 10, 16, [255, 80, 80]));
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn actor_x() -> f32 {
|
||||
ENGINE.with(|cell| {
|
||||
let opt = cell.borrow();
|
||||
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||
engine.actor.as_ref().map(|a| a.x).unwrap_or(0.0)
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn actor_y() -> f32 {
|
||||
ENGINE.with(|cell| {
|
||||
let opt = cell.borrow();
|
||||
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||
engine.actor.as_ref().map(|a| a.y).unwrap_or(0.0)
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn move_actor(dx: f32, dy: f32) {
|
||||
with_engine(|e| {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn update_actor(dt: f32) {
|
||||
with_engine(|e| {
|
||||
if let Some(ref mut a) = e.actor {
|
||||
a.update(&e.grid, dt);
|
||||
a.displace_powders(&mut e.grid);
|
||||
a.vx = a.vx * 0.9;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ fn main() {
|
||||
|
||||
loop {
|
||||
physics.update(&mut grid, cam_x, cam_y, rw, rh, zoom);
|
||||
rb.render(&grid, cam_x, cam_y, zoom);
|
||||
rb.render(&grid, cam_x, cam_y, zoom, &[]);
|
||||
frame += 1;
|
||||
|
||||
if frame % 60 == 0 {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::grid::Grid;
|
||||
use crate::material::PROPS;
|
||||
|
||||
pub struct ActorRect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
pub color: (u8, u8, u8),
|
||||
}
|
||||
|
||||
pub struct RenderBuffer {
|
||||
pub pixels: Vec<u8>,
|
||||
pub width: u32,
|
||||
@@ -24,7 +32,7 @@ impl RenderBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32) {
|
||||
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32, actors: &[ActorRect]) {
|
||||
let rw = self.width as i32;
|
||||
let rh = self.height as i32;
|
||||
let total = (rw * rh) as usize;
|
||||
@@ -162,6 +170,27 @@ impl RenderBuffer {
|
||||
self.pixels[pi + 3] = color[3];
|
||||
}
|
||||
}
|
||||
|
||||
for actor in actors {
|
||||
let ax = ((actor.x - cam_x as f32) * zoom + rw as f32 / 2.0) as i32;
|
||||
let ay = ((actor.y - cam_y as f32) * zoom + rh as f32 / 2.0) as i32;
|
||||
let aw = (actor.w as f32 * zoom).max(1.0) as u32;
|
||||
let ah = (actor.h as f32 * zoom).max(1.0) as u32;
|
||||
let (cr, cg, cb) = actor.color;
|
||||
for dy in 0..ah.min(self.height) {
|
||||
for dx in 0..aw.min(self.width) {
|
||||
let spx = ax + dx as i32;
|
||||
let spy = ay + dy as i32;
|
||||
if spx >= 0 && spx < rw && spy >= 0 && spy < rh {
|
||||
let pi = ((spy * rw + spx) * 4) as usize;
|
||||
self.pixels[pi] = cr;
|
||||
self.pixels[pi + 1] = cg;
|
||||
self.pixels[pi + 2] = cb;
|
||||
self.pixels[pi + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spread_light(&mut self, py: i32, px: i32, rw: i32, rh: i32) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user