feat: Verbatim MVP - terminal renderer, cellular automaton, Verlet physics
- World: 250x250 grid with 14 materials (sand, water, lava, stone, wood, etc.) - Physics: cellular automaton for materials + Verlet solver for entities - Entity: multi-cell humanoid (7 sub-bodies with distance constraints) - Render: terminal renderer with ANSI colors and diff-based updates - Game loop: fixed 60Hz timestep with accumulator pattern - Input: WASD movement, number keys for material painting
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::physics::verlet::SubBody;
|
||||
|
||||
pub struct CollisionResult {
|
||||
pub on_ground: bool,
|
||||
pub in_liquid: bool,
|
||||
pub liquid_density: f32,
|
||||
pub touching_lava: bool,
|
||||
pub touching_fire: bool,
|
||||
pub touching_acid: bool,
|
||||
}
|
||||
|
||||
impl CollisionResult {
|
||||
pub fn none() -> Self {
|
||||
Self {
|
||||
on_ground: false,
|
||||
in_liquid: false,
|
||||
liquid_density: 0.0,
|
||||
touching_lava: false,
|
||||
touching_fire: false,
|
||||
touching_acid: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_grid_collision(grid: &Grid, body: &mut SubBody) -> CollisionResult {
|
||||
let mut result = CollisionResult::none();
|
||||
|
||||
let r = body.radius;
|
||||
let min_x = (body.x - r).floor() as i32;
|
||||
let max_x = (body.x + r).ceil() as i32;
|
||||
let min_y = (body.y - r).floor() as i32;
|
||||
let max_y = (body.y + r).ceil() as i32;
|
||||
|
||||
for cy in min_y..=max_y {
|
||||
for cx in min_x..=max_x {
|
||||
if !grid.in_bounds(cx, cy) {
|
||||
continue;
|
||||
}
|
||||
let cell = grid.get(cx, cy);
|
||||
if cell.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if cell.is_liquid() {
|
||||
result.in_liquid = true;
|
||||
result.liquid_density = result.liquid_density.max(cell.density());
|
||||
if cell.material == MaterialId::Lava {
|
||||
result.touching_lava = true;
|
||||
}
|
||||
if cell.material == MaterialId::Acid {
|
||||
result.touching_acid = true;
|
||||
}
|
||||
apply_liquid_drag(body, cell.density());
|
||||
continue;
|
||||
}
|
||||
|
||||
if cell.material == MaterialId::Fire {
|
||||
result.touching_fire = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if cell.is_solid() {
|
||||
let closest_x = body.x.max(cx as f32).min((cx + 1) as f32);
|
||||
let closest_y = body.y.max(cy as f32).min((cy + 1) as f32);
|
||||
let dx = body.x - closest_x;
|
||||
let dy = body.y - closest_y;
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
if dist_sq < r * r {
|
||||
let dist = dist_sq.sqrt();
|
||||
if dist > 0.0001 {
|
||||
let overlap = r - dist;
|
||||
let nx = dx / dist;
|
||||
let ny = dy / dist;
|
||||
body.x += nx * overlap;
|
||||
body.y += ny * overlap;
|
||||
if ny < -0.5 {
|
||||
result.on_ground = true;
|
||||
}
|
||||
} else {
|
||||
let bcx = cx as f32 + 0.5;
|
||||
let bcy = cy as f32 + 0.5;
|
||||
let dx = body.x - bcx;
|
||||
let dy = body.y - bcy;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist > 0.0001 {
|
||||
body.x = bcx + dx / dist * r * 1.1;
|
||||
body.y = bcy + dy / dist * r * 1.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_liquid_drag(body: &mut SubBody, density: f32) {
|
||||
let drag = 1.0 - density * 0.08;
|
||||
let drag = drag.max(0.5);
|
||||
let vx = body.vx() * drag;
|
||||
let vy = body.vy() * drag;
|
||||
body.set_vel(vx, vy);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod verlet;
|
||||
pub mod collision;
|
||||
|
||||
pub use verlet::{SubBody, Constraint, VerletSolver};
|
||||
pub use collision::resolve_grid_collision;
|
||||
@@ -0,0 +1,154 @@
|
||||
use crate::world::cell::MaterialId;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SubBody {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub old_x: f32,
|
||||
pub old_y: f32,
|
||||
pub ax: f32,
|
||||
pub ay: f32,
|
||||
pub radius: f32,
|
||||
pub material: MaterialId,
|
||||
pub alive: bool,
|
||||
pub health: f32,
|
||||
pub on_fire: bool,
|
||||
pub fire_timer: u32,
|
||||
}
|
||||
|
||||
impl SubBody {
|
||||
pub fn new(x: f32, y: f32, radius: f32, material: MaterialId) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
old_x: x,
|
||||
old_y: y,
|
||||
ax: 0.0,
|
||||
ay: 0.0,
|
||||
radius,
|
||||
material,
|
||||
alive: true,
|
||||
health: 100.0,
|
||||
on_fire: false,
|
||||
fire_timer: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vx(&self) -> f32 {
|
||||
self.x - self.old_x
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vy(&self) -> f32 {
|
||||
self.y - self.old_y
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_vel(&mut self, vx: f32, vy: f32) {
|
||||
self.old_x = self.x - vx;
|
||||
self.old_y = self.y - vy;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add_vel(&mut self, vx: f32, vy: f32) {
|
||||
self.old_x -= vx;
|
||||
self.old_y -= vy;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn apply_force(&mut self, fx: f32, fy: f32) {
|
||||
self.ax += fx;
|
||||
self.ay += fy;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Constraint {
|
||||
pub a: usize,
|
||||
pub b: usize,
|
||||
pub rest_length: f32,
|
||||
pub stiffness: f32,
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn new(a: usize, b: usize, rest_length: f32, stiffness: f32) -> Self {
|
||||
Self {
|
||||
a,
|
||||
b,
|
||||
rest_length,
|
||||
stiffness,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VerletSolver {
|
||||
pub gravity: f32,
|
||||
pub damping: f32,
|
||||
pub dt: f32,
|
||||
}
|
||||
|
||||
impl VerletSolver {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
gravity: 0.3,
|
||||
damping: 0.98,
|
||||
dt: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn integrate(&self, bodies: &mut [SubBody]) {
|
||||
for b in bodies.iter_mut() {
|
||||
if !b.alive {
|
||||
continue;
|
||||
}
|
||||
let vx = (b.x - b.old_x) * self.damping;
|
||||
let vy = (b.y - b.old_y) * self.damping;
|
||||
b.old_x = b.x;
|
||||
b.old_y = b.y;
|
||||
b.x += vx + b.ax * self.dt * self.dt;
|
||||
b.y += vy + (b.ay + self.gravity) * self.dt * self.dt;
|
||||
b.ax = 0.0;
|
||||
b.ay = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn solve_constraints(&self, bodies: &mut [SubBody], constraints: &[Constraint], iterations: u32) {
|
||||
for _ in 0..iterations {
|
||||
for c in constraints {
|
||||
let (ba, bb) = if c.a < bodies.len() && c.b < bodies.len() {
|
||||
(bodies[c.a], bodies[c.b])
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if !ba.alive || !bb.alive {
|
||||
continue;
|
||||
}
|
||||
let dx = bb.x - ba.x;
|
||||
let dy = bb.y - ba.y;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist < 0.0001 {
|
||||
continue;
|
||||
}
|
||||
let diff = (dist - c.rest_length) / dist;
|
||||
let sx = dx * 0.5 * diff * c.stiffness;
|
||||
let sy = dy * 0.5 * diff * c.stiffness;
|
||||
bodies[c.a].x += sx;
|
||||
bodies[c.a].y += sy;
|
||||
bodies[c.b].x -= sx;
|
||||
bodies[c.b].y -= sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step(
|
||||
&self,
|
||||
bodies: &mut [SubBody],
|
||||
constraints: &[Constraint],
|
||||
iterations: u32,
|
||||
) {
|
||||
self.integrate(bodies);
|
||||
self.solve_constraints(bodies, constraints, iterations);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user