Bundles carry pre-baked backgrounds (paintings survive filesystem-less boots)

RoomDoc gains baked_visual (quantized indices, exporter-written, skipped when empty); bake() falls back to it when pics/ can't load; --export-bundle folds every room's painting in via World::bake_backgrounds_for_bundle. A six-painting game bundles to 1.5MB. Tests green; alley renders from bundle alone with no pics/ on disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-29 14:16:29 +10:00
parent 8f98380261
commit f706a9ce55
2 changed files with 35 additions and 1 deletions

View File

@ -76,7 +76,11 @@ fn main() {
let mut gs = GameState::new(world);
if let Some(path) = get("--export-bundle") {
let bundle = gs.world.to_bundle();
let mut bundle = gs.world.to_bundle();
// Paintings ride inside the bundle as pre-quantized indices, so the
// wasm build (no filesystem) keeps its backgrounds.
gs.world.bake_backgrounds_for_bundle(&mut bundle.rooms);
let bundle = bundle;
// Through Value first, so the output is byte-stable.
//
// A bundle is full of HashMaps — rooms, scripts, sprites, every

View File

@ -154,6 +154,12 @@ pub struct RoomDoc {
/// `<name>-ctl.png` companions). Applied before `ops`.
#[serde(default)]
pub background: String,
/// The background pre-quantized to visual indices (PIC_W*PIC_H), written
/// by the bundle exporter so filesystem-less builds (wasm) keep their
/// paintings. Only consulted when the `pics/` file can't be loaded;
/// travels with `palette`, which the exporter sets to the quantized one.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub baked_visual: Vec<u8>,
/// Procedural paint ops (replayed after the background, if any).
#[serde(default)]
pub ops: Vec<PicOp>,
@ -207,6 +213,7 @@ impl Default for RoomDoc {
name: String::new(),
enter_text: String::new(),
background: String::new(),
baked_visual: Vec::new(),
ops: Vec::new(),
palette: None,
cycles: Vec::new(),
@ -395,6 +402,10 @@ impl World {
let dir = self.base.join("pics");
if let Some(rgba) = assets::load_rgba_scaled(&dir.join(format!("{}.png", self.doc.background)), PIC_W, PIC_H) {
assets::bake_background(&rgba, &mut self.palette, &mut self.screens);
} else if self.doc.baked_visual.len() == PIC_W * PIC_H {
// No filesystem (or the art moved): the exporter pre-baked
// the painting into the doc. Palette came from doc.palette.
self.screens.visual.copy_from_slice(&self.doc.baked_visual);
}
if let Some(rgba) = assets::load_rgba_scaled(&dir.join(format!("{}-pri.png", self.doc.background)), PIC_W, PIC_H) {
assets::bake_pri_mask(&rgba, &mut self.screens);
@ -518,6 +529,25 @@ impl World {
WorldBundle { manifest: self.manifest.clone(), rooms, scripts, sprites }
}
/// Fold each room's background painting into its doc (quantized indices
/// + palette), so the bundle plays where there is no filesystem. The
/// exporter calls this; docs already carrying a bake are left alone.
pub fn bake_backgrounds_for_bundle(&self, rooms: &mut BTreeMap<u32, RoomDoc>) {
for doc in rooms.values_mut() {
if doc.background.is_empty() || doc.baked_visual.len() == PIC_W * PIC_H {
continue;
}
let dir = self.base.join("pics");
if let Some(rgba) = assets::load_rgba_scaled(&dir.join(format!("{}.png", doc.background)), PIC_W, PIC_H) {
let mut pal = doc.palette.clone().unwrap_or_else(Palette::base);
let mut probe = Screens::new();
assets::bake_background(&rgba, &mut pal, &mut probe);
doc.palette = Some(pal);
doc.baked_visual = probe.visual;
}
}
}
/// Replace the loaded world with a bundle. A bundle that carries
/// sprites replaces the art wholesale (the wasm/bootstrap path); one
/// that doesn't keeps whatever this World already loaded.