Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
23 kB · 657 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658//! Phase 4 gate: scripted render scenes with screenshot hashes//! (docs/05 §5.2 step 7, wiring-plan §3).//!//! The corridor scene pins the full pipeline — raycast + shade + edge-art +//! fog + ANSI — to exact hashes. Any pixel change (ramp, palette, tint,//! fog, threshold) fails these tests until the golden is deliberately//! re-baselined, so renderer drift can never slip in silently.//!//! Readability bots (docs/05 §5.4): threat/exit/loot presence + contrast//! assertions on generated rooms — the machine-checked half of the//! three-glance law. Render purity: same inputs → byte-identical output.
use hexwork_core::actor::Stance;use hexwork_core::voxel::{material, ChunkedGrid, Voxel};use hexwork_render::ansi::encode_diff;use hexwork_render::camera::{first_person, top_tactical, View, TACTICAL_HEIGHT_M};use hexwork_render::palette::{self, Palette};use hexwork_render::scene::{ apply_fog, fog_for_z, render, render_tactical, render_tactical_map, TacticalMapContext,};use hexwork_render::shade::actor_glyph;use hexwork_render::{Camera, FrameBuffer};
fn stone(x: i32, y: i32, z: i32) -> (i32, i32, i32, Voxel) { ( x, y, z, Voxel { material: material::STONE, ..Voxel::default() }, )}
#[test]fn lantern_lights_air_side_of_wall_but_not_through_wall() { use hexwork_core::light::{sample_light_field, LightSource}; let mut grid = ChunkedGrid::new(); // An entire chunk-crossing wall prevents the radius-6 flood going around it. for y in -6..=6 { for z in -6..=6 { grid.set_world(2, y, z, occupied_voxel()); } } grid.set_world(0, 0, 0, Voxel::default()); grid.set_world(3, 0, 0, Voxel::default()); let source = LightSource { x: 0, y: 0, z: 0, radius: 6, warm: true, }; let field = sample_light_field(&grid, &[source]); assert!(field[&(1, 0, 0)] > 0); assert!(!field.contains_key(&(2, 0, 0))); assert!(!field.contains_key(&(3, 0, 0))); assert_eq!( grid.get_world(1, 0, 0).light, 0, "sampling must be read-only" ); let lights = field .into_iter() .map(|((x, y, z), level)| (x, y, z, level)) .collect::<Vec<_>>(); let view = View::new([0.5, 0.5, 0.5], 0.0, 0.0, 1.0, Camera::FirstPerson); let mut lit = FrameBuffer::new(1, 1); let mut dark = FrameBuffer::new(1, 1); render(&grid, &view, &palette::DEFAULT, &mut lit, &lights, &[]); render(&grid, &view, &palette::DEFAULT, &mut dark, &[], &[]); let brightness = |cell: &hexwork_render::Cell| cell.fg.iter().map(|v| u16::from(*v)).sum::<u16>(); assert!(brightness(&lit.cells[0]) > brightness(&dark.cells[0])); assert_ne!(lit.cells[0].glyph, ' '); assert!( lit.cells[0].fg[1] >= 120, "lit stone must not be near-black" ); assert!( lit.cells[0].fg[1] >= lit.cells[0].bg[1] * 3, "flat wall density must retain a dark background partner" ); assert!( dark.cells[0].fg.iter().all(|channel| *channel <= 10), "display exposure must not illuminate an unlit wall" );}
fn occupied_voxel() -> Voxel { Voxel { material: material::STONE, occ: 1, ..Voxel::default() }}
fn water_voxel() -> Voxel { Voxel { fluid_kind: 1, fluid_amt: 1, ..Voxel::default() }}
fn map_context() -> TacticalMapContext { TacticalMapContext { min: [-100; 3], max: [100; 3], player_cell: [99; 3], }}
fn tactical_plane_fixture(desired_z: i32, former_wrong_z: i32) -> ChunkedGrid { assert!( desired_z.abs_diff(former_wrong_z) > 6, "fixture planes must be well separated with open heads" ); let mut grid = ChunkedGrid::new(); grid.set_world(0, 0, desired_z, occupied_voxel()); grid.set_world(0, 0, former_wrong_z, water_voxel()); assert_eq!( tactical_pixel(&grid, &top_tactical([0.0; 3], former_wrong_z, 1.0)), '~' ); grid}
// Every retained explicit-plane regression exercises both public entry points.fn tactical_pixel(grid: &ChunkedGrid, view: &View) -> char { let mut fb = FrameBuffer::new(1, 1); render_tactical(grid, view, &mut fb); let mut bounded = FrameBuffer::new(1, 1); render_tactical_map(grid, view, &map_context(), &mut bounded); assert_eq!(fb.cells, bounded.cells); fb.cells[0].glyph}
/// Corridor scene: stone walls at x = ±4, copper floor stripe at z = 0,/// lantern light at the eye, one rat billboard. Small on purpose (40×12)/// so the hash stays readable and the test stays fast.fn corridor() -> ChunkedGrid { let mut grid = ChunkedGrid::new(); for y in -8..=8 { for z in -2..=6 { for (x, y, z, v) in [stone(-4, y, z), stone(4, y, z)] { grid.set_world(x, y, z, v); } } } for x in -4..=4 { for y in -8..=8 { grid.set_world( x, y, 0, Voxel { material: material::COPPER, ..Voxel::default() }, ); } } grid}
fn lit_corridor_lights() -> Vec<(i32, i32, i32, u8)> { // Lantern at the eye cell, full bright; the floor stripe glows dimly. let mut lights = vec![(0, 0, 2, 24u8)]; for x in -4..=4 { lights.push((x, 0, 0, 6)); } lights}
/// FNV-1a over glyph + fg + bg of every cell: the screenshot hash.fn frame_hash(fb: &FrameBuffer) -> u64 { let mut h: u64 = 0xcbf2_9ce4_8422_2325; let mut mix = |w: u64| { h ^= w; h = h.wrapping_mul(0x100_0000_01b3); }; for c in &fb.cells { mix(c.glyph as u64); mix(u64::from(c.fg[0]) << 16 | u64::from(c.fg[1]) << 8 | u64::from(c.fg[2])); mix(u64::from(c.bg[0]) << 16 | u64::from(c.bg[1]) << 8 | u64::from(c.bg[2])); } mix(fb.w as u64); mix(fb.h as u64); h}
#[test]fn corridor_scene_hash_is_stable() { let grid = corridor(); let pal = Palette { name: "test", fog_shallows: palette::DEFAULT.fog_shallows, fog_works: palette::DEFAULT.fog_works, fog_deep: palette::DEFAULT.fog_deep, no_flicker: false, }; let view = first_person([0.0, 0.0, 2.0], 0.0, 0.0); let lights = lit_corridor_lights(); let actors = [("rat", [2.5, 0.5, 1.0], Stance::Stand, true, 0u8)]; let mut a = FrameBuffer::new(40, 12); let mut b = FrameBuffer::new(40, 12); render(&grid, &view, &pal, &mut a, &lights, &actors); render(&grid, &view, &pal, &mut b, &lights, &actors); assert_eq!(frame_hash(&a), frame_hash(&b), "render must be pure"); // Golden: re-baseline deliberately if the pipeline changes on purpose. // Rebased for sRGB lit-foreground encoding and density/edge background repair. assert_eq!(frame_hash(&a), 8318643008447625755u64);}
#[test]fn corridor_scene_has_threat_exit_loot() { // Three-glance law (docs/05 §5.4.1): threat glyph, exit edge-art, loot // glint each present within one frame of entering the corridor. let grid = corridor(); let pal = palette::DEFAULT; let view = first_person([0.0, 0.0, 2.0], 0.0, 0.0); let lights = lit_corridor_lights(); let actors = [("rat", [2.5, 0.5, 1.0], Stance::Stand, true, 0u8)]; let mut fb = FrameBuffer::new(40, 12); render(&grid, &view, &pal, &mut fb, &lights, &actors); let glyphs: Vec<char> = fb.cells.iter().map(|c| c.glyph).collect(); assert!( glyphs.contains(&'r'), "threat (rat billboard) must be visible" ); assert!( glyphs.iter().any(|g| matches!(g, '│' | '─' | '+')), "exit edge-art must be visible" ); assert!( glyphs.iter().any(|g| !matches!(g, ' ' | '·')), "lit loot/density detail must be visible" ); // Brightness redundancy: the threat cell is not hue-only — it carries // above-darkness luminance. let rat_cell = fb .cells .iter() .find(|c| c.glyph == 'r') .expect("rat cell present"); let lum = u16::from(rat_cell.fg[0]) + u16::from(rat_cell.fg[1]) + u16::from(rat_cell.fg[2]); assert!(lum > 8, "threat must read by brightness too, got {lum}");}
#[test]fn no_camouflage_kill_lethal_vs_harmless() { // Docs/05 §5.4.2: no lethal monster shares glyph+color behavior with a // harmless one at equal light. Table-driven over actor_glyph. let lethal = ["rat", "cable-rat", "vendric", "mimic"]; let harmless = ["player", "townsfolk", "tinker", "warden", "bell", "cherub"]; for l in lethal { for h in harmless { assert_ne!( actor_glyph(l, Stance::Stand, true, 0), actor_glyph(h, Stance::Stand, true, 0), "lethal '{l}' must not share a glyph with harmless '{h}'" ); } }}
#[test]fn render_purity_same_inputs_identical_ansi() { // Render-purity law (docs/05 §5.4.3): byte-identical ANSI across runs. let grid = corridor(); let pal = palette::DEFAULT; let view = first_person([0.0, 0.0, 2.0], 0.0, 0.0); let lights = lit_corridor_lights(); let actors = [("rat", [2.5, 0.5, 1.0], Stance::Stand, true, 0u8)]; let mut a = FrameBuffer::new(40, 12); let mut b = FrameBuffer::new(40, 12); let blank = FrameBuffer::new(40, 12); render(&grid, &view, &pal, &mut a, &lights, &actors); render(&grid, &view, &pal, &mut b, &lights, &actors); let mut sa = String::new(); let mut sb = String::new(); encode_diff(&blank, &a, &mut sa); encode_diff(&blank, &b, &mut sb); assert_eq!(sa, sb, "ANSI output must be byte-identical"); assert!(!sa.is_empty(), "scene must emit something");}
#[test]fn fog_bands_pin_stratum_colors() { let pal = palette::DEFAULT; assert_eq!(fog_for_z(&pal, 0.0), pal.fog_shallows); assert_eq!(fog_for_z(&pal, -50.0), pal.fog_works); assert_eq!(fog_for_z(&pal, -100.0), pal.fog_deep); // Post-lerp endpoints: t=0 identity, t=1 full fog. let (fg, _) = apply_fog([200, 100, 50], [0, 0, 0], [26, 18, 12], 0.0); assert_eq!(fg, [200, 100, 50]); let (fg, _) = apply_fog([200, 100, 50], [0, 0, 0], [26, 18, 12], 1.0); assert_eq!(fg, [26, 18, 12]);}
#[test]fn tactical_view_renders_slice() { let mut grid = ChunkedGrid::new(); grid.set_world(0, 0, 0, occupied_voxel()); grid.set_world(1, 0, -1, occupied_voxel()); // Explicit body obstruction, support-only floor, and otherwise empty air. let view = top_tactical([0.0, 0.0, 0.0], 0, 1.0); assert_eq!(view.mode, Camera::TopTactical); let mut fb = FrameBuffer::new(20, 10); render_tactical(&grid, &view, &mut fb); let glyphs: Vec<char> = fb.cells.iter().map(|c| c.glyph).collect(); assert!(glyphs.contains(&'#'), "body obstruction must render"); assert!(glyphs.contains(&'.'), "supported floor must render"); assert!(glyphs.contains(&' '), "air must render");}
#[test]fn tactical_negative_slice_samples_requested_plane() { for slice_z in [-12, -30] { // At zoom 1, the former eye heuristic sampled slice + 24 here. let former_wrong_z = slice_z + TACTICAL_HEIGHT_M as i32; let grid = tactical_plane_fixture(slice_z, former_wrong_z); let view = top_tactical([0.0, 0.0, 0.0], slice_z, 1.0); assert_eq!( tactical_pixel(&grid, &view), '#', "slice {slice_z} must sample occupied stone, not open water at {former_wrong_z}" ); }}
#[test]fn tactical_zoom_does_not_change_selected_plane() { let slice_z = 10; for (zoom, former_wrong_z) in [(0.5, 34), (2.0, -2), (4.0, -8)] { let grid = tactical_plane_fixture(slice_z, former_wrong_z); let view = top_tactical([0.0, 0.0, 0.0], slice_z, zoom); assert_eq!( tactical_pixel(&grid, &view), '#', "zoom {zoom} must sample occupied stone at {slice_z}, not open water at {former_wrong_z}" ); }}
#[test]fn tactical_direct_view_uses_explicit_eye_plane() { let grid = tactical_plane_fixture(15, -9); let view = View::new( [0.0, 0.0, 15.0], 0.0, -std::f32::consts::FRAC_PI_2, 75.0, Camera::TopTactical, ); assert_eq!(view.tactical_slice_z, Some(15)); assert_eq!(tactical_pixel(&grid, &view), '#');}
#[test]#[should_panic(expected = "TopTactical view must carry explicit tactical_slice_z metadata")]fn tactical_view_without_slice_metadata_fails_clear_invariant() { let mut view = View::new( [0.0, 0.0, 15.0], 0.0, -std::f32::consts::FRAC_PI_2, 75.0, Camera::TopTactical, ); view.tactical_slice_z = None; let mut fb = FrameBuffer::new(1, 1); render_tactical(&ChunkedGrid::new(), &view, &mut fb);}
fn bounded_glyphs( grid: &ChunkedGrid, view: &View, context: &TacticalMapContext, w: usize, h: usize,) -> String { let mut fb = FrameBuffer::new(w, h); render_tactical_map(grid, view, context, &mut fb); fb.cells.iter().map(|c| c.glyph).collect()}
#[test]fn tactical_classifier_uses_body_head_support_and_hazard_priority() { let air = Voxel::default(); let solid = occupied_voxel(); let water = water_voxel(); let gas = Voxel { gas_kind: 1, gas_amt: 1, ..air }; let both = Voxel { gas_kind: 1, gas_amt: 1, ..water }; let blocked_water = Voxel { occ: 1, ..both }; // Body/head blockage wins over hazards, water over gas, gas over support. for (body, head, support, expected) in [ (solid, air, air, '#'), (blocked_water, air, solid, '#'), (both, solid, solid, '#'), (both, air, solid, '~'), (gas, air, solid, ':'), (air, air, solid, '.'), (air, air, air, ' '), ( Voxel { material: material::GLASS, ..air }, air, air, ' ', ), ( Voxel { fluid_kind: 2, fluid_amt: 1, gas_kind: 1, ..air }, air, air, ' ', ), ] { let mut grid = ChunkedGrid::new(); grid.set_world(0, 0, 0, body); grid.set_world(0, 0, 1, head); grid.set_world(0, 0, -1, support); // A deeper foundation must never be scanned into a floor/wall glyph. grid.set_world(0, 0, -2, solid); assert_eq!( tactical_pixel(&grid, &top_tactical([0.0; 3], 0, 1.0)), expected ); }}
#[test]fn tactical_context_bounds_and_player_overlay_are_honest() { let mut grid = ChunkedGrid::new(); for x in -2..=2 { grid.set_world(x, 0, -1, occupied_voxel()); grid.set_world(x, 0, 1, occupied_voxel()); } grid.set_world(-2, 0, 0, occupied_voxel()); grid.set_world(2, 0, 0, occupied_voxel()); grid.set_world(-1, 0, 0, water_voxel()); grid.set_world( 1, 0, 0, Voxel { gas_kind: 1, gas_amt: 1, ..Voxel::default() }, ); let view = top_tactical([0.0; 3], 0, 1.0); let mut context = TacticalMapContext { min: [-1, 0, 0], max: [1, 0, 0], player_cell: [0, 0, 0], }; // Head/support sentinels outside the location must not influence terrain. assert_eq!(bounded_glyphs(&grid, &view, &context, 5, 1), " ~@: "); context.player_cell = [-1, 0, 0]; assert_eq!(bounded_glyphs(&grid, &view, &context, 5, 1), " @ : "); context.player_cell = [1, 0, 0]; assert_eq!(bounded_glyphs(&grid, &view, &context, 5, 1), " ~ @ "); for player in [[0, 0, 1], [-2, 0, 0], [2, 0, 0], [0, 1, 0]] { context.player_cell = player; assert_eq!(bounded_glyphs(&grid, &view, &context, 5, 1), " ~ : "); } context.player_cell = [1, 0, 0]; // In bounds, but offscreen. assert_eq!(bounded_glyphs(&grid, &view, &context, 1, 1), " "); context.player_cell = [0, 0, 0]; grid.set_world(0, 0, 0, occupied_voxel()); assert_eq!(bounded_glyphs(&grid, &view, &context, 1, 1), "@"); for z in [-1, 1] { assert_eq!( bounded_glyphs(&grid, &top_tactical([0.0; 3], z, 1.0), &context, 5, 1), " " ); }}
#[test]fn tactical_projection_handles_checked_coordinate_edges() { let mut grid = ChunkedGrid::new(); grid.set_world(-1, -2, 0, occupied_voxel()); let view = top_tactical([-1.9, -2.9, 0.0], 0, 1.0); assert_eq!(tactical_pixel(&grid, &view), '#'); // Truncation, not floor. let mut context = TacticalMapContext { min: [i32::MIN; 3], max: [i32::MAX; 3], player_cell: [0, 0, 0], }; for z in [i32::MIN, i32::MAX] { grid.set_world(i32::MIN, i32::MIN, z, water_voxel()); let view = top_tactical([i32::MIN as f32, i32::MIN as f32, 0.0], z, 1.0); context.player_cell = [i32::MIN, i32::MIN, z]; assert_eq!(bounded_glyphs(&grid, &view, &context, 3, 3), " @ "); let mut legacy = FrameBuffer::new(3, 3); render_tactical(&grid, &view, &mut legacy); assert_eq!( legacy.cells.iter().map(|c| c.glyph).collect::<String>(), " ~ " ); // Open edge cells force checked head AND support evaluation. grid.set_world(i32::MIN, i32::MIN, z, Voxel::default()); render_tactical(&grid, &view, &mut legacy); assert!(legacy.cells.iter().all(|c| c.glyph == ' ')); } // Largest finite f32 center below i32::MAX: viewport offsets can still // cross MAX, and must blank rather than wrap onto a MIN sentinel. grid.set_world(i32::MAX, 0, 0, occupied_voxel()); grid.set_world(i32::MIN, 0, 0, occupied_voxel()); let edge_view = top_tactical([2_147_483_520.0, 0.0, 0.0], 0, 1.0); context.player_cell = [i32::MAX, 0, 0]; let edge = bounded_glyphs(&grid, &edge_view, &context, 259, 1); assert_eq!(edge.chars().nth(256), Some('@')); assert_eq!(&edge[257..], " "); let mut legacy = FrameBuffer::new(259, 1); render_tactical(&grid, &edge_view, &mut legacy); assert_eq!(legacy.cells[256].glyph, '#'); assert!(legacy.cells[257..].iter().all(|c| c.glyph == ' ')); for axis in [0, 1] { for value in [ f32::NAN, f32::INFINITY, f32::NEG_INFINITY, i32::MAX as f32, f32::MIN, ] { let mut view = top_tactical([0.0; 3], 0, 1.0); view.eye[axis] = value; let mut fb = FrameBuffer::new(3, 3); fb.cells[0].glyph = '!'; render_tactical(&grid, &view, &mut fb); assert!(fb.cells.iter().all(|c| c.glyph == ' ')); context.player_cell = [0, 0, 0]; assert_eq!(bounded_glyphs(&grid, &view, &context, 3, 3), " "); } } let view = top_tactical([0.0; 3], 0, 1.0); for (w, h) in [(0, 0), (0, 3), (3, 0), (0, usize::MAX), (usize::MAX, 0)] { assert_eq!(bounded_glyphs(&grid, &view, &context, w, h), ""); render_tactical(&grid, &view, &mut FrameBuffer::new(w, h)); } for axis in 0..3 { let mut invalid = context; invalid.min[axis] = 1; invalid.max[axis] = 0; assert_eq!(bounded_glyphs(&grid, &view, &invalid, 3, 3), " "); }}
#[test]fn tactical_map_is_pure_and_ansi_stable() { let grid = tactical_plane_fixture(-12, 12); let before = format!("{grid:?}"); let view = top_tactical([0.0; 3], -12, 2.0); let context = TacticalMapContext { player_cell: [1, 0, -12], ..map_context() }; let original_context = context; let blank = FrameBuffer::new(5, 3); for bounded in [false, true] { let mut a = blank.clone(); let mut b = blank.clone(); b.cells[0].glyph = '!'; for fb in [&mut a, &mut b] { if bounded { render_tactical_map(&grid, &view, &context, fb); } else { render_tactical(&grid, &view, fb); } } assert_eq!(a.cells, b.cells); let (mut sa, mut sb) = (String::new(), String::new()); encode_diff(&blank, &a, &mut sa); encode_diff(&blank, &b, &mut sb); assert_eq!(sa, sb); assert!(!sa.is_empty()); } assert_eq!(context, original_context); assert_eq!(format!("{grid:?}"), before); assert_eq!(view.eye, [0.0, 0.0, -12.0 + TACTICAL_HEIGHT_M / 2.0]); assert_eq!(view.tactical_slice_z, Some(-12));}
#[test]fn tactical_both_apis_preserve_explicit_slice_and_zoom() { tactical_negative_slice_samples_requested_plane(); tactical_zoom_does_not_change_selected_plane(); tactical_direct_view_uses_explicit_eye_plane(); tactical_metadata_preserves_eye_formula(); // Both entry points preserve clear invariants, before invalid-map guards. for bounded in [false, true] { for missing_metadata in [false, true] { let mut view = if missing_metadata { top_tactical([0.0; 3], 0, 1.0) } else { first_person([0.0; 3], 0.0, 0.0) }; if missing_metadata { view.tactical_slice_z = None; } let panic = std::panic::catch_unwind(|| { let mut fb = FrameBuffer::new(0, 0); if bounded { render_tactical_map(&ChunkedGrid::new(), &view, &map_context(), &mut fb); } else { render_tactical(&ChunkedGrid::new(), &view, &mut fb); } }) .expect_err("invalid views must fail clearly"); let message = panic .downcast_ref::<String>() .map(String::as_str) .or_else(|| panic.downcast_ref::<&str>().copied()) .unwrap(); assert!(message.contains(if missing_metadata { "explicit tactical_slice_z metadata" } else { "needs a TopTactical view" })); } }}
#[test]fn tactical_metadata_preserves_eye_formula() { for (slice_z, zoom) in [(-30, 0.5), (-12, 2.0), (10, 4.0)] { let view = top_tactical([3.0, 4.0, 999.0], slice_z, zoom); assert_eq!(view.tactical_slice_z, Some(slice_z)); assert_eq!( view.eye, [3.0, 4.0, slice_z as f32 + TACTICAL_HEIGHT_M / zoom] ); } assert_eq!( first_person([0.0, 0.0, 0.0], 0.0, 0.0).tactical_slice_z, None );}