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.
11 kB · 281 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282//! Material property table: what each voxel material conducts, burns as,//! rusts into, or is eaten by.//!//! Design trace: docs/03-systems-design.md §3.3 law 6 ("Materials — acid//! pits iron, verdigris eats copper, rust spreads in damp, silver stills//! Drowned, copper conducts hymns/shocks"); docs/04 §4.2 phases 6–7//! (fire & heat, signal & electricity); hard rule §4.10.2 (determinism).//!//! This module is a pure lookup table: it takes no grid, no RNG, and emits//! no cause-links, so the "every mutating fn takes tick + CauseChain" rule//! does not apply here — there are no mutating functions.//!//! Material ids are protocol (`crate::voxel::material`, 11 materials)://! AIR=0 WOOD=1 IRON=2 COPPER=3 STONE=4 BRASS=5 FLESH=6 SAINT_BONE=7//! GLASS=8 ROPE=9 PAPER=10.//!//! Hazard ids share one 0–5 kind space for fluids and gases (fluid NONE=0//! WATER=1 OIL=2 BLOOD=3 BILE=4 AETHER_MIST=5; gas NONE=0 SMOKE=1 SPORES=2//! CHLORINE=3 STEAM=4 MIST=5). The two spaces ALIAS (BILE=4=STEAM,//! BLOOD=3=CHLORINE, AETHER_MIST=5=MIST), so each space gets its own//! predicate: [`corrodes_fluid`] takes FLUID ids, [`corrodes_gas`] takes//! GAS ids. Feeding a gas id to the fluid table misreads steam as//! verdigris (docs/03 §3.3.6: verdigris is bile-only).
use crate::voxel::material;
/// True when fluid `hazard` (FLUID kind id, 0–5) corrodes `material`.////// Matrix (fluid × material):/// - WATER(1)/OIL(2): nothing corrodes. Water rusts (see [`rusts`], spread/// owned by the rot workstream); oil only wets./// - BLOOD(3) = cable-rat blood, a weak acid (docs/03 §3.3.4, content/// `blood = "weak_acid"`): pits IRON, burns FLESH, rots PAPER. It does/// NOT touch STONE, GLASS, COPPER, or BRASS./// - BILE(4) = bile/sewage verdigris (docs/03 §3.3.6, bile-only): eats/// COPPER and BRASS (a copper alloy)./// - AETHER_MIST(5): unmakes FLESH and scrambles PAPER (wild magic;/// freeze/melt + full miscast table are Phase 8 hooks).pub fn corrodes_fluid(hazard: u8, mat: u8) -> bool { matches!( (hazard, mat), (3, material::IRON) | (3, material::FLESH) | (3, material::PAPER) | (4, material::COPPER) | (4, material::BRASS) | (5, material::FLESH) | (5, material::PAPER) )}
/// True when gas `hazard` (GAS kind id, 0–5) corrodes `material`.////// Matrix (gas × material):/// - SMOKE(1)/SPORES(2)/STEAM(4)/MIST(5): nothing corrodes. Steam/// flash-oxidation of copper is negligible per-tick (docs/03 §3.3.6:/// verdigris is bile-only); spore infection is owned by the gas/rot/// workstream, not this table; gas-ignition is a Phase 8 hook./// - CHLORINE(3): eats IRON./// - AETHER_MIST is a fluid id here, not a gas id: the mist-gas alias/// (MIST=5) reads as inert. Wild-magic unmaking of FLESH/PAPER belongs/// to fluid AETHER_MIST(5) — see [`corrodes_fluid`].pub fn corrodes_gas(hazard: u8, mat: u8) -> bool { matches!((hazard, mat), (3, material::IRON))}
/// Backward-compatible fluid-only wrapper around [`corrodes_fluid`].////// WARNING: takes FLUID kind ids only. Gas ids alias the fluid space/// (STEAM=4=BILE, CHLORINE=3=BLOOD) and will misread — e.g. steam-as-bile/// false-positives verdigris on copper. Gas callers must use/// [`corrodes_gas`].pub fn corrodes(hazard: u8, mat: u8) -> bool { corrodes_fluid(hazard, mat)}
/// True when dry `material` conducts hymns/shocks (docs/03 §3.3.8:/// "Live cable + water + copper armor = tragedy")./// Copper, brass, and iron conduct; everything else is an insulator dry.pub fn conducts(mat: u8) -> bool { matches!(mat, material::COPPER | material::BRASS | material::IRON)}
/// Conductivity with dampness: wet flesh (sweat, blood, rain) conducts./// `wet` is the voxel WET flag (0x02) as a bool; dry table is [`conducts`].pub fn conducts_wet(mat: u8, wet: bool) -> bool { conducts(mat) || (wet && mat == material::FLESH)}
/// Base flammability rating, 0–100 (fire.rs rolls `rng.below(100) < rating`):/// PAPER 100 (always catches), ROPE 70, WOOD 60, FLESH 15 (corpse-fat/// burns low and slow), everything structural (STONE, GLASS, IRON, COPPER,/// BRASS, SAINT_BONE, AIR) 0.pub fn flammable(mat: u8) -> u8 { match mat { material::PAPER => 100, material::ROPE => 70, material::WOOD => 60, material::FLESH => 15, _ => 0, }}
/// Flammability of an oil-wet cell: oil dominates at 90 unless the base/// material burns hotter (oiled paper stays 100). `oiled` means the voxel/// currently holds OIL fluid. Fire.rs applies this; the table here stays/// a pure function of (material, oiled) for testability.pub fn flammable_oiled(mat: u8, oiled: bool) -> u8 { if oiled { flammable(mat).max(90) } else { flammable(mat) }}
/// Which materials can be silvered against the Drowned: IRON and BRASS./// Silver is NOT a voxel material — silvering is owner/inventory state/// (item record / actor equipment, owned by the Phase 8 inventory/// workstream), not voxel state, so it lives in a `silvered: bool` beside/// the voxel rather than in a voxel flag bit (flag bits are reserved for/// per-cell voxel state). This helper names the silver-plateable set; the/// full rule is [`quells_drowned`].pub fn silvered_quells(mat: u8) -> bool { matches!(mat, material::IRON | material::BRASS)}
/// True when `material` stills the Drowned (docs/03 §3.3.6: "silver stills/// Drowned"): SAINT_BONE always quells; silvered iron/brass quell;/// anything else (including plain iron and silvered wood — you cannot/// plate wood) does not.pub fn quells_drowned(mat: u8, silvered: bool) -> bool { mat == material::SAINT_BONE || (silvered && silvered_quells(mat))}
/// True when `material` rusts while `wet` (voxel WET flag 0x02): iron in/// damp spreads rust. Copper verdigris is [`corrodes_fluid`], not rust; brass,/// stone, and glass never rust. The spreading contagion itself is owned/// by the rot workstream — this is the per-cell predicate.pub fn rusts(mat: u8, wet: bool) -> bool { wet && mat == material::IRON}
#[cfg(test)]mod tests { use super::*;
#[test] fn acid_pits_iron() { // Cable-rat blood is a weak acid: fluid BLOOD = 3. assert!(corrodes(3, material::IRON)); assert!(corrodes(3, material::FLESH)); }
#[test] fn acid_spares_stone_and_glass() { assert!(!corrodes(3, material::STONE)); assert!(!corrodes(3, material::GLASS)); assert!(!corrodes(3, material::COPPER)); assert!(!corrodes(3, material::WOOD)); }
#[test] fn bile_eats_copper_and_brass() { // Verdigris: fluid BILE = 4. assert!(corrodes(4, material::COPPER)); assert!(corrodes(4, material::BRASS)); assert!(!corrodes(4, material::IRON)); assert!(!corrodes(4, material::STONE)); }
#[test] fn metals_conduct_wood_does_not() { assert!(conducts(material::COPPER)); assert!(conducts(material::BRASS)); assert!(conducts(material::IRON)); assert!(!conducts(material::WOOD)); assert!(!conducts(material::STONE)); assert!(!conducts(material::SAINT_BONE)); }
#[test] fn wet_flesh_conducts_dry_does_not() { assert!(!conducts_wet(material::FLESH, false)); assert!(conducts_wet(material::FLESH, true)); // Damp does not save insulators or matter to metals. assert!(!conducts_wet(material::WOOD, true)); assert!(conducts_wet(material::IRON, false)); }
#[test] fn flammability_ratings() { assert_eq!(flammable(material::PAPER), 100); assert_eq!(flammable(material::ROPE), 70); assert_eq!(flammable(material::WOOD), 60); assert_eq!(flammable(material::STONE), 0); assert_eq!(flammable(material::GLASS), 0); assert_eq!(flammable_oiled(material::WOOD, true), 90); assert_eq!(flammable_oiled(material::PAPER, true), 100); assert_eq!(flammable_oiled(material::STONE, true), 90); assert_eq!(flammable_oiled(material::WOOD, false), 60); }
#[test] fn drowned_quell_rule() { assert!(quells_drowned(material::SAINT_BONE, false)); assert!(quells_drowned(material::SAINT_BONE, true)); assert!(quells_drowned(material::IRON, true)); assert!(quells_drowned(material::BRASS, true)); assert!(!quells_drowned(material::IRON, false)); assert!(!quells_drowned(material::WOOD, true)); assert!(silvered_quells(material::IRON)); assert!(!silvered_quells(material::WOOD)); }
#[test] fn rust_needs_wet_iron() { assert!(rusts(material::IRON, true)); assert!(!rusts(material::IRON, false)); assert!(!rusts(material::COPPER, true)); assert!(!rusts(material::BRASS, true)); assert!(!rusts(material::STONE, true)); }
#[test] fn steam_corrodes_nothing() { // Gas STEAM = 4 aliases fluid BILE = 4: verdigris is bile-only // (docs/03 §3.3.6), so post-douse steam must not eat copper. for mat in [ material::AIR, material::WOOD, material::IRON, material::COPPER, material::STONE, material::BRASS, material::FLESH, material::SAINT_BONE, material::GLASS, material::ROPE, material::PAPER, ] { assert!(!corrodes_gas(4, mat), "steam must corrode nothing"); } assert!(!corrodes_gas(4, material::COPPER)); assert!(!corrodes_gas(4, material::BRASS)); }
#[test] fn chlorine_eats_iron_only() { assert!(corrodes_gas(3, material::IRON)); assert!(!corrodes_gas(3, material::COPPER)); assert!(!corrodes_gas(3, material::STONE)); assert!(!corrodes_gas(3, material::GLASS)); assert!(!corrodes_gas(3, material::FLESH)); assert!(!corrodes_gas(3, material::PAPER)); }
#[test] fn gas_table_inert_except_chlorine_iron() { // SMOKE(1)/SPORES(2)/MIST(5) corrode nothing; CHLORINE(3) eats // IRON only; STEAM(4) eats nothing. for mat in [ material::AIR, material::WOOD, material::IRON, material::COPPER, material::STONE, material::BRASS, material::FLESH, material::SAINT_BONE, material::GLASS, material::ROPE, material::PAPER, ] { assert!(!corrodes_gas(0, mat)); assert!(!corrodes_gas(1, mat)); assert!(!corrodes_gas(2, mat)); assert!(!corrodes_gas(5, mat)); } assert!(corrodes_gas(3, material::IRON)); }}