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,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "verbatim"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.28"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
Binary file not shown.
@@ -0,0 +1,195 @@
|
||||
use crate::physics::verlet::{SubBody, Constraint};
|
||||
use crate::world::cell::MaterialId;
|
||||
|
||||
pub type EntityId = u32;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum EntityKind {
|
||||
Player,
|
||||
Goblin,
|
||||
Corpse,
|
||||
}
|
||||
|
||||
pub struct Entity {
|
||||
pub id: EntityId,
|
||||
pub kind: EntityKind,
|
||||
pub bodies: Vec<SubBody>,
|
||||
pub constraints: Vec<Constraint>,
|
||||
pub alive: bool,
|
||||
pub health: f32,
|
||||
pub max_health: f32,
|
||||
pub constraint_stiffness: f32,
|
||||
pub on_fire: bool,
|
||||
pub fire_timer: u32,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
pub fn new(id: EntityId, kind: EntityKind) -> Self {
|
||||
Self {
|
||||
id,
|
||||
kind,
|
||||
bodies: Vec::new(),
|
||||
constraints: Vec::new(),
|
||||
alive: true,
|
||||
health: 100.0,
|
||||
max_health: 100.0,
|
||||
constraint_stiffness: 1.0,
|
||||
on_fire: false,
|
||||
fire_timer: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center(&self) -> (f32, f32) {
|
||||
if self.bodies.is_empty() {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let mut sx = 0.0;
|
||||
let mut sy = 0.0;
|
||||
let mut n = 0;
|
||||
for b in &self.bodies {
|
||||
if b.alive {
|
||||
sx += b.x;
|
||||
sy += b.y;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
(sx / n as f32, sy / n as f32)
|
||||
} else {
|
||||
(self.bodies[0].x, self.bodies[0].y)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn head(&self) -> Option<&SubBody> {
|
||||
self.bodies.first()
|
||||
}
|
||||
|
||||
pub fn head_mut(&mut self) -> Option<&mut SubBody> {
|
||||
self.bodies.first_mut()
|
||||
}
|
||||
|
||||
pub fn kill(&mut self) {
|
||||
self.alive = false;
|
||||
self.health = 0.0;
|
||||
self.constraint_stiffness = 0.0;
|
||||
for c in &mut self.constraints {
|
||||
c.stiffness = 0.0;
|
||||
}
|
||||
if self.kind == EntityKind::Player || self.kind == EntityKind::Goblin {
|
||||
self.kind = EntityKind::Corpse;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_damage(&mut self, dmg: f32) {
|
||||
self.health -= dmg;
|
||||
if self.health <= 0.0 && self.alive {
|
||||
self.kill();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_humanoid(&mut self, cx: f32, cy: f32) {
|
||||
self.bodies.clear();
|
||||
self.constraints.clear();
|
||||
|
||||
let r = 0.4;
|
||||
let mat = match self.kind {
|
||||
EntityKind::Player => MaterialId::Flesh,
|
||||
EntityKind::Goblin => MaterialId::Flesh,
|
||||
EntityKind::Corpse => MaterialId::Flesh,
|
||||
};
|
||||
|
||||
self.bodies.push(SubBody::new(cx, cy - 2.0, r, mat));
|
||||
self.bodies.push(SubBody::new(cx, cy - 1.0, r, mat));
|
||||
self.bodies.push(SubBody::new(cx - 0.8, cy - 1.0, r, mat));
|
||||
self.bodies.push(SubBody::new(cx + 0.8, cy - 1.0, r, mat));
|
||||
self.bodies.push(SubBody::new(cx - 0.5, cy, r, mat));
|
||||
self.bodies.push(SubBody::new(cx + 0.5, cy, r, mat));
|
||||
self.bodies.push(SubBody::new(cx, cy + 1.0, r, MaterialId::Bone));
|
||||
|
||||
let s = self.constraint_stiffness;
|
||||
let mk = |a: usize, b: usize, len: f32| Constraint::new(a, b, len, s);
|
||||
|
||||
self.constraints.push(mk(0, 1, 1.0));
|
||||
self.constraints.push(mk(1, 2, 0.9));
|
||||
self.constraints.push(mk(1, 3, 0.9));
|
||||
self.constraints.push(mk(2, 4, 0.9));
|
||||
self.constraints.push(mk(3, 5, 0.9));
|
||||
self.constraints.push(mk(1, 6, 2.0));
|
||||
self.constraints.push(mk(4, 5, 1.0));
|
||||
self.constraints.push(mk(4, 6, 1.1));
|
||||
self.constraints.push(mk(5, 6, 1.1));
|
||||
}
|
||||
|
||||
pub fn apply_fire_damage(&mut self) {
|
||||
if !self.on_fire {
|
||||
return;
|
||||
}
|
||||
self.fire_timer += 1;
|
||||
let dmg = 0.5;
|
||||
self.take_damage(dmg);
|
||||
for b in &mut self.bodies {
|
||||
if b.alive {
|
||||
b.health -= dmg;
|
||||
}
|
||||
}
|
||||
if self.fire_timer > 180 {
|
||||
self.on_fire = false;
|
||||
self.fire_timer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EntityManager {
|
||||
entities: Vec<Entity>,
|
||||
next_id: EntityId,
|
||||
}
|
||||
|
||||
impl EntityManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entities: Vec::new(),
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&mut self, kind: EntityKind) -> EntityId {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
let mut e = Entity::new(id, kind);
|
||||
match kind {
|
||||
EntityKind::Player => {
|
||||
e.max_health = 100.0;
|
||||
e.health = 100.0;
|
||||
}
|
||||
EntityKind::Goblin => {
|
||||
e.max_health = 40.0;
|
||||
e.health = 40.0;
|
||||
}
|
||||
EntityKind::Corpse => {
|
||||
e.alive = false;
|
||||
}
|
||||
}
|
||||
self.entities.push(e);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get(&self, id: EntityId) -> Option<&Entity> {
|
||||
self.entities.iter().find(|e| e.id == id)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: EntityId) -> Option<&mut Entity> {
|
||||
self.entities.iter_mut().find(|e| e.id == id)
|
||||
}
|
||||
|
||||
pub fn all(&self) -> &[Entity] {
|
||||
&self.entities
|
||||
}
|
||||
|
||||
pub fn all_mut(&mut self) -> &mut [Entity] {
|
||||
&mut self.entities
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Entity> {
|
||||
self.entities.iter_mut()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod entity;
|
||||
pub mod player;
|
||||
|
||||
pub use entity::{Entity, EntityId, EntityManager, EntityKind};
|
||||
pub use player::Player;
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::entity::entity::{Entity, EntityKind, EntityManager, EntityId};
|
||||
|
||||
pub struct Player {
|
||||
pub entity_id: EntityId,
|
||||
pub move_speed: f32,
|
||||
pub jump_force: f32,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(manager: &mut EntityManager) -> Self {
|
||||
let id = manager.spawn(EntityKind::Player);
|
||||
Self {
|
||||
entity_id: id,
|
||||
move_speed: 0.15,
|
||||
jump_force: 0.8,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_at(&self, manager: &mut EntityManager, cx: f32, cy: f32) {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
e.build_humanoid(cx, cy);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&self, manager: &mut EntityManager) {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
if let Some(head) = e.head_mut() {
|
||||
head.add_vel(-self.move_speed, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_right(&self, manager: &mut EntityManager) {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
if let Some(head) = e.head_mut() {
|
||||
head.add_vel(self.move_speed, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jump(&self, manager: &mut EntityManager, on_ground: bool) {
|
||||
if on_ground {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
for b in &mut e.bodies {
|
||||
if b.alive {
|
||||
b.add_vel(0.0, -self.jump_force);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entity<'a>(&self, manager: &'a EntityManager) -> Option<&'a Entity> {
|
||||
manager.get(self.entity_id)
|
||||
}
|
||||
|
||||
pub fn entity_mut<'a>(&self, manager: &'a mut EntityManager) -> Option<&'a mut Entity> {
|
||||
manager.get_mut(self.entity_id)
|
||||
}
|
||||
|
||||
pub fn center(&self, manager: &EntityManager) -> (f32, f32) {
|
||||
manager.get(self.entity_id).map(|e| e.center()).unwrap_or((0.0, 0.0))
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::entity::{EntityManager, EntityKind};
|
||||
use crate::input::{Action, InputHandler};
|
||||
use crate::physics::verlet::VerletSolver;
|
||||
use crate::physics::collision::resolve_grid_collision;
|
||||
use crate::render::Renderer;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::cellular::CellularAutomaton;
|
||||
use crate::entity::player::Player;
|
||||
|
||||
pub struct Game {
|
||||
pub grid: Grid,
|
||||
pub ca: CellularAutomaton,
|
||||
pub verlet: VerletSolver,
|
||||
pub entities: EntityManager,
|
||||
pub player: Player,
|
||||
pub input: InputHandler,
|
||||
pub cam_x: i32,
|
||||
pub cam_y: i32,
|
||||
pub running: bool,
|
||||
pub tick: u64,
|
||||
pub fixed_dt: Duration,
|
||||
pub accumulator: Duration,
|
||||
pub last_time: Instant,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new() -> Self {
|
||||
let mut entities = EntityManager::new();
|
||||
let player = Player::new(&mut entities);
|
||||
Self {
|
||||
grid: Grid::new(),
|
||||
ca: CellularAutomaton::new(),
|
||||
verlet: VerletSolver::new(),
|
||||
entities,
|
||||
player,
|
||||
input: InputHandler::new(),
|
||||
cam_x: 100,
|
||||
cam_y: 100,
|
||||
running: true,
|
||||
tick: 0,
|
||||
fixed_dt: Duration::from_millis(16),
|
||||
accumulator: Duration::ZERO,
|
||||
last_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_world(&mut self) {
|
||||
let w = self.grid.width;
|
||||
let h = self.grid.height;
|
||||
|
||||
for x in 0..w {
|
||||
self.grid.set_material(x as i32, (h - 1) as i32, MaterialId::Stone);
|
||||
self.grid.set_material(x as i32, (h - 2) as i32, MaterialId::Dirt);
|
||||
}
|
||||
|
||||
for x in 0..w {
|
||||
let surface = (h as i32 - 3) - ((x as f32 * 0.1).sin() * 5.0) as i32;
|
||||
let surface = surface.max(10).min(h as i32 - 3);
|
||||
for y in surface..(h as i32 - 2) {
|
||||
if y == surface {
|
||||
self.grid.set_material(x as i32, y, MaterialId::Grass);
|
||||
} else {
|
||||
self.grid.set_material(x as i32, y, MaterialId::Dirt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cx = (w / 2) as f32;
|
||||
let cy = (h as f32 / 2.0) - 20.0;
|
||||
self.player.spawn_at(&mut self.entities, cx, cy);
|
||||
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
self.center_camera_on(px, py);
|
||||
|
||||
self.grid.fill_border(MaterialId::Stone);
|
||||
}
|
||||
|
||||
fn center_camera_on(&mut self, px: f32, py: f32) {
|
||||
self.cam_x = px as i32 - 40;
|
||||
self.cam_y = py as i32 - 12;
|
||||
}
|
||||
|
||||
pub fn run<R: Renderer>(&mut self, renderer: &mut R) {
|
||||
let _ = renderer.init();
|
||||
self.init_world();
|
||||
|
||||
self.last_time = Instant::now();
|
||||
|
||||
while self.running {
|
||||
let now = Instant::now();
|
||||
let frame_time = now.duration_since(self.last_time);
|
||||
self.last_time = now;
|
||||
self.accumulator += frame_time;
|
||||
|
||||
while self.accumulator >= self.fixed_dt {
|
||||
self.fixed_update();
|
||||
self.accumulator -= self.fixed_dt;
|
||||
}
|
||||
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
self.center_camera_on(px, py);
|
||||
|
||||
let _ = renderer.render(&self.grid, &self.entities, self.cam_x, self.cam_y);
|
||||
|
||||
self.handle_input(renderer.viewport_w(), renderer.viewport_h());
|
||||
}
|
||||
|
||||
let _ = renderer.shutdown();
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, vw: usize, vh: usize) {
|
||||
let action = self.input.poll();
|
||||
match action {
|
||||
Action::Quit => self.running = false,
|
||||
Action::MoveLeft => self.player.move_left(&mut self.entities),
|
||||
Action::MoveRight => self.player.move_right(&mut self.entities),
|
||||
Action::Jump => {
|
||||
let on_ground = self.check_on_ground();
|
||||
self.player.jump(&mut self.entities, on_ground);
|
||||
}
|
||||
Action::MoveCameraLeft => self.cam_x -= 5,
|
||||
Action::MoveCameraRight => self.cam_x += 5,
|
||||
Action::MoveCameraUp => self.cam_y -= 5,
|
||||
Action::MoveCameraDown => self.cam_y += 5,
|
||||
Action::Paint(brush) => {
|
||||
let mat = brush.to_material();
|
||||
let cx = self.cam_x + (vw as i32 / 2);
|
||||
let cy = self.cam_y + (vh as i32 / 2);
|
||||
let r = 2;
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
if dx * dx + dy * dy <= r * r + 1 {
|
||||
if let Some(m) = mat {
|
||||
self.grid.set_material(cx + dx, cy + dy, m);
|
||||
} else {
|
||||
self.grid.set(cx + dx, cy + dy, crate::world::cell::Cell::empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_on_ground(&self) -> bool {
|
||||
if let Some(e) = self.player.entity(&self.entities) {
|
||||
for b in &e.bodies {
|
||||
if !b.alive {
|
||||
continue;
|
||||
}
|
||||
let bx = b.x as i32;
|
||||
let by = b.y as i32;
|
||||
let below = self.grid.get(bx, by + 1);
|
||||
if below.is_solid() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn fixed_update(&mut self) {
|
||||
self.tick += 1;
|
||||
|
||||
self.ca.step(&mut self.grid);
|
||||
|
||||
self.update_entities();
|
||||
|
||||
self.apply_world_damage();
|
||||
|
||||
if self.tick % 30 == 0 {
|
||||
self.try_spawn_goblin();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_entities(&mut self) {
|
||||
let solver = self.verlet.clone();
|
||||
let grid = &self.grid;
|
||||
|
||||
let entities_data: Vec<(usize, Vec<crate::physics::verlet::SubBody>, Vec<crate::physics::verlet::Constraint>)>;
|
||||
entities_data = {
|
||||
let mut data = Vec::new();
|
||||
for (i, e) in self.entities.all().iter().enumerate() {
|
||||
data.push((i, e.bodies.clone(), e.constraints.clone()));
|
||||
}
|
||||
data
|
||||
};
|
||||
|
||||
for (idx, mut bodies, constraints) in entities_data {
|
||||
for b in &mut bodies {
|
||||
if !b.alive {
|
||||
continue;
|
||||
}
|
||||
|
||||
if b.on_fire {
|
||||
b.fire_timer += 1;
|
||||
b.health -= 0.3;
|
||||
if b.fire_timer > 120 {
|
||||
b.on_fire = false;
|
||||
b.fire_timer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
solver.integrate(&mut bodies);
|
||||
|
||||
for b in &mut bodies {
|
||||
if !b.alive {
|
||||
continue;
|
||||
}
|
||||
let result = resolve_grid_collision(grid, b);
|
||||
if result.touching_lava {
|
||||
b.health -= 2.0;
|
||||
if !b.on_fire {
|
||||
b.on_fire = true;
|
||||
}
|
||||
}
|
||||
if result.touching_fire {
|
||||
b.health -= 0.5;
|
||||
if !b.on_fire && b.health < 80.0 {
|
||||
b.on_fire = true;
|
||||
}
|
||||
}
|
||||
if result.touching_acid {
|
||||
b.health -= 1.0;
|
||||
}
|
||||
if result.in_liquid {
|
||||
let body_density = crate::world::material::MaterialRegistry::instance()
|
||||
.get(b.material).density;
|
||||
if body_density > result.liquid_density {
|
||||
b.y += 0.02;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
solver.solve_constraints(&mut bodies, &constraints, 3);
|
||||
|
||||
if let Some(e) = self.entities.all_mut().get_mut(idx) {
|
||||
e.bodies = bodies;
|
||||
|
||||
let mut total_health = 0.0;
|
||||
let mut alive_count = 0;
|
||||
for b in &e.bodies {
|
||||
if b.alive {
|
||||
total_health += b.health;
|
||||
alive_count += 1;
|
||||
}
|
||||
}
|
||||
if alive_count > 0 {
|
||||
let avg = total_health / alive_count as f32;
|
||||
if avg < 0.0 && e.alive {
|
||||
e.kill();
|
||||
}
|
||||
}
|
||||
|
||||
let any_on_fire = e.bodies.iter().any(|b| b.alive && b.on_fire);
|
||||
e.on_fire = any_on_fire;
|
||||
if e.on_fire {
|
||||
e.apply_fire_damage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_world_damage(&mut self) {
|
||||
let mut to_kill: Vec<usize> = Vec::new();
|
||||
for (i, e) in self.entities.all().iter().enumerate() {
|
||||
if !e.alive {
|
||||
continue;
|
||||
}
|
||||
let mut dead_parts = 0;
|
||||
for b in &e.bodies {
|
||||
if !b.alive || b.health <= 0.0 {
|
||||
dead_parts += 1;
|
||||
}
|
||||
}
|
||||
if dead_parts == e.bodies.len() {
|
||||
to_kill.push(i);
|
||||
}
|
||||
}
|
||||
for i in to_kill {
|
||||
if let Some(e) = self.entities.all_mut().get_mut(i) {
|
||||
e.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_spawn_goblin(&mut self) {
|
||||
let alive_goblins = self.entities.all().iter().filter(|e| e.alive && e.kind == EntityKind::Goblin).count();
|
||||
if alive_goblins >= 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
let spawn_x = px as i32 + if px as i32 % 2 == 0 { 15 } else { -15 };
|
||||
let spawn_y = py as i32 - 5;
|
||||
|
||||
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.entities.spawn(EntityKind::Goblin);
|
||||
if let Some(g) = self.entities.get_mut(id) {
|
||||
g.build_humanoid(spawn_x as f32, spawn_y as f32);
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::time::Duration;
|
||||
|
||||
pub enum Action {
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
Jump,
|
||||
MoveCameraUp,
|
||||
MoveCameraDown,
|
||||
MoveCameraLeft,
|
||||
MoveCameraRight,
|
||||
Paint(MaterialBrush),
|
||||
Quit,
|
||||
None,
|
||||
}
|
||||
|
||||
pub enum MaterialBrush {
|
||||
Sand,
|
||||
Water,
|
||||
Stone,
|
||||
Lava,
|
||||
Wood,
|
||||
Acid,
|
||||
Grass,
|
||||
Dirt,
|
||||
Fire,
|
||||
Flesh,
|
||||
Erase,
|
||||
}
|
||||
|
||||
pub struct InputHandler {
|
||||
pub paint_brush: MaterialBrush,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
paint_brush: MaterialBrush::Sand,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self) -> Action {
|
||||
if !event::poll(Duration::from_millis(0)).unwrap_or(false) {
|
||||
return Action::None;
|
||||
}
|
||||
match event::read() {
|
||||
Ok(Event::Key(KeyEvent { code, modifiers, .. })) => {
|
||||
if modifiers.contains(KeyModifiers::CONTROL) && code == KeyCode::Char('c') {
|
||||
return Action::Quit;
|
||||
}
|
||||
match code {
|
||||
KeyCode::Char('q') => Action::Quit,
|
||||
KeyCode::Left | KeyCode::Char('a') => Action::MoveLeft,
|
||||
KeyCode::Right | KeyCode::Char('d') => Action::MoveRight,
|
||||
KeyCode::Up | KeyCode::Char('w') | KeyCode::Char(' ') => Action::Jump,
|
||||
KeyCode::Char('h') => Action::MoveCameraLeft,
|
||||
KeyCode::Char('l') => Action::MoveCameraRight,
|
||||
KeyCode::Char('k') => Action::MoveCameraUp,
|
||||
KeyCode::Char('j') => Action::MoveCameraDown,
|
||||
KeyCode::Char('1') => { self.paint_brush = MaterialBrush::Sand; Action::Paint(MaterialBrush::Sand) }
|
||||
KeyCode::Char('2') => { self.paint_brush = MaterialBrush::Water; Action::Paint(MaterialBrush::Water) }
|
||||
KeyCode::Char('3') => { self.paint_brush = MaterialBrush::Stone; Action::Paint(MaterialBrush::Stone) }
|
||||
KeyCode::Char('4') => { self.paint_brush = MaterialBrush::Lava; Action::Paint(MaterialBrush::Lava) }
|
||||
KeyCode::Char('5') => { self.paint_brush = MaterialBrush::Wood; Action::Paint(MaterialBrush::Wood) }
|
||||
KeyCode::Char('6') => { self.paint_brush = MaterialBrush::Acid; Action::Paint(MaterialBrush::Acid) }
|
||||
KeyCode::Char('7') => { self.paint_brush = MaterialBrush::Grass; Action::Paint(MaterialBrush::Grass) }
|
||||
KeyCode::Char('8') => { self.paint_brush = MaterialBrush::Dirt; Action::Paint(MaterialBrush::Dirt) }
|
||||
KeyCode::Char('9') => { self.paint_brush = MaterialBrush::Fire; Action::Paint(MaterialBrush::Fire) }
|
||||
KeyCode::Char('0') => { self.paint_brush = MaterialBrush::Flesh; Action::Paint(MaterialBrush::Flesh) }
|
||||
KeyCode::Char('x') => { self.paint_brush = MaterialBrush::Erase; Action::Paint(MaterialBrush::Erase) }
|
||||
_ => Action::None,
|
||||
}
|
||||
}
|
||||
_ => Action::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaterialBrush {
|
||||
pub fn to_material(&self) -> Option<crate::world::cell::MaterialId> {
|
||||
use crate::world::cell::MaterialId;
|
||||
match self {
|
||||
MaterialBrush::Sand => Some(MaterialId::Sand),
|
||||
MaterialBrush::Water => Some(MaterialId::Water),
|
||||
MaterialBrush::Stone => Some(MaterialId::Stone),
|
||||
MaterialBrush::Lava => Some(MaterialId::Lava),
|
||||
MaterialBrush::Wood => Some(MaterialId::Wood),
|
||||
MaterialBrush::Acid => Some(MaterialId::Acid),
|
||||
MaterialBrush::Grass => Some(MaterialId::Grass),
|
||||
MaterialBrush::Dirt => Some(MaterialId::Dirt),
|
||||
MaterialBrush::Fire => Some(MaterialId::Fire),
|
||||
MaterialBrush::Flesh => Some(MaterialId::Flesh),
|
||||
MaterialBrush::Erase => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
MaterialBrush::Sand => "Sand",
|
||||
MaterialBrush::Water => "Water",
|
||||
MaterialBrush::Stone => "Stone",
|
||||
MaterialBrush::Lava => "Lava",
|
||||
MaterialBrush::Wood => "Wood",
|
||||
MaterialBrush::Acid => "Acid",
|
||||
MaterialBrush::Grass => "Grass",
|
||||
MaterialBrush::Dirt => "Dirt",
|
||||
MaterialBrush::Fire => "Fire",
|
||||
MaterialBrush::Flesh => "Flesh",
|
||||
MaterialBrush::Erase => "Erase",
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
mod world;
|
||||
mod physics;
|
||||
mod entity;
|
||||
mod render;
|
||||
mod input;
|
||||
mod game;
|
||||
|
||||
use clap::Parser;
|
||||
use game::Game;
|
||||
use render::terminal::TerminalRenderer;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "verbatim", about = "ASCII physics RPG - Noita meets Caves of Qud")]
|
||||
struct Cli {
|
||||
#[arg(long, default_value = "terminal")]
|
||||
render_mode: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.render_mode.as_str() {
|
||||
"terminal" => {
|
||||
let mut renderer = TerminalRenderer::new();
|
||||
let mut game = Game::new();
|
||||
game.run(&mut renderer);
|
||||
}
|
||||
"vulkan" => {
|
||||
eprintln!("Vulkan renderer not yet implemented. Use --render-mode terminal.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown render mode: {}. Use 'terminal' or 'vulkan'.", cli.render_mode);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod terminal;
|
||||
|
||||
use crate::entity::EntityManager;
|
||||
use crate::world::grid::Grid;
|
||||
|
||||
pub trait Renderer {
|
||||
fn init(&mut self) -> std::io::Result<()>;
|
||||
fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) -> std::io::Result<()>;
|
||||
fn shutdown(&mut self) -> std::io::Result<()>;
|
||||
fn viewport_w(&self) -> usize;
|
||||
fn viewport_h(&self) -> usize;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use std::io::{self, Write, stdout};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
event::{DisableMouseCapture, EnableMouseCapture},
|
||||
execute, queue,
|
||||
style::{Color, SetBackgroundColor, SetForegroundColor, ResetColor, Print},
|
||||
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, size as term_size},
|
||||
};
|
||||
|
||||
use crate::entity::EntityManager;
|
||||
use crate::render::Renderer;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::material::MaterialRegistry;
|
||||
|
||||
pub struct TerminalRenderer {
|
||||
width: usize,
|
||||
height: usize,
|
||||
prev_frame: Vec<(char, (u8, u8, u8), (u8, u8, u8))>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl TerminalRenderer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
width: 80,
|
||||
height: 25,
|
||||
prev_frame: Vec::new(),
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_size(&mut self) -> io::Result<()> {
|
||||
let (w, h) = term_size().unwrap_or((80, 25));
|
||||
self.width = (w as usize).min(120).max(40);
|
||||
self.height = (h as usize).min(50).max(15);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_cell() -> (char, (u8, u8, u8), (u8, u8, u8)) {
|
||||
(' ', (0, 0, 0), (10, 10, 15))
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for TerminalRenderer {
|
||||
fn init(&mut self) -> io::Result<()> {
|
||||
self.detect_size()?;
|
||||
let total = self.width * self.height;
|
||||
self.prev_frame = vec![Self::empty_cell(); total];
|
||||
|
||||
terminal::enable_raw_mode().map_err(|e| io::Error::other(e))?;
|
||||
execute!(
|
||||
stdout(),
|
||||
EnterAlternateScreen,
|
||||
Hide,
|
||||
EnableMouseCapture,
|
||||
Clear(ClearType::All),
|
||||
)?;
|
||||
|
||||
self.initialized = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) -> io::Result<()> {
|
||||
if !self.initialized {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reg = MaterialRegistry::instance();
|
||||
let mut out = stdout();
|
||||
let mut frame: Vec<(char, (u8, u8, u8), (u8, u8, u8))>;
|
||||
let total = self.width * self.height;
|
||||
frame = vec![Self::empty_cell(); total];
|
||||
|
||||
for dy in 0..self.height {
|
||||
for dx in 0..self.width {
|
||||
let wx = cam_x + dx as i32;
|
||||
let wy = cam_y + dy as i32;
|
||||
if !grid.in_bounds(wx, wy) {
|
||||
let idx = dy * self.width + dx;
|
||||
frame[idx] = ('?', (80, 80, 80), (10, 10, 15));
|
||||
continue;
|
||||
}
|
||||
let cell = grid.get(wx, wy);
|
||||
let mat = reg.get(cell.material);
|
||||
let idx = dy * self.width + dx;
|
||||
if cell.is_empty() {
|
||||
frame[idx] = (' ', mat.color_fg, mat.color_bg);
|
||||
} else {
|
||||
let ch = if cell.material == MaterialId::Lava {
|
||||
if cell.variant % 2 == 0 { '#' } else { '#' }
|
||||
} else if cell.material == MaterialId::Water {
|
||||
if cell.variant % 3 == 0 { '~' } else { '~' }
|
||||
} else {
|
||||
mat.display_char
|
||||
};
|
||||
let fg = if cell.material == MaterialId::Lava {
|
||||
let flicker = cell.variant as u8;
|
||||
let r = 255u8.min(200 + flicker / 2);
|
||||
(r, 60, 20)
|
||||
} else {
|
||||
mat.color_fg
|
||||
};
|
||||
frame[idx] = (ch, fg, mat.color_bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for e in entities.all() {
|
||||
for b in &e.bodies {
|
||||
if !b.alive {
|
||||
continue;
|
||||
}
|
||||
let sx = b.x as i32 - cam_x;
|
||||
let sy = b.y as i32 - cam_y;
|
||||
if sx >= 0 && sx < self.width as i32 && sy >= 0 && sy < self.height as i32 {
|
||||
let idx = sy as usize * self.width + sx as usize;
|
||||
let ch = match e.kind {
|
||||
crate::entity::EntityKind::Player if e.alive => '@',
|
||||
crate::entity::EntityKind::Goblin if e.alive => 'g',
|
||||
_ => '%',
|
||||
};
|
||||
let fg = if e.on_fire {
|
||||
(255, 160, 40)
|
||||
} else if !e.alive {
|
||||
(100, 60, 60)
|
||||
} else {
|
||||
match e.kind {
|
||||
crate::entity::EntityKind::Player => (255, 255, 100),
|
||||
crate::entity::EntityKind::Goblin => (100, 220, 100),
|
||||
_ => (180, 50, 50),
|
||||
}
|
||||
};
|
||||
frame[idx] = (ch, fg, (20, 10, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev_color: Option<(Color, Color)> = None;
|
||||
for i in 0..total {
|
||||
if frame[i] == self.prev_frame[i] {
|
||||
continue;
|
||||
}
|
||||
let dx = i % self.width;
|
||||
let dy = i / self.width;
|
||||
let (ch, fg, bg) = frame[i];
|
||||
|
||||
queue!(out, MoveTo(dx as u16, dy as u16))?;
|
||||
|
||||
let new_fg = Color::Rgb { r: fg.0, g: fg.1, b: fg.2 };
|
||||
let new_bg = Color::Rgb { r: bg.0, g: bg.1, b: bg.2 };
|
||||
if prev_color != Some((new_fg, new_bg)) {
|
||||
queue!(out, SetForegroundColor(new_fg), SetBackgroundColor(new_bg))?;
|
||||
prev_color = Some((new_fg, new_bg));
|
||||
}
|
||||
queue!(out, Print(ch))?;
|
||||
}
|
||||
|
||||
out.flush()?;
|
||||
self.prev_frame = frame;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) -> io::Result<()> {
|
||||
if !self.initialized {
|
||||
return Ok(());
|
||||
}
|
||||
execute!(
|
||||
stdout(),
|
||||
ResetColor,
|
||||
Show,
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture,
|
||||
)?;
|
||||
terminal::disable_raw_mode().map_err(|e| io::Error::other(e))?;
|
||||
self.initialized = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn viewport_w(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn viewport_h(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalRenderer {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use crate::world::material::MaterialRegistry;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum MaterialId {
|
||||
Empty = 0,
|
||||
Sand = 1,
|
||||
Water = 2,
|
||||
Stone = 3,
|
||||
Lava = 4,
|
||||
Wood = 5,
|
||||
Flesh = 6,
|
||||
Bone = 7,
|
||||
Steam = 8,
|
||||
Fire = 9,
|
||||
Acid = 10,
|
||||
Smoke = 11,
|
||||
Grass = 12,
|
||||
Dirt = 13,
|
||||
}
|
||||
|
||||
impl MaterialId {
|
||||
pub const ALL: [MaterialId; 14] = [
|
||||
MaterialId::Empty,
|
||||
MaterialId::Sand,
|
||||
MaterialId::Water,
|
||||
MaterialId::Stone,
|
||||
MaterialId::Lava,
|
||||
MaterialId::Wood,
|
||||
MaterialId::Flesh,
|
||||
MaterialId::Bone,
|
||||
MaterialId::Steam,
|
||||
MaterialId::Fire,
|
||||
MaterialId::Acid,
|
||||
MaterialId::Smoke,
|
||||
MaterialId::Grass,
|
||||
MaterialId::Dirt,
|
||||
];
|
||||
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
unsafe { std::mem::transmute(v) }
|
||||
}
|
||||
|
||||
pub fn display_char(self) -> char {
|
||||
match self {
|
||||
MaterialId::Empty => ' ',
|
||||
MaterialId::Sand => '.',
|
||||
MaterialId::Water => '~',
|
||||
MaterialId::Stone => '#',
|
||||
MaterialId::Lava => '#',
|
||||
MaterialId::Wood => 'T',
|
||||
MaterialId::Flesh => '%',
|
||||
MaterialId::Bone => '`',
|
||||
MaterialId::Steam => '~',
|
||||
MaterialId::Fire => '^',
|
||||
MaterialId::Acid => '~',
|
||||
MaterialId::Smoke => '*',
|
||||
MaterialId::Grass => '"',
|
||||
MaterialId::Dirt => ':',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Cell {
|
||||
pub material: MaterialId,
|
||||
pub temp: f32,
|
||||
pub updated_this_tick: bool,
|
||||
pub variant: u8,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
material: MaterialId::Empty,
|
||||
temp: 20.0,
|
||||
updated_this_tick: false,
|
||||
variant: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(material: MaterialId) -> Self {
|
||||
let temp = match material {
|
||||
MaterialId::Lava => 1200.0,
|
||||
MaterialId::Fire => 800.0,
|
||||
MaterialId::Steam => 150.0,
|
||||
MaterialId::Smoke => 120.0,
|
||||
_ => 20.0,
|
||||
};
|
||||
Self {
|
||||
material,
|
||||
temp,
|
||||
updated_this_tick: false,
|
||||
variant: rand_u8(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(self) -> bool {
|
||||
self.material == MaterialId::Empty
|
||||
}
|
||||
|
||||
pub fn is_solid(self) -> bool {
|
||||
let reg = MaterialRegistry::instance();
|
||||
reg.get(self.material).solid
|
||||
}
|
||||
|
||||
pub fn is_liquid(self) -> bool {
|
||||
let reg = MaterialRegistry::instance();
|
||||
reg.get(self.material).liquid
|
||||
}
|
||||
|
||||
pub fn is_gas(self) -> bool {
|
||||
let reg = MaterialRegistry::instance();
|
||||
reg.get(self.material).gas
|
||||
}
|
||||
|
||||
pub fn is_static(self) -> bool {
|
||||
let reg = MaterialRegistry::instance();
|
||||
reg.get(self.material).static_
|
||||
}
|
||||
|
||||
pub fn density(self) -> f32 {
|
||||
let reg = MaterialRegistry::instance();
|
||||
reg.get(self.material).density
|
||||
}
|
||||
|
||||
pub fn display_char(self) -> char {
|
||||
self.material.display_char()
|
||||
}
|
||||
}
|
||||
|
||||
fn rand_u8() -> u8 {
|
||||
static mut COUNTER: u8 = 0;
|
||||
unsafe {
|
||||
COUNTER = COUNTER.wrapping_add(7);
|
||||
COUNTER
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::material::MaterialRegistry;
|
||||
|
||||
pub struct CellularAutomaton {
|
||||
tick: u64,
|
||||
rng_state: u64,
|
||||
}
|
||||
|
||||
impl CellularAutomaton {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tick: 0,
|
||||
rng_state: 0x1234567890ABCDEF,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rand(&mut self) -> u32 {
|
||||
self.rng_state ^= self.rng_state << 13;
|
||||
self.rng_state ^= self.rng_state >> 7;
|
||||
self.rng_state ^= self.rng_state << 17;
|
||||
(self.rng_state & 0xFFFFFFFF) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rand_bool(&mut self) -> bool {
|
||||
self.rand() & 1 == 1
|
||||
}
|
||||
|
||||
pub fn step(&mut self, grid: &mut Grid) {
|
||||
grid.reset_tick_flags();
|
||||
let flip = self.rand_bool();
|
||||
let h = grid.height;
|
||||
let w = grid.width;
|
||||
|
||||
for y_idx in (0..h).rev() {
|
||||
let y = y_idx as i32;
|
||||
let xs: Vec<i32> = if flip {
|
||||
(0..w as i32).collect()
|
||||
} else {
|
||||
(0..w as i32).rev().collect()
|
||||
};
|
||||
|
||||
for x in xs {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.updated_this_tick || cell.is_empty() || cell.is_static() {
|
||||
continue;
|
||||
}
|
||||
match cell.material {
|
||||
MaterialId::Sand => self.update_sand(grid, x, y),
|
||||
MaterialId::Water => self.update_water(grid, x, y),
|
||||
MaterialId::Lava => self.update_lava(grid, x, y),
|
||||
MaterialId::Steam => self.update_steam(grid, x, y),
|
||||
MaterialId::Fire => self.update_fire(grid, x, y),
|
||||
MaterialId::Smoke => self.update_smoke(grid, x, y),
|
||||
MaterialId::Acid => self.update_acid(grid, x, y),
|
||||
MaterialId::Flesh => self.update_flesh(grid, x, y),
|
||||
MaterialId::Grass => self.update_grass(grid, x, y),
|
||||
MaterialId::Dirt => self.update_dirt(grid, x, y),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.heat_transfer(grid);
|
||||
self.tick += 1;
|
||||
}
|
||||
|
||||
fn try_move_down(&mut self, grid: &mut Grid, x: i32, y: i32, _mat: MaterialId, density: f32) {
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < density) {
|
||||
let src = grid.get(x, y);
|
||||
let i_dst = grid.idx(x, y + 1);
|
||||
let i_src = grid.idx(x, y);
|
||||
grid.cells[i_dst] = src;
|
||||
grid.cells[i_dst].updated_this_tick = true;
|
||||
grid.cells[i_src] = Cell::empty();
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
let dl = grid.get(x - dir, y + 1);
|
||||
let dr = grid.get(x + dir, y + 1);
|
||||
|
||||
let can_left = grid.in_bounds(x - dir, y + 1)
|
||||
&& (dl.is_empty() || (dl.is_liquid() && dl.density() < density));
|
||||
let can_right = grid.in_bounds(x + dir, y + 1)
|
||||
&& (dr.is_empty() || (dr.is_liquid() && dr.density() < density));
|
||||
|
||||
if can_left && can_right {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_left {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
} else if can_right {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_swap(&self, grid: &mut Grid, x1: i32, y1: i32, x2: i32, y2: i32) {
|
||||
let a = grid.get(x1, y1);
|
||||
let b = grid.get(x2, y2);
|
||||
let i1 = grid.idx(x1, y1);
|
||||
let i2 = grid.idx(x2, y2);
|
||||
grid.cells[i1] = b;
|
||||
grid.cells[i2] = a;
|
||||
grid.cells[i2].updated_this_tick = true;
|
||||
}
|
||||
|
||||
fn update_sand(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
self.try_move_down(grid, x, y, MaterialId::Sand, 1.5);
|
||||
}
|
||||
|
||||
fn update_water(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < 1.0) {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
let dl = grid.get(x - dir, y + 1);
|
||||
let dr = grid.get(x + dir, y + 1);
|
||||
|
||||
let can_dl = grid.in_bounds(x - dir, y + 1)
|
||||
&& (dl.is_empty() || (dl.is_liquid() && dl.density() < 1.0));
|
||||
let can_dr = grid.in_bounds(x + dir, y + 1)
|
||||
&& (dr.is_empty() || (dr.is_liquid() && dr.density() < 1.0));
|
||||
|
||||
if can_dl && can_dr {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_dl {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
} else if can_dr {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
} else {
|
||||
let can_l = grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty();
|
||||
let can_r = grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty();
|
||||
if can_l && can_r {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
} else if can_l {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else if can_r {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 100.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Steam;
|
||||
new.temp = 110.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_lava(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 800.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Stone;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
return;
|
||||
}
|
||||
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y + 1) && grid.get(x - dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
return;
|
||||
}
|
||||
if grid.in_bounds(x + dir, y + 1) && grid.get(x + dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.rand() % 10 == 0 {
|
||||
if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
self.lava_interact(grid, x, y);
|
||||
}
|
||||
|
||||
fn lava_interact(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if !grid.in_bounds(nx, ny) {
|
||||
continue;
|
||||
}
|
||||
let neighbor = grid.get(nx, ny);
|
||||
match neighbor.material {
|
||||
MaterialId::Water => {
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Steam);
|
||||
let lava = grid.get(x, y);
|
||||
let mut new_lava = lava;
|
||||
new_lava.temp -= 50.0;
|
||||
let i_l = grid.idx(x, y);
|
||||
grid.cells[i_l] = new_lava;
|
||||
}
|
||||
MaterialId::Wood | MaterialId::Grass | MaterialId::Flesh if neighbor.temp < 300.0 => {
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Fire);
|
||||
}
|
||||
MaterialId::Sand if neighbor.temp > 1700.0 => {
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Stone);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_steam(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 80.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Water;
|
||||
new.temp = 50.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
return;
|
||||
}
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y - 1) && grid.get(x - dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y - 1);
|
||||
return;
|
||||
}
|
||||
if grid.in_bounds(x + dir, y - 1) && grid.get(x + dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.rand() % 3 == 0 {
|
||||
if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_fire(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 100.0 || self.rand() % 20 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
if self.rand() % 3 == 0 {
|
||||
grid.cells[i] = Cell::new(MaterialId::Smoke);
|
||||
} else {
|
||||
grid.cells[i] = Cell::empty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new = cell;
|
||||
new.temp -= 15.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() && self.rand() % 2 == 0 {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
}
|
||||
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if !grid.in_bounds(nx, ny) {
|
||||
continue;
|
||||
}
|
||||
let neighbor = grid.get(nx, ny);
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let mat = reg.get(neighbor.material);
|
||||
if mat.flammable && neighbor.temp < mat.ignition_temp {
|
||||
let mut new_n = neighbor;
|
||||
new_n.material = MaterialId::Fire;
|
||||
new_n.temp = 400.0;
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = new_n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_smoke(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
if self.rand() % 60 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::empty();
|
||||
return;
|
||||
}
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y - 1) && grid.get(x - dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y - 1);
|
||||
} else if grid.in_bounds(x + dir, y - 1) && grid.get(x + dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y - 1);
|
||||
} else if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_acid(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if !grid.in_bounds(nx, ny) {
|
||||
continue;
|
||||
}
|
||||
let neighbor = grid.get(nx, ny);
|
||||
if neighbor.material != MaterialId::Empty
|
||||
&& neighbor.material != MaterialId::Acid
|
||||
&& neighbor.material != MaterialId::Stone
|
||||
&& self.rand() % 4 == 0
|
||||
{
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::empty();
|
||||
if self.rand() % 2 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::empty();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < 1.2) {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y + 1) && grid.get(x - dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
} else if grid.in_bounds(x + dir, y + 1) && grid.get(x + dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
} else if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_flesh(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 200.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Fire;
|
||||
new.temp = 400.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_grass(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 250.0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::new(MaterialId::Fire);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dirt(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 0.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Stone;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
}
|
||||
}
|
||||
|
||||
fn heat_transfer(&mut self, grid: &mut Grid) {
|
||||
let w = grid.width;
|
||||
let h = grid.height;
|
||||
let temps: Vec<f32> = grid.cells.iter().map(|c| c.temp).collect();
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let i = y * w + x;
|
||||
let cell = grid.cells[i];
|
||||
if cell.is_empty() || cell.is_static() {
|
||||
continue;
|
||||
}
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let mat = reg.get(cell.material);
|
||||
let k = mat.heat_conductivity;
|
||||
if k == 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0;
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x as i32 + dx;
|
||||
let ny = y as i32 + dy;
|
||||
if nx < 0 || nx >= w as i32 || ny < 0 || ny >= h as i32 {
|
||||
continue;
|
||||
}
|
||||
let ni = ny as usize * w + nx as usize;
|
||||
sum += temps[ni];
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
let avg = sum / count as f32;
|
||||
let mut new = cell;
|
||||
new.temp += (avg - cell.temp) * k * 0.5;
|
||||
grid.cells[i] = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_count(&self) -> u64 {
|
||||
self.tick
|
||||
}
|
||||
}
|
||||
|
||||
const NEIGHBORS4: [(i32, i32); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
|
||||
@@ -0,0 +1,122 @@
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
|
||||
pub const WORLD_W: usize = 250;
|
||||
pub const WORLD_H: usize = 250;
|
||||
|
||||
pub struct Grid {
|
||||
pub cells: Vec<Cell>,
|
||||
pub next: Vec<Cell>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn new() -> Self {
|
||||
let size = WORLD_W * WORLD_H;
|
||||
Self {
|
||||
cells: vec![Cell::empty(); size],
|
||||
next: vec![Cell::empty(); size],
|
||||
width: WORLD_W,
|
||||
height: WORLD_H,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn idx(&self, x: i32, y: i32) -> usize {
|
||||
(y as usize) * self.width + (x as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn in_bounds(&self, x: i32, y: i32) -> bool {
|
||||
x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get(&self, x: i32, y: i32) -> Cell {
|
||||
if !self.in_bounds(x, y) {
|
||||
return Cell::new(MaterialId::Stone);
|
||||
}
|
||||
self.cells[self.idx(x, y)]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self, x: i32, y: i32) -> &mut Cell {
|
||||
let i = self.idx(x, y);
|
||||
&mut self.cells[i]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: i32, y: i32, cell: Cell) {
|
||||
if self.in_bounds(x, y) {
|
||||
let i = self.idx(x, y);
|
||||
self.cells[i] = cell;
|
||||
self.next[i] = cell;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_material(&mut self, x: i32, y: i32, mat: MaterialId) {
|
||||
if self.in_bounds(x, y) {
|
||||
let cell = Cell::new(mat);
|
||||
let i = self.idx(x, y);
|
||||
self.cells[i] = cell;
|
||||
self.next[i] = cell;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = Cell::empty();
|
||||
}
|
||||
for c in &mut self.next {
|
||||
*c = Cell::empty();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_rect(&mut self, x0: i32, y0: i32, w: i32, h: i32, mat: MaterialId) {
|
||||
for dy in 0..h {
|
||||
for dx in 0..w {
|
||||
self.set_material(x0 + dx, y0 + dy, mat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_border(&mut self, mat: MaterialId) {
|
||||
for x in 0..self.width {
|
||||
self.set_material(x as i32, 0, mat);
|
||||
self.set_material(x as i32, (self.height - 1) as i32, mat);
|
||||
}
|
||||
for y in 0..self.height {
|
||||
self.set_material(0, y as i32, mat);
|
||||
self.set_material((self.width - 1) as i32, y as i32, mat);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap(&mut self) {
|
||||
std::mem::swap(&mut self.cells, &mut self.next);
|
||||
}
|
||||
|
||||
pub fn reset_tick_flags(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
c.updated_this_tick = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_region(&self, x0: i32, y0: i32, w: usize, h: usize) -> String {
|
||||
let mut buf = String::with_capacity(w * h + h);
|
||||
for dy in 0..h {
|
||||
for dx in 0..w {
|
||||
let x = x0 + dx as i32;
|
||||
let y = y0 + dy as i32;
|
||||
let ch = if self.in_bounds(x, y) {
|
||||
self.get(x, y).display_char()
|
||||
} else {
|
||||
'?'
|
||||
};
|
||||
buf.push(ch);
|
||||
}
|
||||
buf.push('\n');
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
use crate::world::cell::MaterialId;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Material {
|
||||
pub id: MaterialId,
|
||||
pub name: &'static str,
|
||||
pub density: f32,
|
||||
pub solid: bool,
|
||||
pub liquid: bool,
|
||||
pub gas: bool,
|
||||
pub static_: bool,
|
||||
pub flammable: bool,
|
||||
pub ignition_temp: f32,
|
||||
pub melt_temp: f32,
|
||||
pub heat_conductivity: f32,
|
||||
pub color_fg: (u8, u8, u8),
|
||||
pub color_bg: (u8, u8, u8),
|
||||
pub display_char: char,
|
||||
}
|
||||
|
||||
impl Material {
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
id: MaterialId::Empty,
|
||||
name: "empty",
|
||||
density: 0.0,
|
||||
solid: false,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.0,
|
||||
color_fg: (0, 0, 0),
|
||||
color_bg: (0, 0, 0),
|
||||
display_char: ' ',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MaterialRegistry {
|
||||
materials: [Material; 14],
|
||||
}
|
||||
|
||||
impl MaterialRegistry {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
materials: [
|
||||
Material {
|
||||
id: MaterialId::Empty,
|
||||
name: "empty",
|
||||
density: 0.0,
|
||||
solid: false,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.0,
|
||||
color_fg: (15, 15, 20),
|
||||
color_bg: (10, 10, 15),
|
||||
display_char: ' ',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Sand,
|
||||
name: "sand",
|
||||
density: 1.5,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: 1700.0,
|
||||
heat_conductivity: 0.2,
|
||||
color_fg: (218, 178, 90),
|
||||
color_bg: (60, 50, 30),
|
||||
display_char: '.',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Water,
|
||||
name: "water",
|
||||
density: 1.0,
|
||||
solid: false,
|
||||
liquid: true,
|
||||
gas: false,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: 0.0,
|
||||
heat_conductivity: 0.4,
|
||||
color_fg: (64, 128, 220),
|
||||
color_bg: (20, 40, 80),
|
||||
display_char: '~',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Stone,
|
||||
name: "stone",
|
||||
density: 3.0,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: 1200.0,
|
||||
heat_conductivity: 0.3,
|
||||
color_fg: (120, 120, 130),
|
||||
color_bg: (40, 40, 50),
|
||||
display_char: '#',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Lava,
|
||||
name: "lava",
|
||||
density: 2.5,
|
||||
solid: false,
|
||||
liquid: true,
|
||||
gas: false,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.5,
|
||||
color_fg: (255, 80, 20),
|
||||
color_bg: (120, 20, 0),
|
||||
display_char: '#',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Wood,
|
||||
name: "wood",
|
||||
density: 0.8,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: true,
|
||||
ignition_temp: 300.0,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.1,
|
||||
color_fg: (140, 90, 50),
|
||||
color_bg: (50, 30, 20),
|
||||
display_char: 'T',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Flesh,
|
||||
name: "flesh",
|
||||
density: 1.06,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: false,
|
||||
flammable: true,
|
||||
ignition_temp: 200.0,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.15,
|
||||
color_fg: (180, 50, 50),
|
||||
color_bg: (60, 15, 15),
|
||||
display_char: '%',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Bone,
|
||||
name: "bone",
|
||||
density: 1.8,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.1,
|
||||
color_fg: (220, 210, 190),
|
||||
color_bg: (60, 55, 50),
|
||||
display_char: '`',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Steam,
|
||||
name: "steam",
|
||||
density: 0.3,
|
||||
solid: false,
|
||||
liquid: false,
|
||||
gas: true,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.2,
|
||||
color_fg: (200, 200, 220),
|
||||
color_bg: (30, 30, 40),
|
||||
display_char: '~',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Fire,
|
||||
name: "fire",
|
||||
density: 0.1,
|
||||
solid: false,
|
||||
liquid: false,
|
||||
gas: true,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.6,
|
||||
color_fg: (255, 160, 40),
|
||||
color_bg: (100, 30, 0),
|
||||
display_char: '^',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Acid,
|
||||
name: "acid",
|
||||
density: 1.2,
|
||||
solid: false,
|
||||
liquid: true,
|
||||
gas: false,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.3,
|
||||
color_fg: (100, 255, 60),
|
||||
color_bg: (20, 60, 10),
|
||||
display_char: '~',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Smoke,
|
||||
name: "smoke",
|
||||
density: 0.2,
|
||||
solid: false,
|
||||
liquid: false,
|
||||
gas: true,
|
||||
static_: false,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.1,
|
||||
color_fg: (100, 100, 100),
|
||||
color_bg: (20, 20, 20),
|
||||
display_char: '*',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Grass,
|
||||
name: "grass",
|
||||
density: 1.0,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: true,
|
||||
ignition_temp: 250.0,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.1,
|
||||
color_fg: (80, 200, 60),
|
||||
color_bg: (20, 50, 15),
|
||||
display_char: '"',
|
||||
},
|
||||
Material {
|
||||
id: MaterialId::Dirt,
|
||||
name: "dirt",
|
||||
density: 1.3,
|
||||
solid: true,
|
||||
liquid: false,
|
||||
gas: false,
|
||||
static_: true,
|
||||
flammable: false,
|
||||
ignition_temp: f32::INFINITY,
|
||||
melt_temp: f32::INFINITY,
|
||||
heat_conductivity: 0.2,
|
||||
color_fg: (100, 70, 50),
|
||||
color_bg: (40, 30, 20),
|
||||
display_char: ':',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, id: MaterialId) -> &Material {
|
||||
&self.materials[id as usize]
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static MaterialRegistry {
|
||||
static REGISTRY: std::sync::OnceLock<MaterialRegistry> = std::sync::OnceLock::new();
|
||||
REGISTRY.get_or_init(MaterialRegistry::new)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod cell;
|
||||
pub mod material;
|
||||
pub mod grid;
|
||||
pub mod cellular;
|
||||
|
||||
pub use cell::{Cell, MaterialId};
|
||||
pub use material::{Material, MaterialRegistry};
|
||||
pub use grid::{Grid, WORLD_W, WORLD_H};
|
||||
pub use cellular::CellularAutomaton;
|
||||
Reference in New Issue
Block a user