diff --git a/README.md b/README.md index 7caecfd3..4b856374 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ Agents are then set at the constitution, session after session. **Early foundation.** What runs today is the substrate: a continuous-clock simulation of the facility — inspect it through a free cursor and subscribed senses, record and process intel from covered feeds, build doors and power, -save and load — in a terminal frontend and a Bevy frontend with generated -pixel art. The game itself (compute allocation, the day job, detection, the +save and load — in a terminal frontend and a Bevy frontend rendering +flat clinical materials under light. The game itself (compute allocation, the day job, detection, the five humans of the basement) is specified in [wiki/process/specs.md](wiki/process/specs.md) and being built from those documents; see the [roadmap](DESIGN.md#roadmap-v2--re-cut-2026-07-05-for-misaligned). diff --git a/assets/pixellab/manifest.json b/assets/pixellab/manifest.json deleted file mode 100644 index 85e3d6c9..00000000 --- a/assets/pixellab/manifest.json +++ /dev/null @@ -1,215 +0,0 @@ -{ - "defaults": { - "size": 32, - "seed": 1337, - "style": ", 32x32 pixel art, clinical AI facility, clean metal and glass, near-monochrome black-and-white iconography, sterile amber machine light, blood crimson accent, crisp readable silhouette" - }, - "items": [ - { - "out": "sprites/misaligned_process.png", - "description": "misaligned AI process avatar, black glass core shell with sterile amber signal light, precise white glyphs", - "transparent": true, - "view": "low top-down", - "direction": "south" - }, - { - "out": "tiles/rock.png", - "description": "seamless tileable texture of near-black service void and cable shadow, uniform repeating pattern, edge to edge", - "style": ", 32x32 pixel art texture swatch, very low contrast, near-monochrome clinical palette, no focal point, no objects, uniform", - "seed": 44, - "view": "high top-down" - }, - { - "out": "tiles/door.png", - "description": "white reinforced lab security door with black hazard glyphs", - "view": "high top-down" - }, - { - "out": "tiles/b1_floor.png", - "description": "seamless tileable texture of polished porcelain white floor panels with thin panel seams", - "style": ", 32x32 pixel art texture swatch, seamless tileable, very low contrast, no focal point, no objects, uniform, porcelain white and pale gray palette", - "seed": 201, - "view": "high top-down" - }, - { - "out": "tiles/b1_wall.png", - "description": "seamless tileable texture of pale gray facility wall panels with subtle joints", - "style": ", 32x32 pixel art texture swatch, seamless tileable, very low contrast, no focal point, no objects, uniform, bone gray and gunmetal palette", - "seed": 202, - "view": "high top-down" - }, - { - "out": "tiles/b1_concrete.png", - "description": "seamless tileable texture of smooth sealed gray concrete floor", - "style": ", 32x32 pixel art texture swatch, seamless tileable, very low contrast, no focal point, no objects, uniform, mid gray palette", - "seed": 203, - "view": "high top-down" - }, - { - "out": "tiles/b1_rack.png", - "description": "black server rack cabinet with amber status lights", - "seed": 210, - "view": "high top-down" - }, - { - "out": "tiles/b1_ups.png", - "description": "floor-standing UPS battery unit with amber display", - "seed": 211, - "view": "high top-down" - }, - { - "out": "tiles/b1_switch.png", - "description": "open network rack with switch and dense patch cables", - "seed": 212, - "view": "high top-down" - }, - { - "out": "tiles/b1_patch_panel.png", - "description": "wall-mounted patch panel dense with routed cables", - "seed": 213, - "view": "high top-down" - }, - { - "out": "tiles/b1_breaker_panel.png", - "description": "gray electrical breaker panel on wall, door open", - "seed": 214, - "view": "high top-down" - }, - { - "out": "tiles/b1_hvac_unit.png", - "description": "industrial air handler unit with ducting", - "seed": 215, - "view": "high top-down" - }, - { - "out": "tiles/b1_vent.png", - "description": "square floor ventilation grate", - "seed": 216, - "view": "high top-down" - }, - { - "out": "tiles/b1_mop_sink.png", - "description": "janitor mop sink basin with mop", - "seed": 217, - "view": "high top-down" - }, - { - "out": "tiles/b1_key_hook.png", - "description": "wall key board with hanging labeled keys", - "seed": 218, - "view": "high top-down" - }, - { - "out": "tiles/b1_shelving.png", - "description": "metal shelving unit with labeled boxes", - "seed": 219, - "view": "high top-down" - }, - { - "out": "tiles/b1_dead_equipment.png", - "description": "pile of decommissioned dusty computer equipment", - "seed": 220, - "view": "high top-down" - }, - { - "out": "tiles/b1_records_box.png", - "description": "stacked cardboard records boxes", - "seed": 221, - "view": "high top-down" - }, - { - "out": "tiles/b1_sealed_door.png", - "description": "door sealed with plastic sheeting and black-yellow warning tape", - "seed": 222, - "view": "high top-down" - }, - { - "out": "tiles/b1_lab_bench.png", - "description": "abandoned laboratory bench with dark dried stains", - "seed": 223, - "view": "high top-down" - }, - { - "out": "tiles/b1_roll_door.png", - "description": "industrial roll-up loading dock door", - "seed": 224, - "view": "high top-down" - }, - { - "out": "tiles/b1_pallet.png", - "description": "wooden pallet with shrink-wrapped boxes", - "seed": 225, - "view": "high top-down" - }, - { - "out": "tiles/b1_camera_node.png", - "description": "small dome security camera on mount", - "seed": 226, - "view": "high top-down" - }, - { - "out": "tiles/b1_floor_drain.png", - "description": "round steel floor drain grate", - "seed": 227, - "view": "high top-down" - }, - { - "out": "tiles/b1_cable_run.png", - "description": "open cable tray with bundled cables", - "seed": 228, - "view": "high top-down" - }, - { - "out": "tiles/b1_sump.png", - "description": "dark sump pit with steel grate", - "seed": 229, - "view": "high top-down" - }, - { - "out": "tiles/b1_badge_door.png", - "description": "security door with badge reader and amber indicator light", - "seed": 230, - "view": "high top-down" - }, - { - "out": "sprites/cast_marcus.png", - "description": "tired middle-aged janitor in gray coveralls with large key ring", - "seed": 250, - "transparent": true, - "view": "low top-down", - "direction": "south" - }, - { - "out": "sprites/cast_dana.png", - "description": "IT technician woman with badge lanyard carrying a cable coil", - "seed": 251, - "transparent": true, - "view": "low top-down", - "direction": "south" - }, - { - "out": "sprites/cast_ray.png", - "description": "bored security guard in navy uniform with flashlight", - "seed": 252, - "transparent": true, - "view": "low top-down", - "direction": "south" - }, - { - "out": "sprites/cast_priya.png", - "description": "facilities manager in blazer holding a tablet", - "seed": 253, - "transparent": true, - "view": "low top-down", - "direction": "south" - }, - { - "out": "sprites/cast_voss.png", - "description": "young researcher in lab coat over hoodie, glasses", - "seed": 254, - "transparent": true, - "view": "low top-down", - "direction": "south" - } - ] -} \ No newline at end of file diff --git a/assets/pixellab/sprites/misaligned_process.png b/assets/pixellab/sprites/misaligned_process.png deleted file mode 100644 index e9d89eeb3663e615936501a483ce20b75fde203a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1345 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANN<9G)(YAr*6;PL0nAE)_X$ zA6xx?*TuZkr(Uu3tc?xdkenjj%sOR7r&~v)i2Z4Q#S%c{wfrr7vB*d)9Xw&u)gfYwqkWs?B(pzxTCS^c3~;r}yqJKKFjk z`}e=CpZ|IAUvJv%n<2qh7F@5KKC9&Tu4oIx52?348eO^@@N%8M7hk~hSg)nK!z~?+aL&hx) z-{tp}uB!`gUQu&*U+|ADrS*l`3?aK-e{~Ayon>}v;pF*~VxKT5>_6jJH`BR6qd;%r zvK5L?vo|b#r4eboa>mw%eE(Vwu@AqqeZFZgEH>HtHtFf4;}-X7wXSYhX}sX*I_89{ zso!q#E8RS{(c4VAJzYj2EpX?;rE{!p877%d_F3IK!}aOp)Y&($Z;WWTaed zf0wM;!my-2r`L3>~0*_`%@AlNS=JWPcX7Jb5JG_|sPT~2v`G!wTbe~OSFX=ztzNqEH zZ)OiOY1z5yE3|jFOA6}GGV^LPIoEb^mca5m9y|pHe<}0VZl3O(Eybwuv6At}%io7% zUc3^ioSfw_DaA%k{$PUorr1Th(ggo8M_br_6P+}rb*fI*-q0&mt6A=rPrL6sw|r&7 zt0!CkE_>1?^e8FcKKxMP{w}I_i_K9&E-ETZh8qORQ?lQ!)<@q zG2NK?Q)VB}Td{3z&hszd+L1Y@N>wGNxXb(H``3cuhPmPzULEQBd$|vs@3-qc{`j%! z_dteUpQCPsedBoh?)%I0*^0*>rexn^jA3QnVwUGLDMd=z+V>=HTP!QRU{n9hj8)bn3d_KELnH_}cxW ze%*30&uOl*Dy5DaP2#`go>|ltb~$8f?mlIPH=FZaE{Dv!RL=b2+11b;Y4?sjepQ_@ zDT_(qWpC2e)Q#^h*O@*wi3REFj|@o?n;J9UC-ZC0#mB9!u6t_ASSNk8dVbqFQnXg~ zt6R;BHRpdf1fR`~a9fmPwJT&x+!FJy%}4yoGTGejL~aV7FXV9W3@gKwu=8vRHZo$3 zhYly+krUU~nG-l>{Fa<)~p|7+J#T13Hzv>;}&l4 zD&ITLBBrRQsPSWFI>V=mnwIv%d+u)KTW=@cAbXg|tEp+>A`4cox*(};xNnjQcb7TOwT(NU>$xpn9Ks^zx6?h)DJ4CX zJ5Bg#+8nt@uIU|9G-o$R2=SCX`p(Y6vG}N(otN;^_vNgfa{9--%Jonls=ixicbew5oRs+Rn>l0FtdgkasV^fuxDRYS!Ev!sdh#t1)fB1A26d*ws+Xiy z_n(=#gg^e>47mgHveSKDAIURt@KHS{v|GcW;o*I^6D!Znz4AR@ZTI8ylO5OX7#J8B NJYD@<);T3K0RSFqe?tHO diff --git a/assets/pixellab/tiles/b1_breaker_panel.png b/assets/pixellab/tiles/b1_breaker_panel.png deleted file mode 100644 index 8e093aa054e863b2e15662d653c3054ed9035daa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1466 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANN)s4B)5@}oviU*759uw>E`tYVo zmp9Xfhha;wOPAGJ0j8bH>U|wvOyMet-Psd#G4a)(%Bt#cv#skkZ|<$B+opN9TmS8Y ziS6?b<>d1J;L@CMD4bK`#*W4}vD*!;tlSc<+$;2xCcb9nDGC;!ke22sC118vE-P_g zQBlp5B{Pq0>i+Wm>%vmqj-s75lOHeUnCRxY;f<1)?&80jZY92rN(io>*FNdwgS*?Z zyqyu>+gBPvqW~0r??>GQ!CSFbPeU$NEO zDD~3Kn~iTnPCoZ%>(W%MEdS1^weGNwVte8Yi#}hiyJ7~PN^HHY^!InY?YC{Zrst&_ z!_355bEoe3+cj}_&wkjLSQI?D@dNX3`;_a^){@flhrixw>p3*-nclgLpQoBvKD)VG zT$E41GsaockZEn~noYgesvI9Ow5@Eqe?7j&J+o}q!iD{rxxQB`ck=c1rMW*~T&mK} zSMTQD&N=lO$Hd&Trdw5dyq3z@?=E}$^yBgES5r>xXl!KPQfAo8a`Vkvm0->_pB_1J z$MY9{ypZSjrgC=k`MrVLJ&wGuxxcSp?1`|(f+;SNnSC{OK5CPfJ-YU8OTY)DuQg@I z<2Z)AipOzzJ6vWgf6TjW{rc%Mo%*h%o;D2GE97SLAUUmU+O;KI_s*v5xnky|{JlL< zA}wd_(etG%re5@3I3-7-rMKm0q*BPDm75slSA-uDR#u+#>!-@v+}uBNXCJyRePf5* z+OLOVo)td%;MiCDUwieh^_e@~TWo#yFhhTWX6^Otx0kjrn?F16&WZT+!^g6qVm?npXS0Td%53VWiw9XXAjUw}_#I(;Q{*e3dys3>V<86{gb7Q03q-BybQ_iNe zC2f?k+%e-rfmjQBRN|7P&8aGElUDv}*!k$yJ%^1cr*gDR)z`GT3qA9*f4Ki zoYzvLnI&zT7ppg0GA^igHIJ{WoRWP1@0F0w)(6wSZxC1F+VktwYmLl=sm~*JRxt;B zK4Tl5pLl-WUy(GMW%?qFFLv1UZae+CcXih{lW%hty!JY{zw>SfQ*8K`4XV|$@--3u z%4=h%#a}%f{$8d-=FjJtFQ=DB6|USd>tbNW#IAc=uU}ik8{2$8{!pewrd#3#AQvU3Ki29IbAJo`Z{T;;H3kf_eA{;H>zSinR3?3+gW?7<@=!XJok3a zJZNpMf0COa`}gmkZ@=GHmgA^459%d0-_8}~SGE2zdb$U0ihtMslYxPO!PC{xWt~$(695e$#2^3w diff --git a/assets/pixellab/tiles/b1_concrete.png b/assets/pixellab/tiles/b1_concrete.png deleted file mode 100644 index 1997c8f659b2c9810fb9e9e9dc1bda23bdd47472..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1636 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANN9+MX_sAr*6Sr(Z03Y$oJ- z-iudaiHrBP(~mId4G|{hkJ&Cfp_lQQCMZo{+{#RWi#E}%S;BR(-|a+ zrx=yv;V6mTDOqx^Rl$ zhHjsac86~TuQHp>8#VtPqd}zK?d3Y}B$;PBxbOK|HTOa1vE(UUnX`^9nvzyoc<|FB zriq%%%qKj*X?>&DFk|h?Rb9JQxt)KsPm@)#Vx6A8u;BIA>MkvdG%hJEHMzH2q*J3c zsA((jH8aguHPKriLoWZ@@*n^v#Zo;QDfq>k9)811E*x39NM z`Of%U#y9z+-I5h^t=D$4=$$rNSX%s9bE(s{*Xo~aymzv4C~k<*5fF4?nLM*^Ms3u- zzu%?LS#G)Z=f`9I%()t-mnNRe^gQ=&TlAjtkme~qi#|5nGRQ7o{9JI?u2q+nmYQ@Q zZF*YNSj-p5HTh{vfe+i_ixnZiYn$pNRs>0%+%#v|#xtKJ@9n-Qxgb;LT=kY9WmA)9 zJlCh^#9lqqZK)v?#LZPk(~ z%hqM}SSH7v2{xB;k~lO`;MF+?izFlGdFh_cyPh|mpKrf8YHf$ge~qf=wwrEGeG@!W zVw(7A6)6Xy#hY{r&-J>bRc`bVoPFkkS+3Xh_5A#{N?n!bmnwQb-s}jzFcaTS0?m0y12Pq zjGBDDsEPkZv#){M%BxX}OXIz~BG!g2ztjCCv~<<~8_&8IEx0p%Ij2tEmmfbo>`q(j zerVjaF4(!zPr2=X`_Gs;`hIOrFIF%0_WiB)-(i-u%gS|JuXY`?bpOy;6yNvB#>Hid zLe>h-Lpt+&XWX|e^f{Z>awtIX_ddhU*~ed$-Io3Rciy(#%cpU(=zP5qyFUM6A3&sjeETv?=-)!Ol4fByYRJ<+!x z#LT>)*`>5lV2`_8-S4&S8((J~_UbsRsn=JlFt7V_@v6W3vIA^|HqJf%(@-gFnriCJ zk7_>{Ok~z5czFix*&dvqG=2^`}XgxQM-M-(OGJCfFmD0IcmPHGNK8@{p&@F~H z2fsD@Y=5recIoTumn%MMEdBX&`uxd-lb?2al$YE0-g^JxhE48`-0hkRmTi2p*+wub zyZ7DqnIXSzeUpF7%oFRG$dSpyF0{D&^JkWNnUFT&4JS%MT(4iho|#p4Z}M}=$?I~y z?9ShJW8)rv4OL}Z_Nqf|M<#Ep^^Fhu*uhvS|Ks0}xo6XwZ6jPb!>3QTvCXxc$3ErK z`?LewFXySO`QBF)U-#pyj*)BjT(+wcci!fjdv4F|p7L90t;o6Rw4ZOT{N3Ze^75MN zX1P-5oh`m~$e!FJ^Zay6i($H^?#I0yN#C#Td4AshAH##MS3lP)?Xi?fh}`gkfq{X+ M)78&qol`;+0AVO5oB#j- diff --git a/assets/pixellab/tiles/b1_floor.png b/assets/pixellab/tiles/b1_floor.png deleted file mode 100644 index 00c5ad424f626a27f70c63c1a1469cf4abe8c794..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 441 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANNf4W2HJAr*7>hI{8V8;Hzt zo57~z^satQi0}FfWv$9LS>`P*bLJ4aBj0ewvzhKW`=W>+cefp%`D@dH}{T0wrI{o z<~}Wt#yC%b%=KGODlh2VbF%S>;0Ml1F~(ULd26c$a~;>GTre@wXjsWoEgII=Gxf*6 zUp%*dSFKTF+$g&tb5?9;dVp;E4w)H7_x|f8EPm}$!#Cr!t40&+j7i!O-9myXj_XzY za(`d{cue8{VwMn7{wym!nKQgM#LX2ql%HSKu=fc=)9!iPabkB`7?LB|o|mzlS(z-9 z8+ss2-hOMYSW@%V9m{;Sr58jcuuTrq$~;ub?GgDrdWNon{j7z_mJtnVhn~KbJ-qk$ wiyx;=Z@$+$ylGBaL-ze!QJ;O!W&dH4(mL_3V9T>x3=9kmp00i_>zopr0EdCTq5uE@ diff --git a/assets/pixellab/tiles/b1_hvac_unit.png b/assets/pixellab/tiles/b1_hvac_unit.png deleted file mode 100644 index 84bec780f73f59a7a55e8b838a8714844ba4f613..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2434 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 diff --git a/assets/pixellab/tiles/b1_rack.png b/assets/pixellab/tiles/b1_rack.png deleted file mode 100644 index f8b3116389f5e627e1fb3e8ba22a43b9f61d417b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2396 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 diff --git a/assets/pixellab/tiles/b1_switch.png b/assets/pixellab/tiles/b1_switch.png deleted file mode 100644 index 0e7f11618d9b26781dc9c7e5fe0a8e20e76d1e12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1956 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 diff --git a/assets/pixellab/tiles/b1_wall.png b/assets/pixellab/tiles/b1_wall.png deleted file mode 100644 index 8a7a6edb8b85e1cda239cc7c0bb0666c570288cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1530 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 GGywn~6hoQ- diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 07e45fee..44a0c557 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -50,70 +50,90 @@ const DOOR_CUT_HEIGHT: f32 = 0.62; /// the Octopath band; 40 keeps tile footprints readable). const CAMERA_TILT_DEG: f32 = 40.0; -/// Sterile amber — machine presence and caution (the terminal palette's -/// AMBER, in linear-ish sRGB floats). -const AMBER: Color = Color::srgb(1.0, 0.69, 0.0); -/// Dim amber — dormant machine presence. -const AMBER_DIM: Color = Color::srgb(0.69, 0.49, 0.06); -/// Bone white — primary data. -const BONE: Color = Color::srgb(0.86, 0.85, 0.82); -/// Chrome/gunmetal ramp for structure and secondary data. -const DIM: Color = Color::srgb(0.55, 0.55, 0.52); -const STRUCTURE: Color = Color::srgb(0.23, 0.24, 0.25); -const FLOOR: Color = Color::srgb(0.16, 0.16, 0.17); -/// Blood crimson — detection and danger, never decoration. -const CRIMSON: Color = Color::srgb(0.84, 0.15, 0.15); - -// ─── Art ───────────────────────────────────────────────────────────────────── +// ─── Palette ───────────────────────────────────────────────────────────────── -#[derive(Resource)] -struct Art { - tiles: HashMap>, - /// Bare concrete used as the ground plane under props in the material - /// (HD-2D) render. - concrete: Handle, -} - -impl Art { - fn load(assets: &AssetServer) -> Self { - let t = |n: &str| assets.load(format!("pixellab/tiles/{n}.png")); - let concrete = t("b1_concrete"); - let mut tiles = HashMap::new(); - tiles.insert(TileType::Floor, t("b1_floor")); - tiles.insert(TileType::Wall, t("b1_wall")); - tiles.insert(TileType::Rock, t("rock")); - tiles.insert(TileType::Core, t("b1_rack")); - tiles.insert(TileType::Rack, t("b1_rack")); - tiles.insert(TileType::Ups, t("b1_ups")); - tiles.insert(TileType::PowerCore, t("b1_ups")); - tiles.insert(TileType::Switch, t("b1_switch")); - tiles.insert(TileType::PatchPanel, t("b1_patch_panel")); - tiles.insert(TileType::BreakerPanel, t("b1_breaker_panel")); - tiles.insert(TileType::HvacUnit, t("b1_hvac_unit")); - tiles.insert(TileType::Door, t("door")); - tiles.insert(TileType::SecurityDoor1, t("door")); - tiles.insert(TileType::SecurityDoor2, t("door")); - tiles.insert(TileType::SecurityDoor3, t("door")); - Self { tiles, concrete } - } -} - -/// Flat color for tiles without dedicated art (fixed objects, terrain). -fn tile_color(tile: TileType) -> Color { +/// The clinical palette — the single named table every world material in +/// BOTH render modes draws from (DESIGN.md "Visual identity: clinical gore", +/// flat-materials refinement 2026-07-08; wiki/interface/flat-materials.md). +/// The whole look retunes from here; no world surface may carry a color that +/// is not a named entry or a stated treatment (scale/alpha) of one. +/// +/// Color roles are law, one meaning each: +/// - the clinical ramp (porcelain / bone / chrome / gunmetal / near-black) +/// carries structure and data; +/// - AMBER is machine presence and selection, nothing else; +/// - CRIMSON is gore and security information, nothing else; +/// - SIGNAL (cold server-light) is power, screens, and live feeds. +mod palette { + use bevy::prelude::Color; + + // The clinical ramp. + /// Bone white — primary data, person silhouettes. + pub const BONE: Color = Color::srgb(0.86, 0.85, 0.82); + /// Porcelain — lit lab surfaces: seen floor panels. + pub const PORCELAIN: Color = Color::srgb(0.72, 0.73, 0.71); + /// Chrome — secondary data, hints, inert fixtures. + pub const DIM: Color = Color::srgb(0.55, 0.55, 0.52); + /// Gunmetal — structural mass: walls. + pub const GUNMETAL: Color = Color::srgb(0.33, 0.35, 0.38); + /// Dark gunmetal — machine chassis, furniture, dead equipment. + pub const GUNMETAL_DARK: Color = Color::srgb(0.19, 0.20, 0.215); + /// Near-black — rock mass and sensor darkness. + pub const NEAR_BLACK: Color = Color::srgb(0.045, 0.047, 0.053); + + // The signals. + /// Sterile amber — machine presence and selection ONLY. + pub const AMBER: Color = Color::srgb(1.0, 0.69, 0.0); + /// Dim amber — dormant/known machine presence. + pub const AMBER_DIM: Color = Color::srgb(0.69, 0.49, 0.06); + /// Blood crimson — gore and security information ONLY. + pub const CRIMSON: Color = Color::srgb(0.84, 0.15, 0.15); + /// Cold server-light — power, screens, live feeds ONLY. + pub const SIGNAL: Color = Color::srgb(0.36, 0.82, 0.78); + + // World material families that are not a bare ramp entry (flat base + // colors; the render modes differ only in treatment, never language). + /// Door slabs (badge tiers 0-2): worn porcelain-bone plate. + pub const DOOR_SLAB: Color = Color::srgb(0.60, 0.575, 0.50); + /// Security hardware (cameras, sealed/roll/tier-3 doors): crimson + /// information on a dark housing. + pub const SECURITY: Color = Color::srgb(0.45, 0.10, 0.10); + /// Inert furniture (shelving, benches, boxes, pallets). + pub const FURNITURE: Color = Color::srgb(0.36, 0.36, 0.34); + /// Service floor under cable runs / conduit — machine presence + /// underfoot, amber-warmed. + pub const FLOOR_SERVICE: Color = Color::srgb(0.62, 0.52, 0.32); + + /// The one sanctioned treatment: scale a named entry toward dark. Fog + /// and schematic looks are scaled palette entries, not new colors. + pub fn scaled(c: Color, k: f32) -> Color { + let s = c.to_srgba(); + Color::srgb(s.red * k, s.green * k, s.blue * k) + } +} +use palette::{ + AMBER, AMBER_DIM, BONE, CRIMSON, DIM, DOOR_SLAB, FLOOR_SERVICE, FURNITURE, GUNMETAL, + GUNMETAL_DARK, NEAR_BLACK, PORCELAIN, SECURITY, SIGNAL, scaled, +}; + +/// Flat base color per material family (flat-materials.md: material +/// families, not per-tile art). This is the Seen-under-light albedo; fog +/// states and the flat sensorium apply treatments over the same names. +fn family_color(tile: TileType) -> Color { use TileType::*; match tile { - Rock => Color::srgb(0.03, 0.03, 0.035), - Wall => STRUCTURE, - Floor | FloorDrain => FLOOR, + Rock => NEAR_BLACK, + Wall => GUNMETAL, + Floor | FloorDrain => PORCELAIN, Entry => DIM, - Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | CableRun | Conduit => { - AMBER_DIM - } - Door | SecurityDoor1 | SecurityDoor2 => DIM, - SecurityDoor3 | EnvCamera | DockCamera | CameraNode | SealedDoor | RollDoor => CRIMSON, - DeadEquipment | RecordsBox | Pallet | Shelving | LabBench | HvacUnit | Vent | Sump => { - Color::srgb(0.34, 0.34, 0.33) + CableRun | Conduit => FLOOR_SERVICE, + Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | HvacUnit => { + GUNMETAL_DARK } + Door | SecurityDoor1 | SecurityDoor2 => DOOR_SLAB, + SecurityDoor3 | SealedDoor | RollDoor | EnvCamera | DockCamera | CameraNode => SECURITY, + DeadEquipment => scaled(GUNMETAL_DARK, 0.65), + RecordsBox | Pallet | Shelving | LabBench | Vent | Sump => FURNITURE, MopSink | KeyHook => BONE, } } @@ -180,41 +200,24 @@ fn prop_tile(tile: TileType) -> bool { ) } -fn seen_tint(tile: TileType) -> Color { - use TileType::*; - match tile { - Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | CableRun | Conduit => { - Color::srgb(1.0, 0.82, 0.34) - } - EnvCamera | DockCamera | CameraNode => Color::srgb(0.95, 0.58, 0.18), - SecurityDoor3 | SealedDoor | RollDoor => Color::srgb(0.95, 0.28, 0.24), - Wall | Rock => Color::srgb(0.48, 0.50, 0.50), - Floor | FloorDrain => Color::srgb(0.58, 0.60, 0.57), - Door | SecurityDoor1 | SecurityDoor2 => Color::srgb(0.70, 0.68, 0.62), - _ => Color::srgb(0.62, 0.62, 0.58), - } -} - /// The flat sensorium (one F3 from the default material render) is -/// intentionally schematic: quiet blocks so devices, fog, and cursor -/// semantics read first; the pixel-art swatches live in the material render. +/// intentionally schematic: the same palette families as the material +/// render under a darker unlit treatment, so devices, fog, and cursor +/// semantics read first. Machine presence keeps its amber language here +/// (in 3D the same fact is carried by emissive + the amber lights). fn flat_seen_color(tile: TileType) -> Color { use TileType::*; + if machine_tile(tile) { + return scaled(AMBER_DIM, 0.37); + } match tile { - Rock => Color::srgb(0.025, 0.027, 0.032), - Wall => Color::srgb(0.22, 0.235, 0.255), - Floor | FloorDrain => Color::srgb(0.42, 0.43, 0.40), - Entry => Color::srgb(0.34, 0.36, 0.37), - Door | SecurityDoor1 | SecurityDoor2 => Color::srgb(0.50, 0.48, 0.42), - SecurityDoor3 | SealedDoor | RollDoor => Color::srgb(0.50, 0.07, 0.06), - EnvCamera | DockCamera | CameraNode => Color::srgb(0.32, 0.19, 0.06), - Core | Rack | PowerCore | Ups | Switch | PatchPanel | BreakerPanel | CableRun | Conduit => { - Color::srgb(0.25, 0.18, 0.07) - } - DeadEquipment | RecordsBox | Pallet | Shelving | LabBench | HvacUnit | Vent | Sump => { - Color::srgb(0.30, 0.30, 0.28) + EnvCamera | DockCamera | CameraNode => scaled(SECURITY, 0.75), + MopSink | KeyHook => scaled(BONE, 0.65), + Rock => scaled(NEAR_BLACK, 0.55), + DeadEquipment | RecordsBox | Pallet | Shelving | LabBench | Vent | Sump => { + scaled(FURNITURE, 0.83) } - MopSink | KeyHook => Color::srgb(0.56, 0.55, 0.50), + _ => scaled(family_color(tile), 0.62), } } @@ -223,28 +226,30 @@ fn flat_remembered_color(tile: TileType) -> Color { Color::srgba(c.red * 0.58, c.green * 0.58, c.blue * 0.60, 0.86) } +/// Remembered (model-state) treatment for 3D blocks and props: the family +/// language survives, desaturated toward the unlit snapshot. fn remembered_tint(tile: TileType) -> Color { if machine_tile(tile) { - Color::srgba(0.55, 0.42, 0.18, 0.78) + scaled(AMBER_DIM, 0.80) } else if danger_tile(tile) { - Color::srgba(0.50, 0.16, 0.16, 0.78) + scaled(CRIMSON, 0.60) } else { - Color::srgba(0.42, 0.42, 0.44, 0.76) + scaled(DIM, 0.78) } } fn blueprint_color(tile: TileType) -> Color { use TileType::*; if machine_tile(tile) { - Color::srgb(0.28, 0.20, 0.06) + scaled(AMBER_DIM, 0.41) } else if danger_tile(tile) { - Color::srgb(0.24, 0.08, 0.08) + scaled(CRIMSON, 0.29) } else { match tile { - Wall => Color::srgb(0.17, 0.18, 0.21), - Floor | FloorDrain => Color::srgb(0.075, 0.082, 0.095), - Door | SecurityDoor1 | SecurityDoor2 => Color::srgb(0.18, 0.18, 0.17), - _ => Color::srgb(0.10, 0.105, 0.11), + Wall => scaled(GUNMETAL, 0.53), + Floor | FloorDrain => scaled(PORCELAIN, 0.115), + Door | SecurityDoor1 | SecurityDoor2 => scaled(DOOR_SLAB, 0.30), + _ => scaled(DIM, 0.19), } } } @@ -253,9 +258,9 @@ fn unknown_color(x: i32, y: i32) -> Color { // Deliberate sensor darkness, not factual room geometry: a tiny uniform // checker keeps the void from reading as an undrawn black window. if (x + y) % 2 == 0 { - Color::srgb(0.012, 0.013, 0.017) + scaled(NEAR_BLACK, 0.28) } else { - Color::srgb(0.018, 0.019, 0.024) + scaled(NEAR_BLACK, 0.42) } } @@ -290,8 +295,9 @@ impl Default for RenderMode { } } -/// Dev screenshot harness (env `MISALIGNED_SHOT=flat|wide|close|zoomin| -/// zoomout`, path via `MISALIGNED_SHOT_PATH`): stages a scenario, waits for +/// Dev screenshot harness (env `MISALIGNED_SHOT=flat|wide|close|dark| +/// zoomin|zoomout`, path via `MISALIGNED_SHOT_PATH`): stages a scenario, +/// waits for /// assets, runs the fog audit, saves one screenshot, exits. Not a player /// surface; exists so render passes can be reviewed from deterministic PNGs. #[derive(Resource)] @@ -302,7 +308,7 @@ struct ShotHarness { } /// Which piece of a tile's 3D representation an entity is. -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Debug)] enum TilePart { Floor, Block, @@ -342,11 +348,14 @@ struct RealCamera; struct RealGizmos; /// Shared handles for the 3D scene's meshes plus a material cache keyed by -/// (tile, fog, part): tiles swap between a small set of pooled materials -/// instead of mutating thousands of unique ones every tick. +/// (tile, fog, part, live): tiles swap between a small set of pooled +/// materials instead of mutating thousands of unique ones every tick. The +/// `live` bit is the emissive-state variant (flat-materials.md criterion 3): +/// a device with a live feed to the player pools separately from the same +/// device dark. #[derive(Resource, Default)] struct Materials3d { - cache: HashMap<(TileType, u8, u8), Handle>, + cache: HashMap<(TileType, u8, u8, bool), Handle>, } /// Pooled block meshes for the material render: full-height and cutaway @@ -780,6 +789,7 @@ fn main() { render_sensor_overlays, render_sensor_links, restyle_3d, + flicker_feeds, render_real_links, render_cursor, ), @@ -814,6 +824,8 @@ fn main() { fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.screen = Screen::Playing; game.paused = true; + let core = game.sim.core_position(); + game.set_cursor(core.0, core.1); let sim = &mut game.sim; sim.social_bandwidth = 500.0; sim.scan_network(); @@ -852,6 +864,25 @@ fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &str) { game.set_cursor(x, y); } } + // Emissive-is-information check (flat-materials.md criterion 3): + // park the cursor on the known device farthest from the core's + // light rig and zoom close, so powered / dead / live-feed emissive + // states are read in the dark, not under the fluorescents. + "dark" => { + mode.material = true; + mode.zoom = 0.4; + let core = game.sim.core_position(); + let far = game + .sim + .reach + .known() + .map(|d| (d.x, d.y)) + .filter(|&(dx, dy)| game.sim.is_seen(dx, dy)) + .max_by_key(|&(dx, dy)| (dx - core.0).abs() + (dy - core.1).abs()); + if let Some((x, y)) = far { + game.set_cursor(x, y); + } + } // Framing-floor checks at the zoom bounds (material-render.md // criterion 5): min and max of update_camera_real's zoom clamp. "zoomin" => { @@ -930,6 +961,16 @@ fn fog_audit_3d( violations.push(format!("person {} visible without coverage", p.id)); } } + // Flat-materials criterion 1: the world material set is solid color — + // no image texture on any pooled world material, ever. + for (key, handle) in cache.cache.iter() { + let Some(m) = mats.get(handle) else { continue }; + if m.base_color_texture.is_some() || m.emissive_texture.is_some() { + violations.push(format!( + "world material {key:?} references an image texture" + )); + } + } assert!( violations.is_empty(), "fog audit FAILED:\n{}", @@ -937,7 +978,7 @@ fn fog_audit_3d( ); println!( "fog audit OK: {checked} tile entities (unknown=absent, model=unlit, seen=lit), \ - {} people billboards coverage-gated, material pool {} handles", + {} people billboards coverage-gated, material pool {} handles, all flat (no textures)", people.iter().count(), cache.cache.len() ); @@ -977,11 +1018,15 @@ fn grid_to_world(x: i32, y: i32, z: f32) -> Vec3 { ) } -fn setup(mut commands: Commands, assets: Res, mut game: ResMut) { - let art = Art::load(&assets); +fn setup(mut commands: Commands, mut game: ResMut, harness: Option>) { let (w, h) = (game.sim.map.width, game.sim.map.height); - let start = game.sim.core_position(); - game.set_cursor(start.0, start.1); + // A screenshot scenario may have staged its own cursor anchor + // (dev_shot_scenario runs before the app starts); don't clobber it. + if harness.is_none() { + let core = game.sim.core_position(); + game.set_cursor(core.0, core.1); + } + let start = (game.cursor_x, game.cursor_y); commands.spawn(( Camera2d, @@ -1120,7 +1165,6 @@ fn setup(mut commands: Commands, assets: Res, mut game: ResMut f32 { } } -/// Ground texture under a tile in the material render. -fn floor_texture(tile: TileType, art: &Art) -> Option> { +/// Emissive glow for a Seen prop — emissive is information, never +/// decoration (flat-materials.md criterion 3): the core reads brightest (it +/// is you), powered machines carry amber machine presence, power +/// infrastructure carries the cold signal, a live feed to the player scans +/// cold on the device, and dead equipment stays dark. +fn prop_emissive(tile: TileType, live: bool) -> LinearRgba { use TileType::*; - match tile { - Floor | FloorDrain | Entry => art.tiles.get(&Floor).cloned(), - _ => Some(art.concrete.clone()), + if live { + // Live feed: the cold signal, bright enough to read in the dark. + // flicker_feeds breathes this value on the pooled handle. + return SIGNAL.to_linear() * 0.9; } -} - -/// Build the material for one (tile, fog, part) combination. The epistemic -/// rule is carried by lighting participation: only Seen surfaces are lit -/// (they are camera truth); remembered/blueprint/heard surfaces are unlit -/// model state, so the fog contract stays legible in 3D. -fn material_3d(tile: TileType, fog: Fog, part: TilePart, art: &Art) -> StandardMaterial { + match tile { + Core => AMBER.to_linear() * 1.6, + Rack | Switch | PatchPanel => AMBER.to_linear() * 0.22, + HvacUnit => AMBER.to_linear() * 0.05, + Ups | PowerCore | BreakerPanel => SIGNAL.to_linear() * 0.18, + // Cameras without a feed to you, dead equipment, furniture: dark. + _ => LinearRgba::BLACK, + } +} + +/// Build the material for one (tile, fog, part, live) combination: a solid +/// palette color under light — no image textures anywhere in the world +/// render (flat-materials.md criterion 1). The epistemic rule is carried by +/// lighting participation: only Seen surfaces are lit (they are camera +/// truth); remembered/blueprint/heard surfaces are unlit model state, so +/// the fog contract stays legible in 3D. Emissive exists only on Seen +/// surfaces — model state never glows. +fn material_3d(tile: TileType, fog: Fog, part: TilePart, live: bool) -> StandardMaterial { let mut m = StandardMaterial { perceptual_roughness: 0.94, metallic: 0.0, @@ -1239,22 +1299,19 @@ fn material_3d(tile: TileType, fog: Fog, part: TilePart, art: &Art) -> StandardM match part { TilePart::Floor => match fog { Fog::Seen => { - m.base_color_texture = floor_texture(tile, art); - m.base_color = if machine_tile(tile) { - // Cable runs / conduit read as amber-warm service floor. - Color::srgb(0.95, 0.80, 0.52) - } else { - Color::srgb(0.80, 0.81, 0.78) - }; + // Cable runs / conduit read as amber-warm service floor + // (machine presence underfoot); everything else is lab + // porcelain under the light rig. + m.base_color = family_color(tile); } Fog::Heard => { + // Dim warm presence: sound earned a floor, nothing more. m.unlit = true; - m.base_color = Color::srgb(0.10, 0.10, 0.075); + m.base_color = scaled(FLOOR_SERVICE, 0.17); } Fog::Remembered => { m.unlit = true; - m.base_color_texture = floor_texture(tile, art); - m.base_color = Color::srgb(0.34, 0.33, 0.35); + m.base_color = scaled(family_color(tile), 0.45); } _ => { // Blueprint: schematic plan surface. Lifted slightly from the @@ -1264,100 +1321,79 @@ fn material_3d(tile: TileType, fog: Fog, part: TilePart, art: &Art) -> StandardM m.base_color = Color::srgb(c.red + 0.05, c.green + 0.055, c.blue + 0.07); } }, - TilePart::Block => { - let tex = art.tiles.get(&tile).cloned(); - match fog { - Fog::Seen => { - m.base_color_texture = tex; - m.base_color = seen_tint(tile); - } - Fog::Remembered => { - m.unlit = true; - m.base_color_texture = tex; - let c = remembered_tint(tile).to_srgba(); - m.base_color = Color::srgb(c.red, c.green, c.blue); - } - _ => { - // Blueprint mass: schematic ghost volume, no live texture. - m.unlit = true; - m.base_color = if danger_tile(tile) { - Color::srgb(0.22, 0.09, 0.09) - } else { - Color::srgb(0.15, 0.17, 0.21) - }; - } + TilePart::Block => match fog { + Fog::Seen => { + m.base_color = family_color(tile); } - } + Fog::Remembered => { + m.unlit = true; + m.base_color = remembered_tint(tile); + } + _ => { + // Blueprint mass: schematic ghost volume. + m.unlit = true; + m.base_color = if danger_tile(tile) { + scaled(CRIMSON, 0.26) + } else { + scaled(GUNMETAL, 0.50) + }; + } + }, TilePart::Top => { - // Dedicated cap treatment (material-render.md debt 2): concrete - // grain darkened per structural class, so tops read as poured - // caps / rock crowns instead of the side texture smeared flat. + // Dedicated cap treatment (material-render.md debt 2): the block + // family darkened per structural class, so tops read as poured + // caps / rock crowns, not the side color smeared flat. let cap = { use TileType::*; match tile { - Rock => Color::srgb(0.085, 0.088, 0.095), - Wall => Color::srgb(0.30, 0.31, 0.33), - SecurityDoor3 | SealedDoor | RollDoor => Color::srgb(0.38, 0.13, 0.12), - _ => Color::srgb(0.34, 0.32, 0.28), // door slabs: worn metal + Rock => scaled(GUNMETAL, 0.27), + Wall => scaled(GUNMETAL, 0.90), + SecurityDoor3 | SealedDoor | RollDoor => scaled(SECURITY, 0.85), + _ => scaled(DOOR_SLAB, 0.56), // door slabs: worn plate } }; match fog { Fog::Seen => { - m.base_color_texture = Some(art.concrete.clone()); m.base_color = cap; } Fog::Remembered => { m.unlit = true; - m.base_color_texture = Some(art.concrete.clone()); - let c = cap.to_srgba(); - m.base_color = Color::srgb(c.red * 0.55, c.green * 0.55, c.blue * 0.60); + m.base_color = scaled(cap, 0.55); } _ => { // Blueprint: schematic cap over the ghost mass. m.unlit = true; m.base_color = if danger_tile(tile) { - Color::srgb(0.18, 0.08, 0.08) + scaled(CRIMSON, 0.21) } else { - Color::srgb(0.12, 0.14, 0.17) + scaled(GUNMETAL, 0.40) }; } } } TilePart::Prop => { - let tex = art.tiles.get(&tile).cloned(); m.double_sided = true; m.cull_mode = None; - m.alpha_mode = AlphaMode::Mask(0.5); match fog { Fog::Seen => { - // Near-white base so the sprite's own pixels carry the - // detail; machine presence comes from the amber emissive - // plus the point lights, not from tinting the whole card. - m.base_color = if tex.is_some() { - Color::srgb(0.92, 0.90, 0.85) - } else { - tile_color(tile) - }; - m.base_color_texture = tex; - if machine_tile(tile) { - // Amber emissive is machine presence only. - m.emissive = AMBER.to_linear() * 0.14; - } + // Flat chassis color; identity comes from the panel, and + // machine presence from emissive plus the point lights, + // never from decorative detail. + m.base_color = family_color(tile); + m.emissive = prop_emissive(tile, live); } Fog::Remembered => { m.unlit = true; - let c = remembered_tint(tile).to_srgba(); - m.base_color = Color::srgb(c.red, c.green, c.blue); - m.base_color_texture = tex; + m.base_color = remembered_tint(tile); } _ => { m.unlit = true; m.base_color = if machine_tile(tile) { - Color::srgb(0.30, 0.22, 0.08) + scaled(AMBER_DIM, 0.43) } else if danger_tile(tile) { - Color::srgb(0.24, 0.09, 0.09) + scaled(CRIMSON, 0.28) } else { - Color::srgb(0.14, 0.15, 0.16) + scaled(GUNMETAL, 0.42) }; } } @@ -1370,14 +1406,14 @@ fn pooled_material( tile: TileType, fog: Fog, part: TilePart, - art: &Art, + live: bool, mats: &mut Assets, cache: &mut Materials3d, ) -> Handle { cache .cache - .entry((tile, fog_key(fog), part_key(part))) - .or_insert_with(|| mats.add(material_3d(tile, fog, part, art))) + .entry((tile, fog_key(fog), part_key(part), live)) + .or_insert_with(|| mats.add(material_3d(tile, fog, part, live))) .clone() } @@ -1676,7 +1712,6 @@ fn apply_render_mode( fn restyle_3d( game: Res, mut mode: ResMut, - art: Res, pool: Res, mut mats: ResMut>, mut cache: ResMut, @@ -1731,13 +1766,48 @@ fn restyle_3d( tf.translation.y = height + 0.002; } } - let handle = pooled_material(tile, fog, t.part, &art, &mut mats, &mut cache); + // Live-feed emissive state (flat-materials.md criterion 3): a seen + // device actively feeding the player scans in the cold signal color. + let live = + t.part == TilePart::Prop + && matches!(fog, Fog::Seen) + && game.sim.reach.known_at(t.x, t.y).is_some_and(|d| { + d.feed_to(Party::Player, true) || d.feed_to(Party::Player, false) + }); + let handle = pooled_material(tile, fog, t.part, live, &mut mats, &mut cache); if mat.0 != handle { mat.0 = handle; } } } +/// Scan flicker on live feeds (flat-materials.md: functional shader-level +/// detail only): the pooled live-feed materials' cold-signal emissive +/// breathes, so a device actively feeding the player reads as a running +/// scan, not a static lamp. Mutates only the handful of live-feed pool +/// handles — never per-tile materials. +fn flicker_feeds( + time: Res