Wire WASM texture loading: static byte cache in bmp_cache.rs with
set_bmp_cache()/get_bmp_bytes()/get_hd_bytes(), real load_texture()
implementation using image::load_from_memory + ctx.load_texture(),
BMP pre-fetch loop in main.rs with manifest-driven HTTP fetch and
progress bar, manifest generator in build-wasm.sh. Error handling:
manifest parse failures logged, per-BMP fetch failures counted and
reported, image decode errors logged with resource ID.
Close all remaining AI parity gaps: dispatch validators #2/#3/#4
CLOSED (per-cycle caps are Rust equivalent), defense facility
priority CLOSED (FUN_00508660 is entity dispatcher not priority fn),
FUN_00558660 CLOSED (no decompiled source, negligible impact),
74 informational GNPRTB parameters documented as complete.
Add advisor BIN diagnostic logging: first 3 parse failures logged
with error variant + hex dump, suppression notice for remainder.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.22.0, 465 tests, ~99% parity. Roadmap gains Knesset Tammuz section
with all 7 phases. AI parity status updated to 15/18 validators.
Plan status: active → completed.
Hex analysis of ~1,500 advisor BIN files revealed four distinct binary
formats where previously only one was recognized. The cascading decoder
(parse_advisor_bin_cascade) tries each in priority order:
- v1 (12%): u16 count + count*u16 frame_ids — explicit list (original)
- v2 (45%): u16 count + u16 base + u16 0 + u16 0 — sequential range
- v3 (38%): u16 0 + u16 ref + u16 9 + u16 count + u16 base — BMP-mapped
- v4 (4%): u16 0 + u16 ref + u16 bmp_id — single BMP frame
v3/v4 frame IDs are literal BMP resource IDs (2001+ range) and resolve
via a new bmp_resource_id_map built during load, replacing the modulo
fallback for those sequences. v1/v2 still use modulo over the sorted
BMP pool.
Load-time logging now reports per-format counts:
[advisor] alliance BIN files: 747/752 valid (99%)
[v1=91, v2=342, v3=283, v4=31], 314 bmp-mapped, ...
8 new tests for the cascade decoder, BMP-mapped lookup, and fallback.
465 workspace tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace controlling_faction references with ControlKind in uprising.rs,
blockade.rs, lib.rs doc comments. Remove unused Color32 import from
research.rs. Prefix unused sh variable in ground_combat.rs.
A2: SpecialForceSpawned now creates SpecialForceUnit in world arena.
R6/R7/R8/R11: Mission telemetry emissions (informant intel, saboteur
detected, character health, character killed from assassination).
R12: Emperor Palpatine 1.5× weapon fire modifier when co-located.
Addresses CRITICAL C3 from the Dabora 3 R11 review: the main-loop branch
at `main.rs:1400-1441` handled `DeathStarEvent::PlanetDestroyed` from
`DeathStarSystem::advance()`, but `advance()` never actually emits
`PlanetDestroyed` — only `ConstructionCompleted` and `NearbyWarning`.
`PlanetDestroyed` comes from `DeathStarSystem::fire()`, which is called
exclusively by `PanelAction::FireDeathStar` (main.rs:2781-2824). That
path DOES call `cleanup_destroyed_system` with the R11 effects
out-param.
The dead branch set `is_destroyed = true` but never called
`cleanup_destroyed_system` — a pre-existing entity leak that would
silently re-activate if someone wired automatic DS firing into
`advance()`. Post-R11 it would ALSO silently drop EVT_CHARACTER_KILLED
telemetry for any characters at the destroyed system.
Fix: replace the dead branch with `unreachable!()` citing the correct
path. If a future refactor adds automatic DS firing, the unreachable
will panic loudly and force the implementer to either:
(a) call cleanup_destroyed_system with the effects buffer (matching
the PanelAction::FireDeathStar path), or
(b) route through DeathStarSystem::fire() which produces the correct
precondition-checked event stream.
Zero test delta — the branch was unreachable.
Addresses 3 HIGH findings from the Dabora 3 R11 review (silent-failure-hunter
+ code-reviewer H2).
1. Rename `CharacterHasActiveMovementOrder` → `CharacterAssignedToFleet`
(events.rs). The old name was a semantic lie — the condition never
inspected MovementState, it just checked `current_fleet.is_some()`.
Renamed to reflect what it actually does. Also tightened to reject
killed characters up-front (mark_killed clears current_fleet, but
explicit is_killed guard is defense-in-depth).
2. `CharacterIsKilled` — `debug_assert!` when the target character is
missing from the arena. R11 invariant says killed characters stay in
the arena; a missing row is a real bug (save-migration race,
accidental delete), not a "dead" state. Debug panics surface
regressions immediately; release falls through to "not killed" so
players don't crash on edge-case data.
3. `SetHeritageKnown` integrator arm — `eprintln!` on missing character.
Matches the Dabora 2 SpawnSpecialForce pattern. A silently-dropped
heritage flip shows the wrong Final Battle cutscene, which is a
story-continuity bug that's extremely hard to diagnose after the
fact.
4. R6/R7/R8 + EVT_CHARACTER_KILLED emissions in simulation.rs now route
through `SYS_MISSIONS` instead of `SYS_STORY`. Every other mission-
side integrator emission already uses `SYS_MISSIONS`; mixing
`SYS_STORY` at ad-hoc emit sites was a split-brain with the
declarative SystemTag scheme from Dabora 1.
5. EVT_CHARACTER_KILLED mission-arm lookup gains a `debug_assert!`
contains_key check because under R11 semantics `<unknown>` fallback
represents a real invariant break, not an expected race.
+3 tests (432 → 435):
- character_assigned_to_fleet_true_when_on_any_fleet
- character_assigned_to_fleet_false_when_killed
- character_is_killed_panics_on_missing_character_in_debug
Plan doc (2026-04-08-001-*.md) still references the old condition name
in historical context; not updated because the plan is a decision
artifact from the original deepening round.
Addresses CRITICAL C2 from the Dabora 3 R11 review: R11 changed cleanup
semantics from `world.characters.remove(ck)` to `is_killed=true, keep in
arena`, but no other system filtered on `is_killed`. Consequences:
- AI dispatched corpses on missions (ai.rs:can_dispatch)
- Dead Mon Mothma could defect (betrayal.rs:advance)
- Killed Luke kept accumulating Jedi XP (jedi.rs:advance — stale comment
said "removed from world", not anymore)
- CharactersCoLocated could match dead Luke at destroyed system via
stale current_system/current_fleet
Fix:
1. `Character::mark_killed()` helper in world/mod.rs clears
current_system, current_fleet, on_mission, on_hidden_mission,
on_mandatory_mission, is_captive, captured_by, capture_tick — leaves
name + dat_id for reactive story-event resolution (DI-M3).
Idempotent.
2. `cleanup_destroyed_system` and `MissionEffect::CharacterKilled`
integrator arm both route through `mark_killed()` — was asymmetric
before (mission arm cleared on_mission flags, DS cleanup cleared
nothing).
3. Short-circuit on `is_killed` at top of:
- `AISystem::can_dispatch` (ai.rs)
- `BetrayalSystem::advance` character loop (betrayal.rs)
- `JediSystem::advance` training loop (jedi.rs — replaces stale
"removed from world" comment)
- `CharactersCoLocated` condition (events.rs — defense-in-depth;
mark_killed clearing current_system/current_fleet already blocks
the primary paths)
4. Missions.rs sites at 717, 1068, 1104 inherit filtering automatically
— they check `current_system == Some(...)` or `is_captive`, both of
which mark_killed() clears.
Next-tick reactivity note (C1 from review): mission kills fire
same-tick story events (mission step 5 → events step 6 in the same
tick is the correct player-facing behavior — player sees the kill and
the death notification simultaneously). Death Star kills are
unavoidably next-tick because cleanup runs at step 11 after events at
step 6. Both paths go through mark_killed() so systems that iterate
world.characters see a consistent "dead" shape regardless of route.
Documented in the integrator arm comment.
+7 tests (425 → 432):
- mark_killed_sets_is_killed_and_clears_alive_state
- mark_killed_is_idempotent
- mark_killed_preserves_name_and_dat_id
- can_dispatch_rejects_killed_character
- killed_character_does_not_betray
- killed_trainee_completes_training_record
- characters_co_located_rejects_killed_character
Zero behavior change for alive characters. All 432 tests pass.
Collapse two-pass patterns flagged by code-simplifier review:
manufacturing.rs (K6 idle detection):
Drop the per-tick HashMap<SystemKey, usize> scratch allocation.
Capture `pre_len = queue.len()` inside the iter_mut loop and check
`pre_len > 0 && queue.is_empty()` after advance_ticks. Blockaded
systems are still excluded by the early continue. ~25 LOC → ~15 LOC.
death_star.rs (cleanup_destroyed_system character kill):
Drop the `killed_characters: Vec<CharacterKey>` two-pass. Clone
`fleet.characters` inside the loop to release the fleets borrow,
then mutate characters and push effects inline. The "borrow checker
issues" comment was wrong — split borrows through distinct struct
fields work fine. ~28 LOC → ~20 LOC.
Idempotency preserved via the `!c.is_killed` check inside the loop.
425 tests pass, zero behavior change.
Net: -24 LOC
Working note synthesizing findings from 3 parallel reviewers
(code-reviewer, silent-failure-hunter, code-simplifier) on commits
ac306e7..f15365c. Codex reviewer pending.
Key findings driving 4 fix-before-R1 commits:
CRITICAL C1 — next-tick reactivity invariant is violated by
mission assassinations. Mission step 5 flips is_killed, then
step 6 events can fire EVT_CHARACTER_KILLED same-tick. Weaken
the invariant: mission kills fire same-tick, DS kills next-tick.
CRITICAL C2 — R11 kept killed characters in the arena but no other
system filters on is_killed. AI dispatches corpses; dead Mon Mothma
can defect; killed Jedi accumulate XP. mark_character_killed()
helper + filters needed across ai.rs, betrayal.rs, jedi.rs,
missions.rs.
CRITICAL C3 — Interactive AI-driven DS fire path (main.rs:1400-1441)
never calls cleanup_destroyed_system. Dead branch today, but
unreachable!() guards against future regression.
HIGH — CharacterIsKilled unwrap_or(false), CharacterHasActiveMovementOrder
semantic lie (returns true for stationary fleets), SetHeritageKnown
silent drop, R6/R7/R8 route through SYS_STORY instead of SYS_MISSIONS.
SIMP wins — K6 HashMap → inline pre_len; cleanup two-pass → single
pass with fleet.characters.clone().
No Dabora 2 regressions. Full action plan + trajectory in note.
Adopt stash from earlier session as the Dabora 3 R11 foundation, rebased
onto Dabora 2 (2e6aaed).
What lands:
- Character::is_killed flag (v8 save bump, no serde default)
- cleanup_destroyed_system gains &mut Vec<GameEffect> out-param
- Killed characters stay in the arena (marked, not deleted) so next-tick
reactive story events can resolve by dat_id/name (DI-M3)
- EVT_CHARACTER_KILLED telemetry emission from mission assassinations +
interactive Death Star fire path
- EVT_CHARACTER_HEALTH constant + mission-side emission on Failure
- EVT_INFORMANT_INTEL (R6) on espionage Success
- EVT_SABOTEUR_DETECTED (R7) on mission Foiled
- EventCondition::CharacterHasActiveMovementOrder (R5 precondition)
- EventCondition::CharacterIsKilled (next-tick reactive condition)
- EventAction::SetHeritageKnown (R4 render-layer trigger)
Integrator arm for SetHeritageKnown flips heritage_known on the target
character. The render-layer branch in event_screen::event_id_to_resource
is the canonical route — we do NOT create 0x222 (SIMP-H5 + ARCH-#9 +
SF-#11 triple-collapse per plan).
Conflict resolution notes:
- main.rs: accepted Dabora 2's deletion of the duplicate apply_event_actions;
stash's panel-action cleanup call site already uses the new signature
- integrator.rs: kept Dabora 2's full SpawnSpecialForce resolution logic
(the stash had a TODO stub); added SetHeritageKnown arm after it
- simulation.rs: result.target_system is now SystemKey (not Option),
removed and_then chain in the R6 EVT_INFORMANT_INTEL emit
Still to land for Dabora 3 completion: R1 EVT_HAN_RESCUE, R2
EVT_JABBA_PRISONERS consolidator, R3 EVT_HAN_PERMANENT_FREEZE, R4
wiring SetHeritageKnown into 0x396 action list + render-layer branch,
R5 wiring EVT_BOUNTY_ATTACK to SpawnSpecialForce with precondition,
R9 EVT_TRAITOR_REVEALED, R10 EVT_SIDE_CHANGE, R13 stale comment, R14
tests (~30).
425 tests pass (no delta — this commit adds primitives, not new
behavior that exercises them).
Implements Amended Phase 2 (Merged Dabora 2/4 "Kothar-Anat") of the
Knesset Shamash-Bet story-events sweep on top of Dabora 1's foundation
(save v8, SystemTag, heritage_known). Delivers 6 economy/manufacturing
notification events, resurrects the two story effects deleted in cleanup,
consolidates the apply_event_actions duplicate, and locks the new
telemetry in the golden oracle.
Task coverage (see docs/plans/2026-04-08-001-feat-knesset-shamash-bet-
story-events-cutscene-plan.md#amended-phase-2):
#K1 EVT_SUPPORT_CHANGE (0x100) — new SupportTier enum + last_support_tier
cached on SystemEconomy; EconomySystem::advance detects tier
transitions per tick.
#K2 EVT_NATURAL_DISASTER (0x154) — dedicated EconomyEvent::NaturalDisaster
variant replaces the umbrella IncidentTriggered "disaster" branch.
Clear-before-emit ordering preserved (SF-#8).
#K3 EVT_RESOURCE_DISCOVERY (0x155) — fires on positive
raw_material_allocated delta; new resource_discovery_armed flag
suppresses the seed tick so the galaxy initialization is not a
"discovery".
#K4 EVT_MAINTENANCE_SHORTFALL_EVENT (0x304) — per-faction 30-tick
cooldown counters on EconomyState (GNPRTB[7694]). Fires on the
first tick with a deficit, then polls every 30 ticks.
#K5 EVT_UNITS_DEPLOYED (0x107) — emitted alongside EVT_BUILD_COMPLETE
in PerceptionIntegrator::apply_build_completions for every
manufacturing completion.
#K6 EVT_MANUFACTURING_IDLE (0x160) — new ManufacturingAdvance struct
+ advance_tracked() method returns (completions, newly_idle).
Intra-tick HashMap snapshot of pre-advance queue lengths detects
non-empty → empty transitions. No persistent was_empty bit (SIMP-H4).
Blockaded systems are correctly excluded from newly_idle.
#K7 Parameterized idempotency tests — 5 new tests in economy.rs and
manufacturing.rs cover K1/K2/K3/K4/K6 "fires exactly once per
transition" contract.
#A1 EVT_HQ_CAPTURED (0x128) — apply_victory now takes &GameWorld and
emits a dedicated HQ_CAPTURED record for VictoryOutcome::HqCaptured
with human-readable system name (DI-H2 — no stale slotmap keys).
#A2 Integrator arm for SpecialForceSpawned — resolves the target
system via character.current_system, falling back to the destination
of any movement order on a fleet carrying the character, or the
fleet's own location for stationary fleets. Structured eprintln
warn on resolution failure. The upstream EVT_BOUNTY_ATTACK chain
must gate the action on CharacterAtSystem OR CharacterHasActiveMovementOrder
(SF-#7, Dabora 3).
#A3 Integrator arm for StoryMessageDisplayed — pushes
GameEffect::StoryMessageDisplayed onto a new story_effects queue on
PerceptionIntegrator. Interactive main.rs drains via the buffer
passed to apply_event_action_to_world; headless simulation.rs
ignores it (the Vec is dropped when finish() consumes the integrator).
#A4 golden_values.json story_events section — `{min, max}` bounds
(DI-M4 — NOT {expected, tolerance}) for all 7 new event types plus
a regeneration_log entry citing the 5000-tick seed 42 baseline
histogram (support_change=1930, natural_disaster=914,
resource_discovery=0, maintenance_shortfall=1, units_deployed=2350,
manufacturing_idle=890, hq_captured=0).
#A5 eval_parity.py grep + regeneration — grep for hardcoded
`SYS_EVENTS`/`SYS_STORY` assertions returned ZERO matches (the
parity oracle only checks event_type strings, not system tags —
#F6's SystemTag field change landed cleanly in Dabora 1 without
needing downstream fixes). eval_parity.py gains a story_events
evaluator loop that checks each new event_type against its
`{min, max}` bounds; rare-event zeros (hq_captured, resource_discovery)
map to `skip` rather than `fail` via the `min == 0 && observed == 0`
special case.
#F7 apply_event_actions consolidation — the old
`apply_event_actions_to_world_inner` in integrator.rs (was private
in Dabora 1) is promoted to `pub #[inline] fn apply_event_action_to_world`
with two new parameters (`effects_out` and `movement`). main.rs
lines ~3273-3486 (the duplicate `apply_event_actions` helper) are
deleted; main.rs line 941 now calls the pub integrator helper and
drains the story_effects buffer into msg_log immediately after.
DisplayMessage routes through GameEffect::StoryMessageDisplayed;
SpawnSpecialForce routes through GameEffect::SpecialForceSpawned.
Judgment calls documented:
* K4 cooldown lives on EconomyState (sim state saved in v8), not on
the zero-sized EconomySystem struct, because the plan's "intra-tick
scratch on EconomySystem itself" wording conflicted with zero-sized
struct reality and with #F2's intent of saving economy state for
incident non-re-fire across loads. EconomyState is still sim state
(not world state), so the #K "no new world state" constraint holds.
* The deleted GameEffect::SpecialForceSpawned variant is resurrected
WITHOUT MessageCategoryTag's full variant set — only the `Event`
variant has a real producer in this sprint. SIMP-H1 applies: extra
variants land when they have real consumers.
* The #A5 grep for hardcoded SYS_EVENTS/SYS_STORY in eval_parity.py
returned zero matches — no regeneration needed. Documented above.
* SpecialForceSpawned currently emits only the effect + telemetry;
full SpecialForceUnit arena wiring is explicitly deferred to Dabora 3
per the plan's dependency chain. The apply arm surfaces a message
log entry in interactive mode so the chain is visible to players.
* WASM target validation was not run (sandbox restriction). No wasm32
code paths were touched; the changes are cfg-agnostic.
Test count: 418 → 425 (+7). Zero new warnings. Economy now uses the
previously-dead GNPRTB_MAINTENANCE_RATE_CONTROLLED constant, dropping
one warning (6 → 5 in rebellion-core).
* effects.rs +1 (story_message_and_special_force_are_command_phase)
* economy.rs +4 (k1_support_change, k2_natural_disaster,
k3_resource_discovery, k4_maintenance_shortfall)
* manufacturing.rs +2 (k6_idle_transition, k6_blockade_excluded)
Playtest validation: 5000-tick seed 42 run (1.95M events) passes all 7
new story_events parity checks. Pre-existing death_star and research
failures in eval_parity.py are baseline, not introduced here.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Read-only forensic analysis of 1,505 advisor BIN files across both
factions. Identifies 4 distinct schemas (v1 frame list, animation
record, pointer chain, event script) and verifies a 4-level indirection
graph that the current 12%-parse-rate v1 parser treats as LengthMismatch.
Unblocks Dabora 5b decoder: target parse rate >= 50% is reachable with
Schema B alone (38% of corpus); full coverage requires all 4 schemas
plus tag=3/17/19/21 outlier inspection.
Per plan SIMP-H7: findings staged in docs/reports/working-notes/ for
inclusion in the post-sprint report appendix. No code changes.
Four reviewers (Claude code-reviewer, silent-failure-hunter, code-simplifier,
and a Codex reviewer) flagged the following Dabora 1 issues. This commit
addresses the CRITICAL and HIGH findings.
Dead code removed (simplifier H1/H2, code-reviewer H#1):
- Delete GameEffect::SpecialForceSpawned — no producer
- Delete GameEffect::StoryMessageDisplayed — no producer
- Delete MessageCategoryTag enum — 3 of 4 variants never referenced
- Dabora 3 can re-add these in five lines when there is an actual emitter
Simplify SpawnSpecialForce integrator stub (silent-failure CRITICAL #3,
code-reviewer I#3, codex L):
- Was: attempted resolution via current_system → fleet.location (wrong —
plan says destination from MovementOrder) + eprintln on failure + silent
drop on success
- Now: pure no-op with TODO(dabora-3). No code path emits the action yet,
so this is unreachable. Stub no longer lies about what it does.
Plan-compliance fixes (silent-failure #5, #6, code-reviewer H#2):
- Remove #[serde(default)] on Character::heritage_known — plan line 790
explicitly said no serde(default); bincode is positional and the v8 bump
is the actual migration boundary
- Remove impl Default for SystemTag — every construction site must choose
explicitly, misrouting is now a compile error
- Remove #[serde(default)] on GameEvent::system_tag — same reason
- Update JSON backward-compat test to include heritage_known and clarify
what it actually tests
Boilerplate reduction (simplifier M1):
- Collapse 12 Character construction sites in missions.rs (10) and ai.rs (2)
to use ..Default::default() + only meaningful fields. Net: -384 LOC
- lib.rs convert_character stays explicit (production DAT mapping, not a
test helper)
Cleanup (simplifier L2):
- Delete is_story_event() tombstone comment from integrator.rs
Deferred to later daborot:
- #F7 apply_event_actions consolidation → Dabora 2/3 when story chains land
- VictoryModal GameMode variant → Dabora 5 (plan)
- Real SpawnSpecialForce implementation → Dabora 3
- WASM localStorage orphan sweep → deferred (medium severity, not blocking)
- event_rolls dynamic budget → deferred (current 16-slot budget safe for
5 Random conditions in stock content)
Still passes: 418 tests, zero warnings, zero new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dabora 1 of Knesset Shamash-Bet lays the foundation for the story events
sweep and cutscene polish sprint.
Foundation changes (#F1–#F10):
- Save format bumped v7 → v8 with EconomyState in SaveState (closes
incident re-fire on reload bug) and v7 rejection arm
- WASM localStorage meta key prefix bumped to rebellion_meta_v8_
- Character::heritage_known bool added (gates Final Battle BMP variant)
- impl Default for Character (simplifies all test construction sites)
- GameEffect::SpecialForceSpawned + StoryMessageDisplayed variants added
with MessageCategoryTag enum (no render deps in rebellion-core)
- EventAction::SpawnSpecialForce { at_character } added with integrator
stub (full wiring deferred to Dabora 3 story chains)
- SystemTag enum (Events/Story/Notification) on GameEvent replaces the
is_story_event() ID-pattern-match filter — misrouting now caught at
define() time instead of runtime
- event_rolls unwrap_or(1.0) replaced with expect() — panics on budget
overflow in release builds (protects autoresearch signal)
- CI grep guard: #[test] in story_events.rs asserts banned notification
IDs never use EventCondition::Random
- Save-panel lockout during Cutscene mode at keyboard + cockpit button
418 tests pass (was 417). Zero new warnings. Zero new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two Codex builders, one reviewer pass, and two P1 follow-up fixes land the
final Resheph deferrals.
U2 — Native video playback (task #131)
- crates/rebellion-render/src/video_player.rs (new): VideoPlayer::open /
advance / current_frame / is_finished / stop. Streams PNG frame sequences
and WAV audio pumped through quad-snd — no ffmpeg/libvpx/gstreamer
runtime deps. VideoError::NotDecoded triggers a graceful in-game skip
message and eprintln warning when decoded assets are missing.
- scripts/decode-cutscenes.sh (new): one-time local ffmpeg decode of
assets/references/ref-videos/*.webm into frame-*.png + metadata.json +
<name>.wav. Atomic: decodes into a temp dir, then renames on success so
a mid-run failure can't clobber a previously decoded cutscene.
- GameMode::Cutscene added to rebellion-app/src/main.rs. 000.webm plays
before the Main Menu on startup; 201.webm / 202.webm play on victory
or defeat. ESC and SPACE skip. The wasm32 build uses a no-op stub.
- New dep: quad-snd on rebellion-render (audio context is owned by the
player to keep cutscenes self-contained).
C1 — BIN-driven advisor animation (task #132)
- parse_advisor_bin() decodes the simple `u16 frame_count + u16 ids[]`
format with BinError::{TruncatedHeader, TruncatedFrames, LengthMismatch}
real-error rejection. Tested with fixture bytes from the 00200/00201/
00204 alsprite samples.
- AdvisorState now carries bin_sequences, current_sequence, frame_cursor.
update(dt) walks authored sequences on their default_interval and picks
the next sequence from idle / normal / high-critical bands (contiguous
thirds of the sorted valid BIN list). BIN frame IDs map into the sorted
BMP pool via modulo — explicitly best-effort until the DLL
resource-index table is decoded.
- Falls back to legacy sorted-frame cycling when zero BINs parse, so the
advisor never freezes.
- load_faction_frames() now emits a per-faction summary line on stderr
showing valid / parse-failed / empty / io-failed counts. Only ~24% of
advisor BINs (183 of ~752 per faction) match the simple format; the
rest declare inconsistent lengths and indicate one or more undocumented
header variants.
Build + tests
- 417 tests / 12 suites passing (322 core + 50 data + 42 render + 3 doc).
- cargo build -p rebellion-app: clean.
- bash scripts/build-wasm.sh: clean, wasm-opt 4,707,752 → 4,312,675 bytes.
Docs
- CLAUDE.md: refreshed header (417 tests, ~98% overall), corrected stale
counts (13 panels, 20 mechanics docs), added cutscene setup note, new
Reports entry. Droid Advisor Known Limitations updated to reflect the
partial BIN decode.
- agent_docs/architecture.md: render tree lists video_player.rs, advisor
description updated, decoded cutscene assets documented.
- agent_docs/roadmap.md: Knesset Kothar wa Khasis section added; "Video
playback (Smacker → WebM)" and the Droid Advisor bullet removed from
Remaining UI.
Knesset Kothar wa Khasis — Resheph is fully closed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Combat:
- Per-weapon-type damage in phase_weapon_fire() multiplies turbolaser, ion
cannon, and laser cannon arcs by their class *_attack_strength scalars.
i64 math throughout (no f64 double-rounding). Zero attack_strength falls
back to raw arc count. 3 tests.
- Death Star constants documented as "best available approximation" after
exhaustive GNPRTB search.
AI parity:
- Dispatch validators 4/18 → 10/18 via new can_dispatch_to_system() and
can_dispatch_fleet() helpers (strength comparison, loyalty gating,
empty-fleet rejection). Unported 8 documented with offset references.
- Faction deploy budgets: alliance_deploy_budget=0.6, empire_deploy_budget=0.8
in AiConfig, wired into evaluate_fleet_deployment() for FUN_00506ea0 parity.
- evaluate_uprising_prevention(): dispatches diplomats to low-support
controlled systems before they flip.
- evaluate_ds_escort(): routes nearest idle fleet to DS location.
WASM:
- Real localStorage save/load via web_sys::Storage. Inline base64 codec,
separate metadata keys for fast list_saves(). Replaces the stub path.
- BmpCache path rebasing: cfg-gated DATA_PREFIX/HD_PREFIX + rebase_path_prefix()
helper. Browser builds resolve textures from web/data/base/.
- Audio path rebasing: cfg-gated AUDIO_PREFIX + audio_base_path() helper.
quad-snd WebAudio backend sees correctly prefixed paths.
- wasm-opt -O3 step in scripts/build-wasm.sh. Recovered 395,869 bytes (8.4%)
of the web-sys/wasm-bindgen overhead. Final +5.8% vs pre-sprint baseline.
UI resources:
- pub mod resources in bmp_cache.rs: named BMP resource ID constants for
COMMON, STRATEGY, TACTICAL, and GOKRES DLL sources. Cockpit buttons,
event screens, tactical HUD, portraits, mini-icons.
Eval infrastructure:
- scripts/golden_values.json: 111 mapped GNPRTB parameters + 46 community-
discovered params + combat/economy/research/AI/movement/victory constants.
- scripts/eval_parity.py rewritten as a golden-value oracle that diffs
playtest JSONL against the oracle. Pass/fail/skip reporting.
- Fixed non-JSON footer tolerance in load_events().
- Corrected ai.current_tick_interval_days from guess (5) to actual (7).
Docs:
- agent_docs/roadmap.md: Knesset Resheph section with all 10 deliverables.
- agent_docs/architecture.md: Save/Load Flow + Parity Eval Flow diagrams,
WASM deps (web-sys, wasm-bindgen) noted in crate graph.
- agent_docs/systems/ai-parity-tracker.md: validators 10/18, faction budgets
DONE, uprising prevention + DS escort behaviors added.
- CLAUDE.md Reports: Knesset Resheph entry.
- docs/plans/2026-04-06-001-test-lint-build-infrastructure-execplan.md:
Codex planner-produced 5-milestone ExecPlan for strengthening the test,
lint, type-check, and build gates (fmt drift, clippy enforcement,
cargo-llvm-cov, playtest smoke gate, parity oracle in CI, integration
tests for crate boundaries, fuzz targets for DAT parsers).
Tests: 410 pass (322 core + 50 data + 35 render + 3 doc). Zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Counter-intelligence: MissionKind::is_covert() classifies 6 mission types
as foilable. compute_defense_score() sums system espionage_rating + enemy
character espionage skills. determine_outcome() now uses real foil_prob()
instead of the hardcoded stub. Failed covert missions distinguish Foiled
(blocked by counter-intel) vs Failure (agent skill insufficient). 6 tests.
Plan: Knesset Resheph final sprint — 5 daborot, 12 tasks, clearing the
full backlog (combat F3/F4, AI A1/A3/A4, WASM 3 tasks, UI/polish 3 tasks,
eval oracle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Autoresearch exp0015 (composite 0.7270→0.7438). Changes:
1. AI_TICK_INTERVAL 7→5: more frequent AI re-evaluation
2. Abduction targets sorted weakest-first: better captive generation
3. Guard slotmap index in assassination dispatch: prevents panics
4. Remove expected_success gate for rescue + dispatch to ALL captives
5. Capture characters from destroyed fleets in combat: new captive source
These changes create a viable rescue pipeline: fleet destruction creates
captives, and the AI dispatches rescue missions to all of them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a character is abducted, their current_system was not updated to
the capture location, making them invisible to the AI rescue evaluator.
Seed 123 now hits mission_completeness 1.0 with all 8 mission kinds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AI now evaluates garrison strength across controlled systems and
redistributes troops from oversupplied (>3) to undersupplied (<1)
systems, prioritizing HQ defense. Parity aggregate 0.8979 → 0.9750
with zero cross-seed variance.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as uprising/betrayal/death_star heartbeats — emit a check
event each tick so system tags appear in telemetry regardless of whether
game conditions trigger the system's primary events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These three systems run every tick but rarely trigger game events (requires
low loyalty, character capture, or DS construction). Adding per-tick check
events ensures their system tags appear in JSONL telemetry regardless.
event_coverage: 0.76-0.82 → 0.94-1.0 across seeds. Seed 42 hits 17/17.
Parity aggregate: 0.8638 → 0.8903 (+2.65%).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
30 iterations, 1 keep. Quality parameter space is narrow — most
mutations cause degenerate games. Score: 0.0→0.3423.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the mission_completeness gap (0.625→0.875-1.0) by adding three
missing AI mission dispatch heuristics:
- InciteUprising: first high-diplomacy character per eval cycle targets
enemy-controlled systems (pop > 0.5) via find_incite_target()
- Abduction: after assassination pass, remaining espionage operatives
attempt to capture enemy major characters
- Rescue: new evaluate_rescue() scans for captive allies and dispatches
best combat-skilled character to their location
Parity score: 0.7254 → 0.8638 aggregate (+13.8% across 3 seeds).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First iteration timed out — Opus needs time to read, implement, test, build, eval.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. HIGH: Dirty-tree guard — all 3 loops refuse to start if git status is dirty
2. MEDIUM: HTTP server binds to 127.0.0.1 only (was 0.0.0.0)
3. MEDIUM: git add -u replaces git add -A (only stage tracked files)
4. LOW: Temp file paths use os.getpid() suffix to avoid collisions
5. LOW: Import tempfile in parity loop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 non-autoresearch parity tasks with detailed prompts in parity_tasks.json.
Runner script picks next unblocked task by priority (F > A > U > C > WASM > Eval),
dispatches claude -p, tests, commits on success, discards on failure.
Usage:
python3 scripts/parity_work_loop.py --dry-run # preview order
python3 scripts/parity_work_loop.py # execute all
python3 scripts/parity_work_loop.py --max-tasks 3 # execute next 3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Threads target_character: Option<CharacterKey> through the entire mission
dispatch chain: MissionState::dispatch/dispatch_guarded, PanelAction,
AIAction::DispatchMission, and both apply paths (main.rs + integrator.rs).
The formulas in compute_table_input() and build_effects() already read
target_character — this commit connects the intake side so Recruitment,
Assassination, and Abduction missions actually receive their targets.
AI assassination dispatch now pairs each operative with a specific enemy
character from enemy_major_chars instead of discarding the target list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Promotes Fleet.capital_ships from Vec<ShipEntry> (aggregate class+count)
to Vec<ShipInstance> (per-hull records with hull_current and alive state).
- ShipEntry struct removed; ShipInstance is now the primary ship record
- Fleet gains ship_count(), ship_counts_by_class(), is_empty() helpers
- Combat snapshot_fleet simplified: 1:1 map from ShipInstance to ShipSnap
- Combat damage application simplified: direct alive-index instead of offset-walking
- Fleet merge simplified: extend() instead of find-or-insert by class
- Repair system now emits real ShipRepaired events with hull deltas (last TODO resolved)
- Repair wired into interactive main.rs (was headless-only)
- Save format v7 (v6 rejected — bincode layout change)
- Deduplicated combat apply functions (main.rs delegates to integrator.rs)
- Fixed hardcoded difficulty_index=2 in tactical auto-resolve
- Removed dead sys_json helper, redundant faction_is_alliance field
- Fixed fog.rs/bombardment.rs alive-filtering consistency
- 403 tests pass, zero TODOs remaining
22 files changed across all 5 crates. ~65 edit sites migrated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4 — Facility type promotion:
- Add is_shipyard: bool to ManufacturingFacilityInstance
- Add is_mine: bool to ProductionFacilityInstance
- Both with #[serde(default)] for save compatibility
- Fix#12: raw material output now counts production_facilities with is_mine=true
(was incorrectly counting manufacturing_facilities.len())
- Fix#13: has_shipyard now checks manufacturing_facilities for is_shipyard=true
(was incorrectly checking defense_facilities)
- Update raw_material_overcap test to use production facilities with is_mine
- Add pending_move_destination to FleetsState (from Phase 3)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 — UI wiring:
- #5: Populate save_slots from rebellion_data::save::list_saves() at startup
- #6: LoadGame from main menu opens save/load panel instead of jumping to galaxy
- #7: OpenMissionTo pre-selects kind + target in MissionsPanelState, opens panel
- #8: InitiateFleetMove sets pending_move_destination on FleetsState, opens panel
- Add pending_move_destination: Option<SystemKey> to FleetsState
- Handle OpenMissionTo + InitiateFleetMove at call site (before dispatch to
apply_panel_action) since they need local UI state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>