feat: smaller cells (10x10px) for higher detail

Cells reduced from 16x16 to 10x10 pixels. Same window size now
shows more of the world — finer detail, more cells visible.
Adaptive viewport recalculates grid_w/grid_h from new cell size.

109 tests, 0 failures
This commit is contained in:
Emil
2026-06-21 10:04:21 +03:00
parent 8b055ac37f
commit 285688bcad
70 changed files with 22764 additions and 624 deletions
+199 -110
View File
@@ -1,18 +1,21 @@
use clap::Parser;
use std::io::Write;
use std::sync::Arc;
use std::time::{Duration, Instant};
use verbatim::ai;
use verbatim::game::Game;
use verbatim::render::terminal::TerminalRenderer;
use verbatim::render::window_input::WindowInput;
use verbatim::world::cell::MaterialId;
use verbatim::ai;
use std::io::Write;
use std::sync::Arc;
use winit::event::{Event, WindowEvent};
use winit::event_loop::{EventLoop, ControlFlow};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::Window;
use std::time::{Duration, Instant};
#[derive(Parser, Debug)]
#[command(name = "verbatim", about = "ASCII physics RPG - Noita meets Caves of Qud")]
#[command(
name = "verbatim",
about = "ASCII physics RPG - Noita meets Caves of Qud"
)]
struct Cli {
#[arg(long, default_value = "ascii")]
mode: String,
@@ -31,8 +34,16 @@ struct Cli {
}
trait GpuRenderer {
fn new(window: Arc<Window>) -> Result<Self, String> where Self: Sized;
fn render(&mut self, grid: &verbatim::world::grid::Grid, entities: &verbatim::entity::EntityManager, cam_x: i32, cam_y: i32);
fn new(window: Arc<Window>) -> Result<Self, String>
where
Self: Sized;
fn render(
&mut self,
grid: &verbatim::world::grid::Grid,
entities: &verbatim::entity::EntityManager,
cam_x: i32,
cam_y: i32,
);
fn grid_w(&self) -> usize;
fn grid_h(&self) -> usize;
}
@@ -41,22 +52,42 @@ impl GpuRenderer for verbatim::render::vulkan::VulkanRenderer {
fn new(window: Arc<Window>) -> Result<Self, String> {
verbatim::render::vulkan::VulkanRenderer::new(window)
}
fn render(&mut self, grid: &verbatim::world::grid::Grid, entities: &verbatim::entity::EntityManager, cam_x: i32, cam_y: i32) {
fn render(
&mut self,
grid: &verbatim::world::grid::Grid,
entities: &verbatim::entity::EntityManager,
cam_x: i32,
cam_y: i32,
) {
verbatim::render::vulkan::VulkanRenderer::render(self, grid, entities, cam_x, cam_y)
}
fn grid_w(&self) -> usize { verbatim::render::vulkan::VulkanRenderer::grid_w(self) }
fn grid_h(&self) -> usize { verbatim::render::vulkan::VulkanRenderer::grid_h(self) }
fn grid_w(&self) -> usize {
verbatim::render::vulkan::VulkanRenderer::grid_w(self)
}
fn grid_h(&self) -> usize {
verbatim::render::vulkan::VulkanRenderer::grid_h(self)
}
}
impl GpuRenderer for verbatim::render::graphics::GraphicsRenderer {
fn new(window: Arc<Window>) -> Result<Self, String> {
verbatim::render::graphics::GraphicsRenderer::new(window)
}
fn render(&mut self, grid: &verbatim::world::grid::Grid, entities: &verbatim::entity::EntityManager, cam_x: i32, cam_y: i32) {
fn render(
&mut self,
grid: &verbatim::world::grid::Grid,
entities: &verbatim::entity::EntityManager,
cam_x: i32,
cam_y: i32,
) {
verbatim::render::graphics::GraphicsRenderer::render(self, grid, entities, cam_x, cam_y)
}
fn grid_w(&self) -> usize { verbatim::render::graphics::GraphicsRenderer::grid_w(self) }
fn grid_h(&self) -> usize { verbatim::render::graphics::GraphicsRenderer::grid_h(self) }
fn grid_w(&self) -> usize {
verbatim::render::graphics::GraphicsRenderer::grid_w(self)
}
fn grid_h(&self) -> usize {
verbatim::render::graphics::GraphicsRenderer::grid_h(self)
}
}
fn main() {
@@ -103,7 +134,10 @@ fn main() {
}
}
_ => {
eprintln!("Unknown mode: {}. Use terminal, ascii, graphics, pipe, test, replay, or headless.", cli.mode);
eprintln!(
"Unknown mode: {}. Use terminal, ascii, graphics, pipe, test, replay, or headless.",
cli.mode
);
std::process::exit(1);
}
}
@@ -111,11 +145,13 @@ fn main() {
fn run_gpu_mode<R: GpuRenderer>(title: &str) {
let event_loop = EventLoop::new().expect("Failed to create event loop");
let window = event_loop.create_window(
Window::default_attributes()
.with_title(title)
.with_inner_size(winit::dpi::LogicalSize::new(160 * 16, 50 * 16))
).expect("Failed to create window");
let window = event_loop
.create_window(
Window::default_attributes()
.with_title(title)
.with_inner_size(winit::dpi::LogicalSize::new(160 * 10, 50 * 10)),
)
.expect("Failed to create window");
let window = Arc::new(window);
let mut renderer = match R::new(Arc::clone(&window)) {
@@ -140,109 +176,123 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
let mut accumulator = Duration::ZERO;
let mut running = true;
event_loop.run(|event, ctrl| {
ctrl.set_control_flow(ControlFlow::Poll);
event_loop
.run(|event, ctrl| {
ctrl.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event, .. } => {
match event {
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => {
running = false;
ctrl.exit();
}
WindowEvent::KeyboardInput { event: key_event, .. } => {
WindowEvent::KeyboardInput {
event: key_event, ..
} => {
input.on_key_event(key_event.physical_key, key_event.state);
}
_ => {}
}
}
Event::AboutToWait => {
if !running {
ctrl.exit();
return;
}
},
Event::AboutToWait => {
if !running {
ctrl.exit();
return;
}
let vw = renderer.grid_w();
let vh = renderer.grid_h();
let vw = renderer.grid_w();
let vh = renderer.grid_h();
let now = Instant::now();
let frame_time = now.duration_since(last_time);
last_time = now;
accumulator += frame_time;
let now = Instant::now();
let frame_time = now.duration_since(last_time);
last_time = now;
accumulator += frame_time;
let mut steps = 0;
while accumulator >= fixed_dt && steps < 5 {
game.fixed_update();
accumulator -= fixed_dt;
steps += 1;
}
let mut steps = 0;
while accumulator >= fixed_dt && steps < 5 {
game.fixed_update();
accumulator -= fixed_dt;
steps += 1;
}
input.update();
input.update();
if input.quit {
running = false;
ctrl.exit();
return;
}
if input.quit {
running = false;
ctrl.exit();
return;
}
if input.jump {
let on_ground = game.check_on_ground();
game.player.jump(&mut game.entities, on_ground);
}
if input.jump {
let on_ground = game.check_on_ground();
game.player.jump(&mut game.entities, on_ground);
}
if input.left {
game.player.move_left(&mut game.entities);
} else if input.right {
game.player.move_right(&mut game.entities);
} else {
game.player.stop_horizontal(&mut game.entities);
}
if input.left {
game.player.move_left(&mut game.entities);
} else if input.right {
game.player.move_right(&mut game.entities);
} else {
game.player.stop_horizontal(&mut game.entities);
}
if input.cam_left { game.cam_x -= 3; }
if input.cam_right { game.cam_x += 3; }
if input.cam_up { game.cam_y -= 3; }
if input.cam_down { game.cam_y += 3; }
if input.cam_left {
game.cam_x -= 3;
}
if input.cam_right {
game.cam_x += 3;
}
if input.cam_up {
game.cam_y -= 3;
}
if input.cam_down {
game.cam_y += 3;
}
if let Some(brush_id) = input.paint {
let mat = match brush_id {
1 => MaterialId::Sand,
2 => MaterialId::Water,
3 => MaterialId::Stone,
4 => MaterialId::Lava,
5 => MaterialId::Wood,
6 => MaterialId::Acid,
7 => MaterialId::Grass,
8 => MaterialId::Dirt,
9 => MaterialId::Fire,
0 => MaterialId::Flesh,
99 => MaterialId::Empty,
_ => MaterialId::Empty,
};
let cx = game.cam_x + (vw as i32 / 2);
let cy = game.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 mat == MaterialId::Empty {
game.grid.set(cx + dx, cy + dy, verbatim::world::cell::Cell::empty());
} else {
game.grid.set_material(cx + dx, cy + dy, mat);
if let Some(brush_id) = input.paint {
let mat = match brush_id {
1 => MaterialId::Sand,
2 => MaterialId::Water,
3 => MaterialId::Stone,
4 => MaterialId::Lava,
5 => MaterialId::Wood,
6 => MaterialId::Acid,
7 => MaterialId::Grass,
8 => MaterialId::Dirt,
9 => MaterialId::Fire,
0 => MaterialId::Flesh,
99 => MaterialId::Empty,
_ => MaterialId::Empty,
};
let cx = game.cam_x + (vw as i32 / 2);
let cy = game.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 mat == MaterialId::Empty {
game.grid.set(
cx + dx,
cy + dy,
verbatim::world::cell::Cell::empty(),
);
} else {
game.grid.set_material(cx + dx, cy + dy, mat);
}
}
}
}
}
let (px, py) = game.player.center(&game.entities);
game.cam_x = px as i32 - (vw as i32 / 2);
game.cam_y = py as i32 - (vh as i32 / 2);
renderer.render(&game.grid, &game.entities, game.cam_x, game.cam_y);
}
let (px, py) = game.player.center(&game.entities);
game.cam_x = px as i32 - (vw as i32 / 2);
game.cam_y = py as i32 - (vh as i32 / 2);
renderer.render(&game.grid, &game.entities, game.cam_x, game.cam_y);
_ => {}
}
_ => {}
}
}).expect("event loop error");
})
.expect("event loop error");
}
fn run_test_mode(cli: &Cli) {
@@ -292,8 +342,10 @@ fn run_replay_mode(cli: &Cli) {
println!("=== Replay: {} events ===", player.recording().events.len());
println!("Final tick: {}", state.tick);
if let Some(ref p) = state.player {
println!("Player: {} hp={:.1}/{:.1} pos=({:.1},{:.1}) alive={}",
p.kind, p.health, p.max_health, p.pos[0], p.pos[1], p.alive);
println!(
"Player: {} hp={:.1}/{:.1} pos=({:.1},{:.1}) alive={}",
p.kind, p.health, p.max_health, p.pos[0], p.pos[1], p.alive
);
}
println!("Entities: {}", state.entities.len());
println!("\nFinal view:\n{}", state.view);
@@ -314,8 +366,14 @@ fn run_headless(ticks: u32) {
let cam_y = py as i32 - 12;
let mut log = String::new();
log.push_str(&format!("=== Verbatim Headless Run: {} ticks ===\n\n", ticks));
log.push_str(&format!("World: {}x{}\n", game.grid.width, game.grid.height));
log.push_str(&format!(
"=== Verbatim Headless Run: {} ticks ===\n\n",
ticks
));
log.push_str(&format!(
"World: {}x{}\n",
game.grid.width, game.grid.height
));
log.push_str(&format!("Player start: ({:.1}, {:.1})\n", px, py));
log.push_str(&format!("Camera: ({}, {})\n\n", cam_x, cam_y));
@@ -339,9 +397,19 @@ fn run_headless(ticks: u32) {
log.push_str(&format!("Player: {:?}\n", player_info(&game)));
log.push_str(&format!("Entities: {}\n", game.entities.all().len()));
let alive: Vec<_> = game.entities.all().iter()
let alive: Vec<_> = game
.entities
.all()
.iter()
.filter(|e| e.alive)
.map(|e| format!("{}(hp={:.0}, pos={:?})", ai::entity_kind_name(e.kind), e.health, e.center()))
.map(|e| {
format!(
"{}(hp={:.0}, pos={:?})",
ai::entity_kind_name(e.kind),
e.health,
e.center()
)
})
.collect();
log.push_str(&format!("Alive entities: {}\n\n", alive.join(", ")));
}
@@ -349,16 +417,27 @@ fn run_headless(ticks: u32) {
let mut f = std::fs::File::create("headless_dump.txt").expect("Cannot create dump file");
f.write_all(log.as_bytes()).expect("Cannot write dump");
eprintln!("Headless run complete: {} ticks, dump written to headless_dump.txt", ticks);
eprintln!(
"Headless run complete: {} ticks, dump written to headless_dump.txt",
ticks
);
}
fn dump_view(grid: &verbatim::world::grid::Grid, entities: &verbatim::entity::EntityManager, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
fn dump_view(
grid: &verbatim::world::grid::Grid,
entities: &verbatim::entity::EntityManager,
cam_x: i32,
cam_y: i32,
vw: usize,
vh: usize,
) -> String {
ai::render_view(grid, entities, cam_x, cam_y, vw, vh)
.lines()
.enumerate()
.map(|(i, line)| format!("{:2}{}", (cam_y + i as i32) % 100, line))
.collect::<Vec<_>>()
.join("\n") + "\n"
.join("\n")
+ "\n"
}
fn player_info(game: &Game) -> String {
@@ -367,7 +446,17 @@ fn player_info(game: &Game) -> String {
let body_count = e.bodies.iter().filter(|b| b.alive).count();
let on_fire = e.on_fire;
let kind = ai::entity_kind_name(e.kind);
format!("{} hp={:.1}/{:.1} pos=({:.1},{:.1}) bodies={}/{} on_fire={}", kind, e.health, e.max_health, cx, cy, body_count, e.bodies.len(), on_fire)
format!(
"{} hp={:.1}/{:.1} pos=({:.1},{:.1}) bodies={}/{} on_fire={}",
kind,
e.health,
e.max_health,
cx,
cy,
body_count,
e.bodies.len(),
on_fire
)
} else {
"None".to_string()
}
+628 -276
View File
File diff suppressed because it is too large Load Diff
+842 -238
View File
File diff suppressed because it is too large Load Diff