using System.Collections.Generic; namespace IdleSpectator; /// /// Built-in agent playtest scenarios expanded by action=scenario. /// internal static class HarnessScenarios { public static List Build(string name) { switch ((name ?? "").Trim().ToLowerInvariant()) { case "smoke": return Smoke(); case "tip_match": return TipMatch(); case "critical": case "critical_smoke": return CriticalSmoke(); case "dossier_nametag": case "nametag_layout": return DossierNametagLayout(); case "settings_full": return SettingsFull(); case "ghost_guard": return GhostGuard(); case "retarget_stability": return RetargetStability(); case "input_exit": return InputExit(); case "director_tiers": return DirectorTiers(); case "director_lifecycle": return DirectorLifecycle(); case "interest_happiness": return InterestHappiness(); case "director_gaps": case "director_coverage": return DirectorGaps(); case "focus_interrupt": case "camera_interrupt": return FocusInterrupt(); case "watch_reason": case "watch_reason_clarity": return WatchReasonClarity(); case "camera_presentability": case "presentability": return CameraPresentability(); case "status_overlap_camera": case "status_overlap": return StatusOverlapCamera(); case "combat_focus": case "combat_tip_align": return CombatFocusAlign(); case "combat_stability_live": case "combat_live_proof": return CombatStabilityLive(); case "sticky_variety_valve": case "variety_valve": return StickyVarietyValve(); case "war_front_sticky": case "war_sticky": return WarFrontSticky(); case "plot_cell_sticky": case "plot_sticky": return PlotCellSticky(); case "family_pack_sticky": case "family_sticky": return FamilyPackSticky(); case "family_pack_yields": case "pack_yields": return FamilyPackYields(); case "status_outbreak_sticky": case "outbreak_sticky": return StatusOutbreakSticky(); case "status_outbreak_not_love": case "outbreak_not_love": return StatusOutbreakNotLove(); case "director_action_priority": case "action_priority": return DirectorActionPriority(); case "discovery": return Discovery(); case "world_log": return WorldLog(); case "world_log_audit": return WorldLogAudit(); case "disaster_war_audit": case "disaster_war_inventory": return DisasterWarAudit(); case "event_coverage": return EventCoverage(); case "event_inject_coverage": return EventInjectCoverage(); case "event_live_apply_loop": return EventLiveApplyLoop(); case "event_live_relationship": return EventLiveRelationship(); case "event_live_pipelines": return EventLivePipelines(); case "event_live_coverage": return EventLiveCoverage(); case "event_live_outcome_smoke": return EventLiveOutcomeSmoke(); case "event_suite": return EventSuite(); case "domain_event_audit": return DomainEventAudit(); case "library_asset_labels_ok": return LibraryAssetLabelsOk(); case "event_tracking": return EventTracking(); case "family_trigger_smoke": return FamilyTriggerSmoke(); case "mutation_discovery": return MutationDiscovery(); case "chronicle": case "chronicle_smoke": return ChronicleSmoke(); case "gravestones": case "grave_smoke": return GravestoneSmoke(); case "activity": case "activity_idle_smoke": return ActivityIdleSmoke(); case "activity_status_smoke": return ActivityStatusSmoke(); case "activity_happiness_smoke": return ActivityHappinessSmoke(); case "activity_happiness_audit": return ActivityHappinessAudit(); case "activity_prose_showcase": return ActivityProseShowcase(); case "activity_taxonomy_audit": return ActivityTaxonomyAudit(); case "activity_status_audit": return ActivityStatusAudit(); case "activity_rate": case "activity_rate_sample": return ActivityRateSample(); case "chronicle_subject_bench": case "subject_bench": return ChronicleSubjectBench(); case "regression": case "full": return Regression(); default: return null; } } private static List ActivityTaxonomyAudit() { return new List { Step("ata0", "wait_world"), Step("ata1", "pick_unit", asset: "auto"), Step("ata2", "focus", asset: "auto"), Step("ata3", "assert", expect: "activity_taxonomy_audit"), Step("ata4", "snapshot") }; } /// Dump sample activity lines for a few species (dev / product check). private static List ActivityProseShowcase() { return new List { Step("aps0", "wait_world"), Step("aps1", "dismiss_windows"), Step("aps2", "assert", expect: "activity_prose_showcase"), Step("aps3", "snapshot") }; } /// /// Timed Fallen/Living list rebuilds at rising subject counts. /// Picks the highest size under a 2ms Fallen avg budget (runtime cap only for that session). /// private static List ChronicleSubjectBench() { return new List { Step("sb0", "wait_world"), Step("sb1", "dismiss_windows"), Step("sb2", "chronicle_clear"), Step( "sb3", "chronicle_subject_bench", value: "320,640,1280,2560,4096,8192", count: 3, expect: "12"), Step("sb4", "assert", expect: "chronicle_subjects_capped"), Step("sb5", "snapshot") }; } /// /// Full regression meta-suite (nested scenarios keep assert counters). /// Battle / map-gen paths stay out of gate. /// private static List Regression() { return new List { Nested("reg_critical", "critical_smoke"), Nested("reg_settings", "settings_full"), Nested("reg_ghost", "ghost_guard"), Nested("reg_retarget", "retarget_stability"), Nested("reg_input", "input_exit"), Nested("reg_director", "director_tiers"), Nested("reg_director_life", "director_lifecycle"), Nested("reg_interest_happiness", "interest_happiness"), Nested("reg_director_gaps", "director_gaps"), Nested("reg_action_priority", "director_action_priority"), Nested("reg_variety_valve", "sticky_variety_valve"), Nested("reg_combat_stability", "combat_stability_live"), Nested("reg_discovery", "discovery"), Nested("reg_worldlog", "world_log"), Nested("reg_presentability", "camera_presentability"), Nested("reg_event_coverage", "event_suite"), Nested("reg_chronicle", "chronicle_smoke"), Nested("reg_gravestones", "gravestones"), Nested("reg_activity", "activity_idle_smoke"), Nested("reg_activity_status", "activity_status_smoke"), Nested("reg_activity_happiness", "activity_happiness_smoke"), Step("reg_end", "fast_timing", value: "false"), Step("reg_snap", "snapshot"), }; } private static List FamilyTriggerSmoke() { return new List { Step("ft0", "wait_world"), Step("ft1", "dismiss_windows"), Step("ft2", "spectator", value: "off"), // Bridge: becomeLoversWith publishes happiness fallen_in_love Step("ft10", "spawn", asset: "human"), Step("ft11", "happiness_remember_partner"), Step("ft12", "spawn", asset: "human"), Step("ft13", "focus", asset: "auto"), Step("ft14", "activity_clear"), Step("ft15", "happiness_reset"), Step("ft16", "happiness_bond_lovers"), Step("ft17", "assert", expect: "activity_happiness_count", value: "2", label: "min"), Step("ft18", "assert", expect: "activity_log_contains", value: "love"), // Retarget: setParent1 emits rel:add_child Step("ft20", "spawn", asset: "human"), Step("ft21", "happiness_remember_partner"), Step("ft22", "spawn", asset: "human"), Step("ft23", "focus", asset: "auto"), Step("ft24", "relationship_set_parent"), Step("ft25", "assert", expect: "interest_has_key", value: "rel:add_child"), Step("ft99", "snapshot"), }; } private static List ActivityHappinessAudit() { return new List { Step("aha0", "wait_world"), Step("aha1", "pick_unit", asset: "auto"), Step("aha2", "focus", asset: "auto"), Step("aha3", "assert", expect: "activity_happiness_audit"), Step("aha4", "snapshot") }; } private static List ActivityHappinessSmoke() { return new List { Step("hp0", "wait_world"), Step("hp1", "dismiss_windows"), Step("hp1b", "spectator", value: "off"), Step("hp2", "spawn", asset: "human"), Step("hp3", "focus", asset: "auto"), Step("hp4", "activity_clear"), Step("hp5", "happiness_reset"), Step("hp6", "assert", expect: "activity_happiness_audit"), // Signal + Life: crown Step("hp10", "happiness_apply", value: "become_king"), Step("hp11", "assert", expect: "activity_happiness_task_id", value: "happiness:become_king"), Step("hp12", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"), Step("hp13", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"), Step("hp14", "assert", expect: "activity_log_contains", value: "ruler"), // Ambient cooldown: second meal within window should not add another ring write Step("hp20", "happiness_reset"), Step("hp21", "happiness_apply", value: "just_ate"), Step("hp22", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"), Step("hp23", "happiness_apply", value: "just_ate"), Step("hp24", "assert", expect: "activity_happiness_count", value: "2", label: "exact"), Step("hp25", "assert", expect: "activity_happiness_ring_count", value: "1", label: "exact"), // Force related grief prose Step("hp30", "happiness_reset"), Step("hp31", "happiness_force_note", value: "death_child", label: "Ava"), Step("hp32", "assert", expect: "activity_log_contains", value: "Ava"), Step("hp33", "assert", expect: "activity_happiness_task_id", value: "happiness:death_child"), // Real lover grief path (+ dossier row → other Lore). // Idle must be on *before* the bond: first spectator enable can ClearSession the ring. Step("hp40", "spectator", value: "on"), Step("hp40b", "spawn", asset: "human"), Step("hp41", "happiness_remember_partner"), Step("hp42", "spawn", asset: "human"), Step("hp43", "focus", asset: "auto"), Step("hp44", "activity_clear"), Step("hp45", "happiness_reset"), Step("hp46", "happiness_bond_lovers"), Step("hp46b", "assert", expect: "activity_happiness_count", value: "2", label: "min"), Step("hp46c", "assert", expect: "activity_log_contains", value: "love"), Step("hp46d", "focus", asset: "auto"), Step("hp46e", "wait", wait: 0.2f), Step("hp46f", "assert", expect: "history_other_rows", value: "1", label: "min"), Step("hp46g", "remember_focus"), Step("hp46h", "dossier_click_history_other"), Step("hp46i", "wait", wait: 0.2f), Step("hp46j", "assert", expect: "lore_detail", value: "true"), Step("hp46k", "assert", expect: "lore_detail_is_other"), Step("hp46l", "lore_close"), Step("hp46m", "spectator", value: "off"), Step("hp47", "wait", value: "0.5"), Step("hp48", "happiness_kill_partner"), Step("hp49", "wait", value: "0.5"), Step("hp50", "assert", expect: "activity_log_contains", value: "lover"), Step("hp51", "assert", expect: "activity_happiness_life_count", value: "1", label: "min"), // Friend bond + canonical merge path Step("hp60", "spawn", asset: "human"), Step("hp61", "happiness_remember_partner"), Step("hp62", "spawn", asset: "human"), Step("hp63", "focus", asset: "auto"), Step("hp64", "activity_clear"), Step("hp65", "happiness_reset"), Step("hp66", "happiness_bond_friends"), Step("hp67", "assert", expect: "activity_happiness_count", value: "1", label: "min"), // Full catalog inject through real API Step("hp70", "happiness_reset"), Step("hp71", "happiness_inject_all"), Step("hp72", "assert", expect: "activity_happiness_count", value: "1", label: "min"), // Aggregate burst budget Step("hp80", "happiness_reset"), Step("hp81", "happiness_burst", value: "just_started_war", count: 48), Step("hp90", "snapshot") }; } private static List ActivityStatusAudit() { return new List { Step("asa0", "wait_world"), Step("asa1", "pick_unit", asset: "auto"), Step("asa2", "focus", asset: "auto"), Step("asa3", "assert", expect: "activity_status_audit"), Step("asa4", "snapshot") }; } private static List ActivityStatusSmoke() { return new List { Step("st0", "dismiss_windows"), Step("st1", "wait_world"), Step("st2", "set_setting", expect: "enabled", value: "true"), Step("st3", "set_setting", expect: "show_dossier_caption", value: "true"), Step("st4", "activity_clear"), Step("st5", "activity_status_reset"), Step("st5b", "spawn", asset: "human", count: 1), Step("st6", "pick_unit", asset: "human"), Step("st7", "spectator", value: "off"), Step("st8", "spectator", value: "on"), Step("st9", "focus", asset: "auto"), Step("st10", "wait", wait: 0.25f), Step("st10b", "activity_status_reset"), Step("st11", "assert", expect: "activity_status_audit"), // Gain path (confused is widely applicable across assets) Step("st12", "status_apply", value: "confused"), Step("st13", "assert", expect: "activity_status_gain_count", value: "1", label: "exact"), Step("st14", "assert", expect: "activity_log_contains", value: "confused"), Step("st15", "assert", expect: "activity_status_task_id", value: "status_gain:confused"), Step("st16", "assert", expect: "activity_status_active", value: "confused", label: "true"), // Refresh must not create another gain line Step("st17", "status_apply", value: "confused"), Step("st18", "assert", expect: "activity_status_gain_count", value: "1", label: "exact"), Step("st19", "assert", expect: "activity_status_refresh_count", value: "1", label: "min"), // Second distinct gain (min: world may emit an extra status mid-suite) Step("st20", "status_apply", value: "sleeping"), Step("st21", "assert", expect: "activity_status_gain_count", value: "2", label: "min"), Step("st22", "assert", expect: "activity_log_contains", value: "asleep"), // Explicit removal Step("st23", "status_remove", value: "confused"), Step("st24", "wait", wait: 0.15f), Step("st25", "assert", expect: "activity_status_loss_count", value: "1", label: "min"), Step("st26", "assert", expect: "activity_log_contains", value: "clarity"), Step("st27", "assert", expect: "activity_status_active", value: "confused", label: "false"), // Authored burning phrase via force path (game may block burning on some units) Step("st27b", "status_force_note", value: "burning", expect: "gain"), Step("st27c", "assert", expect: "activity_log_contains", value: "catches fire"), // Fresh living unit for expiry / clear / death sections Step("st28", "spawn", asset: "human", count: 1), Step("st28a", "pick_unit", asset: "human"), Step("st28b", "focus", asset: "auto"), Step("st28c", "activity_status_reset"), Step("st28d", "fast_timing", value: "true"), Step("st29", "status_apply", value: "surprised", label: "0.25"), Step("st30", "assert", expect: "activity_status_gain_count", value: "1", label: "exact"), Step("st31", "wait", wait: 1.25f), Step("st32", "assert", expect: "activity_status_active", value: "surprised", label: "false"), Step("st33", "assert", expect: "activity_status_loss_count", value: "1", label: "min"), Step("st33b", "fast_timing", value: "false"), // Bulk clear on another fresh unit Step("st34", "spawn", asset: "human", count: 1), Step("st34b", "pick_unit", asset: "human"), Step("st34c", "focus", asset: "auto"), Step("st34d", "activity_status_reset"), Step("st35", "status_apply", value: "confused"), Step("st36", "status_apply", value: "caffeinated"), Step("st37", "status_clear_all"), Step("st38", "wait", wait: 0.15f), Step("st39", "assert", expect: "activity_status_loss_count", value: "2", label: "min"), // Unknown fallback force note Step("st40", "status_force_note", value: "zzz_harness_fake_status", expect: "gain"), Step("st41", "assert", expect: "activity_log_contains", value: "zzz harness fake status"), // Dead actor blocks gain; death dispose does not spam losses Step("st42", "spawn", asset: "human", count: 1), Step("st42b", "pick_unit", asset: "human"), Step("st42c", "focus", asset: "auto"), Step("st42d", "activity_status_reset"), Step("st43", "status_apply", value: "poisoned"), Step("st44", "assert", expect: "activity_status_gain_count", value: "1", label: "exact"), Step("st45", "kill_focus"), Step("st46", "wait", wait: 0.2f), Step("st47", "assert", expect: "activity_status_loss_count", value: "0", label: "exact"), Step("st48", "status_apply", value: "confused", expect: "blocked"), Step("st49", "assert", expect: "activity_status_gain_count", value: "1", label: "exact"), // UI on a fresh living unit Step("st50", "spawn", asset: "human", count: 1), Step("st50b", "pick_unit", asset: "human"), Step("st51", "focus", asset: "auto"), Step("st52", "activity_clear"), Step("st53", "activity_status_reset"), Step("st54", "status_apply", value: "confused"), Step("st55", "status_apply", value: "sleeping"), Step("st56", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "activity"), Step("st57", "assert", expect: "caption_layout_ok"), Step("st58", "screenshot", value: "hud-activity-status-smoke.png"), Step("st59", "wait", wait: 0.45f), Step("st60", "lore_open_focus"), Step("st61", "wait", wait: 0.2f), Step("st62", "lore_detail_pane", value: "activity"), Step("st63", "assert", expect: "lore_activity_shown", value: "1", label: "min"), Step("st64", "assert", expect: "no_bad"), Step("st99", "snapshot"), }; } private static List Smoke() { return new List { Step("s0", "dismiss_windows"), Step("s1", "wait_world"), Step("s2", "reset_counters"), Step("s3", "pick_unit", asset: "auto"), Step("s4", "spectator", value: "off"), Step("s5", "assert", expect: "idle", value: "false"), Step("s6", "spectator", value: "on"), Step("s7", "focus", asset: "auto"), Step("s8", "wait", wait: 0.35f), Step("s9", "assert", expect: "health"), Step("s10", "snapshot"), }; } private static List TipMatch() { return new List { Step("t0", "dismiss_windows"), Step("t1", "wait_world"), Step("t2", "reset_counters"), Step("t3", "pick_unit", asset: "auto"), Step("t4", "spectator", value: "on"), Step("t5", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"), Step("t6", "wait", wait: 0.3f), Step("t7", "assert", expect: "tip_matches_unit"), Step("t8", "assert", expect: "unit_asset", asset: "auto"), Step("t9", "assert", expect: "power_bar", value: "false"), Step("t10", "snapshot"), }; } /// /// Critical E2E against already-living map units (no fragile miracle spawns). /// private static List CriticalSmoke() { return new List { Step("c00", "dismiss_windows"), Step("c01", "wait_world"), Step("c02", "reset_counters"), Step("c05", "pick_unit", asset: "auto"), Step("c06", "wait", wait: 0.25f), Step("c10", "set_setting", expect: "enabled", value: "false"), Step("c11", "assert", expect: "enabled", value: "false"), Step("c12", "spectator", value: "on", expect: "blocked"), Step("c13", "assert", expect: "idle", value: "false"), Step("c14", "set_setting", expect: "enabled", value: "true"), Step("c15", "assert", expect: "enabled", value: "true"), Step("c20", "spectator", value: "off"), Step("c21", "assert", expect: "idle", value: "false"), Step("c22", "spectator", value: "on"), Step("c23", "focus", asset: "auto"), Step("c24", "reset_counters"), Step("c25", "wait", wait: 0.35f), Step("c26", "assert", expect: "health"), Step("c27", "assert", expect: "power_bar", value: "false"), Step("c28", "assert", expect: "no_bad"), Step("c29", "assert", expect: "focus_arrows"), Step("c30", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"), Step("c31", "wait", wait: 0.3f), Step("c32", "assert", expect: "tip_matches_unit"), Step("c33", "assert", expect: "unit_asset", asset: "auto"), Step("c34", "assert", expect: "tip_contains", value: "auto"), Step("c35", "assert", expect: "power_bar", value: "false"), Step("c36", "assert", expect: "no_bad"), Step("c36b", "assert", expect: "no_placeholder_tip"), Step("c40", "watch", asset: "auto", label: "Retarget A: {asset}", tier: "Curiosity"), Step("c41", "wait", wait: 0.25f), Step("c42", "focus", asset: "auto"), Step("c43", "wait", wait: 0.25f), Step("c44", "watch", asset: "auto", label: "Retarget B: {asset}", tier: "Curiosity"), Step("c45", "wait", wait: 0.35f), Step("c46", "assert", expect: "health"), Step("c47", "assert", expect: "no_bad"), Step("c48", "assert", expect: "power_bar", value: "false"), // Long nametag must not paint under the level badge. Step("c49a", "dossier_force_headline", value: "Zezqzo Tikzkek Tak (scorpion)"), Step("c49b", "assert", expect: "caption_layout_ok"), Step("c49c", "assert", expect: "dossier_contains", value: "Zezqzo"), Step("c49d", "screenshot", value: "hud-dossier-long-name.png"), Step("c49e", "wait", wait: 0.35f), Step("c50", "simulate_input"), Step("c51", "wait", wait: 0.25f), Step("c52", "assert", expect: "idle", value: "false"), Step("c52b", "assert", expect: "caption_contains", value: "Paused"), Step("c53", "assert", expect: "no_bad"), Step("c99", "snapshot"), }; } /// Long "Name (species)" must leave room for the level chip (no overlap). private static List DossierNametagLayout() { return new List { Step("dn0", "dismiss_windows"), Step("dn1", "wait_world"), Step("dn2", "set_setting", expect: "enabled", value: "true"), Step("dn3", "set_setting", expect: "show_dossier_caption", value: "true"), Step("dn4", "pick_unit", asset: "auto"), Step("dn5", "spectator", value: "off"), Step("dn6", "spectator", value: "on"), Step("dn7", "focus", asset: "auto"), Step("dn8", "wait", wait: 0.3f), Step("dn10", "dossier_force_headline", value: "Zezqzo Tikzkek Tak (scorpion)"), Step("dn11", "assert", expect: "dossier_contains", value: "Zezqzo"), Step("dn12", "assert", expect: "caption_layout_ok"), Step("dn13", "screenshot", value: "hud-dossier-long-name.png"), Step("dn14", "wait", wait: 0.4f), // Extremely long: must ellipsis rather than paint under level. Step("dn20", "dossier_force_headline", value: "Zezqzo Tikzkek Tak the Unbelievably Long-Named Elder (scorpion)"), Step("dn21", "assert", expect: "caption_layout_ok"), Step("dn22", "assert", expect: "dossier_contains", value: "Zezqzo"), Step("dn23", "screenshot", value: "hud-dossier-long-name-ellipsis.png"), Step("dn24", "wait", wait: 0.4f), Step("dn90", "assert", expect: "no_bad"), Step("dn99", "snapshot"), }; } private static List SettingsFull() { return new List { Step("sf0", "dismiss_windows"), Step("sf1", "wait_world"), Step("sf2", "set_setting", expect: "enabled", value: "true"), Step("sf3", "pick_unit", asset: "auto"), Step("sf10", "set_setting", expect: "enabled", value: "false"), Step("sf11", "assert", expect: "enabled", value: "false"), Step("sf12", "spectator", value: "on", expect: "blocked"), Step("sf13", "assert", expect: "idle", value: "false"), Step("sf20", "set_setting", expect: "enabled", value: "true"), Step("sf21", "spectator", value: "on"), Step("sf22", "focus", asset: "auto"), Step("sf23", "assert", expect: "idle", value: "true"), // Disable while active must turn idle off (settings callback). Step("sf30", "set_setting", expect: "enabled", value: "false"), Step("sf31", "assert", expect: "idle", value: "false"), Step("sf32", "assert", expect: "enabled", value: "false"), Step("sf40", "set_setting", expect: "enabled", value: "true"), Step("sf41", "set_setting", expect: "show_watch_reasons", value: "false"), Step("sf42", "assert", expect: "show_watch_reasons", value: "false"), Step("sf43", "set_setting", expect: "show_watch_reasons", value: "true"), Step("sf44", "assert", expect: "show_watch_reasons", value: "true"), Step("sf99", "snapshot"), }; } private static List GhostGuard() { return new List { Step("g0", "dismiss_windows"), Step("g1", "wait_world"), Step("g2", "set_setting", expect: "enabled", value: "true"), Step("g3", "pick_unit", asset: "auto"), Step("g4", "spectator", value: "on"), Step("g5", "focus", asset: "auto"), Step("g6", "reset_counters"), Step("g7", "remember_focus"), Step("g8", "watch_ghost"), Step("g9", "wait", wait: 0.25f), Step("g10", "assert", expect: "focus_same"), Step("g11", "assert", expect: "health"), Step("g12", "assert", expect: "no_bad"), Step("g13", "assert", expect: "power_bar", value: "false"), Step("g99", "snapshot"), }; } private static List RetargetStability() { return new List { Step("rs0", "dismiss_windows"), Step("rs1", "wait_world"), Step("rs2", "set_setting", expect: "enabled", value: "true"), Step("rs3", "pick_unit", asset: "auto"), Step("rs4", "spectator", value: "on"), Step("rs5", "focus", asset: "auto"), Step("rs6", "reset_counters"), Step("rs10", "watch", asset: "auto", label: "Stab 1: {asset}", tier: "Curiosity"), Step("rs11", "wait", wait: 0.2f), Step("rs12", "watch", asset: "auto", label: "Stab 2: {asset}", tier: "Curiosity"), Step("rs13", "wait", wait: 0.2f), Step("rs14", "watch", asset: "auto", label: "Stab 3: {asset}", tier: "Curiosity"), Step("rs15", "wait", wait: 0.2f), Step("rs16", "watch", asset: "auto", label: "Stab 4: {asset}", tier: "Curiosity"), Step("rs17", "wait", wait: 0.25f), Step("rs20", "assert", expect: "health"), Step("rs21", "assert", expect: "no_bad"), Step("rs22", "assert", expect: "power_bar", value: "false"), Step("rs23", "assert", expect: "unit_asset", asset: "auto"), Step("rs99", "snapshot"), }; } private static List InputExit() { return new List { Step("ie0", "dismiss_windows"), Step("ie1", "wait_world"), Step("ie2", "set_setting", expect: "enabled", value: "true"), Step("ie3", "fast_timing", value: "true"), Step("ie4", "pick_unit", asset: "auto"), Step("ie5", "spectator", value: "off"), Step("ie6", "spectator", value: "on"), Step("ie7", "focus", asset: "auto"), // Still inside post-enable grace: drag must not exit. Step("ie10", "assert", expect: "in_grace", value: "true"), Step("ie11", "simulate_input", value: "drag", expect: "grace"), Step("ie12", "assert", expect: "idle", value: "true"), Step("ie20", "expire_grace"), Step("ie21", "assert", expect: "in_grace", value: "false"), Step("ie22", "simulate_input", value: "drag"), Step("ie23", "assert", expect: "idle", value: "false"), Step("ie24", "assert", expect: "no_bad"), // Outside dismiss closes Lore + dossier and keeps idle off (no auto-resume). Step("ie30", "spectator", value: "on"), Step("ie31", "focus", asset: "auto"), Step("ie32", "lore_open_focus"), Step("ie33", "wait", wait: 0.5f), Step("ie34", "assert", expect: "idle", value: "false"), Step("ie35", "inspect_dismiss"), Step("ie36", "assert", expect: "inspect_cleared"), Step("ie37", "assert", expect: "idle", value: "false"), Step("ie90", "fast_timing", value: "false"), Step("ie99", "snapshot"), }; } private static List DirectorTiers() { return new List { Step("dt0", "dismiss_windows"), Step("dt1", "wait_world"), Step("dt2", "set_setting", expect: "enabled", value: "true"), Step("dt3", "fast_timing", value: "true"), // Calm durable unit + spectator restart clears collector so world tips cannot steal CurioA. Step("dt4", "spawn", asset: "dragon"), Step("dt5", "spectator", value: "off"), Step("dt6", "spectator", value: "on"), Step("dt7", "focus", asset: "dragon"), Step("dt8", "reset_counters"), // Curiosity hold. Step("dt10", "interest_force_session", asset: "dragon", label: "CurioA", tier: "Curiosity", expect: "curio_a"), Step("dt11", "assert", expect: "tip_contains", value: "CurioA"), // Action margin-cuts curiosity (instant; no settle required). Step("dt20", "trigger_interest", asset: "dragon", label: "ActionA", tier: "Action"), Step("dt21", "director_run", wait: 0.8f), Step("dt22", "assert", expect: "tip_contains", value: "ActionA"), // Curiosity must not steal the camera while Action holds (no director_run). Step("dt30", "trigger_interest", asset: "dragon", label: "CurioBlocked", tier: "Curiosity"), Step("dt31", "assert", expect: "tip_contains", value: "ActionA"), Step("dt32", "assert", expect: "would_accept_curiosity", value: "false"), Step("dt33", "reset_counters"), Step("dt34", "assert", expect: "health"), Step("dt35", "assert", expect: "no_bad"), Step("dt90", "fast_timing", value: "false"), Step("dt99", "snapshot"), }; } private static List DirectorLifecycle() { return new List { Step("dl0", "dismiss_windows"), Step("dl1", "wait_world"), Step("dl2", "set_setting", expect: "enabled", value: "true"), Step("dl3", "fast_timing", value: "true"), Step("dl4", "spawn", asset: "dragon"), Step("dl5", "spectator", value: "off"), Step("dl6", "spectator", value: "on"), Step("dl7", "focus", asset: "dragon"), // Protected Action: peer Story (+0 margin) waits. Step("dl10", "interest_force_session", asset: "dragon", label: "HoldAction", tier: "Action", expect: "hold_action"), Step("dl11", "assert", expect: "tip_contains", value: "HoldAction"), Step("dl12", "assert", expect: "session_active", value: "true"), Step("dl13", "trigger_interest", asset: "dragon", label: "StoryWait", tier: "Story"), Step("dl14", "director_run", wait: 0.8f), Step("dl15", "assert", expect: "tip_contains", value: "HoldAction"), Step("dl16", "assert", expect: "tip_contains", value: "HoldAction"), // Epic margin-cuts Action instantly (no settle). Step("dl20", "trigger_interest", asset: "dragon", label: "EpicWin", tier: "Epic"), Step("dl21", "director_run", wait: 0.8f), Step("dl22", "assert", expect: "tip_contains", value: "EpicWin"), Step("dl23", "assert", expect: "tip_contains", value: "EpicWin"), Step("dl24", "reset_counters"), Step("dl25", "assert", expect: "no_bad"), Step("dl90", "fast_timing", value: "false"), Step("dl99", "snapshot"), }; } private static List InterestHappiness() { return new List { Step("ih0", "dismiss_windows"), Step("ih1", "wait_world"), Step("ih2", "set_setting", expect: "enabled", value: "true"), Step("ih3", "fast_timing", value: "true"), Step("ih4", "spawn", asset: "human"), Step("ih5", "spectator", value: "off"), Step("ih6", "spectator", value: "on"), Step("ih7", "focus", asset: "auto"), Step("ih8", "happiness_reset"), // Force Action hold; ambient happiness must not steal. Step("ih10", "interest_force_session", asset: "auto", label: "ActionHold", tier: "Action", expect: "action_hold"), Step("ih11", "happiness_apply", value: "just_ate"), Step("ih12", "age_current", wait: 1.2f), Step("ih13", "director_run", wait: 1.2f), Step("ih14", "assert", expect: "tip_contains", value: "ActionHold"), Step("ih15", "assert", expect: "tip_contains", value: "ActionHold"), // Grief Signal registers into registry (force-note + direct register). Step("ih20", "happiness_force_note", value: "death_child", label: "Ava"), Step("ih21", "assert", expect: "interest_has_key", value: "death_child"), Step("ih22", "assert", expect: "tip_contains", value: "ActionHold"), Step("ih23", "assert", expect: "happiness_drain_seq"), // Epic can leave Action; grief may then surface. Step("ih30", "trigger_interest", asset: "auto", label: "EpicClear", tier: "Epic"), Step("ih31", "director_run", wait: 0.8f), Step("ih32", "assert", expect: "tip_contains", value: "EpicClear"), Step("ih33", "assert", expect: "no_bad"), Step("ih90", "fast_timing", value: "false"), Step("ih99", "snapshot"), }; } /// /// Full director gap matrix: resume, variety, lifecycle edges, scoring, happiness filters. /// private static List DirectorGaps() { return new List { Step("dg0", "dismiss_windows"), Step("dg1", "wait_world"), Step("dg2", "set_setting", expect: "enabled", value: "true"), Step("dg3", "fast_timing", value: "true"), Step("dg4", "spawn", asset: "dragon", count: 1), Step("dg5", "spectator", value: "off"), Step("dg6", "spectator", value: "on"), Step("dg7", "focus", asset: "dragon"), Step("dg8", "interest_variety_clear"), // --- Epic margin-cuts Story; Action does not (below cutInMargin) --- // Hold as EventLed so CharacterLed char-weight cannot outscore Epic. Step("dg10", "interest_force_session", asset: "dragon", label: "HoldStory", tier: "Story", expect: "hold_story", value: "lead=event;evt=80"), Step("dg11", "assert", expect: "tip_contains", value: "HoldStory"), Step("dg12", "trigger_interest", asset: "dragon", label: "ActionBlocked", tier: "Action"), Step("dg13", "director_run", wait: 0.8f), Step("dg14", "assert", expect: "tip_contains", value: "HoldStory"), Step("dg15", "assert", expect: "tip_contains", value: "HoldStory"), Step("dg17", "trigger_interest", asset: "dragon", label: "EpicOverStory", tier: "Epic"), Step("dg18", "director_run", wait: 0.8f), Step("dg19", "assert", expect: "tip_contains", value: "EpicOverStory"), Step("dg20", "assert", expect: "tip_contains", value: "EpicOverStory"), // --- Resume after Epic: Action hold interrupted then restored --- Step("dg30", "spectator", value: "off"), Step("dg31", "spectator", value: "on"), Step("dg32", "focus", asset: "dragon"), Step("dg33", "interest_force_session", asset: "dragon", label: "HoldResume", tier: "Action", expect: "hold_resume", value: "lead=event;evt=70"), Step("dg34", "assert", expect: "session_key", value: "hold_resume"), Step("dg35", "trigger_interest", asset: "dragon", label: "EpicInterrupt", tier: "Epic"), Step("dg36", "director_run", wait: 0.8f), Step("dg37", "assert", expect: "tip_contains", value: "EpicInterrupt"), Step("dg38", "assert", expect: "interrupted_key", value: "hold_resume"), // Max-cap the Epic scene (fast timing caps ~8s) so EndCurrent resumes Action. Step("dg40", "age_current", wait: 9f), Step("dg41", "director_run", wait: 1.2f), Step("dg42", "assert", expect: "tip_contains", value: "HoldResume"), Step("dg43", "assert", expect: "tip_contains", value: "HoldResume"), Step("dg44", "assert", expect: "interrupted_key", value: "none"), // --- Max cap ends a forced scene --- Step("dg50", "interest_force_session", asset: "dragon", label: "CapMe", tier: "Action", expect: "cap_me"), Step("dg51", "age_current", wait: 9f), Step("dg52", "director_run", wait: 1.2f), Step("dg54", "assert", expect: "interest_no_key", value: "cap_me"), // --- Quiet grace: inactive scene ends after grace --- Step("dg60", "interest_force_session", asset: "dragon", label: "QuietEnd", tier: "Action", expect: "quiet_end"), Step("dg61", "interest_mark_inactive", value: "1.0"), Step("dg62", "assert", expect: "session_active", value: "false"), Step("dg63", "director_run", wait: 1.2f), Step("dg64", "assert", expect: "interest_no_key", value: "quiet_end"), // --- Stale expiry / dedupe refresh --- Step("dg70", "interest_inject", asset: "dragon", label: "StaleTip", tier: "Curiosity", expect: "stale_tip", value: "lead=character;ttl=0.01"), Step("dg71", "assert", expect: "interest_has_key", value: "stale_tip"), Step("dg72", "interest_expire_pending", value: "stale_tip"), Step("dg73", "assert", expect: "interest_no_key", value: "stale_tip"), Step("dg74", "interest_inject", asset: "dragon", label: "RefreshA", tier: "Action", expect: "refresh_a", value: "lead=event;evt=20"), Step("dg75", "interest_inject", asset: "dragon", label: "RefreshA", tier: "Action", expect: "refresh_a", value: "lead=event;evt=90"), Step("dg76", "assert", expect: "interest_registry_count", value: "1", label: "refresh_a"), // --- Death handoff: FollowUnit cleared → RelatedUnit --- Step("dg80", "interest_force_session", asset: "dragon", label: "HandoffHold", tier: "Action", expect: "handoff_hold"), Step("dg81", "interest_clear_follow", asset: "dragon"), Step("dg82", "director_run", wait: 0.8f), Step("dg83", "assert", expect: "session_key", value: "handoff_hold"), Step("dg84", "assert", expect: "session_active", value: "true"), // --- Soft 70/30: character-heavy mix prefers event when both pools exist --- Step("dg90", "interest_end_session"), Step("dg91", "interest_variety_clear"), Step("dg91b", "interest_expire_pending", value: ""), Step("dg92", "interest_variety_seed", value: "2:10"), Step("dg93", "assert", expect: "interest_event_share", value: "0.17", label: "0.2"), Step("dg94", "interest_inject", asset: "dragon", label: "EvtWin", tier: "Curiosity", expect: "evt_win", value: "lead=event;evt=80"), Step("dg95", "interest_inject", asset: "dragon", label: "CharLose", tier: "Curiosity", expect: "char_lose", value: "lead=character;char=95"), Step("dg96", "assert", expect: "interest_peek_lead", value: "EventLed"), // Empty event pool → character-led, no invented events Step("dg97", "interest_expire_pending", value: "evt_win"), Step("dg98", "assert", expect: "interest_peek_lead", value: "CharacterLed"), // --- Event strength beats idle celebrity (within event pool / score order) --- Step("dg100", "interest_expire_pending", value: ""), Step("dg101", "interest_inject", asset: "dragon", label: "StrongEvt", tier: "Action", expect: "strong_evt", value: "lead=event;evt=95;char=5"), Step("dg102", "interest_inject", asset: "dragon", label: "CelebWeak", tier: "Action", expect: "celeb_weak", value: "lead=event;evt=10;char=99"), Step("dg103", "assert", expect: "interest_score_order", value: "strong_evt", label: "celeb_weak"), // --- Ambient happiness never owns camera; psychopath suppress; drain duplex; aggregate --- Step("dg109", "spawn", asset: "human"), Step("dg109b", "focus", asset: "human"), Step("dg110", "interest_force_session", asset: "human", label: "ActionKeep", tier: "Action", expect: "action_keep"), Step("dg111", "happiness_apply", value: "just_ate"), Step("dg112", "interest_feeds_tick"), Step("dg113", "age_current", wait: 1.2f), Step("dg114", "director_run", wait: 1.2f), Step("dg115", "assert", expect: "tip_contains", value: "ActionKeep"), Step("dg116", "assert", expect: "tip_contains", value: "ActionKeep"), Step("dg117", "assert", expect: "interest_no_key", value: "just_ate"), Step("dg120", "happiness_suppress", value: "death_child"), Step("dg121", "interest_feeds_tick"), Step("dg122", "assert", expect: "interest_no_key", value: "death_child"), Step("dg130", "happiness_reset"), Step("dg131", "happiness_remember_only", value: "death_lover", label: "Ava"), Step("dg132", "interest_feeds_tick"), Step("dg133", "assert", expect: "interest_has_key", value: "death_lover"), Step("dg134", "assert", expect: "interest_registry_count", value: "1", label: "death_lover"), Step("dg135", "interest_feeds_tick"), Step("dg136", "assert", expect: "interest_registry_count", value: "1", label: "death_lover"), Step("dg140", "happiness_burst", value: "just_started_war", count: 32), Step("dg141", "interest_feeds_tick"), Step("dg142", "assert", expect: "interest_no_key", value: "just_started_war"), Step("dg143", "interest_civic_boost", value: "just_started_war", label: "harness", expect: "40"), Step("dg144", "assert", expect: "civic_boost", value: "just_started_war"), Step("dg190", "assert", expect: "no_bad"), Step("dg191", "fast_timing", value: "false"), Step("dg199", "snapshot"), }; } private static List FocusInterrupt() { return new List { Step("fi0", "dismiss_windows"), Step("fi1", "wait_world"), Step("fi2", "set_setting", expect: "enabled", value: "true"), Step("fi3", "fast_timing", value: "true"), Step("fi4", "spawn", asset: "sheep"), Step("fi5", "spectator", value: "off"), Step("fi6", "spectator", value: "on"), Step("fi7", "focus", asset: "sheep"), // Hold Action; peer Story does not cut (below cutInMargin). Step("fi10", "interest_force_session", asset: "sheep", label: "HoldA", tier: "Action", expect: "hold_a"), Step("fi11", "trigger_interest", asset: "sheep", label: "PeerStory", tier: "Story"), Step("fi12", "director_run", wait: 0.6f), Step("fi13", "assert", expect: "tip_contains", value: "HoldA"), // Epic margin-cuts instantly (no age/settle). Step("fi20", "trigger_interest", asset: "sheep", label: "CutEpic", tier: "Epic"), Step("fi21", "director_run", wait: 0.6f), Step("fi22", "assert", expect: "tip_contains", value: "CutEpic"), // Quiet grace then ambient empty reason path. Step("fi30", "interest_force_session", asset: "sheep", label: "GraceEnd", tier: "Action", expect: "grace_end"), Step("fi31", "interest_mark_inactive", value: "1.0"), Step("fi32", "assert", expect: "session_active", value: "false"), Step("fi33", "director_run", wait: 1.2f), Step("fi34", "assert", expect: "interest_no_key", value: "grace_end"), Step("fi35", "assert", expect: "no_bad"), Step("fi90", "fast_timing", value: "false"), Step("fi99", "snapshot"), }; } /// /// Selection = presentation: unpresentable Labels never register A; /// presentable EventReason does; focus survives max_cap; no placeholder tip. /// private static List CameraPresentability() { return new List { Step("cp0", "dismiss_windows"), Step("cp1", "wait_world"), Step("cp2", "set_setting", expect: "enabled", value: "true"), Step("cp3", "fast_timing", value: "true"), Step("cp4", "pick_unit", asset: "auto"), Step("cp5", "spectator", value: "off"), Step("cp6", "spectator", value: "on"), Step("cp7", "focus", asset: "auto"), Step("cp8", "reset_counters"), // Article-led fall shape must not enter the registry. Step("cp10", "presentability_probe", value: "fall"), Step("cp11", "assert", expect: "drop_contains", value: "unpresentable"), Step("cp12", "assert", expect: "interest_no_key", value: "probe:presentability"), // Subject-led fight Label may register. Step("cp20", "presentability_probe", value: "good"), Step("cp21", "assert", expect: "interest_has_key", value: "probe:presentability"), // Clear probe pending so force-session owns the tip (regression worlds are noisy). Step("cp22", "interest_expire_pending", value: ""), Step("cp23", "interest_end_session"), // Presentable tip takes Watching; never placeholder. Step("cp30", "interest_force_session", asset: "auto", label: "{a} is fighting", tier: "Action", expect: "cp_fight"), Step("cp30b", "assert", expect: "tip_contains", value: "is fighting"), Step("cp31", "director_run", wait: 0.6f), Step("cp32", "assert", expect: "tip_contains", value: "is fighting"), Step("cp33", "assert", expect: "no_placeholder_tip"), Step("cp34", "assert", expect: "has_focus", value: "true"), // Mid-scene camera clear must restore focus within one director tick (Phase D). Step("cp35", "reset_counters"), Step("cp36", "camera_clear_focus"), Step("cp37", "director_run", wait: 0.25f), Step("cp38", "assert", expect: "has_focus", value: "true"), // Intentional clear trips one focus-dropped BAD; after repair there must be no gap spam. Step("cp38b", "reset_counters"), Step("cp39", "director_run", wait: 0.4f), Step("cp39b", "assert", expect: "no_bad"), // Max-cap end must keep a living focus unit. Step("cp40", "age_current", wait: 9f), Step("cp41", "director_run", wait: 1.2f), Step("cp42", "assert", expect: "has_focus", value: "true"), Step("cp43", "reset_counters"), Step("cp43b", "director_run", wait: 0.4f), Step("cp43c", "assert", expect: "no_bad"), Step("cp44", "assert", expect: "no_placeholder_tip"), // Clear again with no current scene after max_cap. Step("cp50", "interest_end_session"), Step("cp51", "camera_clear_focus"), Step("cp52", "director_run", wait: 0.25f), Step("cp53", "assert", expect: "has_focus", value: "true"), Step("cp53b", "reset_counters"), Step("cp54", "director_run", wait: 0.4f), Step("cp54b", "assert", expect: "no_bad"), Step("cp90", "fast_timing", value: "false"), Step("cp99", "snapshot"), Nested("cp_status_overlap", "status_overlap_camera"), Nested("cp_combat_focus", "combat_focus"), }; } /// /// Combat tip subject must match focus; death handoff rewrites tip; cold combat clears fight tip. /// private static List CombatFocusAlign() { return new List { Step("cf0", "dismiss_windows"), Step("cf1", "wait_world"), Step("cf2", "set_setting", expect: "enabled", value: "true"), Step("cf3", "fast_timing", value: "true"), Step("cf4", "spawn", asset: "human", count: 1), Step("cf5", "spawn", asset: "wolf", count: 1), Step("cf6", "spectator", value: "off"), Step("cf7", "spectator", value: "on"), Step("cf8", "pick_unit", asset: "human"), Step("cf9", "focus", asset: "human"), Step("cf10", "interest_end_session"), // Combat scene: human fighting wolf. Clear follow → related (wolf) must own tip+focus. // expect suffix no_attack: ownership assert must not race a fast-timing kill mid-wait. Step("cf20", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_fight_no_attack"), // Drop ambient packs left by prior escalate / --repeat so tip_same stays a true 1v1. Step("cf20b", "combat_isolate_pair", value: "20"), Step("cf21", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "), // Pair ownership: maintain must not flip Duel - A vs B ↔ B vs A. Step("cf21a", "remember_tip"), Step("cf21b", "combat_maintain_focus"), Step("cf21d", "combat_maintain_focus"), Step("cf21e", "assert", expect: "tip_same"), Step("cf21f", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "), Step("cf21w", "combat_wire_attack_sides", asset: "human", value: "wolf", expect: "pair"), // Attack-target swap must keep locked pair partner (tip may escalate 1v2→Skirmish). Step("cf21p", "remember_related"), Step("cf21q", "combat_swap_attack_target", asset: "wolf"), Step("cf21r", "wait", value: "0.35"), Step("cf21s", "director_run", wait: 0.6f), Step("cf21t", "combat_maintain_focus"), Step("cf21u", "combat_maintain_focus"), Step("cf21v", "assert", expect: "related_same"), Step("cf21x", "assert", expect: "tip_matches_any", value: "Duel|Skirmish|Battle|Mass|fighting| vs "), Step("cf22", "interest_clear_follow", asset: "wolf"), Step("cf23", "combat_maintain_focus"), Step("cf24", "assert", expect: "has_focus", value: "true"), Step("cf25", "assert", expect: "tip_matches_focus"), Step("cf25b", "assert", expect: "dossier_matches_focus"), Step("cf26", "assert", expect: "unit_asset", asset: "wolf"), // 1v1 → multi: sided species framing + live sticky counts. // Re-seed human/wolf so a polluted world cannot escalate an angle camp. Step("cf39", "interest_end_session"), Step("cf39b", "spawn", asset: "human", count: 1), Step("cf39c", "spawn", asset: "wolf", count: 1), Step("cf39d", "pick_unit", asset: "human"), Step("cf40", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_ensemble"), Step("cf41", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "), Step("cf42", "combat_ensemble_escalate", value: "6"), Step("cf43", "assert", expect: "participant_count_min", value: "6"), Step("cf42b", "combat_wire_attack_sides", asset: "human", value: "wolf"), Step("cf44", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -| vs "), Step("cf45", "assert", expect: "tip_matches_focus"), Step("cf46", "assert", expect: "dossier_matches_focus"), Step("cf47", "assert", expect: "has_focus", value: "true"), // Theater lead: settle once, then maintain must not thrash Follow across the mob. Step("cf47a", "combat_maintain_focus"), Step("cf47b", "remember_focus"), Step("cf47c", "combat_maintain_focus"), Step("cf47d", "wait", value: "0.35"), Step("cf47e", "combat_maintain_focus"), Step("cf47f", "wait", value: "0.35"), Step("cf47g", "combat_maintain_focus"), Step("cf47h", "assert", expect: "focus_same"), // Spectacle theater lead: evil mage vs human mob holds the mage, not a random human. Step("cf48", "interest_end_session"), Step("cf48b", "spawn", asset: "evil_mage", count: 1), Step("cf48c", "spawn", asset: "human", count: 1), Step("cf48d", "pick_unit", asset: "evil_mage"), Step("cf48e", "interest_combat_session", asset: "evil_mage", value: "human", expect: "cf_mage"), Step("cf48f", "combat_ensemble_escalate", value: "8"), Step("cf48g", "combat_wire_attack_sides", asset: "evil_mage", value: "human"), Step("cf48h", "wait", value: "0.35"), Step("cf48i", "combat_maintain_focus"), Step("cf48j", "assert", expect: "unit_asset", asset: "evil_mage"), Step("cf48k", "remember_focus"), Step("cf48l", "combat_maintain_focus"), Step("cf48m", "wait", value: "0.35"), Step("cf48n", "combat_maintain_focus"), Step("cf48o", "wait", value: "0.35"), Step("cf48p", "combat_maintain_focus"), // Re-wire / refresh camps mid-hold - tip may reframe; focus must stay on the mage. Step("cf48p2", "combat_wire_attack_sides", asset: "evil_mage", value: "human"), Step("cf48p3", "combat_maintain_focus"), Step("cf48p4", "wait", value: "0.35"), Step("cf48p5", "combat_maintain_focus"), Step("cf48q", "assert", expect: "focus_same"), Step("cf48r", "assert", expect: "unit_asset", asset: "evil_mage"), Step("cf48s", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), // Natural Duel → multi: seed packs without tip rewrite; maintain must escalate. Step("cf50", "interest_end_session"), Step("cf50b", "spawn", asset: "human", count: 1), Step("cf50c", "spawn", asset: "wolf", count: 1), Step("cf50d", "pick_unit", asset: "human"), Step("cf51", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_natural"), Step("cf51b", "combat_isolate_pair", value: "20"), Step("cf52", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "), Step("cf53", "combat_ensemble_escalate", value: "6", expect: "spawn_only"), Step("cf54", "combat_wire_attack_sides", asset: "human", value: "wolf"), Step("cf55", "wait", value: "0.4"), Step("cf56", "combat_maintain_focus"), Step("cf57", "wait", value: "0.4"), Step("cf58", "combat_maintain_focus"), Step("cf59", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), Step("cf59b", "assert", expect: "tip_not_contains", value: "Duel"), Step("cf59c", "assert", expect: "tip_matches_any", value: " vs "), // Cold combat must not keep fight / ensemble tips. Step("cf30", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_cold"), Step("cf31", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "), Step("cf32", "interest_release_force"), Step("cf33", "interest_mark_inactive", value: "1"), Step("cf34", "combat_maintain_focus"), Step("cf35", "assert", expect: "tip_not_contains", value: " vs "), Step("cf35b", "assert", expect: "tip_not_contains", value: "Duel"), Step("cf35c", "assert", expect: "tip_not_contains", value: "fighting"), Step("cf90", "fast_timing", value: "false"), Step("cf99", "snapshot"), }; } /// /// Live pack-fight proof: scale hold vs Duel flap, collective species tip, sleeper cannot steal focus. /// private static List CombatStabilityLive() { return new List { Step("csl0", "dismiss_windows"), Step("csl1", "wait_world"), Step("csl2", "set_setting", expect: "enabled", value: "true"), Step("csl3", "fast_timing", value: "true"), Step("csl4", "spawn", asset: "human", count: 1), Step("csl5", "spawn", asset: "wolf", count: 1), Step("csl6", "spectator", value: "off"), Step("csl7", "spectator", value: "on"), Step("csl8", "pick_unit", asset: "human"), Step("csl9", "focus", asset: "human"), Step("csl10", "interest_end_session"), // Peak at 8 (Mass band). Sticky sides lock now; live counts start at 0 until wired. Step("csl20", "interest_combat_session", asset: "human", value: "wolf", expect: "csl_pack"), Step("csl21", "combat_ensemble_escalate", value: "8"), Step("csl22", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), Step("csl22b", "assert", expect: "tip_matches_any", value: "Humans (|Wolves ("), Step("csl24", "combat_wire_attack_sides", asset: "human", value: "wolf", expect: "pair"), // Live rebuild: sticky Humans vs Wolves, live counts (may be 1/1 after pair wire). Step("csl30", "combat_maintain_focus"), Step("csl31", "wait", value: "0.4"), Step("csl32", "combat_maintain_focus"), Step("csl33", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), Step("csl34", "assert", expect: "tip_not_contains", value: "Duel"), Step("csl35", "assert", expect: "tip_not_contains", value: "melee"), Step("csl36", "assert", expect: "tip_matches_any", value: "Humans (|Wolves ("), Step("csl37", "assert", expect: "tip_matches_any", value: " vs "), // Wire both camps → counts climb back up on the same sticky frame. Step("csl39", "combat_maintain_focus"), Step("csl40", "combat_wire_attack_sides", asset: "human", value: "wolf"), Step("csl41", "wait", value: "0.5"), Step("csl42", "combat_maintain_focus"), Step("csl42b", "assert", expect: "participant_count_min", value: "6"), Step("csl43", "assert", expect: "tip_matches_any", value: "Humans (|Wolves ("), Step("csl44", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), Step("csl45", "assert", expect: "tip_not_contains", value: "Duel"), Step("csl45b", "assert", expect: "tip_not_contains", value: "melee"), Step("csl46", "assert", expect: "unit_asset", value: "human|wolf"), Step("csl47", "assert", expect: "dossier_matches_focus"), // Theater lead hold across maintain ticks (no mob Follow thrash). Step("csl47a", "remember_focus"), Step("csl47b", "combat_maintain_focus"), Step("csl47c", "wait", value: "0.35"), Step("csl47d", "combat_maintain_focus"), Step("csl47e", "wait", value: "0.35"), Step("csl47f", "combat_maintain_focus"), Step("csl47g", "assert", expect: "focus_same"), // Chore bystander: park Follow on a sleeping rostered unit; maintain must retarget. Step("csl48", "combat_park_bystander_follow", asset: "human", value: "wolf"), Step("csl48w", "wait", value: "0.2"), Step("csl48a", "assert", expect: "focus_fighting", value: "false"), Step("csl48b", "combat_maintain_focus"), Step("csl48c", "wait", value: "0.35"), Step("csl48d", "combat_maintain_focus"), Step("csl48e", "assert", expect: "focus_fighting", value: "true"), Step("csl48f", "assert", expect: "unit_asset", value: "human|wolf"), Step("csl48g", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -|fighting| vs "), // Sleeper bystander must not stick the camera. Step("csl50", "spawn", asset: "bear", count: 1), Step("csl51", "focus", asset: "bear"), Step("csl52", "status_apply", value: "sleeping", label: "8"), Step("csl53", "wait", value: "0.35"), Step("csl54", "combat_maintain_focus"), Step("csl55", "wait", value: "0.35"), Step("csl56", "combat_maintain_focus"), Step("csl57", "assert", expect: "unit_asset", value: "human|wolf"), Step("csl58", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -|fighting| vs "), Step("csl59", "assert", expect: "tip_not_contains", value: "Duel -"), Step("csl59b", "assert", expect: "tip_not_contains", value: "melee"), Step("csl60", "assert", expect: "dossier_matches_focus"), Step("csl61", "assert", expect: "has_focus", value: "true"), Step("csl90", "fast_timing", value: "false"), Step("csl98", "screenshot", value: "hud-combat-stability-live.png"), Step("csl99", "snapshot"), }; } /// /// After sticky combat matures, a Story/lover peer may cut in via the variety valve. /// private static List StickyVarietyValve() { return new List { Step("sv0", "dismiss_windows"), Step("sv1", "wait_world"), Step("sv2", "set_setting", expect: "enabled", value: "true"), Step("sv3", "fast_timing", value: "true"), Step("sv4", "spawn", asset: "human", count: 1), Step("sv5", "spawn", asset: "wolf", count: 1), Step("sv6", "spectator", value: "off"), Step("sv7", "spectator", value: "on"), Step("sv8", "pick_unit", asset: "human"), Step("sv9", "focus", asset: "human"), Step("sv10", "interest_end_session"), Step("sv20", "interest_combat_session", asset: "human", value: "wolf", expect: "sv_pack"), Step("sv21", "combat_ensemble_escalate", value: "8"), Step("sv22", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"), Step("sv22b", "combat_wire_attack_sides", asset: "human", value: "wolf"), Step("sv22c", "interest_expire_pending", value: ""), Step("sv23", "age_current", wait: 21f), Step("sv24", "interest_inject", asset: "human", label: "falls in love", tier: "Action", expect: "valve_lover", value: "lead=event;evt=78;char=10;force=false;ttl=60"), Step("sv24b", "assert", expect: "pending_contains", value: "valve_lover"), Step("sv25", "director_run", wait: 1.2f), Step("sv26", "assert", expect: "tip_matches_any", value: "love|lover|falls in|in scene"), Step("sv27", "assert", expect: "tip_not_contains", value: "Mass -"), Step("sv28", "assert", expect: "tip_asset", value: "set_lover"), Step("sv90", "fast_timing", value: "false"), Step("sv99", "snapshot"), }; } /// /// Sticky WarFront tips stay kingdom-framed across count dips and follow loss. /// private static List WarFrontSticky() { return new List { Step("wfs0", "dismiss_windows"), Step("wfs1", "wait_world"), Step("wfs2", "set_setting", expect: "enabled", value: "true"), Step("wfs3", "fast_timing", value: "true"), Step("wfs4", "spawn", asset: "human", count: 2), Step("wfs5", "spectator", value: "off"), Step("wfs6", "spectator", value: "on"), Step("wfs7", "pick_unit", asset: "human"), Step("wfs8", "focus", asset: "human"), Step("wfs9", "interest_end_session"), Step("wfs20", "interest_war_session", asset: "human", expect: "wfs_war"), Step("wfs21", "assert", expect: "tip_matches_any", value: "War -"), Step("wfs22", "assert", expect: "tip_matches_any", value: " vs "), Step("wfs23", "assert", expect: "tip_not_contains", value: "fighting"), Step("wfs24", "assert", expect: "tip_not_contains", value: "Duel"), Step("wfs30", "war_ensemble_apply", value: "2:0"), Step("wfs31", "war_maintain_focus"), Step("wfs32", "assert", expect: "tip_matches_any", value: "War -"), Step("wfs33", "assert", expect: "tip_matches_any", value: " vs "), Step("wfs34", "assert", expect: "tip_not_contains", value: "fighting"), Step("wfs40", "interest_clear_follow"), Step("wfs41", "war_maintain_focus"), Step("wfs42", "assert", expect: "has_focus", value: "true"), Step("wfs43", "assert", expect: "tip_matches_any", value: "War -| vs "), Step("wfs44", "assert", expect: "tip_not_contains", value: " is fighting"), Step("wfs50", "war_ensemble_apply", value: "15:11"), Step("wfs51", "war_maintain_focus"), Step("wfs52", "assert", expect: "tip_matches_any", value: "War -"), Step("wfs53", "assert", expect: "dossier_matches_focus"), Step("wfs90", "fast_timing", value: "false"), Step("wfs98", "screenshot", value: "hud-war-front-sticky.png"), Step("wfs99", "snapshot"), }; } /// /// Sticky PlotCell tips stay plotter-vs-kingdom framed across count dips and follow loss. /// private static List PlotCellSticky() { return new List { Step("pcs0", "dismiss_windows"), Step("pcs1", "wait_world"), Step("pcs2", "set_setting", expect: "enabled", value: "true"), Step("pcs3", "fast_timing", value: "true"), Step("pcs4", "spawn", asset: "human", count: 2), Step("pcs5", "spectator", value: "off"), Step("pcs6", "spectator", value: "on"), Step("pcs7", "pick_unit", asset: "human"), Step("pcs8", "focus", asset: "human"), Step("pcs9", "interest_end_session"), Step("pcs20", "interest_plot_session", asset: "human", expect: "pcs_plot"), Step("pcs21", "assert", expect: "tip_matches_any", value: "Plot -"), Step("pcs22", "assert", expect: "tip_matches_any", value: "Plotters"), Step("pcs23", "assert", expect: "tip_matches_any", value: " vs "), Step("pcs24", "assert", expect: "tip_not_contains", value: "fighting"), Step("pcs25", "assert", expect: "tip_not_contains", value: " is plotting"), Step("pcs26", "assert", expect: "tip_not_contains", value: "Plotters (1)"), // Solo plotter is not a sticky group - tip must clear. Step("pcs30", "plot_ensemble_apply", value: "1:0"), Step("pcs31", "plot_maintain_focus"), Step("pcs32", "assert", expect: "tip_not_contains", value: "Plot -"), Step("pcs33", "assert", expect: "tip_not_contains", value: "Plotters (1)"), // Restore a real cell, then promote-follow still keeps the framed tip. Step("pcs35", "plot_ensemble_apply", value: "3:12"), Step("pcs36", "plot_maintain_focus"), Step("pcs37", "assert", expect: "tip_matches_any", value: "Plot -"), Step("pcs38", "assert", expect: "tip_matches_any", value: "Plotters (3)"), Step("pcs40", "interest_clear_follow"), Step("pcs41", "plot_maintain_focus"), Step("pcs42", "assert", expect: "has_focus", value: "true"), Step("pcs43", "assert", expect: "tip_matches_any", value: "Plot -|Plotters| vs "), Step("pcs44", "assert", expect: "tip_not_contains", value: " is plotting"), Step("pcs50", "plot_ensemble_apply", value: "4:18"), Step("pcs51", "plot_maintain_focus"), Step("pcs52", "assert", expect: "tip_matches_any", value: "Plot -"), Step("pcs53", "assert", expect: "dossier_matches_focus"), // Intent-only AI decision must not own the camera - wait for a live Plot instead. Step("pcs60", "assert", expect: "decision_worthy", value: "try_new_plot", label: "false"), Step("pcs61", "assert", expect: "decision_worthy", value: "find_lover", label: "true"), Step("pcs90", "fast_timing", value: "false"), Step("pcs98", "screenshot", value: "hud-plot-cell-sticky.png"), Step("pcs99", "snapshot"), }; } /// /// Sticky FamilyPack tips stay species-framed across count dips and follow loss. /// private static List FamilyPackSticky() { return new List { Step("fps0", "dismiss_windows"), Step("fps1", "wait_world"), Step("fps2", "set_setting", expect: "enabled", value: "true"), Step("fps3", "fast_timing", value: "true"), Step("fps4", "spawn", asset: "human", count: 2), Step("fps5", "spectator", value: "off"), Step("fps6", "spectator", value: "on"), Step("fps7", "pick_unit", asset: "human"), Step("fps8", "focus", asset: "human"), Step("fps9", "interest_end_session"), Step("fps20", "interest_family_session", asset: "human", expect: "fps_pack"), Step("fps21", "assert", expect: "tip_matches_any", value: "Pack -"), Step("fps22", "assert", expect: "tip_matches_any", value: "Humans|Human"), Step("fps22b", "assert", expect: "tip_matches_any", value: "gathering"), Step("fps23", "assert", expect: "tip_not_contains", value: "fighting"), Step("fps24", "assert", expect: "tip_not_contains", value: "joins a family"), Step("fps25", "assert", expect: "reason_matches_any", value: "Pack -"), Step("fps26", "assert", expect: "reason_matches_any", value: "gathering"), Step("fps30", "family_ensemble_apply", value: "1"), Step("fps31", "family_maintain_focus"), Step("fps32", "assert", expect: "tip_matches_any", value: "Pack -"), Step("fps32b", "assert", expect: "tip_matches_any", value: "(1)"), Step("fps33", "assert", expect: "tip_not_contains", value: "fighting"), Step("fps34", "assert", expect: "tip_matches_any", value: "gathering"), Step("fps40", "interest_clear_follow"), Step("fps41", "family_maintain_focus"), Step("fps42", "assert", expect: "has_focus", value: "true"), Step("fps43", "assert", expect: "tip_matches_any", value: "Pack -"), Step("fps44", "assert", expect: "tip_not_contains", value: "joins a family"), Step("fps45", "assert", expect: "reason_matches_any", value: "gathering"), Step("fps50", "family_ensemble_apply", value: "7"), Step("fps51", "family_maintain_focus"), Step("fps52", "assert", expect: "tip_matches_any", value: "Pack -"), Step("fps52b", "assert", expect: "tip_matches_any", value: "(7)"), Step("fps53", "assert", expect: "tip_matches_any", value: "gathering"), Step("fps54", "assert", expect: "reason_matches_any", value: "Pack -|gathering"), // Spawning pack members can briefly drop MoveCamera focus; re-seat before dossier check. Step("fps54b", "family_maintain_focus"), Step("fps54c", "assert", expect: "has_focus", value: "true"), Step("fps55", "assert", expect: "dossier_matches_focus"), Step("fps90", "fast_timing", value: "false"), Step("fps98", "screenshot", value: "hud-family-pack-sticky.png"), Step("fps99", "snapshot"), }; } /// /// Soft FamilyPack must not fortress the camera: Action-tier unit events margin-cut immediately. /// private static List FamilyPackYields() { return new List { Step("fpy0", "dismiss_windows"), Step("fpy1", "wait_world"), Step("fpy2", "set_setting", expect: "enabled", value: "true"), Step("fpy3", "fast_timing", value: "true"), Step("fpy4", "spawn", asset: "human", count: 2), Step("fpy5", "spectator", value: "off"), Step("fpy6", "spectator", value: "on"), Step("fpy7", "pick_unit", asset: "human"), Step("fpy8", "focus", asset: "human"), Step("fpy9", "interest_end_session"), Step("fpy20", "interest_family_session", asset: "human", expect: "fpy_pack"), Step("fpy21", "assert", expect: "tip_matches_any", value: "Pack -"), Step("fpy22", "assert", expect: "tip_matches_any", value: "gathering"), // Action margin-cuts soft pack immediately (no sticky fortress). // Assert immediately - ambient birth noise can clear a short FixedDwell tip later. Step("fpy53", "trigger_interest", asset: "human", label: "ActionCutsPack", tier: "Action"), Step("fpy55", "assert", expect: "tip_contains", value: "ActionCutsPack"), Step("fpy56", "assert", expect: "tip_not_contains", value: "Pack -"), Step("fpy57", "assert", expect: "session_key", value: "ActionCutsPack"), Step("fpy90", "fast_timing", value: "false"), Step("fpy99", "snapshot"), }; } /// /// Sticky StatusOutbreak tips stay status-framed across count dips and follow loss. /// private static List StatusOutbreakSticky() { return new List { Step("sos0", "dismiss_windows"), Step("sos1", "wait_world"), Step("sos2", "set_setting", expect: "enabled", value: "true"), Step("sos3", "fast_timing", value: "true"), Step("sos4", "spawn", asset: "human", count: 2), Step("sos5", "spectator", value: "off"), Step("sos6", "spectator", value: "on"), Step("sos7", "pick_unit", asset: "human"), Step("sos8", "focus", asset: "human"), Step("sos9", "interest_end_session"), Step("sos20", "interest_outbreak_session", asset: "human", value: "cursed", expect: "sos_outbreak"), Step("sos21", "assert", expect: "tip_matches_any", value: "Outbreak -"), Step("sos22", "assert", expect: "tip_matches_any", value: "Cursed"), Step("sos22b", "assert", expect: "tip_matches_any", value: "(4)"), Step("sos23", "assert", expect: "tip_not_contains", value: "fighting"), Step("sos24", "assert", expect: "tip_not_contains", value: " is cursed"), Step("sos25", "assert", expect: "reason_matches_any", value: "Outbreak -|Cursed"), Step("sos30", "outbreak_ensemble_apply", value: "1"), Step("sos31", "outbreak_maintain_focus"), Step("sos32", "assert", expect: "tip_matches_any", value: "Outbreak -"), Step("sos32b", "assert", expect: "tip_matches_any", value: "(1)"), Step("sos33", "assert", expect: "tip_not_contains", value: "fighting"), Step("sos40", "interest_clear_follow"), Step("sos41", "outbreak_maintain_focus"), Step("sos42", "assert", expect: "has_focus", value: "true"), Step("sos43", "assert", expect: "tip_matches_any", value: "Outbreak -"), Step("sos44", "assert", expect: "tip_not_contains", value: " is cursed"), Step("sos50", "outbreak_ensemble_apply", value: "6"), Step("sos51", "outbreak_maintain_focus"), Step("sos52", "assert", expect: "tip_matches_any", value: "Outbreak -"), Step("sos52b", "assert", expect: "tip_matches_any", value: "(6)"), Step("sos53", "assert", expect: "reason_matches_any", value: "Outbreak -|Cursed|(6)"), Step("sos54", "assert", expect: "dossier_matches_focus"), Step("sos90", "fast_timing", value: "false"), Step("sos98", "screenshot", value: "hud-status-outbreak-sticky.png"), Step("sos99", "snapshot"), }; } /// /// Relationship/emotion statuses must never become Outbreak tips, even when many units share them. /// private static List StatusOutbreakNotLove() { return new List { Step("son0", "dismiss_windows"), Step("son1", "wait_world"), Step("son2", "set_setting", expect: "enabled", value: "true"), Step("son3", "fast_timing", value: "true"), Step("son4", "spawn", asset: "human", count: 6), Step("son5", "spectator", value: "off"), Step("son6", "spectator", value: "on"), Step("son7", "pick_unit", asset: "human"), Step("son8", "focus", asset: "human"), Step("son9", "interest_end_session"), Step("son10", "force_live_feeds", value: "true"), // Cluster of fell_in_love must stay unit/relationship prose - never a love Outbreak. // Do not ban every "Outbreak -" tip: leftover cursed clusters from other scenarios are fine. Step("son20", "status_apply_nearby", value: "fell_in_love", label: "30", count: 6), Step("son21", "wait", value: "0.45"), Step("son22", "assert", expect: "tip_not_contains", value: "Outbreak - Fell"), Step("son23", "assert", expect: "tip_not_contains", value: "Outbreak - Love"), Step("son24", "assert", expect: "interest_no_key", value: "status:outbreak:fell_in_love"), Step("son25", "assert", expect: "outbreak_worthy", value: "fell_in_love", label: "false"), Step("son26", "assert", expect: "outbreak_worthy", value: "drowning", label: "true"), Step("son27", "assert", expect: "outbreak_worthy", value: "cursed", label: "true"), // Brief combat FX never become Outbreak - even when many units share them. Step("son28", "assert", expect: "outbreak_worthy", value: "stunned", label: "false"), Step("son29", "assert", expect: "outbreak_worthy", value: "confused", label: "false"), Step("son30", "assert", expect: "outbreak_worthy", value: "shield", label: "false"), Step("son31", "assert", expect: "outbreak_worthy", value: "recovery_combat_action", label: "false"), Step("son32", "assert", expect: "outbreak_worthy", value: "poisoned", label: "true"), Step("son33", "assert", expect: "outbreak_worthy", value: "plague", label: "true"), Step("son90", "fast_timing", value: "false"), Step("son99", "snapshot"), }; } /// /// Status-overlap happiness must not FixedDwell-claim a camera-worthy status the unit lacks. /// Hold while status is live; offset end-pings still register briefly without it. /// private static List StatusOverlapCamera() { return new List { Step("so0", "dismiss_windows"), Step("so1", "wait_world"), Step("so2", "set_setting", expect: "enabled", value: "true"), Step("so3", "fast_timing", value: "true"), Step("so4", "spawn", asset: "human", count: 1), Step("so5", "pick_unit", asset: "human"), Step("so6", "spectator", value: "off"), Step("so7", "spectator", value: "on"), Step("so8", "focus", asset: "auto"), Step("so9", "interest_end_session"), Step("so9b", "interest_expire_pending", value: ""), Step("so9c", "force_live_feeds", value: "true"), // Orphan onset: just_cursed without cursed status must not own the camera. Step("so10", "happiness_reset"), Step("so11", "drop_clear"), Step("so12", "happiness_remember_only", value: "just_cursed"), Step("so13", "interest_feeds_tick"), Step("so14", "assert", expect: "drop_contains", value: "status_overlap_absent"), Step("so15", "assert", expect: "interest_no_key", value: "just_cursed"), Step("so16", "assert", expect: "interest_no_key", value: "status:cursed"), // Same class: possessed onset without status. Step("so20", "drop_clear"), Step("so21", "happiness_remember_only", value: "just_possessed"), Step("so22", "interest_feeds_tick"), Step("so23", "assert", expect: "drop_contains", value: "status_overlap_absent"), Step("so24", "assert", expect: "interest_no_key", value: "just_possessed"), // Offset end-ping still registers without the status. Step("so30", "drop_clear"), Step("so31", "happiness_remember_only", value: "just_inspired"), Step("so32", "interest_feeds_tick"), Step("so33", "assert", expect: "interest_has_key", value: "just_inspired"), Step("so34", "interest_expire_pending", value: "just_inspired"), // Live cursed status owns StatusPhase; happiness Continuation merges. // Short timer: cursed may not clear via finishStatusEffect (re-applies / sticky). Step("so40", "interest_end_session"), Step("so41", "interest_expire_pending", value: ""), Step("so42", "drop_clear"), Step("so43", "status_apply", value: "cursed", label: "0.4"), Step("so44", "assert", expect: "interest_has_key", value: "status:cursed"), Step("so45", "happiness_remember_only", value: "just_cursed"), Step("so46", "interest_feeds_tick"), Step("so47", "assert", expect: "interest_has_key", value: "status:cursed"), Step("so48", "assert", expect: "interest_no_key", value: "happiness:just_cursed"), Step("so49", "wait", wait: 1.1f), Step("so50", "assert", expect: "activity_status_active", value: "cursed", label: "false"), Step("so50b", "age_current", wait: 3f), Step("so50c", "director_run", wait: 1.2f), Step("so51", "interest_end_session"), Step("so52", "interest_expire_pending", value: "status:cursed"), Step("so53", "assert", expect: "interest_no_key", value: "status:cursed"), Step("so90", "force_live_feeds", value: "false"), Step("so91", "fast_timing", value: "false"), Step("so99", "snapshot"), }; } /// /// Watch-reason row must explain why the camera is here (event), not who (title/name). /// private static List WatchReasonClarity() { return new List { Step("wr0", "dismiss_windows"), Step("wr1", "wait_world"), Step("wr2", "set_setting", expect: "enabled", value: "true"), Step("wr3", "set_setting", expect: "show_watch_reasons", value: "true"), Step("wr4", "set_setting", expect: "show_dossier_caption", value: "true"), Step("wr5", "spawn", asset: "human"), Step("wr6", "spectator", value: "off"), Step("wr7", "spectator", value: "on"), Step("wr8", "focus", asset: "human"), Step("wr9", "dossier_clear_job"), // EventReason sentence with subject name kept. Step("wr10", "interest_force_session", asset: "human", label: "{a} is fighting Waaaf", tier: "Action", expect: "fight_reason"), Step("wr11", "assert", expect: "dossier_contains", value: "is fighting"), Step("wr12", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr12b", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr13", "assert", expect: "tip_contains", value: "is fighting"), Step("wr14", "screenshot", value: "hud-watch-reason-fighting.png"), Step("wr15", "wait", wait: 0.55f), // Stale identity label → empty or non-identity beat (never "Live · King …"). Step("wr20", "interest_force_session", asset: "human", label: "King Isemward of Essionan", tier: "Action", expect: "king_identity"), Step("wr21", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr21b", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr22", "assert", expect: "dossier_not_contains", value: "King Isemward"), Step("wr22b", "assert", expect: "no_placeholder_tip"), Step("wr23", "screenshot", value: "hud-watch-reason-identity.png"), Step("wr24", "wait", wait: 0.55f), // WorldLog-style war phrase stays concrete. Step("wr30", "interest_force_session", asset: "human", label: "War: Essionan vs North", tier: "Epic", expect: "war_reason"), Step("wr31", "assert", expect: "dossier_contains", value: "War"), Step("wr32", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr32b", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr33", "screenshot", value: "hud-watch-reason-war.png"), Step("wr34", "wait", wait: 0.55f), // Weak singleton crumb → never "Lone beetle" / tier badges. Step("wr40", "interest_force_session", asset: "human", label: "Lone beetle", tier: "Curiosity", expect: "lone_crumb"), Step("wr41", "assert", expect: "dossier_not_contains", value: "Lone beetle"), Step("wr42", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr43", "assert", expect: "dossier_not_contains", value: "Curious ·"), Step("wr44", "screenshot", value: "hud-watch-reason-singleton.png"), Step("wr45", "wait", wait: 0.55f), // Character-led fill / ambient → no catch-all reason (row hidden). Step("wr46", "interest_force_session", asset: "human", label: "Ambient stroll", tier: "Ambient", expect: "ambient_stroll", value: "lead=character;evt=20"), Step("wr47", "assert", expect: "dossier_not_contains", value: "Going about their day"), Step("wr47b", "assert", expect: "dossier_not_contains", value: "Curious glance"), Step("wr47c", "assert", expect: "dossier_not_contains", value: "Looking around"), Step("wr48", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr49", "wait", wait: 0.35f), // Ownership: reason must belong to focused subject (not a stranger name). Step("wr60", "spawn", asset: "human", count: 2), Step("wr61", "interest_force_session", asset: "human", label: "Cabo loses infected", tier: "Action", expect: "stranger_label"), Step("wr62", "assert", expect: "reason_owns_focus"), Step("wr63", "assert", expect: "dossier_not_contains", value: "Cabo"), Step("wr64", "interest_force_session", asset: "human", label: "{a} became lovers with a partner", tier: "Action", expect: "lovers_owned"), Step("wr65", "assert", expect: "reason_owns_focus"), Step("wr65b", "assert", expect: "dossier_contains", value: "became lovers"), Step("wr66", "wait", wait: 0.35f), // Low-strength former-Ambient event still owns camera + shows sentence. Step("wr67", "interest_force_session", asset: "human", label: "{a} is seeking a lover", tier: "Action", expect: "seek_lover_low", value: "lead=event;evt=45"), Step("wr68", "assert", expect: "dossier_contains", value: "is seeking a lover"), Step("wr69", "assert", expect: "reason_owns_focus"), // Hatch reason (live path is egg status-loss → EmitHatch; force here for reason text). Step("wr69a", "interest_force_session", asset: "human", label: "{a} hatches from an egg", tier: "Action", expect: "hatch_reason", value: "lead=event;evt=88"), Step("wr69b", "assert", expect: "dossier_contains", value: "hatches from an egg"), Step("wr69c", "assert", expect: "reason_owns_focus"), // Label-sourced clash (not inventing Clash from fighter counts). Step("wr50", "interest_expire_pending", value: ""), Step("wr51", "interest_inject", asset: "human", label: "Clash · 8 fighting", tier: "Action", expect: "mass_clash", value: "lead=event;evt=90;char=5;fighters=8;notables=0;force=true"), Step("wr52", "age_current", wait: 0.8f), Step("wr53", "director_run", wait: 1.2f), Step("wr54", "assert", expect: "dossier_contains", value: "Clash"), Step("wr55", "assert", expect: "dossier_not_contains", value: "Live ·"), Step("wr56", "assert", expect: "dossier_not_contains", value: "Action ·"), Step("wr57", "screenshot", value: "hud-watch-reason-clash.png"), Step("wr58", "wait", wait: 0.55f), // After active dwell ends, orange reason clears even if focus remains. Step("wr70", "interest_force_session", asset: "human", label: "War: Essionan vs North", tier: "Epic", expect: "dwell_end_war"), Step("wr71", "assert", expect: "dossier_contains", value: "War"), Step("wr72", "interest_mark_inactive", value: "1.0"), Step("wr73", "assert", expect: "reason_empty", value: "true"), Step("wr74", "assert", expect: "reason_owns_focus"), // Cold-boot / force-session focus gaps are unrelated to caption clarity. Step("wr89", "reset_counters"), Step("wr90", "assert", expect: "no_bad"), Step("wr99", "snapshot"), }; } /// /// Actions outrank characters; same action → more important character wins; /// multi-fighter battles outrank anonymous 1v1; notable duels can beat nameless melees. /// private static List DirectorActionPriority() { return new List { Step("ap0", "dismiss_windows"), Step("ap1", "wait_world"), Step("ap1b", "scoring_reload"), Step("ap1c", "assert", expect: "scoring_config_loaded", value: "true"), Step("ap2", "set_setting", expect: "enabled", value: "true"), Step("ap3", "fast_timing", value: "true"), Step("ap4", "spawn", asset: "sheep", count: 2), Step("ap5", "spectator", value: "off"), Step("ap6", "spectator", value: "on"), Step("ap7", "focus", asset: "sheep"), Step("ap8", "interest_variety_clear"), Step("ap9", "interest_expire_pending", value: ""), // Same action intensity: higher character significance wins. Step("ap10", "interest_inject", asset: "sheep", label: "FightKing", tier: "Action", expect: "fight_king", value: "lead=event;evt=70;char=40;fighters=2;notables=1"), Step("ap11", "interest_inject", asset: "sheep", label: "FightPeasant", tier: "Action", expect: "fight_peasant", value: "lead=event;evt=70;char=5;fighters=2;notables=0"), Step("ap12", "assert", expect: "interest_score_order", value: "fight_king", label: "fight_peasant"), // Multi-person anonymous melee beats anonymous 1v1 at same base evt. Step("ap20", "interest_expire_pending", value: ""), Step("ap21", "interest_inject", asset: "sheep", label: "Melee", tier: "Action", expect: "melee_mass", value: "lead=event;evt=55;char=5;fighters=8;notables=0"), Step("ap22", "interest_inject", asset: "sheep", label: "DuelAnon", tier: "Action", expect: "duel_anon", value: "lead=event;evt=55;char=5;fighters=2;notables=0"), Step("ap23", "assert", expect: "interest_score_order", value: "melee_mass", label: "duel_anon"), // Notable duel can beat nameless melee. Step("ap30", "interest_expire_pending", value: ""), Step("ap31", "interest_inject", asset: "sheep", label: "MeleeNoName", tier: "Action", expect: "melee_noname", value: "lead=event;evt=55;char=5;fighters=8;notables=0"), Step("ap32", "interest_inject", asset: "sheep", label: "DuelKings", tier: "Action", expect: "duel_kings", value: "lead=event;evt=55;char=40;fighters=2;notables=2"), Step("ap33", "assert", expect: "interest_score_order", value: "duel_kings", label: "melee_noname"), // Balanced contested scrap outranks extreme lopsided pack headcount. Step("ap34", "interest_expire_pending", value: ""), Step("ap35", "interest_inject", asset: "sheep", label: "LopsidedPack", tier: "Action", expect: "lopsided_pack", value: "lead=event;evt=88;char=5;fighters=39;notables=0;sideA=38;sideB=1"), Step("ap36", "interest_inject", asset: "sheep", label: "BalancedScrap", tier: "Action", expect: "balanced_scrap", value: "lead=event;evt=88;char=5;fighters=12;notables=0;sideA=6;sideB=6"), Step("ap37", "assert", expect: "interest_score_order", value: "balanced_scrap", label: "lopsided_pack"), // Combat Action force-session replaces celebrity Story vignette. Step("ap40", "interest_expire_pending", value: ""), Step("ap41", "interest_force_session", asset: "sheep", label: "KingStroll", tier: "Story", expect: "king_stroll"), Step("ap42", "assert", expect: "tip_contains", value: "KingStroll"), Step("ap43", "interest_inject", asset: "sheep", label: "RealFight", tier: "Action", expect: "real_fight", value: "lead=event;evt=120;char=10;fighters=4;notables=1;force=true"), Step("ap44", "assert", expect: "tip_contains", value: "RealFight"), Step("ap45", "assert", expect: "tip_not_contains", value: "KingStroll"), // Cold-boot focus gaps from welcome/dismiss are unrelated to score order. Step("ap89", "reset_counters"), Step("ap90", "assert", expect: "no_bad"), Step("ap91", "fast_timing", value: "false"), Step("ap99", "snapshot"), }; } private static List Discovery() { return new List { Step("d0", "dismiss_windows"), Step("d1", "wait_world"), Step("d2", "set_setting", expect: "enabled", value: "true"), Step("d3", "fast_timing", value: "true"), Step("d4", "discovery_reset"), // Calm animal focus so Action-tier combat does not block curiosity drain. Step("d5", "spawn", asset: "sheep"), Step("d6", "spectator", value: "off"), Step("d7", "spectator", value: "on"), Step("d8", "focus", asset: "sheep"), Step("d9", "reset_counters"), // Live-path dedupe: mark presented drops buffered copy. Step("d10", "discovery_note", asset: "sheep"), Step("d11", "assert", expect: "pending_discovery", value: "1"), Step("d12", "discovery_mark", asset: "sheep"), Step("d13", "assert", expect: "pending_discovery", value: "0"), Step("d14", "assert", expect: "presented", asset: "sheep", value: "true"), // Force drain path once curiosity is acceptable. Step("d20", "discovery_reset"), Step("d21", "discovery_note", asset: "sheep"), Step("d22", "assert", expect: "pending_discovery", value: "1"), Step("d23", "age_current", wait: 2f), Step("d24", "assert", expect: "would_accept_curiosity", value: "true"), Step("d25", "discovery_drain"), Step("d26", "assert", expect: "pending_discovery_asset", value: "false"), Step("d27", "assert", expect: "presented", asset: "sheep", value: "true"), Step("d28", "director_run", wait: 1.2f), Step("d29", "assert", expect: "tip_matches_unit"), Step("d30", "assert", expect: "no_bad"), Step("d90", "fast_timing", value: "false"), Step("d99", "snapshot"), }; } private static List WorldLog() { return new List { Step("wl0", "dismiss_windows"), Step("wl1", "wait_world"), Step("wl2", "set_setting", expect: "enabled", value: "true"), Step("wl3", "fast_timing", value: "true"), Step("wl4", "pick_unit", asset: "auto"), Step("wl5", "spectator", value: "off"), Step("wl6", "spectator", value: "on"), Step("wl7", "focus", asset: "auto"), Step("wl8", "reset_counters"), Step("wl10", "trigger_interest", asset: "auto", label: "Story: kingdom_new", tier: "Story"), Step("wl11", "age_current", wait: 1.2f), Step("wl12", "director_run", wait: 1.2f), Step("wl13", "assert", expect: "tip_contains", value: "kingdom_new"), Step("wl14", "assert", expect: "tip_contains", value: "kingdom_new"), Step("wl20", "trigger_interest", asset: "auto", label: "Epic: diplomacy_war_started", tier: "Epic"), Step("wl21", "age_current", wait: 1.2f), Step("wl22", "director_run", wait: 1.2f), Step("wl23", "assert", expect: "tip_contains", value: "diplomacy_war_started"), Step("wl24", "assert", expect: "tip_contains", value: "diplomacy_war_started"), Step("wl25", "assert", expect: "health"), Step("wl26", "assert", expect: "no_bad"), Step("wl90", "fast_timing", value: "false"), Step("wl99", "snapshot"), }; } private static List WorldLogAudit() { return new List { Step("wla0", "wait_world"), Step("wla1", "assert", expect: "world_log_audit"), Step("wla2", "snapshot") }; } private static List DisasterWarAudit() { return new List { Step("dwa0", "wait_world"), Step("dwa1", "assert", expect: "disaster_war_audit"), Step("dwa2", "snapshot") }; } private static List DomainEventAudit() { return new List { Step("dea0", "wait_world"), Step("dea1", "assert", expect: "domain_event_audit"), Step("dea1b", "assert", expect: "library_asset_labels_ok"), Step("dea1c", "assert", expect: "library_ambient_policy_ok"), Step("dea2", "snapshot") }; } private static List LibraryAssetLabelsOk() { return new List { Step("lal0", "wait_world"), Step("lal1", "assert", expect: "library_asset_labels_ok"), Step("lal2", "snapshot") }; } private static List EventTracking() { return new List { Step("et0", "dismiss_windows"), Step("et1", "wait_world"), Step("et2", "set_setting", expect: "enabled", value: "true"), Step("et3", "pick_unit", asset: "auto"), Step("et4", "spectator", value: "off"), Step("et5", "spectator", value: "on"), Step("et6", "focus", asset: "auto"), Step("et10", "assert", expect: "domain_event_audit"), Step("et20", "domain_feed", asset: "relationship", value: "set_lover"), Step("et21", "assert", expect: "interest_has_key", value: "rel:set_lover"), Step("et21a", "domain_feed", asset: "relationship", value: "become_alpha"), Step("et21b", "assert", expect: "interest_has_key", value: "rel:become_alpha"), Step("et21c", "interest_force_session", asset: "human", label: "{a} becomes the family alpha", tier: "Action", expect: "alpha_reason", value: "lead=event;evt=72"), Step("et21d", "assert", expect: "dossier_contains", value: "family alpha"), Step("et22", "domain_feed", asset: "plot", value: "summon_meteor_rain"), Step("et23", "assert", expect: "interest_has_key", value: "plot:summon_meteor_rain"), Step("et23b", "assert", expect: "interest_label_contains", value: "plots to summon"), Step("et24", "domain_feed", asset: "era", value: "age_chaos"), Step("et25", "assert", expect: "interest_has_key", value: "era:age_chaos"), Step("et26", "domain_feed", asset: "meta", value: "new_city"), Step("et27", "assert", expect: "interest_has_key", value: "meta:new_city"), Step("et27b", "domain_feed", asset: "meta", value: "new_kingdom"), Step("et27c", "assert", expect: "interest_has_key", value: "meta:new_kingdom"), Step("et28", "domain_feed", asset: "book", value: "bad_story_about_king"), Step("et29", "assert", expect: "interest_has_key", value: "book:bad_story_about_king"), Step("et30", "domain_feed", asset: "trait", value: "immortal"), Step("et31", "assert", expect: "interest_has_key", value: "trait:immortal"), Step("et32", "domain_feed", asset: "boat"), Step("et33", "assert", expect: "interest_has_key", value: "boat:"), Step("et34", "domain_feed", asset: "building"), Step("et35", "assert", expect: "interest_has_key", value: "building:"), Step("et40", "domain_feed", asset: "spell", value: "summon_lightning"), Step("et41", "assert", expect: "interest_has_key", value: "spell:summon_lightning"), Step("et42", "domain_feed", asset: "item", value: "sword"), Step("et43", "assert", expect: "interest_has_key", value: "item:"), Step("et44", "domain_feed", asset: "decision", value: "declare_war"), Step("et45", "assert", expect: "interest_has_key", value: "decision:"), Step("et45b", "interest_force_session", asset: "human", label: "{a} decides to declare war", tier: "Action", expect: "decision_prose", value: "lead=event;evt=86"), Step("et45c", "assert", expect: "dossier_contains", value: "decides to declare war"), Step("et45d", "assert", expect: "dossier_not_contains", value: "decides Declare"), Step("et45e", "interest_force_session", asset: "human", label: "{a} decides to change the kingdom's culture", tier: "Action", expect: "king_decision_prose", value: "lead=event;evt=86"), Step("et45f", "assert", expect: "dossier_contains", value: "decides to change the kingdom's culture"), Step("et45g", "assert", expect: "dossier_not_contains", value: "decides to king"), Step("et46", "domain_feed", asset: "power", value: "lightning"), Step("et47", "assert", expect: "interest_has_key", value: "power:"), Step("et48", "domain_feed", asset: "subspecies_trait", value: "fire_blood"), Step("et49", "assert", expect: "interest_has_key", value: "subspecies_trait:"), Step("et50", "domain_feed", asset: "gene", value: "warfare_1"), Step("et51", "assert", expect: "interest_has_key", value: "gene:"), Step("et90", "assert", expect: "health"), Step("et91", "assert", expect: "no_bad"), Step("et99", "snapshot") }; } private static List EventCoverage() { return new List { Step("ec0", "dismiss_windows"), Step("ec1", "wait_world"), Step("ec2", "set_setting", expect: "enabled", value: "true"), Step("ec3", "pick_unit", asset: "auto"), Step("ec4", "spectator", value: "off"), Step("ec5", "spectator", value: "on"), Step("ec6", "focus", asset: "auto"), // Full authored coverage gates Step("ec10", "assert", expect: "world_log_audit"), Step("ec11", "assert", expect: "disaster_war_audit"), Step("ec12", "assert", expect: "activity_status_audit"), Step("ec13", "assert", expect: "activity_happiness_audit"), Step("ec14", "assert", expect: "domain_event_audit"), // Every camera-worthy catalog id must A-register (data-driven). Step("ec30", "assert", expect: "event_inject_coverage"), Step("ec90", "assert", expect: "health"), Step("ec91", "assert", expect: "no_bad"), Step("ec99", "snapshot") }; } private static List EventInjectCoverage() { return new List { Step("eic0", "dismiss_windows"), Step("eic1", "wait_world"), Step("eic2", "set_setting", expect: "enabled", value: "true"), Step("eic3", "pick_unit", asset: "auto"), Step("eic4", "spectator", value: "off"), Step("eic5", "spectator", value: "on"), Step("eic6", "focus", asset: "auto"), Step("eic7", "assert", expect: "event_inject_coverage"), Step("eic8", "assert", expect: "no_bad"), Step("eic9", "snapshot") }; } /// /// Curated live API/Beh outcomes (not full catalog). Expands when patches gain /// deterministic harness wrappers. Inject coverage owns "every id registers." /// private static List EventLiveOutcomeSmoke() { return new List { Nested("elo_family", "family_trigger_smoke"), Step("elo0", "dismiss_windows"), Step("elo1", "wait_world"), Step("elo2", "pick_unit", asset: "human"), Step("elo3", "spectator", value: "off"), Step("elo4", "spectator", value: "on"), Step("elo5", "focus", asset: "auto"), Step("elo10", "activity_status_reset"), Step("elo11", "status_apply", value: "confused"), Step("elo12", "assert", expect: "activity_status_gain_count", value: "1", label: "min"), Step("elo13", "assert", expect: "activity_log_contains", value: "confused"), Step("elo19", "happiness_reset"), Step("elo20", "happiness_apply", value: "just_became_adult"), Step("elo21", "assert", expect: "activity_happiness_count", value: "1", label: "min"), Step("elo22", "assert", expect: "activity_log_contains", value: "adult"), Step("elo90", "assert", expect: "no_bad"), Step("elo99", "snapshot") }; } /// /// Phase 1: loop every camera-worthy happiness Signal + status gain through real apply APIs. /// Writes event-live-coverage.tsv. /// private static List EventLiveApplyLoop() { return new List { Step("ela0", "dismiss_windows"), Step("ela1", "wait_world"), Step("ela2", "set_setting", expect: "enabled", value: "true"), Step("ela3", "spawn", asset: "human"), Step("ela4", "pick_unit", asset: "human"), Step("ela5", "spectator", value: "off"), Step("ela6", "spectator", value: "on"), Step("ela7", "focus", asset: "auto"), Step("ela8", "assert", expect: "event_live_apply_loop"), Step("ela9", "assert", expect: "event_live_coverage"), Step("ela10", "assert", expect: "no_bad"), Step("ela99", "snapshot") }; } private static List EventLiveRelationship() { return new List { Step("elr0", "dismiss_windows"), Step("elr1", "wait_world"), Step("elr2", "set_setting", expect: "enabled", value: "true"), Step("elr3", "spawn", asset: "human"), Step("elr4", "pick_unit", asset: "human"), Step("elr5", "spectator", value: "off"), Step("elr6", "spectator", value: "on"), Step("elr7", "focus", asset: "auto"), Step("elr8", "assert", expect: "event_live_relationship"), Step("elr9", "assert", expect: "event_live_coverage"), Step("elr10", "assert", expect: "no_bad"), Step("elr99", "snapshot") }; } private static List EventLivePipelines() { return new List { Step("elp0", "dismiss_windows"), Step("elp1", "wait_world"), Step("elp2", "set_setting", expect: "enabled", value: "true"), Step("elp3", "spawn", asset: "human"), Step("elp4", "pick_unit", asset: "human"), Step("elp5", "spectator", value: "off"), Step("elp6", "spectator", value: "on"), Step("elp7", "focus", asset: "auto"), Step("elp8", "assert", expect: "event_live_pipelines"), Step("elp9", "assert", expect: "event_live_coverage"), Step("elp10", "assert", expect: "no_bad"), Step("elp99", "snapshot") }; } /// Phase 0: seed + write coverage ledger (fail only if any live_level=fail). private static List EventLiveCoverage() { return new List { Step("elc0", "wait_world"), Step("elc1", "assert", expect: "event_live_coverage"), Step("elc2", "snapshot") }; } private static List EventSuite() { return new List { Nested("es_coverage", "event_coverage"), Nested("es_apply", "event_live_apply_loop"), Nested("es_rel", "event_live_relationship"), Nested("es_live", "event_live_outcome_smoke"), }; } private static List MutationDiscovery() { return new List { Step("md0", "wait_world"), Step("md1", "assert", expect: "mutation_discovery"), Step("md2", "snapshot") }; } private static List ActivityRateSample() { return new List { Step("ar0", "wait_world"), Step("ar1", "activity_sample_reset"), Step("ar2", "wait", wait: 20f), Step("ar3", "activity_stats"), Step("ar4", "snapshot"), }; } private static List ActivityIdleSmoke() { return new List { Step("act0", "dismiss_windows"), Step("act1", "wait_world"), Step("act2", "set_setting", expect: "enabled", value: "true"), Step("act3", "set_setting", expect: "show_dossier_caption", value: "true"), Step("act4", "set_setting", expect: "chronicle_enabled", value: "true"), Step("act5", "chronicle_clear"), Step("act6", "activity_clear"), Step("act7", "pick_unit", asset: "auto"), Step("act8", "spectator", value: "off"), Step("act9", "spectator", value: "on"), Step("act10", "focus", asset: "auto"), Step("act11", "wait", wait: 0.35f), Step("act12", "activity_probe"), Step("act13", "assert", expect: "activity_first_scoring"), Step("act13b", "assert", expect: "activity_prose_sentence"), Step("act13b2", "assert", expect: "activity_prose_alive"), Step("act13c", "assert", expect: "activity_prose_species"), Step("act13d", "assert", expect: "activity_prose_grounding"), Step("act13e", "assert", expect: "activity_prose_library"), Step("act13f", "assert", expect: "activity_taxonomy_audit"), Step("act13g", "assert", expect: "activity_status_audit"), Step("act13h", "assert", expect: "activity_verb_dedupe"), Step("act13h2", "assert", expect: "activity_verb_exempt"), Step("act13i", "assert", expect: "dossier_task_refresh"), Step("act14", "activity_force", asset: "laughing", label: "Laughing", count: 1, value: "angle"), Step("act14b", "assert", expect: "activity_log_contains", value: "celestial"), Step("act14c", "activity_force", asset: "eating", label: "Taking a meal", count: 1), Step("act15", "activity_force", asset: "farm", label: "Tends the fields", count: 1), Step("act16", "activity_force", asset: "hunt", label: "Hunts", count: 1, expect: "Barkley"), Step("act16b", "activity_force", asset: "task_unit_play", label: "Playing", value: "dog"), Step("act16c", "assert", expect: "activity_log_contains", value: "bounds"), Step("act16d", "activity_force", asset: "task_unit_play", label: "Playing", value: "cat"), Step("act16e", "assert", expect: "activity_prose_variety", value: "2"), Step("act16f", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold", tier: "beat", value: "human"), Step("act16g", "assert", expect: "activity_log_contains", value: "wheat"), Step("act16g2", "assert", expect: "activity_log_contains", value: "Riverhold"), Step("act16g3", "assert", expect: "activity_log_not_contains", value: "while carrying"), Step("act16h", "activity_force", asset: "BehSocializeTalk", label: "Talks", count: 1, tier: "beat", expect: "Barkley"), Step("act16i", "assert", expect: "activity_log_contains", value: "Barkley"), Step("act16i2", "assert", expect: "activity_log_not_contains", value: "with another"), // JobLabel on nametag identity tag (Species/Job); reason stays story-only. // Labels come from live citizen_job_library discovery (not job-id allowlists). Step("act16j0", "assert", expect: "citizen_job_labels_ok"), Step("act16j", "dossier_force_job", value: "farmer"), Step("act16k", "assert", expect: "dossier_contains", value: "Farmer"), Step("act16k2", "assert", expect: "dossier_contains", value: "/"), Step("act16k3", "dossier_force_job", value: "gatherer_herbs"), Step("act16k4", "assert", expect: "dossier_contains", value: "Herb Gatherer"), Step("act16k5", "dossier_force_job", value: "gatherer_bushes"), Step("act16k6", "assert", expect: "dossier_contains", value: "Bush Gatherer"), Step("act16k7", "dossier_force_job", value: "miner_deposit"), Step("act16k8", "assert", expect: "dossier_contains", value: "Miner Deposit"), Step("act16l", "screenshot", value: "hud-dossier-job-only.png"), Step("act16m", "wait", wait: 0.45f), Step("act16n", "dossier_force_job", value: "farmer", label: "Story"), Step("act16o", "assert", expect: "dossier_contains", value: "Story"), Step("act16o2", "assert", expect: "dossier_contains", value: "Farmer"), Step("act16p", "screenshot", value: "hud-dossier-reason-job.png"), Step("act16q", "wait", wait: 0.45f), Step("act16r", "dossier_clear_job"), // Statuses row (priority, max 4). Step("act16s", "status_apply", value: "cursed", label: "20"), // Optional second chip - some units refuse poison; cursed alone still gates the row. Step("act16t", "status_apply", value: "poisoned", label: "20", expect: "optional"), Step("act16u", "wait", wait: 0.25f), Step("act16v", "assert", expect: "dossier_statuses_ok", value: "1", label: "min"), Step("act16w", "assert", expect: "dossier_contains", value: "Cursed"), Step("act17", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 3, tier: "beat"), Step("act17b", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1), // Live-relevance peek: after soft window, aged hunt drops; newest unload remains. // Isolated ring so older move-matching lines cannot crowd the peek. Step("act17c", "activity_clear"), Step("act17d", "activity_force", asset: "BehAttackActorHuntingTarget", label: "Hunts prey", count: 1, tier: "beat", expect: "PreyThing"), Step("act17e", "activity_force", asset: "BehUnloadResources", label: "wheat@Riverhold", count: 1, tier: "beat", value: "human"), Step("act17f", "activity_age", value: "12"), // Needle PreyThing (not "Hunts") - species voice may omit "hunt(s)" entirely. Step("act17g", "assert", expect: "dossier_history_not_contains", value: "PreyThing"), Step("act17h", "assert", expect: "dossier_history_contains", value: "wheat"), Step("act17i", "assert", expect: "activity_log_contains", value: "PreyThing"), // Rebuild a busy ring for later variety / count asserts. Step("act17j", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"), Step("act17k", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1), Step("act17l", "activity_force", asset: "BehSocializeTalk", label: "Talks", count: 1, tier: "beat", expect: "Barkley"), Step("act18", "wait", wait: 0.25f), Step("act19", "assert", expect: "activity_count", value: "4", label: "min"), Step("act20", "assert", expect: "activity_prose_variety", value: "2"), Step("act20b", "assert", expect: "activity_names_colored"), Step("act20c", "assert", expect: "activity_log_contains", value: "Barkley"), // Life milestones mirror into Activity (kill + Became lovers) while Life stays on Chronicle. Step("act20d", "chronicle_milestone", value: "kill"), Step("act20e", "assert", expect: "activity_log_contains", value: "killed"), Step("act20f", "assert", expect: "chronicle_latest_contains", value: "Killed"), Step("act20g", "chronicle_milestone", value: "lover"), Step("act20h", "assert", expect: "activity_log_contains", value: "Became lovers"), Step("act20i", "assert", expect: "chronicle_latest_contains", value: "Became lovers"), // Whole-row other-person click → Lore biography (then restore original focus). Step("act20j", "assert", expect: "tooltip_library_ok"), Step("act20j2", "remember_focus"), Step("act20k", "assert", expect: "history_other_rows", value: "1", label: "min"), Step("act20l", "dossier_click_history_other"), Step("act20m", "wait", wait: 0.3f), Step("act20n", "assert", expect: "lore_detail", value: "true"), Step("act20o", "assert", expect: "lore_detail_is_other"), Step("act20p", "lore_close"), Step("act20q", "spectator", value: "on"), Step("act20r", "wait", wait: 0.2f), // Return to a living subject so later activity-count asserts stay valid. // Orange reason click → related unit Lore (same contract as history rows). Step("act20r2", "spawn", asset: "human", count: 1), Step("act20r2b", "happiness_remember_partner"), Step("act20r3", "spawn", asset: "human", count: 1), Step("act20r4", "pick_unit", asset: "human"), Step("act20r5", "focus", asset: "auto"), Step("act20r6", "wait", wait: 0.2f), Step("act20s", "dossier_force_named_reason", value: "is fighting", expect: "auto"), Step("act20t", "assert", expect: "reason_names_colored"), Step("act20t2", "assert", expect: "reason_other_clickable"), Step("act20t3", "remember_focus"), Step("act20t4", "dossier_click_reason_other"), Step("act20t5", "wait", wait: 0.2f), Step("act20t6", "assert", expect: "lore_detail", value: "true"), Step("act20t7", "assert", expect: "lore_detail_is_other"), Step("act20t8", "lore_close"), Step("act20t9", "spectator", value: "on"), Step("act20t10", "focus", asset: "auto"), Step("act20u", "dossier_clear_job"), // Re-seed activity ring after the other-person Lore detour. Step("act20v", "activity_force", asset: "BehPollinate", label: "Works the flower", count: 2, tier: "beat"), Step("act20w", "activity_force", asset: "zzz_harness_act", label: "Harness pollen trail", count: 1), Step("act20x", "activity_force", asset: "BehSocializeTalk", label: "Talks", count: 1, tier: "beat", expect: "Barkley"), Step("act21", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "activity"), Step("act22", "assert", expect: "dossier_history_not_contains", value: "Harness pollen"), Step("act23", "assert", expect: "caption_layout_ok"), Step("act24", "screenshot", value: "hud-activity_idle_smoke.png"), Step("act25", "wait", wait: 0.55f), // Life still peeks when chronicle history exists beside activity. Step("act26", "chronicle_force", label: "Killed a serpent"), Step("act27", "wait", wait: 0.2f), Step("act28", "assert", expect: "dossier_history_contains", value: "serpent"), Step("act29", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "life"), Step("act29b", "screenshot", value: "hud-activity-life-split.png"), Step("act29c", "wait", wait: 0.45f), // Mixed activity icons + kill/lover milestones in dossier peek. Step("act29d", "screenshot", value: "hud-activity-icons-milestones.png"), Step("act29e", "wait", wait: 0.45f), // Books: Activity / Life tabs. Step("act40", "lore_open_focus"), Step("act41", "wait", wait: 0.25f), Step("act42", "assert", expect: "lore_detail_pane", value: "life"), Step("act43", "lore_detail_pane", value: "activity"), Step("act44", "wait", wait: 0.2f), Step("act45", "assert", expect: "lore_detail_pane", value: "activity"), Step("act46", "assert", expect: "lore_activity_shown", value: "4", label: "min"), Step("act47", "screenshot", value: "hud-lore-activity-tab.png"), Step("act48", "wait", wait: 0.45f), Step("act49", "lore_detail_pane", value: "life"), Step("act50", "assert", expect: "lore_history_shown", value: "1", label: "min"), Step("act51", "assert", expect: "idle", value: "false"), Step("act52", "assert", expect: "caption_layout_ok"), Step("act53", "assert", expect: "no_bad"), // Closing Lore (opened via L while idle) auto-resumes spectator. Step("act54", "lore_close"), Step("act55", "wait", wait: 0.2f), Step("act56", "assert", expect: "idle", value: "true"), Step("act99", "snapshot"), }; } private static List ChronicleSmoke() { return new List { Step("ch0", "dismiss_windows"), Step("ch1", "wait_world"), Step("ch2", "set_setting", expect: "enabled", value: "true"), Step("ch3", "set_setting", expect: "show_dossier_caption", value: "true"), Step("ch4", "set_setting", expect: "chronicle_enabled", value: "true"), Step("ch4b", "chronicle_clear"), // Fresh spawn so prior scenarios (lava / Miracle Born) cannot steal focus. Step("ch5", "spawn", asset: "human", count: 1), Step("ch6", "spectator", value: "off"), Step("ch7", "spectator", value: "on"), Step("ch8", "focus", asset: "auto"), Step("ch9", "wait", wait: 0.35f), Step("ch9b", "focus", asset: "auto"), Step("ch10", "assert", expect: "health"), Step("ch11", "assert", expect: "dossier_contains", value: "auto"), Step("ch12", "assert", expect: "caption_contains", value: "auto"), Step("ch12b", "assert", expect: "caption_layout_ok"), Step("ch12c", "assert", expect: "dossier_traits_ok"), Step("ch12d", "screenshot", value: "hud-dossier-empty-hist.png"), Step("ch12e", "wait", wait: 0.55f), // History inject Step("ch13", "chronicle_force", label: "Killed"), Step("ch14", "assert", expect: "chronicle_count", value: "1", label: "min"), Step("ch15", "assert", expect: "chronicle_latest_contains", value: "Killed"), Step("ch15a2", "assert", expect: "chronicle_latest_dated"), Step("ch15b", "assert", expect: "chronicle_latest_contains", value: "auto"), Step("ch15c", "wait", wait: 0.2f), Step("ch15d", "assert", expect: "dossier_history_contains", value: "Killed"), // Real kill / lover milestones also land in Activity. Step("ch15d2", "chronicle_milestone", value: "kill"), Step("ch15d3", "assert", expect: "activity_log_contains", value: "killed"), Step("ch15d4", "assert", expect: "chronicle_latest_contains", value: "Killed"), Step("ch15d5", "chronicle_milestone", value: "lover"), Step("ch15d6", "assert", expect: "activity_log_contains", value: "Became lovers"), Step("ch15d7", "wait", wait: 0.2f), Step("ch15d8", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "activity"), // Long event wraps in the narrow dossier history column (no wider panel). Step("ch15e", "chronicle_force", label: "Became legendary after surviving the great crab migration across three kingdoms, returning home with a stolen crown, and telling the tale for forty winters"), Step("ch15f", "wait", wait: 0.35f), Step("ch15g", "assert", expect: "dossier_history_contains", value: "crab migration"), Step("ch15h", "assert", expect: "caption_layout_ok"), Step("ch15i", "screenshot", value: "hud-dossier-long-hist.png"), Step("ch15j", "wait", wait: 0.65f), // Death-cause wording samples (victim POV) - flavored FormatDeathCause lines. Step("ch16a", "chronicle_death_sample", value: "Age"), Step("ch16b", "assert", expect: "chronicle_latest_contains", value: "old age"), Step("ch16c", "chronicle_death_sample", value: "Starvation"), Step("ch16d", "assert", expect: "chronicle_latest_contains", value: "hunger"), Step("ch16e", "chronicle_death_sample", value: "Drowning"), Step("ch16f", "assert", expect: "chronicle_latest_contains", value: "water"), Step("ch16g", "chronicle_death_sample", value: "Fire"), Step("ch16h", "assert", expect: "chronicle_latest_contains", value: "ash"), // Character kills/deaths must stay on character history, not World Memory. Step("ch16i", "assert", expect: "world_memory_world_only"), // Soft-cap prune: temporarily lower the cap, flood past it, then restore. Step("ch16j0", "chronicle_set_subject_cap", value: "500"), Step("ch16j", "chronicle_orphan_flood", count: 580), Step("ch16k", "assert", expect: "chronicle_subjects_capped"), Step( "ch16k2", "chronicle_set_subject_cap", value: Chronicle.DefaultMaxHistorySubjects.ToString()), // World Memory landmarks (replaces History|World tabs) Step("ch17a", "chronicle_world_force", label: "War: harness_alpha vs harness_beta"), Step("ch17b", "assert", expect: "chronicle_count", value: "1", label: "world"), Step("ch17c", "assert", expect: "world_memory_contains", value: "harness_alpha"), Step("ch17d", "assert", expect: "chronicle_latest_contains", value: "ash"), Step("ch17e", "assert", expect: "world_memory_age"), Step("ch17e2", "assert", expect: "world_memory_world_only"), Step("ch17e3", "assert", expect: "chronicle_memory_capped"), Step("ch17e4", "assert", expect: "chronicle_subjects_capped"), // Selecting a world event pans to its recorded place (even with no unit there). Step("ch17j", "lore_jump_world"), Step("ch17k", "wait", wait: 0.2f), Step("ch17l", "assert", expect: "world_jump_place"), Step("ch17m", "assert", expect: "focus", value: "false"), // Restore a living focus for the rest of the scenario. Step("ch17n", "focus", asset: "auto"), Step("ch17o", "wait", wait: 0.2f), Step("ch17p", "assert", expect: "focus", value: "true"), // World Memory panel + dossier controls Step("ch18", "chronicle_open"), Step("ch18a", "screenshot", value: "hud-chronicle_smoke.png"), Step("ch18a2", "wait", wait: 0.6f), Step("ch18b", "assert", expect: "world_memory_compact"), Step("ch18b2", "assert", expect: "hud_no_overlap"), Step("ch18c", "assert", expect: "caption_layout_ok"), Step("ch18c2", "assert", expect: "dossier_traits_ok"), Step("ch18d", "assert", expect: "world_memory_contains", value: "harness_alpha"), Step("ch18e", "dossier_favorite", value: "true"), Step("ch18f", "assert", expect: "is_favorite", value: "true"), Step("ch18g", "dossier_favorite", value: "false"), Step("ch18h", "assert", expect: "is_favorite", value: "false"), // Dossier peeks 3 lines; L / lore_open_focus opens full history and pauses idle. Step("ch18h1", "chronicle_force", label: "Hist", count: 40), Step("ch18h2", "wait", wait: 0.25f), Step("ch18k", "assert", expect: "dossier_history_shown", value: "3", label: "exact"), Step("ch18k3", "assert", expect: "dossier_history_fills_body"), Step("ch18l", "assert", expect: "caption_layout_ok"), Step("ch18l1", "assert", expect: "hud_no_overlap"), Step("ch18l2", "screenshot", value: "hud-dossier-peek.png"), Step("ch18l3", "wait", wait: 0.45f), // L shortcut syncs to the focused dossier unit (Follow mode) and locks idle. Step("ch18m", "lore_open_focus"), Step("ch18n", "wait", wait: 0.3f), Step("ch18o", "assert", expect: "idle", value: "false"), Step("ch18o1", "lore_tab", value: "living"), Step("ch18o1b", "wait", wait: 0.15f), Step("ch18o2", "assert", expect: "lore_tab", value: "living"), Step("ch18o3", "assert", expect: "lore_detail", value: "true"), Step("ch18o3b", "assert", expect: "lore_follow", value: "true"), Step("ch18o4", "assert", expect: "lore_history_shown", value: "40", label: "min"), Step("ch18o4b", "assert", expect: "dossier_lore_history_match"), Step("ch18o4c", "assert", expect: "dossier_pinned", value: "true"), Step("ch18o4d", "assert", expect: "lore_zoom_blocked"), Step("ch18o5", "screenshot", value: "hud-lore-unit-history.png"), Step("ch18o6", "wait", wait: 0.55f), // Banner used to expire at 5s and hide the dossier - pin must outlive that. Step("ch18o7", "wait", wait: 5.5f), Step("ch18o8", "assert", expect: "dossier_pinned", value: "true"), Step("ch18o9", "assert", expect: "idle", value: "false"), Step("ch18o10", "assert", expect: "dossier_lore_history_match"), Step("ch18p", "assert", expect: "hud_no_overlap"), // Resume idle: Follow mode Lore must retarget when dossier focus changes. Step("ch18f1", "spectator", value: "on"), Step("ch18f2", "wait", wait: 0.2f), Step("ch18f3", "spawn", asset: "rabbit", count: 1), Step("ch18f4", "focus", asset: "auto"), Step("ch18f5", "wait", wait: 0.35f), Step("ch18f6", "assert", expect: "lore_follows_focus"), Step("ch18f7", "assert", expect: "lore_follow", value: "true"), // lore_open_focus / dossier_history_open still open Follow mode (books button removed). Step("ch18p0", "lore_close"), Step("ch18p0a", "wait", wait: 0.15f), Step("ch18p0a2", "assert", expect: "idle", value: "true"), Step("ch18p0b", "dossier_history_open"), Step("ch18p0c", "wait", wait: 0.2f), Step("ch18p0d", "assert", expect: "dossier_lore_history_match"), Step("ch18p0e", "assert", expect: "idle", value: "false"), Step("ch18p0f", "assert", expect: "lore_follow", value: "true"), // Empty-history subject must not keep a previous unit's peek lines. Step("ch18p1", "lore_back"), Step("ch18p2", "spawn", asset: "cow", count: 1), Step("ch18p3", "focus", asset: "auto"), Step("ch18p4", "wait", wait: 0.25f), Step("ch18p5", "dossier_history_open"), Step("ch18p6", "wait", wait: 0.25f), Step("ch18p7", "assert", expect: "idle", value: "false"), Step("ch18p8", "assert", expect: "lore_detail", value: "true"), // Fresh spawn still gets just_born Life; empty History UI is gone - expect birth biography. Step("ch18p9", "assert", expect: "dossier_history_shown", value: "1", label: "min", tier: "life"), Step("ch18p10", "assert", expect: "lore_history_shown", value: "1", label: "min"), Step("ch18p11", "assert", expect: "dossier_lore_history_match"), Step("ch18p12", "screenshot", value: "hud-lore-empty-history.png"), Step("ch18p13", "wait", wait: 0.55f), // Fallen (dead) archive: pinned; old age → death place. Step("ch18d1", "lore_back"), Step("ch18d1b", "focus", asset: "auto"), Step("ch18d1c", "wait", wait: 0.2f), Step( "ch18d2", "chronicle_orphan", label: "Oldbone", asset: "skeleton", value: "Fallen", count: 5, expect: "Age", tier: "place"), Step("ch18d3", "lore_pick_fallen", value: "Oldbone"), Step("ch18d4", "wait", wait: 0.25f), Step("ch18d5", "assert", expect: "lore_detail", value: "true"), Step("ch18d6", "assert", expect: "lore_follow", value: "false"), Step("ch18d7", "assert", expect: "lore_history_shown", value: "5", label: "exact"), Step("ch18d7b", "assert", expect: "fallen_focus", value: "place"), Step("ch18d7c", "assert", expect: "death_manner", value: "Old Age"), Step("ch18d7d", "assert", expect: "dossier_contains", value: "Oldbone"), Step("ch18d7e", "assert", expect: "dossier_unit_matches_lore"), Step("ch18d7f", "assert", expect: "dossier_species", value: "skeleton"), Step("ch18d7g", "assert", expect: "dossier_avatar_species"), Step("ch18d7h", "assert", expect: "fallen_place_no_follow"), Step("ch18d8", "screenshot", value: "hud-lore-fallen.png"), Step("ch18d9", "wait", wait: 0.55f), // Fallen slain → place/skull first; Killer button follows the living killer. Step("ch18k1", "lore_back"), Step("ch18k2", "spawn", asset: "human", count: 1), Step("ch18k3", "focus", asset: "auto"), Step("ch18k4", "wait", wait: 0.25f), Step( "ch18k5", "chronicle_orphan", label: "Slainbone", asset: "skeleton", value: "Murder", count: 3, expect: "Weapon", tier: "killer"), Step("ch18k6", "lore_pick_fallen", value: "Slainbone"), Step("ch18k7", "wait", wait: 0.25f), Step("ch18k8", "assert", expect: "lore_detail", value: "true"), Step("ch18k9", "assert", expect: "lore_follow", value: "false"), Step("ch18k10", "assert", expect: "fallen_focus", value: "place"), Step("ch18k10b", "assert", expect: "fallen_place_no_follow"), Step("ch18k11", "assert", expect: "death_manner", value: "Slain"), Step("ch18k11b", "assert", expect: "dossier_contains", value: "Slainbone"), Step("ch18k11c", "assert", expect: "dossier_unit_matches_lore"), Step("ch18k11d", "assert", expect: "dossier_species", value: "skeleton"), Step("ch18k11e", "assert", expect: "dossier_avatar_species"), Step("ch18k11f", "lore_focus_killer"), Step("ch18k11g", "wait", wait: 0.2f), Step("ch18k11h", "assert", expect: "fallen_focus", value: "killer"), Step("ch18k11i", "assert", expect: "fallen_killer_camera"), Step("ch18k11i2", "assert", expect: "dossier_matches_focus"), Step("ch18k11j", "assert", expect: "fallen_killer_banner"), Step("ch18k12", "screenshot", value: "hud-lore-fallen-slain.png"), Step("ch18k13", "wait", wait: 0.55f), // Regression: fallen species portrait must not stick onto the next living dossier. Step("ch18av1", "lore_back"), Step("ch18av1b", "lore_close"), Step("ch18av1c", "spectator", value: "on"), Step("ch18av1d", "wait", wait: 0.2f), Step("ch18av2", "spawn", asset: "human", count: 1), Step("ch18av3", "focus", asset: "auto"), Step("ch18av4", "wait", wait: 0.25f), Step("ch18av5", "assert", expect: "dossier_matches_focus"), Step("ch18av6", "assert", expect: "dossier_species", value: "human"), Step("ch18av7", "assert", expect: "dossier_avatar_live"), Step("ch18av8", "screenshot", value: "hud-dossier-after-fallen.png"), Step("ch18av9", "wait", wait: 0.55f), // Also verify list-pick living after a fallen species portrait. Step("ch18av10", "lore_pick_fallen", value: "Oldbone"), Step("ch18av11", "wait", wait: 0.2f), Step("ch18av12", "assert", expect: "dossier_avatar_species"), Step("ch18av13", "lore_back"), Step("ch18av14", "lore_pick", value: "auto"), Step("ch18av15", "wait", wait: 0.25f), Step("ch18av16", "assert", expect: "dossier_matches_focus"), Step("ch18av17", "assert", expect: "dossier_avatar_live"), Step("ch18av18", "screenshot", value: "hud-dossier-after-fallen-pick.png"), Step("ch18av19", "wait", wait: 0.55f), // Character list browse (pinned archive) - Living tab. Step("ch18q", "lore_tab", value: "living"), Step("ch18q0", "lore_back"), Step("ch18q2", "assert", expect: "lore_tab", value: "living"), Step("ch18q3", "assert", expect: "lore_recent", value: "1", label: "min"), Step("ch18q4", "lore_pick", value: "auto"), Step("ch18q5", "wait", wait: 0.25f), Step("ch18q6", "assert", expect: "idle", value: "false"), Step("ch18q7", "assert", expect: "lore_detail", value: "true"), Step("ch18q7b", "assert", expect: "lore_follow", value: "false"), Step("ch18q7c", "assert", expect: "dossier_unit_matches_lore"), Step("ch18q7d", "assert", expect: "dossier_matches_focus"), Step("ch18q7e", "assert", expect: "dossier_avatar_live"), Step("ch18q7f", "assert", expect: "lore_tab", value: "living"), Step("ch18q8", "screenshot", value: "hud-lore-characters.png"), Step("ch18q9", "wait", wait: 0.55f), // Resume idle: pinned list pick flips back to Follow mode. Step("ch18q10", "spectator", value: "on"), Step("ch18q11", "wait", wait: 0.25f), Step("ch18q11b", "assert", expect: "lore_follow", value: "true"), Step("ch18q11c", "spawn", asset: "sheep", count: 1), Step("ch18q11d", "focus", asset: "auto"), Step("ch18q11e", "wait", wait: 0.35f), Step("ch18q11f", "assert", expect: "lore_follows_focus"), // Fallen tab: newest death first (Slainbone after Oldbone). Step("ch18ft1", "lore_back"), Step("ch18ft2", "lore_tab", value: "fallen"), Step("ch18ft3", "wait", wait: 0.2f), Step("ch18ft4", "assert", expect: "lore_tab", value: "fallen"), Step("ch18ft5", "assert", expect: "fallen_list_before", value: "Slainbone", label: "Oldbone"), // Snapshot stays put until Refresh (new deaths do not auto-rebuild). // Refresh first so live world deaths during the long scenario do not leave stale=true. Step("ch18ft5a", "lore_refresh"), Step("ch18ft5b", "assert", expect: "lore_list_stale", value: "false"), Step( "ch18ft5c", "chronicle_orphan", label: "FreshFallen", asset: "human", value: "Fresh", count: 1, expect: "Age", tier: "place"), Step("ch18ft5d", "assert", expect: "lore_list_stale", value: "true"), Step("ch18ft5e", "assert", expect: "fallen_list_top_not", value: "FreshFallen"), Step("ch18ft5f", "lore_refresh"), Step("ch18ft5g", "assert", expect: "lore_list_stale", value: "false"), Step("ch18ft5h", "assert", expect: "fallen_list_top", value: "FreshFallen"), // Favorite filter on Fallen (same star control as Living). Step( "ch18ft5i", "chronicle_orphan", label: "FavFallen", asset: "human", value: "Fav", count: 1, expect: "Age", tier: "place+fav"), Step("ch18ft5j", "lore_favorites", value: "true"), Step("ch18ft5k", "lore_refresh"), Step("ch18ft5l", "assert", expect: "fallen_list_top", value: "FavFallen"), Step("ch18ft5m", "lore_favorites", value: "false"), Step("ch18ft5n", "lore_refresh"), Step("ch18ft6", "screenshot", value: "hud-lore-fallen-tab.png"), Step("ch18ft7", "wait", wait: 0.55f), Step("ch18q12", "lore_tab", value: "world"), Step("ch19", "assert", expect: "focus", value: "true"), Step("ch20", "assert", expect: "health"), Step("ch25", "assert", expect: "no_bad"), Step("ch99", "snapshot"), }; } /// /// Gravestone stacks: skull → Lore stack list/search/Keep, place focus, Killer, cap. /// private static List GravestoneSmoke() { return new List { Step("gv0", "wait_world"), Step("gv1", "dismiss_windows"), Step("gv2", "fast_timing", value: "true"), Step("gv3", "set_setting", expect: "gravestones_enabled", value: "true"), Step("gv5", "set_setting", expect: "gravestone_max_stacks", value: "1000"), Step("gv6", "grave_clear"), Step("gv7", "spawn", asset: "human", count: 1), Step("gv8", "focus", asset: "auto"), Step("gv9", "wait", wait: 0.25f), // Stack three named deaths on one captured tile (unit can walk between steps). Step("gv10", "grave_force", value: "GraveAlice,GraveBob,GraveCara"), Step("gv13", "assert", expect: "grave_stacks", count: 1, label: "eq"), Step("gv14", "assert", expect: "grave_entries", count: 3), Step("gv15", "grave_open"), Step("gv16", "wait", wait: 0.15f), Step("gv17", "assert", expect: "grave_stack_scope", value: "true"), Step("gv17b", "assert", expect: "lore_detail", value: "false"), Step("gv17c", "assert", expect: "grave_search_layout"), Step("gv18", "grave_search", value: "Bob"), Step("gv19", "wait", wait: 0.1f), Step("gv20", "assert", expect: "grave_search_filtered", count: 1), Step("gv21", "grave_search", value: ""), // Per-grave Keep / Del in Lore chrome (not Mod Settings). Step("gv21b", "grave_set_permanent", value: "true"), Step("gv21c", "assert", expect: "grave_permanent", value: "true"), Step("gv22", "grave_open_lore"), Step("gv23", "wait", wait: 0.25f), Step("gv24", "assert", expect: "lore_detail", value: "true"), Step("gv25", "assert", expect: "fallen_focus", value: "place"), Step("gv26", "assert", expect: "fallen_place_no_follow"), // Outside click / dismiss closes Lore + dossier and turns idle off. Step("gv26b", "inspect_dismiss"), Step("gv26c", "assert", expect: "grave_stack_scope", value: "false"), Step("gv26d", "assert", expect: "lore_detail", value: "false"), Step("gv26d1", "assert", expect: "idle", value: "false"), Step("gv26d2", "assert", expect: "inspect_cleared"), // Delete removes the skull and closes Lore. Step("gv26e0", "grave_clear"), Step("gv26e", "grave_force", value: "GraveDel", count: 1), Step("gv26f", "grave_open"), Step("gv26g", "assert", expect: "grave_stack_scope", value: "true"), Step("gv26h", "grave_delete"), Step("gv26i", "assert", expect: "grave_stacks", value: "0", label: "eq"), Step("gv26j", "assert", expect: "grave_stack_scope", value: "false"), // Killer path: orphan with living killer, place-open then Focus Killer button. Step("gv27", "lore_back"), Step("gv28", "lore_close"), Step("gv28b", "grave_clear"), Step("gv29", "spawn", asset: "human", count: 1), Step("gv30", "focus", asset: "auto"), Step("gv31", "wait", wait: 0.2f), Step( "gv32", "chronicle_orphan", label: "GraveSlain", asset: "skeleton", value: "Murder", count: 1, expect: "Weapon", tier: "killer"), Step("gv33", "grave_open_lore", value: "GraveSlain"), Step("gv34", "wait", wait: 0.2f), Step("gv34b", "assert", expect: "grave_death_subtitle", value: "Slain by"), Step("gv35", "assert", expect: "fallen_focus", value: "place"), Step("gv36", "lore_focus_killer"), Step("gv37", "wait", wait: 0.2f), Step("gv38", "assert", expect: "fallen_focus", value: "killer"), Step("gv39", "assert", expect: "fallen_killer_camera"), Step("gv39b", "assert", expect: "dossier_matches_focus"), // Cap eviction: shrink max stacks and flood tiles via orphans at offset positions. Step("gv40", "lore_close"), Step("gv41", "set_setting", expect: "gravestone_max_stacks", value: "2"), Step("gv42", "grave_clear"), Step("gv43", "grave_force", value: "CapA", count: 1, tier: "0,0"), Step("gv44", "grave_force", value: "CapB", count: 1, tier: "2,0"), Step("gv45", "grave_force", value: "CapC", count: 1, tier: "4,0"), Step("gv46", "assert", expect: "grave_stacks", count: 2, label: "eq"), Step("gv47", "set_setting", expect: "gravestone_max_stacks", value: "1000"), Step("gv48", "grave_clear"), Step("gv50", "grave_force", value: "PermOne", count: 1), Step("gv50b", "grave_open"), Step("gv50c", "grave_set_permanent", value: "true"), Step("gv50d", "assert", expect: "grave_permanent", value: "true"), Step("gv51", "assert", expect: "grave_stacks", count: 1, label: "min"), Step("gv52", "grave_set_permanent", value: "false"), // Live death → gravestone Step("gv53", "grave_clear"), Step("gv54", "spawn", asset: "sheep", count: 1), Step("gv55", "focus", asset: "auto"), Step("gv56", "wait", wait: 0.2f), Step("gv57", "kill_focus"), Step("gv58", "wait", wait: 0.35f), Step("gv59", "assert", expect: "grave_stacks", count: 1, label: "min"), Step("gv60", "grave_clear"), Step("gv61", "screenshot", value: "hud-gravestones.png"), Step("gv62", "wait", wait: 0.4f), Step("gv63", "fast_timing", value: "false"), Step("gv64", "snapshot"), }; } private static HarnessCommand Nested(string id, string scenario) { return Step(id, "scenario", value: scenario, expect: "keep"); } private static HarnessCommand Step( string id, string action, string value = "", string asset = "", string label = "", string tier = "", string expect = "", int count = 1, float wait = 0f) { return new HarnessCommand { id = id, action = action, value = value, asset = asset, label = label, tier = tier, expect = expect, count = count, wait = wait }; } }