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:
+199
-110
@@ -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
File diff suppressed because it is too large
Load Diff
+842
-238
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user