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 "spectacle_mage_focus":
case "evil_mage_focus":
case "mage_focus":
return SpectacleMageFocus();
case "combat_stability_live":
case "combat_live_proof":
return CombatStabilityLive();
case "sticky_variety_valve":
case "variety_valve":
return StickyVarietyValve();
case "combat_hold_until_end":
case "combat_hold":
case "soft_duel_yield":
case "duel_yield":
return CombatHoldUntilEnd();
case "story_aftermath_combat":
case "story_aftermath":
return StoryAftermathCombat();
case "story_aftermath_prefers_mc":
return StoryAftermathPrefersMc();
case "story_recover_follow_mc":
return StoryRecoverFollowMc();
case "story_park_trim_keeps_mc":
return StoryParkTrimKeepsMc();
case "story_crisis_beats_mc_aftermath":
case "crisis_beats_mc_aftermath":
return StoryCrisisBeatsMcAftermath();
case "story_pick_opens_stranger_arc":
case "pick_opens_stranger_arc":
return StoryPickOpensStrangerArc();
case "story_aftermath_cast_noise":
case "soft_duel_aftermath_noise":
return StoryAftermathCastNoise();
case "reason_duel_principal":
case "story_reason_principal":
return ReasonDuelPrincipal();
case "reason_life_principal":
case "story_reason_life":
return ReasonLifePrincipal();
case "story_intimacy_cool":
case "intimacy_cool":
return StoryIntimacyCool();
case "story_settlement_cool":
case "settlement_cool":
return StorySettlementCool();
case "story_kill_cool":
case "kill_cool":
return StoryKillCool();
case "story_courtship_cool":
case "courtship_cool":
case "story_love_cool":
return StoryCourtshipCool();
case "combat_wiped_side":
case "story_wiped_side":
return CombatWipedSide();
case "story_decision_intent_cool":
case "decision_intent_cool":
return StoryDecisionIntentCool();
case "story_construction_cool":
case "construction_cool":
return StoryConstructionCool();
case "story_status_fx_cool":
case "status_fx_cool":
return StoryStatusFxCool();
case "story_resume_cooled":
case "resume_cooled":
return StoryResumeCooled();
case "story_worldlog_meta_cool":
case "worldlog_meta_cool":
return StoryWorldLogMetaCool();
case "story_rest_cool":
case "rest_cool":
return StoryRestCool();
case "story_decision_intent_live":
case "decision_intent_live":
return StoryDecisionIntentLive();
case "story_aftermath_war":
return StoryAftermathWar();
case "story_crisis_hygiene":
case "crisis_hygiene":
return StoryCrisisHygiene();
case "story_crisis_war":
case "crisis_war":
return StoryCrisisWar();
case "story_crisis_disaster":
case "crisis_disaster":
return StoryCrisisDisaster();
case "story_aftermath_partner_truth":
case "aftermath_partner_truth":
return StoryAftermathPartnerTruth();
case "story_near_tie":
case "variety_near_tie":
return StoryNearTie();
case "story_arc_combat":
return StoryArcCombat();
case "story_lore_pause_keeps":
case "lore_pause_keeps_story":
return StoryLorePauseKeeps();
case "story_board_park_resume":
case "story_park_resume":
return StoryBoardParkResume();
case "story_cross_arc_correlation":
return StoryCrossArcCorrelation();
case "narrative_scheduler_policy":
case "narrative_interrupt_policy":
return NarrativeSchedulerPolicy();
case "narrative_episode_grammar":
case "narrative_episode_no_forced_footage":
return NarrativeEpisodeGrammar();
case "narrative_ensemble_balance":
case "narrative_prolific_mc_guard":
case "narrative_dormant_reintroduction":
return NarrativeEnsembleBalance();
case "narrative_prose_redundancy":
case "narrative_relationship_prose":
return NarrativeProseRedundancy();
case "narrative_presentation_coverage":
case "narrative_typed_intake":
return NarrativePresentationCoverage();
case "narrative_relationship_ingestion":
case "narrative_ingestion_idempotent":
return NarrativeRelationshipIngestion();
case "narrative_canonical_ingestion":
case "narrative_ingestion_matrix":
return NarrativeCanonicalIngestion();
case "narrative_persistence_roundtrip":
case "narrative_persistence_no_replay":
return NarrativePersistenceRoundTrip();
case "narrative_persistence_corrupt":
case "narrative_persistence_unknown_version":
return NarrativePersistenceInvalid();
case "narrative_camera_cutover":
case "story_first_camera":
return NarrativeCameraCutover();
case "narrative_story_inputs":
case "story_potential_roster":
return NarrativeStoryInputs();
case "narrative_story_first_soak":
case "story_first_soak":
return NarrativeStoryFirstSoak();
case "narrative_scheduler_health":
case "scheduler_health":
return NarrativeSchedulerHealth();
case "narrative_catalog":
case "narrative_catalog_policy":
return NarrativeCatalogPolicy();
case "narrative_natural_soak":
case "story_first_natural_soak":
return NarrativeNaturalSoak();
case "narrative_multithread_stress":
case "story_first_multithread":
return NarrativeMultiThreadStress();
case "narrative_editorial_replay":
case "story_first_replay_guard":
return NarrativeEditorialReplay();
case "story_arc_preempt_resume":
return StoryArcPreemptResume();
case "story_ledger_revisit":
// Retired with CharacterLedger - soft MC bias lives in saga_* scenarios.
return SagaCameraPrefersMc();
case "saga_roster_diversity":
return SagaRosterDiversity();
case "saga_roster_cap_five":
return SagaRosterCapFive();
case "saga_rare_antagonist_acceptance":
return SagaRareAntagonistAcceptance();
case "saga_connection_dossier":
return SagaConnectionDossier();
case "saga_camera_prefers_mc":
return SagaCameraPrefersMc();
case "saga_replace_on_death":
return SagaReplaceOnDeath();
case "saga_prefer_soft_bias":
return SagaPreferSoftBias();
case "saga_wins_moderate_gap":
return SagaWinsModerateGap();
case "saga_combat_follow_mc":
return SagaCombatFollowMc();
case "saga_cast_soft_bias":
return SagaCastSoftBias();
case "saga_cross_mc_light":
return SagaCrossMcLight();
case "saga_cross_mc_wins_gap":
return SagaCrossMcWinsGap();
case "saga_cross_mc_kill":
return SagaCrossMcKill();
case "saga_rival_not_single_kill":
return SagaRivalNotSingleKill();
case "saga_legacy_combat_summary":
return SagaLegacyCombatSummary();
case "saga_rival_rematch":
return SagaRivalRematch();
case "saga_prefer_cuts_ambient":
return SagaPreferCutsAmbient();
case "saga_overview_has_mc":
return SagaOverviewHasMc();
case "saga_snapshot_popover":
case "saga_adaptive_dossier":
return SagaAdaptiveDossier();
case "saga_memory_survives":
return SagaMemorySurvives();
case "saga_war_memory_both_sides":
return SagaWarMemoryBothSides();
case "saga_plot_cast_members":
return SagaPlotCastMembers();
case "saga_cast_kin_non_pack_lens":
return SagaCastKinNonPackLens();
case "saga_stake_from_role_live":
return SagaStakeFromRoleLive();
case "saga_legacy_quality":
return SagaLegacyQuality();
case "saga_session_reset":
return SagaSessionReset();
case "saga_relation_cache_revision":
return SagaRelationCacheRevision();
case "saga_cast_stable_snapshot":
return SagaCastStableSnapshot();
case "saga_live_child_reverse":
return SagaLiveChildReverse();
case "saga_hover_read_pause":
return SagaHoverReadPause();
case "saga_admit_roles":
return SagaAdmitRoles();
case "saga_replace_weaker":
return SagaReplaceWeaker();
case "saga_rail_active_highlight":
return SagaRailActiveHighlight();
case "saga_rail_live_animal_frame":
return SagaRailLiveAnimalFrame();
case "saga_rail_prefer_click":
return SagaRailPreferClick();
case "saga_soft_fill_no_pack":
return SagaSoftFillNoPack();
case "caption_character_beat":
case "caption_mc_identity":
return CaptionCharacterBeat();
case "caption_status_banner_live":
return CaptionStatusBannerLive();
case "caption_prose_voice":
return CaptionProseVoice();
case "story_spine_scoped":
return StorySpineScoped();
case "story_family_variety":
return StoryFamilyVariety();
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_hold", "combat_hold_until_end"),
Nested("reg_combat_stability", "combat_stability_live"),
Nested("reg_story_aftermath", "story_aftermath_combat"),
Nested("reg_story_aftermath_mc", "story_aftermath_prefers_mc"),
Nested("reg_story_recover_mc", "story_recover_follow_mc"),
Nested("reg_story_park_trim_mc", "story_park_trim_keeps_mc"),
Nested("reg_story_crisis_beats_mc", "story_crisis_beats_mc_aftermath"),
Nested("reg_story_pick_stranger", "story_pick_opens_stranger_arc"),
Nested("reg_story_aftermath_noise", "story_aftermath_cast_noise"),
Nested("reg_story_aftermath_partner_truth", "story_aftermath_partner_truth"),
Nested("reg_reason_duel_principal", "reason_duel_principal"),
Nested("reg_reason_life_principal", "reason_life_principal"),
Nested("reg_story_intimacy_cool", "story_intimacy_cool"),
Nested("reg_story_settlement_cool", "story_settlement_cool"),
Nested("reg_story_kill_cool", "story_kill_cool"),
Nested("reg_story_courtship_cool", "story_courtship_cool"),
Nested("reg_story_decision_intent_cool", "story_decision_intent_cool"),
Nested("reg_story_construction_cool", "story_construction_cool"),
Nested("reg_story_status_fx_cool", "story_status_fx_cool"),
Nested("reg_story_resume_cooled", "story_resume_cooled"),
Nested("reg_story_worldlog_meta_cool", "story_worldlog_meta_cool"),
Nested("reg_story_rest_cool", "story_rest_cool"),
Nested("reg_story_decision_intent_live", "story_decision_intent_live"),
Nested("reg_combat_wiped_side", "combat_wiped_side"),
Nested("reg_story_near_tie", "story_near_tie"),
Nested("reg_story_arc", "story_arc_combat"),
Nested("reg_story_lore_pause", "story_lore_pause_keeps"),
Nested("reg_story_board_park", "story_board_park_resume"),
Nested("reg_story_cross_arc", "story_cross_arc_correlation"),
Nested("reg_narrative_scheduler", "narrative_scheduler_policy"),
Nested("reg_narrative_camera", "narrative_camera_cutover"),
Nested("reg_narrative_inputs", "narrative_story_inputs"),
Nested("reg_narrative_soak", "narrative_story_first_soak"),
Nested("reg_narrative_catalog", "narrative_catalog_policy"),
Nested("reg_narrative_natural", "narrative_natural_soak"),
Nested("reg_narrative_multithread", "narrative_multithread_stress"),
Nested("reg_narrative_replay", "narrative_editorial_replay"),
Nested("reg_caption_status_banner", "caption_status_banner_live"),
Nested("reg_saga_relation_revision", "saga_relation_cache_revision"),
Nested("reg_saga_cast_snapshot", "saga_cast_stable_snapshot"),
Nested("reg_story_aftermath_war", "story_aftermath_war"),
Nested("reg_story_crisis_hygiene", "story_crisis_hygiene"),
Nested("reg_story_spine", "story_spine_scoped"),
Nested("reg_story_family", "story_family_variety"),
Nested("reg_saga_camera", "saga_camera_prefers_mc"),
Nested("reg_saga_prefer", "saga_prefer_soft_bias"),
Nested("reg_saga_moderate_gap", "saga_wins_moderate_gap"),
Nested("reg_saga_combat_follow", "saga_combat_follow_mc"),
Nested("reg_saga_cast_bias", "saga_cast_soft_bias"),
Nested("reg_saga_cross_light", "saga_cross_mc_light"),
Nested("reg_saga_cross_gap", "saga_cross_mc_wins_gap"),
Nested("reg_saga_cross_kill", "saga_cross_mc_kill"),
Nested("reg_saga_rival_kill", "saga_rival_not_single_kill"),
Nested("reg_saga_rival_rematch", "saga_rival_rematch"),
Nested("reg_saga_ambient_cut", "saga_prefer_cuts_ambient"),
Nested("reg_saga_overview", "saga_overview_has_mc"),
Nested("reg_saga_snapshot", "saga_adaptive_dossier"),
Nested("reg_saga_memory", "saga_memory_survives"),
Nested("reg_saga_war_memory", "saga_war_memory_both_sides"),
Nested("reg_saga_plot_cast", "saga_plot_cast_members"),
Nested("reg_saga_cast_kin", "saga_cast_kin_non_pack_lens"),
Nested("reg_saga_stake_role", "saga_stake_from_role_live"),
Nested("reg_saga_legacy_quality", "saga_legacy_quality"),
Nested("reg_saga_legacy_combat", "saga_legacy_combat_summary"),
Nested("reg_saga_session_reset", "saga_session_reset"),
Nested("reg_saga_hover_pause", "saga_hover_read_pause"),
Nested("reg_saga_rare_antagonist", "saga_rare_antagonist_acceptance"),
Nested("reg_saga_diversity", "saga_roster_diversity"),
Nested("reg_saga_death", "saga_replace_on_death"),
Nested("reg_saga_admit", "saga_admit_roles"),
Nested("reg_saga_replace", "saga_replace_weaker"),
Nested("reg_saga_rail_active", "saga_rail_active_highlight"),
Nested("reg_saga_rail_prefer", "saga_rail_prefer_click"),
Nested("reg_saga_soft_fill", "saga_soft_fill_no_pack"),
Nested("reg_caption_beat", "caption_character_beat"),
Nested("reg_caption_prose", "caption_prose_voice"),
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", asset: "confused"),
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", asset: "confused"),
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", asset: "surprised"),
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("c30a", "status_apply", asset: "auto", value: "invincible"),
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"),
// Isolate the tier contract from production rolling novelty/frequency history.
Step("dt12", "interest_variety_clear"),
// 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).
// Keep this decisively above the current action after same-subject variety penalties.
Step("dl20", "trigger_interest", asset: "dragon", label: "EpicWin", tier: "Epic", value: "evt=250"),
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("ih7b", "status_apply", asset: "auto", value: "invincible"),
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"),
// --- Epic replacement: shown moment beats are cooled, not parked for replay ---
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"),
// Queue through the normal selector so interruption/resume bookkeeping is exercised.
Step("dg35", "interest_inject", asset: "dragon", label: "EpicInterrupt", tier: "Epic",
expect: "epic_interrupt", value: "lead=event;evt=250"),
Step("dg36", "director_run", wait: 0.8f),
Step("dg37", "assert", expect: "tip_contains", value: "EpicInterrupt"),
Step("dg38", "assert", expect: "interrupted_key", value: "none"),
// Max-cap the Epic scene (fast timing caps ~8s); the cooled action does not replay.
Step("dg40", "age_current", wait: 9f),
Step("dg41", "director_run", wait: 1.2f),
Step("dg42", "assert", expect: "tip_not_contains", value: "HoldResume"),
Step("dg43", "assert", expect: "tip_not_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("dg109c", "status_apply", asset: "human", value: "invincible"),
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("cp6b", "spawn", asset: "human"),
Step("cp7", "focus", asset: "human"),
Step("cp7b", "status_apply", asset: "auto", value: "invincible"),
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"),
};
}
///
/// Idle evil mages must win ambient fill over civ chores (soak: mages never focused).
/// Also gates mage drop powers as camera Signal.
///
private static List SpectacleMageFocus()
{
return new List
{
Step("smf0", "dismiss_windows"),
Step("smf1", "wait_world"),
Step("smf2", "set_setting", expect: "enabled", value: "true"),
Step("smf3", "fast_timing", value: "true"),
Step("smf4", "assert", expect: "library_ambient_policy_ok"),
// One civ chore unit + one mage - polluted leftover wars must not own the pick.
Step("smf5", "spawn", asset: "human", count: 1),
Step("smf6", "spawn", asset: "evil_mage", count: 1),
Step("smf7", "spectator", value: "off"),
Step("smf8", "spectator", value: "on"),
Step("smf9", "interest_end_session"),
Step("smf10", "interest_story_clear"),
Step("smf11", "interest_variety_clear"),
Step("smf12", "interest_expire_pending", value: ""),
Step("smf12b", "interest_expire_pending", value: "war"),
Step("smf12c", "interest_expire_pending", value: "combat"),
Step("smf13", "pick_unit", asset: "evil_mage"),
Step("smf14", "focus", asset: "evil_mage"),
Step("smf15", "interest_end_session"),
// Quiet map: ambient fill should land on the spectacle, not a random human.
Step("smf16", "director_run", wait: 2.0f),
Step("smf17", "assert", expect: "unit_asset", asset: "evil_mage"),
Step("smf18", "assert", expect: "has_focus", value: "true"),
Step("smf90", "fast_timing", value: "false"),
Step("smf99", "snapshot"),
};
}
///
/// 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"),
// Main-camera policy requires combat to touch an MC or exact MC relation.
Step("cf10a", "interest_saga_clear"),
Step("cf10b", "saga_force_admit_focus"),
// 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 and Duel tip order (A↔B hold).
Step("cf21p", "remember_related"),
Step("cf21p2", "remember_tip"),
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("cf21v2", "assert", expect: "tip_same"),
// Past duel MaxWatch while scrap still hot: must not max_cap into a new partner.
Step("cf21y", "combat_wire_attack_sides", asset: "human", value: "wolf", expect: "pair"),
Step("cf21y2", "remember_tip"),
Step("cf21y3", "remember_related"),
Step("cf21z", "age_current", value: "30"),
Step("cf21z2", "director_run", wait: 0.55f),
Step("cf21z3", "combat_maintain_focus"),
Step("cf21z4", "assert", expect: "related_same"),
// A real local mob may grow during the aged hold. Preserve the locked pair, but
// accept a semantic Duel → Skirmish/Battle/Mass escalation as a valid reframe.
Step("cf21z5", "assert", expect: "tip_same_or_collective_escalation"),
// First partner dies mid-scrap: lock stays; tip must not hop to a new Duel name.
Step("cf21aa", "remember_related"),
Step("cf21ab", "combat_kill_related"),
Step("cf21ae", "director_run", wait: 0.55f),
Step("cf21af", "combat_maintain_focus"),
Step("cf21ag", "assert", expect: "related_same"),
Step("cf21ah", "assert", expect: "tip_matches_any", value: "is fighting|Skirmish -|Battle -|Mass -"),
Step("cf21ai", "assert", expect: "tip_not_contains", value: "Duel"),
Step("cf21x", "assert", expect: "tip_matches_any", value: "is fighting|Skirmish|Battle|Mass|fighting| vs "),
// Re-seed a living wolf for death-handoff (original partner was killed above).
Step("cf21aj", "spawn", asset: "wolf", count: 1),
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("cf39e", "focus", asset: "human"),
Step("cf39f", "saga_force_admit_focus"),
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 a huge civ mob holds the mage, not a king.
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("cf48d2", "focus", asset: "evil_mage"),
Step("cf48d3", "saga_consider_admit_focus"),
Step("cf48e", "interest_combat_session", asset: "evil_mage", value: "human", expect: "cf_mage"),
// Soak shape: 1 spectacle vs a massed civ side (not a tiny 1v8 skirmish).
Step("cf48f", "combat_ensemble_escalate", value: "24"),
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 -"),
Step("cf48s2", "assert", expect: "tip_not_contains", value: "Duel"),
// 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("cf50e", "focus", asset: "human"),
Step("cf50f", "saga_force_admit_focus"),
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 "),
// Natural cold: locked pair sleeps (Moving shape). Must go inactive / leave Duel
// without mark_combat_cold - ambient nearby scraps must not refresh hotness.
Step("cf29b", "combat_isolate_pair", value: "20"),
Step("cf30", "interest_combat_session", asset: "human", value: "wolf", expect: "cf_cold"),
Step("cf30w", "combat_wire_attack_sides", asset: "human", value: "wolf", expect: "pair"),
Step("cf31", "assert", expect: "tip_matches_any", value: "Duel|fighting| vs "),
Step("cf31a", "assert", expect: "interest_session_active", value: "true"),
Step("cf31b", "interest_release_force"),
Step("cf31b2", "combat_isolate_pair", value: "20"),
Step("cf31c", "combat_disengage_pair"),
Step("cf31c2", "status_apply", value: "sleeping", label: "120"),
Step("cf31c3", "focus", asset: "wolf"),
Step("cf31c4", "status_apply", value: "sleeping", label: "120"),
Step("cf31c5", "combat_disengage_pair"),
Step("cf31c6", "focus", asset: "human"),
Step("cf31c7", "assert", expect: "focus_fighting", value: "false"),
Step("cf31c8", "assert", expect: "related_fighting", value: "false"),
// Re-stamp LastSeenAt after the last assert tick. The simulation can immediately
// reacquire a target, so freeze the already-proven cold boundary before cleanup.
Step("cf31c9", "combat_disengage_pair"),
Step("cf31c9b", "interest_mark_combat_cold"),
Step("cf31c10", "age_current", value: "70"),
Step("cf31h", "director_run", wait: 1.0f),
// Cold duel key must be gone. A later natural scrap may still say Duel - that is
// fine. Living partner: must never mint false "stands over the fallen".
Step("cf31i", "assert", expect: "interest_no_key", value: "cf_cold"),
Step("cf31k2", "assert", expect: "tip_not_contains", value: "stands over"),
// Force-cold tip scrub still required when a combat session remains.
Step("cf32", "interest_release_force"),
Step("cf33", "interest_mark_combat_cold"),
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"),
};
}
///
/// Live Mass combat holds through story peers; after the scrap goes cold, variety valve may cut.
///
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: 17f),
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),
// Live scrap: hold through the peer (see who wins).
Step("sv26", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
Step("sv27", "assert", expect: "tip_not_contains", value: "falls in love"),
// Fight over → story peer may cut. Clear discovery noise so the lover is the peer under test.
Step("sv28", "interest_expire_pending", value: ""),
Step("sv29", "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("sv30", "interest_mark_combat_cold"),
Step("sv31", "director_run", wait: 1.2f),
Step("sv32", "assert", expect: "tip_matches_any", value: "love|lover|falls in|in scene"),
Step("sv33", "assert", expect: "tip_not_contains", value: "Mass -"),
Step("sv34", "assert", expect: "tip_asset", value: "set_lover"),
Step("sv90", "fast_timing", value: "false"),
Step("sv99", "snapshot"),
};
}
///
/// Live Duels hold through ordinary peers; catalog epic-world urgency may cut mid-fight;
/// after the scrap goes cold, Story peers may cut.
///
private static List CombatHoldUntilEnd()
{
return new List
{
Step("ch0", "dismiss_windows"),
Step("ch1", "wait_world"),
Step("ch2", "set_setting", expect: "enabled", value: "true"),
Step("ch3", "fast_timing", value: "true"),
Step("ch4", "spawn", asset: "human", count: 1),
Step("ch5", "spawn", asset: "wolf", count: 1),
Step("ch6", "spectator", value: "off"),
Step("ch7", "spectator", value: "on"),
Step("ch8", "pick_unit", asset: "human"),
Step("ch9", "focus", asset: "human"),
Step("ch10", "interest_end_session"),
// Wire attack so CombatStillActive stays hot; invincible so the pair survives the assert.
// Isolate so leftover world packs cannot escalate a 1v1 into Battle mid-hold.
Step("ch20", "interest_combat_session", asset: "human", value: "wolf", expect: "ch_pack"),
Step("ch20b", "combat_isolate_pair", value: "20"),
Step("ch21b", "status_apply", asset: "human", value: "invincible"),
Step("ch21c", "status_apply", asset: "wolf", value: "invincible"),
Step("ch22", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ch23", "assert", expect: "tip_not_contains", value: "Mass -"),
Step("ch29", "interest_expire_pending", value: ""),
Step("ch30", "age_current", wait: 9f),
Step("ch30b", "combat_maintain_focus"),
Step("ch30c", "assert", expect: "tip_matches_any", value: "Duel -"),
// Ordinary Story/Action peer: hold the scrap.
Step("ch31", "interest_inject", asset: "human", label: "falls in love", tier: "Action",
expect: "ch_lover", value: "lead=event;evt=78;char=10;force=false;ttl=60"),
Step("ch32", "assert", expect: "pending_contains", value: "ch_lover"),
Step("ch33", "director_run", wait: 1.2f),
Step("ch34", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ch35", "assert", expect: "tip_not_contains", value: "falls in love"),
// Leadership death (king_killed=88 < epic floor) must still cut mid-scrap
// (soak: king slaying dropped while parked on a Duel).
Step("ch35b", "interest_expire_pending", value: "ch_lover"),
Step("ch35c", "world_log_feed", asset: "king_killed"),
Step("ch35d", "assert", expect: "pending_contains", value: "king_killed"),
Step("ch35e", "director_run", wait: 1.2f),
Step("ch35f", "assert", expect: "tip_matches_any", value: "King killed|king_killed|King"),
Step("ch35g", "assert", expect: "tip_not_contains", value: "Duel -"),
// Extremely urgent (catalog epic-world EventStrength) may cut mid-fight.
Step("ch36", "interest_end_session"),
Step("ch36b", "interest_expire_pending", value: ""),
Step("ch36c", "interest_combat_session", asset: "human", value: "wolf", expect: "ch_pack_u"),
Step("ch36d", "combat_isolate_pair", value: "20"),
Step("ch36e", "status_apply", asset: "human", value: "invincible"),
Step("ch36f", "status_apply", asset: "wolf", value: "invincible"),
Step("ch36g", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ch37", "interest_inject", asset: "human", label: "UrgentCut", tier: "Epic",
expect: "ch_urgent", value: "lead=event;evt=150;char=10;force=false;ttl=60"),
Step("ch38", "assert", expect: "pending_contains", value: "ch_urgent"),
Step("ch39", "director_run", wait: 1.2f),
Step("ch39b", "assert", expect: "tip_contains", value: "UrgentCut"),
Step("ch39c", "assert", expect: "tip_not_contains", value: "Duel -"),
// Fresh scrap: after cold, ordinary peers may cut again.
Step("ch40", "interest_end_session"),
Step("ch41", "interest_expire_pending", value: ""),
Step("ch42", "interest_combat_session", asset: "human", value: "wolf", expect: "ch_pack2"),
Step("ch42b", "combat_isolate_pair", value: "20"),
Step("ch42c", "status_apply", asset: "human", value: "invincible"),
Step("ch42d", "status_apply", asset: "wolf", value: "invincible"),
Step("ch43", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ch44", "interest_inject", asset: "human", label: "falls in love", tier: "Action",
expect: "ch_lover2", value: "lead=event;evt=78;char=10;force=false;ttl=60"),
Step("ch45", "director_run", wait: 1.2f),
Step("ch46", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ch47", "interest_mark_combat_cold"),
Step("ch48", "director_run", wait: 1.2f),
Step("ch49", "assert", expect: "tip_matches_any", value: "love|lover|falls in|in scene"),
Step("ch50", "assert", expect: "tip_not_contains", value: "Duel -"),
Step("ch51", "assert", expect: "tip_asset", value: "set_lover"),
Step("ch90", "fast_timing", value: "false"),
Step("ch99", "snapshot"),
};
}
///
/// Combat cold injects story aftermath (survivor tip) when no stronger peer is pending.
///
private static List StoryAftermathCombat()
{
return new List
{
Step("sa0", "dismiss_windows"),
Step("sa1", "wait_world"),
Step("sa2", "set_setting", expect: "enabled", value: "true"),
Step("sa3", "fast_timing", value: "true"),
Step("sa4", "spawn", asset: "human", count: 1),
Step("sa5", "spawn", asset: "wolf", count: 1),
Step("sa6", "spectator", value: "off"),
Step("sa7", "spectator", value: "on"),
Step("sa8", "pick_unit", asset: "human"),
Step("sa9", "focus", asset: "human"),
Step("sa10", "interest_end_session"),
Step("sa10b", "interest_story_clear"),
Step("sa10c", "interest_variety_clear"),
Step("sa10d", "interest_expire_pending", value: ""),
Step("sa20", "interest_combat_session", asset: "human", value: "wolf", expect: "sa_pack"),
Step("sa20b", "combat_isolate_pair", value: "20"),
Step("sa21", "status_apply", asset: "human", value: "invincible"),
Step("sa23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("sa24", "interest_expire_pending", value: ""),
// Survivor aftermath requires a real fallen partner - kill the wolf first.
Step("sa24b", "combat_kill_related"),
Step("sa25", "age_current", wait: 10f),
Step("sa26", "interest_mark_combat_cold"),
// Inject runs on director tick after cold (not on mark alone).
Step("sa27", "director_run", wait: 1.5f),
Step("sa28", "assert", expect: "tip_matches_any", value: "stands over|mourns|lingers"),
Step("sa29", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sa30", "assert", expect: "tip_not_contains", value: "Duel -"),
Step("sa31", "assert", expect: "story_phase", value: "Aftermath"),
Step("sa31b", "assert", expect: "story_spine", value: "Duel · Aftermath"),
// Drop Follow mid-aftermath - cast recovery must keep the story tip.
Step("sa32", "interest_drop_follow"),
Step("sa33", "director_run", wait: 0.8f),
Step("sa34", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sa35", "assert", expect: "story_phase", value: "Aftermath"),
Step("sa90", "fast_timing", value: "false"),
Step("sa99", "snapshot"),
};
}
/// Aftermath follow prefers a roster MC among living theater candidates.
private static List StoryAftermathPrefersMc()
{
return new List
{
Step("sam0", "dismiss_windows"),
Step("sam1", "wait_world"),
Step("sam2", "set_setting", expect: "enabled", value: "true"),
Step("sam3", "fast_timing", value: "true"),
Step("sam4", "spawn", asset: "human", count: 1),
Step("sam5", "spawn", asset: "wolf", count: 1),
Step("sam6", "spectator", value: "off"),
Step("sam7", "spectator", value: "on"),
Step("sam8", "pick_unit", asset: "human"),
Step("sam9", "focus", asset: "human"),
Step("sam10", "interest_end_session"),
Step("sam11", "interest_story_clear"),
Step("sam12", "interest_saga_clear"),
Step("sam13", "saga_force_admit_focus"),
Step("sam14", "assert", expect: "saga_has_focus"),
Step("sam15", "interest_expire_pending", value: ""),
Step("sam20", "interest_combat_session", asset: "human", value: "wolf", expect: "sam_pack"),
Step("sam21", "combat_isolate_pair", value: "20"),
Step("sam22", "status_apply", asset: "human", value: "invincible"),
Step("sam23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("sam24", "combat_kill_related"),
Step("sam25", "age_current", wait: 10f),
Step("sam26", "interest_mark_combat_cold"),
Step("sam27", "director_run", wait: 1.5f),
Step("sam28", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sam29", "assert", expect: "story_phase", value: "Aftermath"),
Step("sam30", "assert", expect: "saga_has_focus"),
Step("sam90", "fast_timing", value: "false"),
Step("sam99", "snapshot"),
};
}
/// Cast recovery after follow drop prefers a living roster MC on the arc.
private static List StoryRecoverFollowMc()
{
return new List
{
Step("srm0", "dismiss_windows"),
Step("srm1", "wait_world"),
Step("srm2", "set_setting", expect: "enabled", value: "true"),
Step("srm3", "fast_timing", value: "true"),
Step("srm4", "spawn", asset: "human", count: 1),
Step("srm5", "spawn", asset: "wolf", count: 1),
Step("srm6", "spectator", value: "off"),
Step("srm7", "spectator", value: "on"),
Step("srm8", "pick_unit", asset: "human"),
Step("srm9", "focus", asset: "human"),
Step("srm10", "interest_end_session"),
Step("srm11", "interest_story_clear"),
Step("srm12", "interest_saga_clear"),
Step("srm13", "saga_force_admit_focus"),
Step("srm14", "interest_expire_pending", value: ""),
Step("srm20", "interest_combat_session", asset: "human", value: "wolf", expect: "srm_pack"),
Step("srm21", "combat_isolate_pair", value: "20"),
Step("srm22", "status_apply", asset: "human", value: "invincible"),
Step("srm23", "combat_kill_related"),
Step("srm24", "age_current", wait: 10f),
Step("srm25", "interest_mark_combat_cold"),
Step("srm26", "director_run", wait: 1.5f),
Step("srm27", "assert", expect: "tip_asset", value: "aftermath_"),
Step("srm28", "interest_drop_follow"),
Step("srm29", "director_run", wait: 0.8f),
Step("srm30", "assert", expect: "tip_asset", value: "aftermath_"),
Step("srm31", "assert", expect: "story_phase", value: "Aftermath"),
Step("srm32", "assert", expect: "saga_has_focus"),
Step("srm90", "fast_timing", value: "false"),
Step("srm99", "snapshot"),
};
}
///
/// Parked-board trim keeps Prefer/MC arcs when over cap (anonymous scraps drop first).
///
private static List StoryParkTrimKeepsMc()
{
return new List
{
Step("spt0", "dismiss_windows"),
Step("spt1", "wait_world"),
Step("spt2", "set_setting", expect: "enabled", value: "true"),
Step("spt3", "fast_timing", value: "true"),
Step("spt4", "spawn", asset: "human", count: 2),
Step("spt5", "spectator", value: "off"),
Step("spt6", "spectator", value: "on"),
Step("spt7", "pick_unit", asset: "human"),
Step("spt8", "focus", asset: "human"),
Step("spt9", "interest_end_session"),
Step("spt10", "interest_story_clear"),
Step("spt11", "interest_saga_clear"),
Step("spt12", "saga_force_admit_focus"),
Step("spt20", "interest_war_session", asset: "human", expect: "spt_war_mc"),
Step("spt21", "director_run", wait: 0.6f),
Step("spt22", "assert", expect: "story_phase", value: "Climax"),
Step("spt23", "interest_force_session", asset: "human",
label: "ParkAway", tier: "Epic", expect: "spt_park",
value: "lead=event;evt=150"),
Step("spt24", "assert", expect: "story_parked_count", value: "1", label: "min"),
Step("spt25", "assert", expect: "saga_has_focus", value: "true"),
Step("spt90", "fast_timing", value: "false"),
Step("spt99", "snapshot"),
};
}
///
/// Non-goal gate: crisis ladder still beats Prefer/MC short-arc aftermath.
/// War crisis tip must cut a live MC aftermath; aftermath must not reclaim the camera.
///
private static List StoryCrisisBeatsMcAftermath()
{
return new List
{
Step("scm0", "dismiss_windows"),
Step("scm1", "wait_world"),
Step("scm2", "set_setting", expect: "enabled", value: "true"),
Step("scm3", "fast_timing", value: "true"),
Step("scm4", "spawn", asset: "human", count: 2),
Step("scm5", "spawn", asset: "wolf", count: 1),
Step("scm6", "spectator", value: "off"),
Step("scm7", "spectator", value: "on"),
Step("scm8", "pick_unit", asset: "human"),
Step("scm9", "focus", asset: "human"),
Step("scm10", "interest_end_session"),
Step("scm11", "interest_story_clear"),
Step("scm12", "interest_saga_clear"),
Step("scm13", "saga_force_admit_focus"),
Step("scm14", "assert", expect: "saga_has_focus"),
Step("scm15", "interest_expire_pending", value: ""),
Step("scm20", "interest_combat_session", asset: "human", value: "wolf", expect: "scm_pack"),
Step("scm21", "combat_isolate_pair", value: "20"),
Step("scm22", "status_apply", asset: "human", value: "invincible"),
Step("scm23", "combat_kill_related"),
Step("scm24", "age_current", wait: 10f),
Step("scm25", "interest_mark_combat_cold"),
Step("scm26", "director_run", wait: 1.5f),
Step("scm27", "assert", expect: "tip_asset", value: "aftermath_"),
Step("scm28", "assert", expect: "story_phase", value: "Aftermath"),
Step("scm29", "assert", expect: "saga_has_focus"),
// Crisis theater: war tip + forced chapter must cut MC aftermath.
Step("scm30", "interest_expire_pending", value: ""),
Step("scm31", "interest_war_session", asset: "human", expect: "scm_war"),
Step("scm32", "director_run", wait: 0.6f),
Step("scm33", "assert", expect: "tip_matches_any", value: "War -"),
Step("scm34", "interest_crisis_begin"),
Step("scm35", "assert", expect: "crisis_active", value: "true"),
Step("scm36", "assert", expect: "crisis_kind", value: "War"),
// Hot non-crisis peer must not reclaim while the war crisis is live.
Step("scm40", "interest_inject", asset: "human",
label: "MCAftermathNoise", tier: "Action", expect: "scm_after",
value: "lead=event;evt=90;char=20;force=false;ttl=60"),
Step("scm41", "director_run", wait: 1.2f),
Step("scm42", "assert", expect: "tip_matches_any", value: "War -"),
Step("scm43", "assert", expect: "tip_not_contains", value: "MCAftermathNoise"),
Step("scm44", "assert", expect: "crisis_active", value: "true"),
Step("scm90", "fast_timing", value: "false"),
Step("scm99", "snapshot"),
};
}
///
/// Non-goal gate: Pick still opens short arcs for strangers (not Prefer/MC-only).
/// Empty roster + duel climax must advance StoryPlanner without saga admit first.
///
private static List StoryPickOpensStrangerArc()
{
return new List
{
Step("spo0", "dismiss_windows"),
Step("spo1", "wait_world"),
Step("spo2", "set_setting", expect: "enabled", value: "true"),
Step("spo3", "fast_timing", value: "true"),
Step("spo4", "spawn", asset: "human", count: 1),
Step("spo5", "spawn", asset: "wolf", count: 1),
Step("spo6", "spectator", value: "off"),
Step("spo7", "spectator", value: "on"),
Step("spo8", "interest_end_session"),
Step("spo9", "interest_story_clear"),
Step("spo10", "interest_saga_clear"),
Step("spo11", "assert", expect: "saga_roster_count", value: "0"),
Step("spo12", "pick_unit", asset: "human"),
Step("spo13", "focus", asset: "human"),
// Deliberately no saga_force_admit / Prefer - stranger must still open the arc.
Step("spo14", "interest_expire_pending", value: ""),
Step("spo20", "interest_combat_session", asset: "human", value: "wolf", expect: "spo_pack"),
Step("spo21", "combat_isolate_pair", value: "20"),
Step("spo22", "status_apply", asset: "human", value: "invincible"),
Step("spo23", "status_apply", asset: "wolf", value: "invincible"),
Step("spo24", "director_run", wait: 0.8f),
Step("spo25", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("spo26", "assert", expect: "story_phase", value: "Climax"),
// Hard-arc stamp may admit the fighter onto Cap; Prefer must still be empty.
Step("spo27", "assert", expect: "saga_prefer_count", value: "0"),
Step("spo90", "fast_timing", value: "false"),
Step("spo99", "snapshot"),
};
}
private static List StoryAftermathWar()
{
return new List
{
Step("saw0", "dismiss_windows"),
Step("saw1", "wait_world"),
Step("saw2", "set_setting", expect: "enabled", value: "true"),
Step("saw3", "fast_timing", value: "true"),
Step("saw4", "spawn", asset: "human", count: 2),
Step("saw5", "spectator", value: "off"),
Step("saw6", "spectator", value: "on"),
Step("saw7", "pick_unit", asset: "human"),
Step("saw8", "focus", asset: "human"),
Step("saw9", "interest_end_session"),
Step("saw20", "interest_war_session", asset: "human", expect: "saw_war"),
Step("saw21", "assert", expect: "tip_matches_any", value: "War -"),
Step("saw22", "interest_expire_pending", value: ""),
Step("saw23", "interest_mark_sticky_cold"),
Step("saw24", "director_run", wait: 1.5f),
Step("saw25", "assert", expect: "tip_matches_any", value: "lingers|stands over|war front"),
Step("saw26", "assert", expect: "tip_asset", value: "aftermath_"),
// Large war auto-opens a crisis; short-arc linger must quiet-close it (no dual closer).
Step("saw27", "assert", expect: "crisis_active", value: "false"),
Step("saw28", "assert", expect: "soft_fill_quiet", value: "true"),
Step("saw29", "director_run", wait: 2.0f),
Step("saw30", "assert", expect: "caption_not_contains", value: "surveys the aftermath of the crisis"),
Step("saw90", "fast_timing", value: "false"),
Step("saw99", "snapshot"),
};
}
///
/// Large WarFront opens crisis; short-arc war linger owns the exit; purge leaves no
/// pending epilogue_crisis for free AFK.
///
private static List StoryCrisisHygiene()
{
return new List
{
Step("sch0", "dismiss_windows"),
Step("sch1", "wait_world"),
Step("sch2", "set_setting", expect: "enabled", value: "true"),
Step("sch3", "fast_timing", value: "true"),
Step("sch4", "spawn", asset: "human", count: 2),
Step("sch5", "spectator", value: "off"),
Step("sch6", "spectator", value: "on"),
Step("sch7", "pick_unit", asset: "human"),
Step("sch8", "focus", asset: "human"),
Step("sch9", "interest_end_session"),
Step("sch10", "interest_story_clear"),
Step("sch20", "interest_war_session", asset: "human", expect: "sch_war"),
Step("sch21", "director_run", wait: 0.8f),
Step("sch22", "assert", expect: "tip_matches_any", value: "War -"),
// Live 12v8 sides open crisis without harness force.
Step("sch23", "assert", expect: "crisis_active", value: "true"),
Step("sch24", "assert", expect: "crisis_kind", value: "War"),
Step("sch30", "interest_expire_pending", value: ""),
Step("sch31", "interest_mark_sticky_cold"),
Step("sch32", "director_run", wait: 1.5f),
Step("sch33", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sch34", "assert", expect: "crisis_active", value: "false"),
Step("sch35", "assert", expect: "caption_not_contains", value: "surveys the aftermath of the crisis"),
Step("sch36", "assert", expect: "soft_fill_quiet", value: "true"),
Step("sch40", "interest_story_purge_leftovers"),
Step("sch41", "assert", expect: "crisis_active", value: "false"),
Step("sch42", "director_run", wait: 1.0f),
Step("sch43", "assert", expect: "caption_not_contains", value: "surveys the aftermath of the crisis"),
Step("sch90", "fast_timing", value: "false"),
Step("sch99", "snapshot"),
};
}
///
/// Large WarFront opens a crisis chapter; forced closer is epilogue_crisis.
///
private static List StoryCrisisWar()
{
return new List
{
Step("scw0", "dismiss_windows"),
Step("scw1", "wait_world"),
Step("scw2", "set_setting", expect: "enabled", value: "true"),
Step("scw3", "fast_timing", value: "true"),
Step("scw4", "spawn", asset: "human", count: 2),
Step("scw5", "spectator", value: "off"),
Step("scw6", "spectator", value: "on"),
Step("scw7", "pick_unit", asset: "human"),
Step("scw8", "focus", asset: "human"),
Step("scw9", "interest_end_session"),
Step("scw10", "interest_story_clear"),
Step("scw20", "interest_war_session", asset: "human", expect: "scw_war"),
Step("scw21", "director_run", wait: 0.6f),
Step("scw22", "assert", expect: "tip_matches_any", value: "War -"),
// Small harness kingdoms are below live enter threshold - force chapter open.
Step("scw22b", "interest_crisis_begin"),
Step("scw23", "assert", expect: "crisis_active", value: "true"),
Step("scw24", "assert", expect: "crisis_kind", value: "War"),
Step("scw30", "interest_expire_pending", value: ""),
Step("scw31", "interest_mark_sticky_cold"),
Step("scw32", "interest_crisis_end"),
Step("scw33", "director_run", wait: 1.5f),
Step("scw34", "assert", expect: "tip_asset", value: "epilogue_crisis"),
Step("scw35", "assert", expect: "tip_matches_any", value: "surveys the aftermath of the crisis"),
Step("scw36", "assert", expect: "story_spine", value: "empty"),
Step("scw37", "assert", expect: "caption_not_contains", value: "Fighting ·"),
Step("scw90", "fast_timing", value: "false"),
Step("scw99", "snapshot"),
};
}
///
/// EarthquakeActive disaster opens a crisis chapter; forced closer is epilogue_crisis.
/// Stacked storm tips during disaster cool must not open a second chapter.
///
private static List StoryCrisisDisaster()
{
return new List
{
Step("scd0", "dismiss_windows"),
Step("scd1", "wait_world"),
Step("scd2", "set_setting", expect: "enabled", value: "true"),
Step("scd3", "fast_timing", value: "true"),
Step("scd4", "spawn", asset: "human", count: 2),
Step("scd5", "spectator", value: "off"),
Step("scd6", "spectator", value: "on"),
Step("scd7", "pick_unit", asset: "human"),
Step("scd8", "focus", asset: "human"),
Step("scd9", "interest_end_session"),
Step("scd10", "interest_story_clear"),
Step("scd20", "interest_earthquake_session", asset: "human", expect: "scd_quake"),
Step("scd21", "director_run", wait: 0.6f),
Step("scd22", "assert", expect: "tip_matches_any", value: "earthquake"),
Step("scd22b", "interest_crisis_begin"),
Step("scd23", "assert", expect: "crisis_active", value: "true"),
Step("scd24", "assert", expect: "crisis_kind", value: "Disaster"),
Step("scd30", "interest_expire_pending", value: ""),
Step("scd31", "interest_mark_sticky_cold"),
Step("scd32", "interest_crisis_end"),
Step("scd33", "director_run", wait: 1.5f),
Step("scd34", "assert", expect: "tip_asset", value: "epilogue_crisis"),
Step("scd35", "assert", expect: "tip_matches_any", value: "surveys the aftermath of the crisis"),
// End closer tip → Done + disaster cool. Fresh storm tip must not reopen.
Step("scd36", "interest_end_session"),
Step("scd37", "assert", expect: "crisis_active", value: "false"),
Step("scd40", "interest_expire_pending", value: ""),
Step("scd41", "interest_earthquake_session", asset: "human", expect: "scd_quake2"),
Step("scd42", "director_run", wait: 1.0f),
Step("scd43", "assert", expect: "crisis_active", value: "false"),
Step("scd90", "fast_timing", value: "false"),
Step("scd99", "snapshot"),
};
}
///
/// Living theater partner must not mint survivor "stands over" prose
/// (soak: Norron vs Nerari attack gaps).
///
private static List StoryAftermathPartnerTruth()
{
return new List
{
Step("saf0", "dismiss_windows"),
Step("saf1", "wait_world"),
Step("saf2", "set_setting", expect: "enabled", value: "true"),
Step("saf3", "fast_timing", value: "true"),
// Lover bystander (not in the duel) remembered first; fighter is the next spawn.
Step("saf4", "spawn", asset: "human", count: 1),
Step("saf5", "happiness_remember_partner"),
Step("saf6", "spawn", asset: "human", count: 1),
Step("saf7", "focus", asset: "auto"),
Step("saf8", "happiness_bond_lovers"),
Step("saf9", "spawn", asset: "wolf", count: 1),
Step("saf10", "spectator", value: "off"),
Step("saf11", "spectator", value: "on"),
// Keep the fighter (human focus), not the wolf, as combat Follow.
Step("saf12", "pick_unit", asset: "human"),
Step("saf12b", "focus", asset: "human"),
Step("saf13", "interest_end_session"),
Step("saf14", "interest_story_clear"),
Step("saf20", "interest_combat_session", asset: "human", value: "wolf", expect: "saf_pack"),
Step("saf20b", "combat_isolate_pair", value: "20"),
Step("saf21", "status_apply", asset: "human", value: "invincible"),
Step("saf22", "status_apply", asset: "wolf", value: "invincible"),
Step("saf23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("saf24", "interest_expire_pending", value: ""),
Step("saf25", "age_current", wait: 10f),
Step("saf26", "interest_mark_combat_cold"),
Step("saf27", "director_run", wait: 1.5f),
// Living theater partner: never mint survivor "stands over the fallen".
Step("saf28", "assert", expect: "tip_not_contains", value: "stands over"),
Step("saf29", "assert", expect: "tip_not_partner"),
Step("saf90", "fast_timing", value: "false"),
Step("saf99", "snapshot"),
};
}
///
/// Soft-duel cold still injects aftermath when high-strength cast life noise is pending
/// (hatch/eat must not count as natural grief/love peers).
///
private static List StoryAftermathCastNoise()
{
return new List
{
Step("san0", "dismiss_windows"),
Step("san1", "wait_world"),
Step("san2", "set_setting", expect: "enabled", value: "true"),
Step("san3", "fast_timing", value: "true"),
Step("san4", "spawn", asset: "human", count: 1),
Step("san5", "spawn", asset: "wolf", count: 1),
Step("san6", "spectator", value: "off"),
Step("san7", "spectator", value: "on"),
Step("san8", "pick_unit", asset: "human"),
Step("san9", "focus", asset: "human"),
Step("san10", "interest_end_session"),
Step("san10b", "interest_story_clear"),
Step("san10c", "interest_variety_clear"),
Step("san10d", "interest_expire_pending", value: ""),
Step("san20", "interest_combat_session", asset: "human", value: "wolf", expect: "san_pack"),
Step("san20b", "combat_isolate_pair", value: "20"),
Step("san21", "status_apply", asset: "human", value: "invincible"),
Step("san23", "assert", expect: "tip_matches_any", value: "Duel -"),
// High-score cast-life distractor (expect id must NOT contain "hatch" or the
// harness maps it onto just_got_out_of_egg and it can sticky-win the cold).
Step("san24", "interest_inject", asset: "human", label: "{a} munches quietly nearby",
tier: "Action", expect: "san_cast_noise",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
// Real fallen partner required for survivor aftermath.
Step("san24b", "combat_kill_related"),
Step("san25", "age_current", wait: 10f),
Step("san26", "interest_mark_combat_cold"),
Step("san27", "director_run", wait: 1.5f),
Step("san28", "assert", expect: "tip_matches_any", value: "stands over|mourns|lingers"),
Step("san29", "assert", expect: "tip_asset", value: "aftermath_"),
Step("san30", "assert", expect: "tip_not_contains", value: "Duel -"),
Step("san31", "assert", expect: "story_phase", value: "Aftermath"),
Step("san90", "fast_timing", value: "false"),
Step("san99", "snapshot"),
};
}
///
/// Life-tip reason principals: Follow hijack onto a bystander must not keep a
/// subject-led intimacy Label on the bystander dossier.
///
private static List ReasonLifePrincipal()
{
return new List
{
Step("rlp0", "dismiss_windows"),
Step("rlp1", "wait_world"),
Step("rlp2", "set_setting", expect: "enabled", value: "true"),
Step("rlp3", "fast_timing", value: "true"),
Step("rlp4", "spawn", asset: "human", count: 3),
Step("rlp5", "spectator", value: "off"),
Step("rlp6", "spectator", value: "on"),
Step("rlp7", "pick_unit", asset: "human"),
Step("rlp8", "focus", asset: "human"),
Step("rlp9", "interest_end_session"),
Step("rlp20", "interest_force_session", asset: "human",
label: "{a} shares intimacy with a partner", tier: "Action", expect: "just_kissed",
value: "lead=event;evt=62"),
Step("rlp21", "assert", expect: "reason_matches_any", value: "intimacy|mates"),
Step("rlp22", "assert", expect: "reason_owns_focus"),
// Follow drifts to a bystander; SubjectId + Label stay on the couple tip.
Step("rlp30", "interest_hijack_follow", asset: "human"),
Step("rlp31", "wait", value: "0.25"),
Step("rlp32", "assert", expect: "dossier_matches_focus"),
Step("rlp33", "assert", expect: "reason_empty", value: "true"),
Step("rlp34", "assert", expect: "reason_owns_focus"),
Step("rlp35", "assert", expect: "dossier_not_contains", value: "shares intimacy"),
Step("rlp90", "fast_timing", value: "false"),
Step("rlp99", "snapshot"),
};
}
///
/// just_found_house / finds-a-home soft-life cool: settlement crumbs must not
/// keep cutting between combat beats.
///
private static List StorySettlementCool()
{
return new List
{
Step("ssc0", "dismiss_windows"),
Step("ssc1", "wait_world"),
Step("ssc2", "set_setting", expect: "enabled", value: "true"),
Step("ssc3", "fast_timing", value: "true"),
Step("ssc4", "spawn", asset: "human", count: 2),
Step("ssc5", "spectator", value: "off"),
Step("ssc6", "spectator", value: "on"),
Step("ssc7", "pick_unit", asset: "human"),
Step("ssc8", "focus", asset: "human"),
Step("ssc9", "interest_end_session"),
Step("ssc10", "interest_variety_clear"),
Step("ssc20", "interest_force_session", asset: "human",
label: "{a} finds a home", tier: "Action", expect: "just_found_house",
value: "lead=event;evt=58"),
Step("ssc21", "assert", expect: "tip_contains", value: "finds a home"),
Step("ssc22", "assert", expect: "beat_cooled", value: "just_found_house"),
Step("ssc23", "interest_end_session"),
Step("ssc30", "interest_inject", asset: "human",
label: "{a} finds a home", tier: "Action", expect: "just_found_house",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("ssc31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "ssc_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("ssc32", "director_run", wait: 1.2f),
Step("ssc33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("ssc34", "assert", expect: "tip_not_contains", value: "finds a home"),
Step("ssc35", "assert", expect: "beat_cooled", value: "just_found_house"),
Step("ssc90", "fast_timing", value: "false"),
Step("ssc99", "snapshot"),
};
}
///
/// just_kissed / intimacy soft-life cool: after one show, the same beat cannot
/// immediately cut back in over a stronger peer.
///
private static List StoryIntimacyCool()
{
return new List
{
Step("sic0", "dismiss_windows"),
Step("sic1", "wait_world"),
Step("sic2", "set_setting", expect: "enabled", value: "true"),
Step("sic3", "fast_timing", value: "true"),
Step("sic4", "spawn", asset: "human", count: 2),
Step("sic5", "spectator", value: "off"),
Step("sic6", "spectator", value: "on"),
Step("sic7", "pick_unit", asset: "human"),
Step("sic8", "focus", asset: "human"),
Step("sic9", "interest_end_session"),
Step("sic10", "interest_variety_clear"),
Step("sic20", "interest_force_session", asset: "human",
label: "{a} shares intimacy with a partner", tier: "Action", expect: "just_kissed",
value: "lead=event;evt=62"),
Step("sic21", "assert", expect: "tip_matches_any", value: "intimacy|mates"),
Step("sic22", "assert", expect: "beat_cooled", value: "just_kissed"),
Step("sic23", "interest_end_session"),
// Same intimacy beat pending again - must stay cooled while a clear peer wins.
Step("sic30", "interest_inject", asset: "human",
label: "{a} shares intimacy with a partner", tier: "Action", expect: "just_kissed",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sic31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "sic_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("sic32", "director_run", wait: 1.2f),
Step("sic33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("sic34", "assert", expect: "tip_not_contains", value: "intimacy"),
Step("sic35", "assert", expect: "beat_cooled", value: "just_kissed"),
Step("sic90", "fast_timing", value: "false"),
Step("sic99", "snapshot"),
};
}
///
/// Chronicle kill crumbs (Killed X) soft-life cool: after one show, kill beats
/// cannot keep cutting between stronger peers / mass tips.
///
private static List StoryKillCool()
{
return new List
{
Step("skc0", "dismiss_windows"),
Step("skc1", "wait_world"),
Step("skc2", "set_setting", expect: "enabled", value: "true"),
Step("skc3", "fast_timing", value: "true"),
Step("skc4", "spawn", asset: "human", count: 2),
Step("skc5", "spectator", value: "off"),
Step("skc6", "spectator", value: "on"),
Step("skc7", "pick_unit", asset: "human"),
Step("skc8", "focus", asset: "human"),
Step("skc8b", "status_apply", asset: "human", value: "invincible"),
Step("skc9", "interest_end_session"),
Step("skc10", "interest_variety_clear"),
Step("skc20", "interest_force_session", asset: "human",
label: "Killed Phixzen (fox)", tier: "Action", expect: "milestone_kill",
value: "lead=event;evt=60"),
Step("skc21", "assert", expect: "tip_contains", value: "Killed"),
Step("skc22", "assert", expect: "beat_cooled", value: "milestone_kill"),
Step("skc23", "interest_end_session"),
Step("skc30", "interest_inject", asset: "human",
label: "Killed OtherFox (fox)", tier: "Action", expect: "milestone_kill",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("skc31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "skc_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("skc32", "director_run", wait: 1.2f),
Step("skc33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("skc34", "assert", expect: "tip_not_contains", value: "Killed"),
Step("skc35", "assert", expect: "beat_cooled", value: "milestone_kill"),
Step("skc90", "fast_timing", value: "false"),
Step("skc99", "snapshot"),
};
}
///
/// find_lover / fallen_in_love courtship soft-life cool: seeking stamps the shared
/// love class so smitten/seeking crumbs cannot cut over a clear peer.
///
private static List StoryCourtshipCool()
{
return new List
{
Step("scc0", "dismiss_windows"),
Step("scc1", "wait_world"),
Step("scc2", "set_setting", expect: "enabled", value: "true"),
Step("scc3", "fast_timing", value: "true"),
Step("scc4", "spawn", asset: "human", count: 3),
Step("scc5", "spectator", value: "off"),
Step("scc6", "spectator", value: "on"),
Step("scc7", "pick_unit", asset: "human"),
Step("scc8", "focus", asset: "human"),
// Dirty worlds often have leftover wolves from combat gates.
Step("scc8b", "status_apply", asset: "human", value: "invincible"),
Step("scc9", "interest_end_session"),
Step("scc10", "interest_variety_clear"),
Step("scc11", "interest_story_clear"),
Step("scc20", "interest_force_session", asset: "human",
label: "{a} is seeking a lover", tier: "Action", expect: "find_lover",
value: "lead=event;evt=62"),
Step("scc21", "assert", expect: "tip_contains", value: "seeking a lover"),
Step("scc22", "assert", expect: "beat_cooled", value: "find_lover"),
// Shared arc:life:love world cool also covers smitten.
Step("scc23", "assert", expect: "beat_cooled", value: "fallen_in_love"),
Step("scc24", "interest_end_session"),
Step("scc25", "interest_story_clear"),
Step("scc30", "interest_inject", asset: "human",
label: "{a} is seeking a lover", tier: "Action", expect: "find_lover",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("scc31", "interest_inject", asset: "human",
label: "{a} is smitten with a partner", tier: "Action", expect: "fallen_in_love",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("scc32", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "scc_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("scc33", "director_run", wait: 1.2f),
Step("scc34", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("scc35", "assert", expect: "tip_not_contains", value: "smitten"),
Step("scc36", "assert", expect: "tip_not_contains", value: "seeking a lover"),
Step("scc37", "assert", expect: "beat_cooled", value: "fallen_in_love"),
Step("scc38", "assert", expect: "beat_cooled", value: "find_lover"),
Step("scc90", "fast_timing", value: "false"),
Step("scc99", "snapshot"),
};
}
///
/// Life-intent decisions (reproduce / find lover) soft-cool as a class:
/// one show cools the whole intent family so ClearPeer wins.
///
private static List StoryDecisionIntentCool()
{
return new List
{
Step("sdc0", "dismiss_windows"),
Step("sdc1", "wait_world"),
Step("sdc2", "set_setting", expect: "enabled", value: "true"),
Step("sdc3", "fast_timing", value: "true"),
Step("sdc4", "spawn", asset: "human", count: 3),
Step("sdc5", "spectator", value: "off"),
Step("sdc6", "spectator", value: "on"),
Step("sdc7", "pick_unit", asset: "human"),
Step("sdc8", "focus", asset: "human"),
Step("sdc8b", "status_apply", asset: "human", value: "invincible"),
Step("sdc9", "interest_end_session"),
Step("sdc10", "interest_variety_clear"),
Step("sdc11", "interest_story_clear"),
Step("sdc20", "interest_force_session", asset: "human",
label: "{a} decides to try to reproduce", tier: "Action",
expect: "sexual_reproduction_try", value: "lead=event;evt=66"),
Step("sdc21", "assert", expect: "tip_contains", value: "try to reproduce"),
Step("sdc22", "assert", expect: "beat_cooled", value: "sexual_reproduction_try"),
// Shared intent class also cools decision find_lover.
Step("sdc23", "assert", expect: "beat_cooled", value: "decision_find_lover"),
Step("sdc24", "interest_end_session"),
Step("sdc25", "interest_story_clear"),
Step("sdc30", "interest_inject", asset: "human",
label: "{a} decides to try to reproduce", tier: "Action",
expect: "sexual_reproduction_try",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sdc31", "interest_inject", asset: "human",
label: "{a} decides to find a lover", tier: "Action",
expect: "decision_find_lover",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sdc32", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "sdc_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("sdc33", "director_run", wait: 1.2f),
Step("sdc34", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("sdc35", "assert", expect: "tip_not_contains", value: "reproduce"),
Step("sdc36", "assert", expect: "tip_not_contains", value: "find a lover"),
Step("sdc90", "fast_timing", value: "false"),
Step("sdc99", "snapshot"),
};
}
///
/// Ordinary civ construction finishes soft-cool world-wide so tent spam cannot chain.
///
private static List StoryConstructionCool()
{
return new List
{
Step("sco0", "dismiss_windows"),
Step("sco1", "wait_world"),
Step("sco2", "set_setting", expect: "enabled", value: "true"),
Step("sco3", "fast_timing", value: "true"),
Step("sco4", "spawn", asset: "human", count: 3),
Step("sco5", "spectator", value: "off"),
Step("sco6", "spectator", value: "on"),
Step("sco7", "pick_unit", asset: "human"),
Step("sco8", "focus", asset: "human"),
Step("sco8b", "status_apply", asset: "human", value: "invincible"),
Step("sco9", "interest_end_session"),
Step("sco10", "interest_variety_clear"),
Step("sco11", "interest_story_clear"),
Step("sco20", "interest_force_session", asset: "human",
label: "{a} finishes Tent Human", tier: "Action", expect: "tent_human",
value: "lead=event;evt=72"),
Step("sco21", "assert", expect: "tip_contains", value: "finishes"),
Step("sco22", "assert", expect: "beat_cooled", value: "tent_human"),
Step("sco23", "interest_end_session"),
Step("sco30", "interest_inject", asset: "human",
label: "{a} finishes Tent Human", tier: "Action", expect: "tent_human",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sco31", "interest_inject", asset: "human",
label: "{a} finishes Tent Dwarf", tier: "Action", expect: "construction",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sco32", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "sco_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("sco33", "director_run", wait: 1.2f),
Step("sco34", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("sco35", "assert", expect: "tip_not_contains", value: "finishes"),
Step("sco90", "fast_timing", value: "false"),
Step("sco99", "snapshot"),
};
}
///
/// Soft-life tip interrupted by a stronger peer must not resume after the peer ends.
///
private static List StoryResumeCooled()
{
return new List
{
Step("src0", "dismiss_windows"),
Step("src1", "wait_world"),
Step("src2", "set_setting", expect: "enabled", value: "true"),
Step("src3", "fast_timing", value: "true"),
Step("src4", "spawn", asset: "human", count: 3),
Step("src5", "spectator", value: "off"),
Step("src6", "spectator", value: "on"),
Step("src7", "pick_unit", asset: "human"),
Step("src8", "focus", asset: "human"),
Step("src8b", "status_apply", asset: "human", value: "invincible"),
Step("src9", "interest_end_session"),
Step("src10", "interest_variety_clear"),
Step("src11", "interest_story_clear"),
// Soft crumb first (resumable FixedDwell - not ForceActive, so epic can cut in).
Step("src20", "interest_inject", asset: "human",
label: "{a} decides to try to reproduce", tier: "Action",
expect: "sexual_reproduction_try",
value: "lead=event;evt=66;char=5;force=false;ttl=60"),
Step("src21", "director_run", wait: 1.2f),
Step("src22", "assert", expect: "tip_contains", value: "try to reproduce"),
Step("src23", "assert", expect: "beat_cooled", value: "sexual_reproduction_try"),
// Stronger peer cuts in (margin above cutInMargin).
Step("src30", "interest_inject", asset: "human", label: "EpicCutIn", tier: "Epic",
expect: "src_epic", value: "lead=event;evt=150;char=10;force=false;ttl=60"),
Step("src31", "director_run", wait: 1.2f),
Step("src32", "assert", expect: "tip_contains", value: "EpicCutIn"),
Step("src33", "assert", expect: "tip_not_contains", value: "reproduce"),
// Ending the cut-in must not resume the cooled reproduce tip.
Step("src40", "interest_end_session"),
Step("src41", "interest_story_clear"),
Step("src42", "interest_inject", asset: "human",
label: "{a} decides to try to reproduce", tier: "Action",
expect: "sexual_reproduction_try",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("src43", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "src_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("src44", "director_run", wait: 1.2f),
Step("src45", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("src46", "assert", expect: "tip_not_contains", value: "reproduce"),
Step("src47", "assert", expect: "beat_cooled", value: "sexual_reproduction_try"),
Step("src90", "fast_timing", value: "false"),
Step("src99", "snapshot"),
};
}
///
/// WorldLog politics meta (king_dead) soft-cools so sticky death crumbs cannot re-monopolize.
///
private static List StoryWorldLogMetaCool()
{
return new List
{
Step("swm0", "dismiss_windows"),
Step("swm1", "wait_world"),
Step("swm2", "set_setting", expect: "enabled", value: "true"),
Step("swm3", "fast_timing", value: "true"),
Step("swm4", "spawn", asset: "human", count: 3),
Step("swm5", "spectator", value: "off"),
Step("swm6", "spectator", value: "on"),
Step("swm7", "pick_unit", asset: "human"),
Step("swm8", "focus", asset: "human"),
Step("swm8b", "status_apply", asset: "human", value: "invincible"),
Step("swm9", "interest_end_session"),
Step("swm10", "interest_variety_clear"),
Step("swm11", "interest_story_clear"),
Step("swm20", "interest_force_session", asset: "human",
label: "King died: Frortied (Rugen)", tier: "Action", expect: "king_dead",
value: "lead=event;evt=84"),
Step("swm21", "assert", expect: "tip_contains", value: "King died"),
Step("swm22", "assert", expect: "beat_cooled", value: "king_dead"),
Step("swm23", "interest_end_session"),
Step("swm30", "interest_inject", asset: "human",
label: "King died: Frortied (Rugen)", tier: "Action", expect: "king_dead",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("swm31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "swm_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("swm32", "director_run", wait: 1.2f),
Step("swm33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("swm34", "assert", expect: "tip_not_contains", value: "King died"),
Step("swm90", "fast_timing", value: "false"),
Step("swm99", "snapshot"),
};
}
///
/// Soft personal status FX (taking_roots) cool world-wide after one show.
///
private static List StoryStatusFxCool()
{
return new List
{
Step("sfx0", "dismiss_windows"),
Step("sfx1", "wait_world"),
Step("sfx2", "set_setting", expect: "enabled", value: "true"),
Step("sfx3", "fast_timing", value: "true"),
Step("sfx4", "spawn", asset: "human", count: 3),
Step("sfx5", "spectator", value: "off"),
Step("sfx6", "spectator", value: "on"),
Step("sfx7", "pick_unit", asset: "human"),
Step("sfx8", "focus", asset: "human"),
Step("sfx8b", "status_apply", asset: "human", value: "invincible"),
Step("sfx9", "interest_end_session"),
Step("sfx10", "interest_variety_clear"),
Step("sfx11", "interest_story_clear"),
Step("sfx20", "interest_force_session", asset: "human",
label: "{a} · Starts taking root", tier: "Action", expect: "taking_roots",
value: "lead=event;evt=54"),
Step("sfx21", "assert", expect: "tip_contains", value: "taking root"),
Step("sfx22", "assert", expect: "beat_cooled", value: "taking_roots"),
Step("sfx23", "interest_end_session"),
Step("sfx30", "interest_inject", asset: "human",
label: "{a} · Starts taking root", tier: "Action", expect: "taking_roots",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("sfx31", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "sfx_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("sfx32", "director_run", wait: 1.2f),
Step("sfx33", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("sfx34", "assert", expect: "tip_not_contains", value: "taking root"),
Step("sfx90", "fast_timing", value: "false"),
Step("sfx99", "snapshot"),
};
}
///
/// Sleep / wake / dream rest cycle soft-cools as one class after one show.
///
private static List StoryRestCool()
{
return new List
{
Step("srt0", "dismiss_windows"),
Step("srt1", "wait_world"),
Step("srt2", "set_setting", expect: "enabled", value: "true"),
Step("srt3", "fast_timing", value: "true"),
Step("srt4", "spawn", asset: "human", count: 3),
Step("srt5", "spectator", value: "off"),
Step("srt6", "spectator", value: "on"),
Step("srt7", "pick_unit", asset: "human"),
Step("srt8", "focus", asset: "human"),
Step("srt8b", "status_apply", asset: "human", value: "invincible"),
Step("srt9", "interest_end_session"),
Step("srt10", "interest_variety_clear"),
Step("srt11", "interest_story_clear"),
Step("srt20", "interest_force_session", asset: "human",
label: "{a} · Falls asleep", tier: "Action", expect: "sleeping",
value: "lead=event;evt=54"),
Step("srt21", "assert", expect: "tip_contains", value: "Falls asleep"),
Step("srt22", "assert", expect: "beat_cooled", value: "sleeping"),
// Shared rest class also cools wake / dream crumbs.
Step("srt23", "assert", expect: "beat_cooled", value: "just_slept"),
Step("srt24", "interest_end_session"),
Step("srt30", "interest_inject", asset: "human",
label: "{a} · Falls asleep", tier: "Action", expect: "sleeping",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("srt31", "interest_inject", asset: "human",
label: "{a} ends a rest", tier: "Action", expect: "just_slept",
value: "lead=event;evt=88;char=10;force=false;ttl=60"),
Step("srt32", "interest_inject", asset: "human", label: "ClearPeer", tier: "Action",
expect: "srt_peer", value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("srt33", "director_run", wait: 1.2f),
Step("srt34", "assert", expect: "tip_contains", value: "ClearPeer"),
Step("srt35", "assert", expect: "tip_not_contains", value: "Falls asleep"),
Step("srt36", "assert", expect: "tip_not_contains", value: "ends a rest"),
Step("srt90", "fast_timing", value: "false"),
Step("srt99", "snapshot"),
};
}
///
/// Reproduction intent is not a story beat. The live DeferredInterestFeed path must
/// reject it before registry intake and leave the camera free for confirmed outcomes.
///
private static List StoryDecisionIntentLive()
{
return new List
{
Step("sdl0", "dismiss_windows"),
Step("sdl1", "wait_world"),
Step("sdl2", "set_setting", expect: "enabled", value: "true"),
Step("sdl3", "fast_timing", value: "true"),
Step("sdl4", "spawn", asset: "human", count: 3),
Step("sdl5", "spectator", value: "off"),
Step("sdl6", "spectator", value: "on"),
Step("sdl7", "pick_unit", asset: "human"),
Step("sdl8", "focus", asset: "human"),
Step("sdl8b", "status_apply", asset: "human", value: "invincible"),
Step("sdl9", "interest_end_session"),
Step("sdl10", "interest_variety_clear"),
Step("sdl11", "interest_story_clear"),
Step("sdl20", "assert", expect: "decision_worthy",
value: "sexual_reproduction_try", label: "false"),
Step("sdl21", "decision_emit", expect: "sexual_reproduction_try"),
Step("sdl22", "director_run", wait: 1.2f),
Step("sdl23", "assert", expect: "tip_not_contains", value: "reproduce"),
Step("sdl90", "fast_timing", value: "false"),
Step("sdl99", "snapshot"),
};
}
///
/// Peak Mass with one sticky camp wiped to 0 must not print "vs … (0)".
///
private static List CombatWipedSide()
{
return new List
{
Step("cws0", "dismiss_windows"),
Step("cws1", "wait_world"),
Step("cws2", "set_setting", expect: "enabled", value: "true"),
Step("cws3", "fast_timing", value: "true"),
Step("cws4", "spawn", asset: "human", count: 4),
Step("cws5", "spawn", asset: "wolf", count: 4),
Step("cws6", "spectator", value: "off"),
Step("cws7", "spectator", value: "on"),
Step("cws8", "pick_unit", asset: "human"),
Step("cws9", "focus", asset: "human"),
Step("cws10", "interest_end_session"),
Step("cws20", "interest_combat_session", asset: "human", value: "wolf", expect: "cws_pack"),
Step("cws21", "combat_ensemble_escalate", value: "8"),
Step("cws22", "combat_wire_attack_sides", asset: "human", value: "wolf"),
Step("cws23", "combat_maintain_focus"),
Step("cws24", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
Step("cws25", "assert", expect: "tip_matches_any", value: " vs "),
// Wipe sticky B only (do not maintain - live wolves would re-enroll on SideBKey).
Step("cws30", "combat_wipe_sticky_side", value: "b"),
Step("cws31", "assert", expect: "tip_not_contains", value: "(0)"),
Step("cws32", "assert", expect: "tip_not_contains", value: " vs "),
Step("cws33", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
// Mop-up hysteresis: thin 1-foe count must stay pack-framed (no vs), not Mass↔Battle.
// Do not maintain here - live foes would re-enroll SideB outside mop-up policy.
Step("cws40", "combat_set_sticky_side_count", value: "b:1"),
Step("cws41", "assert", expect: "tip_not_contains", value: " vs "),
Step("cws42", "assert", expect: "tip_not_contains", value: "(0)"),
Step("cws43", "combat_set_sticky_side_count", value: "b:1"),
Step("cws44", "assert", expect: "tip_not_contains", value: " vs "),
Step("cws45", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
Step("cws90", "fast_timing", value: "false"),
Step("cws99", "snapshot"),
};
}
///
/// Pair-locked duel reason only on PairOwner/Partner - parked sticky Follow bystander
/// must not show "Duel - A vs B" on their dossier.
///
private static List ReasonDuelPrincipal()
{
return new List
{
Step("rdp0", "dismiss_windows"),
Step("rdp1", "wait_world"),
Step("rdp2", "set_setting", expect: "enabled", value: "true"),
Step("rdp3", "fast_timing", value: "true"),
Step("rdp4", "spawn", asset: "human", count: 1),
Step("rdp5", "spawn", asset: "wolf", count: 1),
Step("rdp6", "spectator", value: "off"),
Step("rdp7", "spectator", value: "on"),
Step("rdp8", "pick_unit", asset: "human"),
Step("rdp9", "focus", asset: "human"),
Step("rdp10", "interest_end_session"),
Step("rdp20", "interest_combat_session", asset: "human", value: "wolf", expect: "rdp_duel"),
Step("rdp20b", "combat_isolate_pair", value: "20"),
Step("rdp21", "status_apply", asset: "human", value: "invincible"),
Step("rdp22", "status_apply", asset: "wolf", value: "invincible"),
Step("rdp23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("rdp24", "assert", expect: "reason_matches_any", value: "Duel -"),
Step("rdp25", "assert", expect: "reason_owns_focus"),
// Park Follow on a rostered sleeper who is not PairOwner/Partner.
Step("rdp30", "combat_park_bystander_follow", asset: "human"),
Step("rdp31", "wait", value: "0.25"),
Step("rdp32", "assert", expect: "dossier_matches_focus"),
Step("rdp33", "assert", expect: "reason_empty", value: "true"),
Step("rdp34", "assert", expect: "reason_owns_focus"),
Step("rdp90", "fast_timing", value: "false"),
Step("rdp99", "snapshot"),
};
}
/// Large score gap always picks #1; within epsilon may roll among top-3.
private static List StoryNearTie()
{
return new List
{
Step("nt0", "dismiss_windows"),
Step("nt1", "wait_world"),
Step("nt2", "set_setting", expect: "enabled", value: "true"),
Step("nt3", "fast_timing", value: "true"),
Step("nt4", "spawn", asset: "human", count: 2),
Step("nt5", "spectator", value: "off"),
Step("nt6", "spectator", value: "on"),
Step("nt7", "pick_unit", asset: "human"),
Step("nt8", "focus", asset: "human"),
Step("nt9", "interest_end_session"),
Step("nt10", "interest_expire_pending", value: ""),
Step("nt20", "interest_inject", asset: "human", label: "ClearWinner", tier: "Action",
expect: "nt_hi", value: "lead=event;evt=90;char=5;force=false;ttl=60"),
Step("nt21", "interest_inject", asset: "human", label: "ClearLoser", tier: "Action",
expect: "nt_lo", value: "lead=event;evt=40;char=5;force=false;ttl=60"),
Step("nt22", "director_run", wait: 1.2f),
Step("nt23", "assert", expect: "tip_contains", value: "ClearWinner"),
Step("nt24", "assert", expect: "tip_not_contains", value: "ClearLoser"),
Step("nt90", "fast_timing", value: "false"),
Step("nt99", "snapshot"),
};
}
///
/// Spine only while the watch tip is the story beat; find_lover intent is not a Love climax;
/// leaving a climax for an unrelated tip clears the arc.
///
private static List StorySpineScoped()
{
return new List
{
Step("ss0", "dismiss_windows"),
Step("ss1", "wait_world"),
Step("ss2", "set_setting", expect: "enabled", value: "true"),
Step("ss3", "fast_timing", value: "true"),
Step("ss4", "spawn", asset: "human", count: 1),
Step("ss5", "spawn", asset: "wolf", count: 1),
Step("ss6", "spectator", value: "off"),
Step("ss7", "spectator", value: "on"),
Step("ss8", "pick_unit", asset: "human"),
Step("ss9", "focus", asset: "human"),
Step("ss10", "interest_end_session"),
// Seeking a lover must stay camera-worthy without opening Love · Climax.
Step("ss20", "interest_force_session", asset: "human",
label: "{a} is seeking a lover", tier: "Action", expect: "find_lover",
value: "lead=event;evt=45"),
Step("ss21", "assert", expect: "tip_contains", value: "seeking a lover"),
Step("ss22", "assert", expect: "story_phase", value: "None"),
Step("ss23", "assert", expect: "story_spine_formatted", value: "empty"),
// Real bond opens Love climax + spine.
Step("ss30", "interest_force_session", asset: "human",
label: "{a} became lovers with a partner", tier: "Action", expect: "set_lover",
value: "lead=event;evt=88"),
Step("ss31", "assert", expect: "story_phase", value: "Climax"),
Step("ss32", "assert", expect: "story_spine", value: "Love · Climax"),
// Unrelated hatch tip must clear the arc and hide the spine.
Step("ss40", "interest_force_session", asset: "human",
label: "{a} hatches from an egg", tier: "Action", expect: "hatch_leave_love",
value: "lead=event;evt=95"),
Step("ss41", "assert", expect: "tip_contains", value: "hatches from an egg"),
Step("ss42", "assert", expect: "story_phase", value: "None"),
Step("ss43", "assert", expect: "story_spine_formatted", value: "empty"),
// Combat climax spine, then leave for hatch again.
Step("ss50", "interest_end_session"),
Step("ss51", "interest_combat_session", asset: "human", value: "wolf", expect: "ss_pack"),
Step("ss52", "combat_isolate_pair", value: "20"),
Step("ss53", "status_apply", asset: "human", value: "invincible"),
Step("ss54", "status_apply", asset: "wolf", value: "invincible"),
Step("ss55", "assert", expect: "story_phase", value: "Climax"),
// Tip already leads with Duel - hide redundant Kind · Climax spine row.
Step("ss56", "assert", expect: "story_spine_not_formatted"),
// In-place escalate must retarget Kind Mass without a director SwitchTo.
Step("ss57", "combat_ensemble_escalate", value: "6"),
Step("ss57b", "combat_wire_attack_sides", asset: "human", value: "wolf"),
Step("ss57c", "combat_maintain_focus"),
Step("ss57d", "wait", value: "0.35"),
Step("ss57e", "combat_maintain_focus"),
Step("ss58", "assert", expect: "tip_matches_any", value: "Skirmish -|Battle -|Mass -"),
// Climax spine suppressed when tip already names the kind.
Step("ss59", "assert", expect: "story_spine_not_formatted"),
// Shrink back to a named duel - spine Kind must follow the Label, not scale-hold.
Step("ss59b", "combat_isolate_pair", value: "20"),
Step("ss59c", "combat_maintain_focus"),
Step("ss59d", "wait", value: "0.35"),
Step("ss59e", "combat_maintain_focus"),
Step("ss59f", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("ss59g", "assert", expect: "story_spine_not_formatted"),
Step("ss60", "interest_force_session", asset: "human",
label: "{a} appears in the world", tier: "Action", expect: "spawn_leave_duel",
value: "lead=event;evt=95"),
Step("ss61", "assert", expect: "tip_contains", value: "appears in the world"),
Step("ss62", "assert", expect: "story_phase", value: "None"),
Step("ss63", "assert", expect: "story_spine_formatted", value: "empty"),
Step("ss90", "fast_timing", value: "false"),
Step("ss99", "snapshot"),
};
}
/// Combat climax → aftermath ownership; soft ambient must not steal mid-aftermath.
///
/// Lore browse freeze must not wipe StoryPlanner; close resumes the same climax.
///
private static List StoryLorePauseKeeps()
{
return new List
{
Step("slp0", "dismiss_windows"),
Step("slp1", "wait_world"),
Step("slp2", "set_setting", expect: "enabled", value: "true"),
Step("slp3", "fast_timing", value: "true"),
Step("slp4", "spawn", asset: "human", count: 2),
Step("slp5", "spectator", value: "off"),
Step("slp6", "spectator", value: "on"),
Step("slp7", "pick_unit", asset: "human"),
Step("slp8", "focus", asset: "human"),
Step("slp9", "interest_end_session"),
Step("slp10", "interest_story_clear"),
Step("slp20", "interest_war_session", asset: "human", expect: "slp_war"),
Step("slp21", "director_run", wait: 0.6f),
Step("slp22", "assert", expect: "tip_matches_any", value: "War -"),
Step("slp23", "assert", expect: "story_phase", value: "Climax"),
Step("slp24", "assert", expect: "story_board_count", value: "1", label: "min"),
Step("slp30", "lore_open_focus"),
Step("slp31", "assert", expect: "idle", value: "false"),
Step("slp32", "assert", expect: "browse_paused", value: "true"),
Step("slp33", "assert", expect: "story_phase", value: "Climax"),
Step("slp34", "assert", expect: "story_board_count", value: "1", label: "min"),
Step("slp40", "lore_close"),
Step("slp41", "wait", wait: 0.25f),
Step("slp42", "assert", expect: "idle", value: "true"),
Step("slp43", "assert", expect: "browse_paused", value: "false"),
Step("slp44", "assert", expect: "story_phase", value: "Climax"),
Step("slp45", "assert", expect: "tip_matches_any", value: "War -"),
Step("slp90", "fast_timing", value: "false"),
Step("slp99", "snapshot"),
};
}
///
/// Hard war storyline parks when forced off-story; returning to War tip resumes the board slot.
///
private static List StoryBoardParkResume()
{
return new List
{
Step("sbp0", "dismiss_windows"),
Step("sbp1", "wait_world"),
Step("sbp2", "set_setting", expect: "enabled", value: "true"),
Step("sbp3", "fast_timing", value: "true"),
Step("sbp4", "spawn", asset: "human", count: 2),
Step("sbp5", "spectator", value: "off"),
Step("sbp6", "spectator", value: "on"),
Step("sbp7", "pick_unit", asset: "human"),
Step("sbp8", "focus", asset: "human"),
Step("sbp9", "interest_end_session"),
Step("sbp10", "interest_story_clear"),
Step("sbp20", "interest_war_session", asset: "human", expect: "sbp_war"),
Step("sbp21", "director_run", wait: 0.6f),
Step("sbp22", "assert", expect: "tip_matches_any", value: "War -"),
Step("sbp23", "assert", expect: "story_phase", value: "Climax"),
Step("sbp23b", "director_run", wait: 0.3f),
// Force an unrelated tip (war hold blocks ordinary epic inject cut-ins).
Step("sbp30", "interest_force_session", asset: "human",
label: "ParkProbe", tier: "Epic", expect: "sbp_park_probe",
value: "lead=event;evt=150"),
Step("sbp31", "assert", expect: "tip_contains", value: "ParkProbe"),
Step("sbp32", "assert", expect: "story_parked_count", value: "1", label: "min"),
Step("sbp33", "assert", expect: "story_board_count", value: "1", label: "min"),
// Resume parked short arc without rail Commit (rail is life-saga now).
Step("sbp40", "story_resume_parked"),
Step("sbp41", "assert", expect: "story_parked_count", value: "0"),
Step("sbp42", "assert", expect: "story_phase", value: "Climax"),
Step("sbp43", "assert", expect: "story_board_count", value: "1"),
// A second distinct war theater parks the first and watches the new climax.
Step("sbp50", "interest_force_session", asset: "human",
label: "ParkProbe2", tier: "Epic", expect: "sbp_park_probe2",
value: "lead=event;evt=150"),
Step("sbp51", "assert", expect: "story_parked_count", value: "1", label: "min"),
Step("sbp52", "interest_war_session", asset: "human", expect: "sbp_war2"),
Step("sbp53", "director_run", wait: 1.0f),
Step("sbp54", "assert", expect: "tip_matches_any", value: "War -"),
Step("sbp55", "assert", expect: "story_phase", value: "Climax"),
Step("sbp56", "assert", expect: "story_board_count", value: "2", label: "min"),
Step("sbp90", "fast_timing", value: "false"),
Step("sbp99", "snapshot"),
};
}
private static List StoryArcCombat()
{
return new List
{
Step("sac0", "dismiss_windows"),
Step("sac1", "wait_world"),
Step("sac2", "set_setting", expect: "enabled", value: "true"),
Step("sac3", "fast_timing", value: "true"),
Step("sac4", "spawn", asset: "human", count: 1),
Step("sac5", "spawn", asset: "wolf", count: 1),
Step("sac6", "spectator", value: "off"),
Step("sac7", "spectator", value: "on"),
Step("sac8", "pick_unit", asset: "human"),
Step("sac9", "focus", asset: "human"),
Step("sac10", "interest_end_session"),
Step("sac10b", "interest_story_clear"),
Step("sac10c", "interest_variety_clear"),
Step("sac10d", "interest_expire_pending", value: ""),
Step("sac20", "interest_combat_session", asset: "human", value: "wolf", expect: "sac_pack"),
Step("sac20b", "combat_isolate_pair", value: "20"),
Step("sac21", "status_apply", asset: "human", value: "invincible"),
Step("sac22", "status_apply", asset: "wolf", value: "invincible"),
Step("sac23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("sac24", "assert", expect: "story_phase", value: "Climax"),
Step("sac24b", "assert", expect: "story_spine", value: "empty"),
Step("sac25", "interest_expire_pending", value: ""),
// Survivor aftermath requires a fallen theater partner (both-alive must not mint stands-over).
Step("sac25b", "combat_kill_related"),
Step("sac26", "age_current", wait: 10f),
Step("sac26b", "interest_expire_pending", value: ""),
Step("sac27", "interest_mark_combat_cold"),
Step("sac28", "director_run", wait: 1.5f),
Step("sac29", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sac30", "assert", expect: "story_phase", value: "Aftermath"),
Step("sac30b", "assert", expect: "story_spine", value: "Duel · Aftermath"),
Step("sac30c", "assert", expect: "story_hold_margin", value: "gt0"),
// Correlated aftermath ownership raises the hold even for a soft anonymous duel.
// Weak ambient peer must not steal mid-aftermath.
Step("sac31", "interest_inject", asset: "human", label: "WeakPeer", tier: "Curiosity",
expect: "sac_weak", value: "lead=event;evt=30;char=5;force=false;ttl=60"),
Step("sac32", "director_run", wait: 1.0f),
Step("sac33", "assert", expect: "tip_asset", value: "aftermath_"),
Step("sac34", "assert", expect: "tip_not_contains", value: "WeakPeer"),
// Leaving the story beat for cast life must drop the raised hold wall.
Step("sac40", "interest_force_session", asset: "human",
label: "{a} feels the joy of parenthood", tier: "Action", expect: "just_had_child",
value: "lead=event;evt=76"),
Step("sac41", "assert", expect: "tip_contains", value: "parenthood"),
Step("sac42", "assert", expect: "story_phase", value: "None"),
Step("sac43", "assert", expect: "story_hold_margin", value: "0"),
Step("sac44", "assert", expect: "story_spine_formatted", value: "empty"),
Step("sac90", "fast_timing", value: "false"),
Step("sac99", "snapshot"),
};
}
private static List StoryCrossArcCorrelation()
{
return new List
{
Step("scac0", "dismiss_windows"),
Step("scac1", "wait_world"),
Step("scac2", "story_cross_arc_probe"),
Step("scac3", "assert", expect: "no_bad"),
Step("scac99", "snapshot"),
};
}
private static List NarrativeSchedulerPolicy()
{
return new List
{
Step("nsp0", "dismiss_windows"),
Step("nsp1", "wait_world"),
Step("nsp2", "narrative_scheduler_probe"),
Step("nsp3", "assert", expect: "no_bad"),
Step("nsp99", "snapshot")
};
}
private static List NarrativeEpisodeGrammar()
{
return new List
{
Step("neg0", "dismiss_windows"),
Step("neg1", "wait_world"),
Step("neg2", "narrative_episode_grammar_probe"),
Step("neg3", "assert", expect: "no_bad"),
Step("neg99", "snapshot")
};
}
private static List NarrativeEnsembleBalance()
{
return new List
{
Step("neb0", "dismiss_windows"),
Step("neb1", "wait_world"),
Step("neb2", "narrative_ensemble_balance_probe"),
Step("neb3", "assert", expect: "no_bad"),
Step("neb99", "snapshot")
};
}
private static List NarrativeProseRedundancy()
{
return new List
{
Step("npr0", "dismiss_windows"),
Step("npr1", "wait_world"),
Step("npr2", "narrative_prose_redundancy_probe"),
Step("npr3", "assert", expect: "no_bad"),
Step("npr99", "snapshot")
};
}
private static List NarrativePresentationCoverage()
{
return new List
{
Step("npc0", "dismiss_windows"),
Step("npc1", "wait_world"),
Step("npc2", "spawn", asset: "human", count: 1),
Step("npc3", "focus", asset: "human"),
Step("npc4", "narrative_presentation_coverage_probe"),
Step("npc4b", "narrative_iteration_probe"),
Step("npc5", "assert", expect: "no_bad"),
Step("npc99", "snapshot")
};
}
private static List NarrativeRelationshipIngestion()
{
return new List
{
Step("nri0", "dismiss_windows"),
Step("nri1", "wait_world"),
Step("nri2", "spawn", asset: "human", count: 3),
Step("nri3", "focus", asset: "human"),
Step("nri4", "narrative_relationship_ingestion_probe"),
Step("nri5", "assert", expect: "no_bad"),
Step("nri99", "snapshot")
};
}
private static List NarrativeCanonicalIngestion()
{
return new List
{
Step("nci0", "dismiss_windows"),
Step("nci1", "wait_world"),
Step("nci2", "spawn", asset: "human", count: 2),
Step("nci3", "focus", asset: "human"),
Step("nci4", "narrative_ingestion_matrix_probe"),
Step("nci5", "assert", expect: "no_bad"),
Step("nci99", "snapshot")
};
}
private static List NarrativePersistenceRoundTrip()
{
return new List
{
Step("npr0", "dismiss_windows"),
Step("npr1", "wait_world"),
Step("npr2", "spawn", asset: "human", count: 2),
Step("npr3", "focus", asset: "human"),
Step("npr4", "narrative_persistence_probe"),
Step("npr5", "assert", expect: "no_bad"),
Step("npr99", "snapshot")
};
}
private static List NarrativePersistenceInvalid()
{
return new List
{
Step("npi0", "dismiss_windows"),
Step("npi1", "wait_world"),
Step("npi2", "narrative_persistence_invalid_probe"),
Step("npi3", "assert", expect: "no_bad"),
Step("npi99", "snapshot")
};
}
private static List NarrativeCameraCutover()
{
return new List
{
Step("ncc0", "dismiss_windows"),
Step("ncc1", "wait_world"),
Step("ncc2", "spawn", asset: "human", count: 3),
Step("ncc3", "focus", asset: "human"),
Step("ncc4", "narrative_camera_cutover_probe"),
Step("ncc5", "assert", expect: "no_bad"),
Step("ncc6", "assert", expect: "camera_exceptional_policy"),
Step("ncc7", "assert", expect: "stability_budgets"),
Step("ncc8", "assert", expect: "saga_replacement_evidence"),
Step("ncc9", "assert", expect: "beat_significance_policy"),
Step("ncc10", "happiness_remember_partner"),
Step("ncc11", "pick_unit", asset: "human", value: "other"),
Step("ncc12", "focus", asset: "human"),
Step("ncc13", "interest_saga_clear"),
Step("ncc14", "saga_force_admit_focus"),
Step("ncc15", "saga_memory_record_focus", asset: "child"),
Step("ncc16", "assert", expect: "beat_significance_live_partner"),
Step("ncc17", "assert", expect: "saga_civilized_no_pack_language"),
Step("ncc99", "snapshot")
};
}
private static List NarrativeStoryInputs()
{
return new List
{
Step("nsi0", "dismiss_windows"),
Step("nsi1", "wait_world"),
Step("nsi2", "spawn", asset: "human", count: 3),
Step("nsi3", "focus", asset: "human"),
Step("nsi4", "narrative_story_inputs_probe"),
Step("nsi5", "assert", expect: "no_bad"),
Step("nsi99", "snapshot")
};
}
private static List NarrativeStoryFirstSoak()
{
return new List
{
Step("nss0", "dismiss_windows"),
Step("nss1", "wait_world"),
Step("nss2", "spawn", asset: "human", count: 4),
Step("nss3", "spectator", value: "off"),
Step("nss4", "spectator", value: "on"),
Step("nss5", "focus", asset: "human"),
Step("nss8", "narrative_soak_seed"),
Step("nss9", "director_run", wait: 8f),
Step("nss10", "narrative_telemetry_probe"),
Step("nss11", "assert", expect: "has_focus"),
Step("nss12", "assert", expect: "tip_matches_focus"),
Step("nss13", "assert", expect: "no_bad"),
Step("nss99", "snapshot")
};
}
private static List NarrativeSchedulerHealth()
{
return new List
{
Step("nsh0", "dismiss_windows"),
Step("nsh1", "wait_world"),
Step("nsh2", "hitch_probe", value: "true"),
Step("nsh3", "spectator", value: "on"),
Step("nsh4", "director_run", wait: 15f),
Step("nsh5", "narrative_scheduler_health_probe"),
Step("nsh6", "hitch_probe", value: "false"),
Step("nsh7", "soak_probe"),
Step("nsh8", "assert", expect: "no_bad"),
Step("nsh99", "snapshot", value: "preserve_saga")
};
}
private static List NarrativeCatalogPolicy()
{
return new List
{
Step("ncp0", "dismiss_windows"),
Step("ncp1", "wait_world"),
Step("ncp2", "narrative_catalog_probe"),
Step("ncp3", "assert", expect: "no_bad"),
Step("ncp99", "snapshot")
};
}
private static List NarrativeNaturalSoak()
{
return new List
{
Step("nns0", "dismiss_windows"),
Step("nns1", "wait_world"),
Step("nns2", "spawn", asset: "human", count: 12),
Step("nns3", "spectator", value: "off"),
Step("nns4", "spectator", value: "on"),
Step("nns5", "focus", asset: "human"),
Step("nns6", "force_live_feeds", value: "true"),
Step("nns9", "narrative_natural_soak_begin"),
Step("nns10", "director_run", wait: 20f),
Step("nns11", "narrative_natural_soak_probe"),
Step("nns12", "assert", expect: "tip_matches_focus"),
Step("nns13", "assert", expect: "no_bad"),
Step("nns15", "force_live_feeds", value: "false"),
Step("nns99", "snapshot")
};
}
private static List NarrativeMultiThreadStress()
{
return new List
{
Step("nmt0", "dismiss_windows"),
Step("nmt1", "wait_world"),
Step("nmt2", "spawn", asset: "human", count: 6),
Step("nmt3", "narrative_multithread_probe"),
Step("nmt4", "assert", expect: "no_bad"),
Step("nmt99", "snapshot")
};
}
private static List NarrativeEditorialReplay()
{
return new List
{
Step("ner0", "dismiss_windows"),
Step("ner1", "wait_world"),
Step("ner2", "editorial_replay_probe"),
Step("ner3", "assert", expect: "no_bad"),
Step("ner99", "snapshot")
};
}
///
/// Parenthood cools world-wide for a while; a second villager cannot chain the same tip.
///
private static List StoryFamilyVariety()
{
return new List
{
Step("sf0", "dismiss_windows"),
Step("sf1", "wait_world"),
Step("sf2", "set_setting", expect: "enabled", value: "true"),
Step("sf3", "fast_timing", value: "true"),
Step("sf4", "spawn", asset: "human", count: 2),
Step("sf5", "spectator", value: "off"),
Step("sf6", "spectator", value: "on"),
Step("sf7", "pick_unit", asset: "human"),
Step("sf8", "focus", asset: "human"),
Step("sf9", "interest_end_session"),
Step("sf10", "interest_variety_clear"),
Step("sf20", "interest_force_session", asset: "human",
label: "{a} feels the joy of parenthood", tier: "Action", expect: "just_had_child",
value: "lead=event;evt=76"),
Step("sf21", "assert", expect: "tip_contains", value: "parenthood"),
Step("sf22", "assert", expect: "beat_cooled", value: "just_had_child"),
Step("sf23", "assert", expect: "variety_arc", value: "life:family"),
// Second human parenthood must stay beat-cooled; a peer tip may still take the camera.
Step("sf30", "interest_end_session"),
Step("sf31", "interest_expire_pending", value: ""),
Step("sf32", "interest_inject", asset: "human",
label: "{a} feels the joy of parenthood", tier: "Action", expect: "just_had_child_b",
value: "lead=event;evt=90;char=5;force=false;ttl=60;pick=other"),
Step("sf33", "interest_inject", asset: "human", label: "VillagePeer", tier: "Action",
expect: "sf_peer", value: "lead=event;evt=55;char=5;force=true;ttl=60;pick=other"),
Step("sf34", "director_run", wait: 0.8f),
Step("sf35", "assert", expect: "tip_contains", value: "VillagePeer"),
Step("sf36", "assert", expect: "tip_not_contains", value: "parenthood"),
Step("sf37", "assert", expect: "beat_cooled", value: "just_had_child"),
Step("sf90", "fast_timing", value: "false"),
Step("sf99", "snapshot"),
};
}
/// Epic preempt during climax; aftermath still available after cold.
private static List StoryArcPreemptResume()
{
return new List
{
Step("spr0", "dismiss_windows"),
Step("spr1", "wait_world"),
Step("spr2", "set_setting", expect: "enabled", value: "true"),
Step("spr3", "fast_timing", value: "true"),
Step("spr4", "spawn", asset: "human", count: 1),
Step("spr5", "spawn", asset: "wolf", count: 1),
Step("spr6", "spectator", value: "off"),
Step("spr7", "spectator", value: "on"),
Step("spr8", "pick_unit", asset: "human"),
Step("spr9", "focus", asset: "human"),
Step("spr10", "interest_end_session"),
Step("spr20", "interest_combat_session", asset: "human", value: "wolf", expect: "spr_pack"),
Step("spr20b", "combat_isolate_pair", value: "20"),
Step("spr21", "status_apply", asset: "human", value: "invincible"),
Step("spr22", "status_apply", asset: "wolf", value: "invincible"),
Step("spr23", "assert", expect: "tip_matches_any", value: "Duel -"),
Step("spr24", "interest_inject", asset: "human", label: "EpicPreempt", tier: "Epic",
expect: "spr_epic", value: "lead=event;evt=150;char=10;force=false;ttl=60"),
Step("spr25", "director_run", wait: 1.2f),
Step("spr26", "assert", expect: "tip_contains", value: "EpicPreempt"),
Step("spr27", "interest_end_session"),
Step("spr28", "interest_expire_pending", value: ""),
Step("spr29", "interest_combat_session", asset: "human", value: "wolf", expect: "spr_pack2"),
Step("spr29b", "combat_isolate_pair", value: "20"),
Step("spr30", "status_apply", asset: "human", value: "invincible"),
Step("spr31", "status_apply", asset: "wolf", value: "invincible"),
Step("spr32", "age_current", wait: 10f),
Step("spr33", "interest_mark_combat_cold"),
Step("spr34", "director_run", wait: 1.5f),
Step("spr35", "assert", expect: "tip_asset", value: "aftermath_"),
Step("spr90", "fast_timing", value: "false"),
Step("spr99", "snapshot"),
};
}
/// MC on the life-saga roster wins a near-tie against a stranger tip.
private static List SagaCameraPrefersMc()
{
return new List
{
Step("scm0", "dismiss_windows"),
Step("scm1", "wait_world"),
Step("scm2", "set_setting", expect: "enabled", value: "true"),
Step("scm3", "fast_timing", value: "true"),
Step("scm4", "spawn", asset: "human", count: 2),
Step("scm5", "spectator", value: "off"),
Step("scm6", "spectator", value: "on"),
Step("scm7", "pick_unit", asset: "human"),
Step("scm8", "focus", asset: "human"),
Step("scm9", "interest_end_session"),
Step("scm10", "interest_saga_clear"),
Step("scm11", "saga_force_admit_focus"),
Step("scm12", "assert", expect: "saga_has_focus"),
Step("scm13", "interest_expire_pending", value: ""),
Step("scm20", "interest_inject", asset: "human", label: "SagaMcWin", tier: "Action",
expect: "scm_win", value: "lead=event;evt=55;char=5;force=false;ttl=60"),
Step("scm21", "interest_inject", asset: "human", label: "SagaMcLose", tier: "Action",
expect: "scm_lose", value: "lead=event;evt=55;char=5;force=false;ttl=60;pick=other"),
Step("scm22", "director_run", wait: 1.2f),
Step("scm23", "assert", expect: "tip_contains", value: "SagaMcWin"),
Step("scm24", "assert", expect: "tip_not_contains", value: "SagaMcLose"),
Step("scm90", "fast_timing", value: "false"),
Step("scm99", "snapshot"),
};
}
private static List SagaConnectionDossier()
{
return new List
{
Step("scd0", "dismiss_windows"),
Step("scd1", "wait_world"),
Step("scd2", "set_setting", expect: "enabled", value: "true"),
Step("scd3", "fast_timing", value: "true"),
Step("scd4", "spawn", asset: "human", count: 2),
Step("scd5", "spectator", value: "off"),
Step("scd6", "spectator", value: "on"),
Step("scd7", "pick_unit", asset: "human"),
Step("scd8", "focus", asset: "human"),
Step("scd9", "interest_saga_clear"),
Step("scd10", "saga_force_admit_focus"),
Step("scd11", "happiness_remember_partner"),
Step("scd12", "interest_expire_pending", value: ""),
Step("scd13", "interest_inject", asset: "human", label: "{a} faces a turning point",
tier: "Epic", expect: "scd_related",
value: "lead=event;evt=90;char=20;force=false;ttl=60;pick=other;related=partner"),
Step("scd14", "director_run", wait: 1.2f),
Step("scd15", "assert", expect: "caption_connection_present"),
Step("scd16", "assert", expect: "caption_connection_anchor", value: "partner"),
Step("scd17", "screenshot", value: "saga-connection-dossier.png"),
Step("scd90", "fast_timing", value: "false"),
Step("scd99", "snapshot")
};
}
private static List SagaRosterCapFive()
{
return new List
{
Step("src0", "dismiss_windows"),
Step("src1", "wait_world"),
Step("src2", "interest_saga_clear"),
Step("src3", "spawn", asset: "human", count: 6),
Step("src4", "saga_force_admit_six"),
Step("src5", "assert", expect: "saga_roster_count", value: "5"),
Step("src99", "snapshot")
};
}
private static List SagaRareAntagonistAcceptance()
{
return new List
{
Step("sra0", "dismiss_windows"),
Step("sra1", "wait_world"),
Step("sra2", "set_setting", expect: "enabled", value: "true"),
Step("sra3", "fast_timing", value: "true"),
Step("sra4", "spawn", asset: "human", count: 3),
Step("sra5", "spectator", value: "off"),
Step("sra6", "spectator", value: "on"),
Step("sra7", "pick_unit", asset: "human"),
Step("sra8", "happiness_remember_partner"),
Step("sra9", "pick_unit", asset: "human", value: "other"),
Step("sra10", "focus", asset: "human"),
Step("sra11", "interest_saga_clear"),
Step("sra12", "saga_force_admit_focus"),
Step("sra13", "happiness_bond_lovers"),
Step("sra14", "assert", expect: "saga_rare_antagonist_policy"),
Step("sra15", "assert", expect: "saga_cast_harm_evidence"),
Step("sra16", "assert", expect: "camera_connection_attribution"),
// Exercise the real live world-scan path after the pure policy boundaries.
Step("sra20", "interest_saga_clear"),
Step("sra21", "spawn", asset: "human", count: 6),
Step("sra22", "saga_force_admit_six"),
Step("sra23", "assert", expect: "saga_roster_count", value: "5"),
Step("sra24", "assert", expect: "saga_roster_repeat_stable"),
Step("sra90", "fast_timing", value: "false"),
Step("sra99", "snapshot")
};
}
/// Prefer toggle soft-biases a near-tie toward the Prefer'd MC.
private static List SagaPreferSoftBias()
{
return new List
{
Step("spb0", "dismiss_windows"),
Step("spb1", "wait_world"),
Step("spb2", "set_setting", expect: "enabled", value: "true"),
Step("spb3", "fast_timing", value: "true"),
Step("spb4", "spawn", asset: "human", count: 2),
Step("spb5", "spectator", value: "off"),
Step("spb6", "spectator", value: "on"),
Step("spb7", "pick_unit", asset: "human"),
Step("spb8", "focus", asset: "human"),
Step("spb9", "interest_end_session"),
Step("spb10", "interest_saga_clear"),
Step("spb11", "saga_force_admit_focus"),
Step("spb12", "saga_prefer_focus"),
Step("spb13", "assert", expect: "saga_prefer_focus_on"),
Step("spb13b", "status_apply", asset: "human", value: "invincible"),
Step("spb14", "interest_expire_pending", value: ""),
Step("spb20", "interest_inject", asset: "human", label: "PreferWin", tier: "Action",
expect: "spb_win", value: "lead=event;evt=55;char=5;force=false;ttl=60"),
Step("spb21", "interest_inject", asset: "human", label: "PreferLose", tier: "Action",
expect: "spb_lose", value: "lead=event;evt=55;char=5;force=false;ttl=60;pick=other"),
Step("spb22", "director_run", wait: 1.2f),
Step("spb23", "assert", expect: "tip_contains", value: "PreferWin"),
Step("spb24", "assert", expect: "tip_not_contains", value: "PreferLose"),
Step("spb90", "fast_timing", value: "false"),
Step("spb99", "snapshot"),
};
}
/// Prefer/MC tip wins when a stranger is moderately hotter (inside sagaNearTieEpsilon).
private static List SagaWinsModerateGap()
{
return new List
{
Step("smg0", "dismiss_windows"),
Step("smg1", "wait_world"),
Step("smg2", "set_setting", expect: "enabled", value: "true"),
Step("smg3", "fast_timing", value: "true"),
Step("smg4", "spawn", asset: "human", count: 2),
Step("smg5", "spectator", value: "off"),
Step("smg6", "spectator", value: "on"),
Step("smg7", "pick_unit", asset: "human"),
Step("smg8", "focus", asset: "human"),
Step("smg9", "interest_end_session"),
Step("smg10", "interest_saga_clear"),
Step("smg11", "saga_force_admit_focus"),
Step("smg12", "assert", expect: "saga_has_focus"),
Step("smg13", "interest_expire_pending", value: ""),
// Stranger hotter by ~15-20 after RecalcTotal; sagaNearTieEpsilon (28) still picks MC.
Step("smg20", "interest_inject", asset: "human", label: "ModGapMc", tier: "Action",
expect: "smg_mc", value: "lead=event;evt=55;char=5;force=false;ttl=60"),
Step("smg21", "interest_inject", asset: "human", label: "ModGapStranger", tier: "Action",
expect: "smg_stranger", value: "lead=event;evt=78;char=5;force=false;ttl=60;pick=other"),
Step("smg22", "director_run", wait: 1.2f),
Step("smg23", "assert", expect: "tip_contains", value: "ModGapMc"),
Step("smg24", "assert", expect: "tip_not_contains", value: "ModGapStranger"),
Step("smg90", "fast_timing", value: "false"),
Step("smg99", "snapshot"),
};
}
/// MC CombatFocusWeight beats a non-MC partner (mid-fight follow bias).
private static List SagaCombatFollowMc()
{
return new List
{
Step("scf0", "dismiss_windows"),
Step("scf1", "wait_world"),
Step("scf2", "set_setting", expect: "enabled", value: "true"),
Step("scf3", "spawn", asset: "human", count: 2),
Step("scf4", "spectator", value: "off"),
Step("scf5", "spectator", value: "on"),
// Partner first (non-MC), then admit a different focus as MC.
Step("scf6", "pick_unit", asset: "human"),
Step("scf7", "happiness_remember_partner"),
Step("scf8", "pick_unit", asset: "human", value: "other"),
Step("scf9", "focus", asset: "human"),
Step("scf10", "interest_saga_clear"),
Step("scf11", "saga_force_admit_focus"),
Step("scf12", "assert", expect: "saga_has_focus"),
Step("scf13", "assert", expect: "saga_combat_weight_beats_partner"),
Step("scf99", "snapshot"),
};
}
/// MC lover tip beats an equal stranger tip via cast soft bias.
private static List SagaCastSoftBias()
{
return new List
{
Step("scb0", "dismiss_windows"),
Step("scb1", "wait_world"),
Step("scb2", "set_setting", expect: "enabled", value: "true"),
Step("scb3", "fast_timing", value: "true"),
Step("scb4", "spawn", asset: "human", count: 3),
Step("scb5", "spectator", value: "off"),
Step("scb6", "spectator", value: "on"),
// Remember lover candidate first, then admit a different MC and bond them.
Step("scb7", "pick_unit", asset: "human"),
Step("scb8", "happiness_remember_partner"),
Step("scb9", "pick_unit", asset: "human", value: "other"),
Step("scb10", "focus", asset: "human"),
Step("scb11", "interest_end_session"),
Step("scb12", "interest_saga_clear"),
Step("scb13", "saga_force_admit_focus"),
Step("scb14", "happiness_bond_lovers"),
Step("scb15", "interest_expire_pending", value: ""),
Step("scb20", "focus_partner"),
Step("scb21", "interest_inject", asset: "human", label: "CastLoverWin", tier: "Action",
expect: "scb_cast", value: "lead=event;evt=55;char=5;force=false;ttl=60"),
Step("scb22", "interest_inject", asset: "human", label: "CastStrangerLose", tier: "Action",
expect: "scb_stranger", value: "lead=event;evt=55;char=5;force=false;ttl=60;pick=other"),
Step("scb23", "director_run", wait: 1.2f),
Step("scb24", "assert", expect: "tip_contains", value: "CastLoverWin"),
Step("scb25", "assert", expect: "tip_not_contains", value: "CastStrangerLose"),
Step("scb90", "fast_timing", value: "false"),
Step("scb99", "snapshot"),
};
}
/// Two admitted MCs on one tip light Active + Involved rail chrome.
private static List SagaCrossMcLight()
{
return new List
{
Step("scl0", "dismiss_windows"),
Step("scl1", "wait_world"),
Step("scl2", "set_setting", expect: "enabled", value: "true"),
Step("scl3", "set_setting", expect: "show_dossier_caption", value: "true"),
Step("scl4", "spawn", asset: "human", count: 2),
Step("scl5", "spectator", value: "off"),
Step("scl6", "spectator", value: "on"),
Step("scl7", "pick_unit", asset: "human"),
Step("scl8", "happiness_remember_partner"),
Step("scl9", "pick_unit", asset: "human", value: "other"),
Step("scl10", "focus", asset: "human"),
Step("scl11", "interest_end_session"),
Step("scl12", "interest_saga_clear"),
Step("scl13", "saga_force_admit_focus"),
Step("scl14", "saga_force_admit_partner"),
Step("scl15", "interest_expire_pending", value: ""),
Step("scl20", "interest_inject", asset: "human", label: "CrossMcTip", tier: "Action",
expect: "scl_tip", value: "lead=event;evt=60;char=5;force=true;ttl=60"),
Step("scl21", "interest_bind_related_partner"),
Step("scl22", "wait", wait: 0.4f),
Step("scl23", "assert", expect: "saga_rail_active_highlight"),
Step("scl24", "assert", expect: "saga_rail_involved_count", value: "1"),
Step("scl99", "snapshot"),
};
}
///
/// Two roster MCs on one tip get CrossMC SoftBias + cross bonus and Involved chrome;
/// a hotter stranger does not displace the live cross tip.
///
private static List SagaCrossMcWinsGap()
{
return new List
{
Step("scg0", "dismiss_windows"),
Step("scg1", "wait_world"),
Step("scg2", "set_setting", expect: "enabled", value: "true"),
Step("scg3", "fast_timing", value: "true"),
Step("scg4", "spawn", asset: "human", count: 3),
Step("scg5", "spectator", value: "off"),
Step("scg6", "spectator", value: "on"),
Step("scg7", "pick_unit", asset: "human"),
Step("scg8", "happiness_remember_partner"),
Step("scg9", "pick_unit", asset: "human", value: "other"),
Step("scg10", "focus", asset: "human"),
Step("scg11", "interest_end_session"),
Step("scg12", "interest_saga_clear"),
Step("scg13", "saga_force_admit_focus"),
Step("scg14", "saga_force_admit_partner"),
Step("scg15", "interest_expire_pending", value: ""),
Step("scg16", "interest_story_purge_leftovers"),
Step("scg20", "interest_inject", asset: "human", label: "CrossGapWin", tier: "Action",
expect: "scg_cross",
value: "lead=event;evt=55;char=5;force=true;ttl=60;related=partner"),
Step("scg21", "assert", expect: "tip_contains", value: "CrossGapWin"),
Step("scg22", "assert", expect: "saga_soft_bias_rank", value: "3", label: "min"),
Step("scg23", "assert", expect: "saga_score_bonus_min", value: "20"),
Step("scg24", "assert", expect: "saga_rail_involved_count", value: "1", label: "min"),
Step("scg90", "fast_timing", value: "false"),
Step("scg99", "snapshot"),
};
}
/// MC kills MC → both memories hold McCrossover facts.
private static List SagaCrossMcKill()
{
return new List
{
Step("sck0", "dismiss_windows"),
Step("sck1", "wait_world"),
Step("sck2", "set_setting", expect: "enabled", value: "true"),
Step("sck3", "spawn", asset: "human", count: 2),
Step("sck4", "spectator", value: "off"),
Step("sck5", "spectator", value: "on"),
Step("sck6", "pick_unit", asset: "human"),
Step("sck7", "happiness_remember_partner"),
Step("sck8", "pick_unit", asset: "human", value: "other"),
Step("sck9", "focus", asset: "human"),
Step("sck10", "interest_saga_clear"),
Step("sck11", "saga_force_admit_focus"),
Step("sck12", "saga_force_admit_partner"),
Step("sck13", "saga_record_kill_partner"),
Step("sck14", "assert", expect: "saga_crossover_focus"),
Step("sck15", "assert", expect: "saga_rival_focus", value: "false"),
Step("sck99", "snapshot"),
};
}
/// One kill+death cycle must not earn RivalEarned.
private static List SagaRivalNotSingleKill()
{
return new List
{
Step("srk0", "dismiss_windows"),
Step("srk1", "wait_world"),
Step("srk2", "set_setting", expect: "enabled", value: "true"),
Step("srk3", "spawn", asset: "human", count: 2),
Step("srk4", "spectator", value: "off"),
Step("srk5", "spectator", value: "on"),
Step("srk6", "pick_unit", asset: "human"),
Step("srk7", "happiness_remember_partner"),
Step("srk8", "pick_unit", asset: "human", value: "other"),
Step("srk9", "focus", asset: "human"),
Step("srk10", "interest_saga_clear"),
Step("srk11", "saga_force_admit_focus"),
Step("srk12", "saga_record_kill_partner"),
Step("srk13", "assert", expect: "saga_rival_focus", value: "false"),
Step("srk14", "assert", expect: "saga_antagonist_partner", value: "false"),
Step("srk99", "snapshot"),
};
}
private static List SagaLegacyCombatSummary()
{
return new List
{
Step("slcs0", "dismiss_windows"),
Step("slcs1", "wait_world"),
Step("slcs2", "spawn", asset: "human", count: 4),
Step("slcs3", "focus", asset: "human"),
Step("slcs4", "interest_saga_clear"),
Step("slcs5", "saga_legacy_combat_probe"),
Step("slcs99", "snapshot"),
};
}
/// Threshold living rematches earn RivalEarned.
private static List SagaRivalRematch()
{
return new List
{
Step("srr0", "dismiss_windows"),
Step("srr1", "wait_world"),
Step("srr2", "set_setting", expect: "enabled", value: "true"),
Step("srr3", "spawn", asset: "human", count: 2),
Step("srr4", "spectator", value: "off"),
Step("srr5", "spectator", value: "on"),
Step("srr6", "pick_unit", asset: "human"),
Step("srr7", "happiness_remember_partner"),
Step("srr8", "pick_unit", asset: "human", value: "other"),
Step("srr9", "focus", asset: "human"),
Step("srr10", "interest_saga_clear"),
Step("srr11", "saga_force_admit_focus"),
Step("srr12", "saga_note_encounters", value: "3"),
Step("srr13", "assert", expect: "saga_rival_focus"),
Step("srr14", "assert", expect: "saga_antagonist_partner"),
Step("srr15", "saga_consider_antagonist_partner"),
Step("srr16", "assert", expect: "saga_roster_count", value: "2", label: "min"),
Step("srr99", "snapshot"),
};
}
/// Prefer tip cuts an ambient/fill hold without waiting for full margin.
private static List SagaPreferCutsAmbient()
{
return new List
{
Step("spa0", "dismiss_windows"),
Step("spa1", "wait_world"),
Step("spa2", "set_setting", expect: "enabled", value: "true"),
Step("spa3", "fast_timing", value: "true"),
Step("spa4", "spawn", asset: "human", count: 2),
Step("spa5", "spectator", value: "off"),
Step("spa6", "spectator", value: "on"),
Step("spa7", "pick_unit", asset: "human"),
Step("spa8", "focus", asset: "human"),
Step("spa9", "interest_end_session"),
Step("spa10", "interest_saga_clear"),
Step("spa11", "saga_force_admit_focus"),
Step("spa12", "saga_prefer_focus"),
Step("spa13", "interest_expire_pending", value: ""),
// Low fill-band ambient tip first.
Step("spa20", "interest_inject", asset: "human", label: "AmbientFillHold", tier: "Curiosity",
expect: "spa_fill", value: "lead=character;evt=12;char=8;force=true;ttl=60;pick=other"),
Step("spa21", "director_run", wait: 0.6f),
Step("spa22", "assert", expect: "tip_contains", value: "AmbientFillHold"),
Step("spa23", "interest_inject", asset: "human", label: "PreferCutsAmbient", tier: "Action",
expect: "spa_prefer", value: "lead=event;evt=50;char=5;force=false;ttl=60"),
Step("spa24", "director_run", wait: 1.0f),
Step("spa25", "assert", expect: "tip_contains", value: "PreferCutsAmbient"),
Step("spa90", "fast_timing", value: "false"),
Step("spa99", "snapshot"),
};
}
///
/// Compact saga snapshot: admission headline, collapsible layout, structured chapters,
/// no planner jargon / trait descs / Chronicle dumps.
///
private static List SagaOverviewHasMc()
{
return new List
{
Step("som0", "dismiss_windows"),
Step("som1", "wait_world"),
Step("som2", "set_setting", expect: "enabled", value: "true"),
Step("som3", "fast_timing", value: "true"),
Step("som4", "spawn", asset: "human", count: 1),
Step("som5", "spectator", value: "off"),
Step("som6", "spectator", value: "on"),
Step("som7", "pick_unit", asset: "human"),
Step("som8", "focus", asset: "human"),
Step("som9", "interest_saga_clear"),
Step("som10", "saga_force_admit_focus"),
Step("som12", "assert", expect: "saga_roster_count", value: "1", label: "min"),
Step("som13", "assert", expect: "saga_overview_tip_not",
value: "Climax|Aftermath|Epilogue|parked|watching|Appearance brings comfort|Standing|Known for|Harness chapter|Fighting "),
Step("som13b", "assert", expect: "saga_overview_tip",
value: "At stake|Cast|Legacy|King|Leader|Alpha|Dragon|Human|life"),
Step("som14", "assert", expect: "story_rail_count", value: "1", label: "min"),
Step("som15", "assert", expect: "saga_admission_persisted"),
Step("som15b", "saga_rail_refresh"),
Step("som15c", "assert", expect: "saga_admission_persisted"),
Step("som16", "saga_show_hover_first"),
Step("som17", "assert", expect: "saga_tab_effective", value: "saga"),
Step("som17b", "assert", expect: "saga_snapshot_layout"),
Step("som20", "saga_stamp_chapter_focus", asset: "Combat",
value: "Survived a bloody duel against a rival"),
Step("som21", "saga_rail_refresh"),
Step("som22", "saga_show_hover_first"),
Step("som23", "assert", expect: "saga_overview_tip", value: "Survived a bloody duel"),
Step("som24", "assert", expect: "saga_overview_tip_not",
value: "Climax|Aftermath|Epilogue|Appearance brings comfort|Standing age|Fighting "),
Step("som25", "assert", expect: "saga_snapshot_layout"),
Step("som26", "screenshot", value: "saga-adaptive-hover"),
Step("som90", "fast_timing", value: "false"),
Step("som99", "snapshot"),
};
}
/// Hover depth panel under always-on character beat caption (no Dossier/Saga tabs).
private static List SagaAdaptiveDossier()
{
return new List
{
Step("sad0", "dismiss_windows"),
Step("sad1", "wait_world"),
Step("sad2", "set_setting", expect: "enabled", value: "true"),
Step("sad3", "fast_timing", value: "true"),
Step("sad4", "spawn", asset: "human", count: 2),
Step("sad5", "spectator", value: "off"),
Step("sad6", "spectator", value: "on"),
Step("sad7", "interest_saga_clear"),
Step("sad8", "pick_unit", asset: "human"),
Step("sad8a", "happiness_remember_partner"),
Step("sad8b", "pick_unit", asset: "human", value: "other"),
Step("sad9", "focus", asset: "human"),
Step("sad10", "saga_force_admit_focus"),
Step("sad10p", "saga_force_admit_partner"),
Step("sad10a", "saga_memory_record_focus", asset: "War", value: "opposing commander",
expect: "harness_cast_relation"),
Step("sad11", "assert", expect: "saga_tab_selected", value: "dossier"),
Step("sad11a", "saga_rail_refresh"),
// saga_select_tab saga → hover preview of watched MC (compat).
Step("sad11b", "caption_capture_beat"),
Step("sad12", "saga_select_tab", value: "saga"),
Step("sad13", "assert", expect: "saga_tab_effective", value: "saga"),
Step("sad14", "assert", expect: "saga_panel_bound"),
Step("sad14b", "assert", expect: "saga_panel_fixed_size"),
Step("sad14c", "assert", expect: "caption_hover_keeps_beat"),
Step("sad14c2", "assert", expect: "saga_hover_visual_stable"),
Step("sad14d", "assert", expect: "saga_panel_cast_first"),
Step("sad14e", "assert", expect: "saga_panel_cast_relations", value: "1"),
Step("sad15", "screenshot", value: "saga-hover-depth"),
Step("sad15b", "saga_stamp_chapter_focus", asset: "Love", value: "Bound with a lasting partner"),
Step("sad15c", "saga_memory_record_focus", asset: "Founding", value: "kingdom"),
Step("sad15d", "saga_select_tab", value: "saga"),
Step("sad15e", "assert", expect: "saga_panel_fixed_size"),
Step("sad15e2", "assert", expect: "saga_panel_lens", value: "true"),
Step("sad15e3", "assert", expect: "saga_overview_tip_not",
value: "Evidence|kills ·|remembered clashes|Open Lore|Earned rival|saga hero"),
Step("sad15f", "screenshot", value: "saga-hover-rich"),
Step("sad16", "saga_select_tab", value: "dossier"),
Step("sad16b", "assert", expect: "saga_panel_fixed_size"),
Step("sad16c", "screenshot", value: "saga-caption-idle"),
Step("sad17", "saga_show_hover_first"),
Step("sad17b", "assert", expect: "saga_panel_fixed_size"),
Step("sad18", "assert", expect: "saga_tab_selected", value: "dossier"),
Step("sad19", "assert", expect: "saga_tab_effective", value: "saga"),
Step("sad20", "assert", expect: "saga_hover_preview", value: "true"),
Step("sad21", "assert", expect: "saga_read_pause", value: "true"),
Step("sad21a", "saga_scrub_hover_second"),
Step("sad21a2", "assert", expect: "saga_hover_nametag_ownership"),
Step("sad21b", "saga_hide_hover"),
Step("sad21d", "saga_hover_tick_away"),
Step("sad21h", "assert", expect: "saga_hover_preview", value: "false"),
Step("sad21i", "assert", expect: "saga_tab_effective", value: "dossier"),
Step("sad21j", "assert", expect: "saga_read_pause", value: "false"),
Step("sad21j2", "assert", expect: "saga_dossier_chrome_restored"),
// Prefer click does not open hover / pin saga subject.
// Re-seed a human - long hover cycles can leave the map without living units.
Step("sad30", "saga_select_tab", value: "dossier"),
Step("sad30b", "spawn", asset: "human", count: 2),
Step("sad30c", "pick_unit", asset: "human"),
Step("sad30d", "focus", asset: "human"),
Step("sad30e", "interest_saga_clear"),
Step("sad30f", "saga_force_admit_focus"),
Step("sad30g", "saga_rail_refresh"),
Step("sad31", "saga_click_first"),
Step("sad32", "assert", expect: "saga_prefer_focus_on", value: "true"),
Step("sad33", "assert", expect: "saga_hover_preview", value: "false"),
Step("sad34", "assert", expect: "caption_mc_identity_or_species"),
Step("sad43", "screenshot", value: "saga-rail-prefer"),
Step("sad90", "fast_timing", value: "false"),
Step("sad99", "snapshot"),
};
}
/// Memory survives roster eviction and captures without Chronicle.
private static List SagaMemorySurvives()
{
return new List
{
Step("sms0", "dismiss_windows"),
Step("sms1", "wait_world"),
Step("sms2", "set_setting", expect: "enabled", value: "true"),
Step("sms3", "set_setting", expect: "chronicle_enabled", value: "false"),
Step("sms4", "fast_timing", value: "true"),
Step("sms5", "spawn", asset: "human", count: 2),
Step("sms6", "spectator", value: "off"),
Step("sms7", "spectator", value: "on"),
Step("sms8", "interest_saga_clear"),
Step("sms9", "pick_unit", asset: "human"),
Step("sms10", "focus", asset: "human"),
Step("sms11", "saga_force_admit_focus"),
Step("sms12", "saga_memory_record_focus", asset: "Kill", value: "Named foe"),
Step("sms13", "assert", expect: "saga_memory_has_focus", value: "true"),
Step("sms14", "assert", expect: "saga_memory_fact_focus", value: "Kill"),
Step("sms15", "saga_set_scores_focus", value: "0.1", label: "0"),
Step("sms16", "pick_unit", asset: "human", label: "other"),
Step("sms17", "focus", asset: "human"),
Step("sms18", "saga_force_admit_focus"),
Step("sms19", "saga_world_scan"),
Step("sms20", "assert", expect: "saga_memory_life_count", value: "1", label: "min"),
Step("sms21", "set_setting", expect: "chronicle_enabled", value: "true"),
Step("sms90", "fast_timing", value: "false"),
Step("sms99", "snapshot"),
};
}
/// WarJoin snapshots both principals with Other ids and resolves on end.
private static List SagaWarMemoryBothSides()
{
return new List
{
Step("swm0", "dismiss_windows"),
Step("swm1", "wait_world"),
Step("swm2", "set_setting", expect: "enabled", value: "true"),
Step("swm3", "fast_timing", value: "true"),
Step("swm4", "spawn", asset: "human", count: 2),
Step("swm5", "spectator", value: "off"),
Step("swm6", "spectator", value: "on"),
Step("swm7", "interest_saga_clear"),
Step("swm8", "pick_unit", asset: "human"),
Step("swm9", "happiness_remember_partner"),
Step("swm10", "pick_unit", asset: "human", value: "other"),
Step("swm11", "focus", asset: "human"),
Step("swm12", "saga_force_admit_focus"),
Step("swm13", "saga_force_admit_partner"),
Step("swm14", "saga_memory_record_focus", asset: "War", value: "North vs South",
expect: "harness_war", label: "North"),
Step("swm15", "assert", expect: "saga_memory_fact_focus", value: "WarJoin"),
Step("swm16", "assert", expect: "saga_memory_fact_other", value: "WarJoin"),
Step("swm17", "assert", expect: "saga_stake_contains", value: "Still at war|North vs South|War with"),
Step("swm18", "saga_memory_resolve_war", value: "harness_war"),
Step("swm19", "assert", expect: "saga_memory_fact_resolved", value: "harness_war", label: "true"),
// Original partner from swm9 is the other principal - focus them directly.
Step("swm20", "focus_partner"),
Step("swm21", "assert", expect: "saga_memory_fact_other", value: "War", label: "nonzero"),
Step("swm22", "assert", expect: "saga_memory_fact_resolved", value: "harness_war", label: "true"),
Step("swm90", "fast_timing", value: "false"),
Step("swm99", "snapshot"),
};
}
/// Unresolved PlotJoin Other lands in Cast as Plot ally.
private static List SagaPlotCastMembers()
{
return new List
{
Step("spc0", "dismiss_windows"),
Step("spc1", "wait_world"),
Step("spc2", "set_setting", expect: "enabled", value: "true"),
Step("spc3", "fast_timing", value: "true"),
Step("spc4", "spawn", asset: "human", count: 2),
Step("spc5", "spectator", value: "off"),
Step("spc6", "spectator", value: "on"),
Step("spc7", "interest_saga_clear"),
Step("spc8", "pick_unit", asset: "human"),
Step("spc9", "happiness_remember_partner"),
Step("spc10", "pick_unit", asset: "human", value: "other"),
Step("spc11", "focus", asset: "human"),
Step("spc12", "saga_force_admit_focus"),
Step("spc13", "saga_memory_record_focus", asset: "Plot", value: "coup", expect: "harness_plot"),
Step("spc14", "assert", expect: "saga_memory_fact_other", value: "PlotJoin"),
Step("spc15", "saga_select_tab", value: "saga"),
Step("spc16", "assert", expect: "saga_cast_contains", value: "Plot Partner"),
Step("spc17", "assert", expect: "saga_stake_contains", value: "Still tangled|Plotting|Joined|Plotted"),
Step("spc90", "fast_timing", value: "false"),
Step("spc99", "snapshot"),
};
}
/// Crown lens still shows family Parent in Cast (peer fill not Pack-only).
private static List SagaCastKinNonPackLens()
{
return new List
{
Step("sck0", "dismiss_windows"),
Step("sck1", "wait_world"),
Step("sck2", "set_setting", expect: "enabled", value: "true"),
Step("sck3", "fast_timing", value: "true"),
Step("sck4", "spawn", asset: "human", count: 2),
Step("sck5", "spectator", value: "off"),
Step("sck6", "spectator", value: "on"),
Step("sck7", "interest_saga_clear"),
Step("sck8", "pick_unit", asset: "human"),
Step("sck9", "happiness_remember_partner"),
Step("sck10", "pick_unit", asset: "human", value: "other"),
Step("sck11", "focus", asset: "human"),
Step("sck12", "saga_force_admit_focus"),
Step("sck13", "saga_role_rise_focus", value: "become_king"),
Step("sck14", "relationship_set_parent"),
Step("sck15", "saga_select_tab", value: "saga"),
Step("sck16", "assert", expect: "saga_panel_lens", value: "true"),
Step("sck17", "assert", expect: "saga_stake_contains", value: "Crown|Became king"),
Step("sck18", "assert", expect: "saga_cast_contains", value: "Parent"),
Step("sck19", "saga_memory_record_focus", asset: "War", value: "North vs South",
expect: "harness_war_crown", label: "North"),
Step("sck20", "assert", expect: "saga_cast_contains", value: "War Enemy|Parent"),
Step("sck90", "fast_timing", value: "false"),
Step("sck99", "snapshot"),
};
}
///
/// Legacy is turning points, not Cast-echo child soup: war + lineage summary, no raw "Child X".
///
private static List SagaLegacyQuality()
{
return new List
{
Step("slq0", "dismiss_windows"),
Step("slq1", "wait_world"),
Step("slq2", "set_setting", expect: "enabled", value: "true"),
Step("slq3", "fast_timing", value: "true"),
// Parent (MC) + lover from initial spawn; children and foe are spawned fresh below.
Step("slq4", "spawn", asset: "human", count: 2),
Step("slq5", "spectator", value: "off"),
Step("slq6", "spectator", value: "on"),
Step("slq7", "interest_saga_clear"),
Step("slq8", "pick_unit", asset: "human"),
Step("slq9", "focus", asset: "human"),
Step("slq10", "saga_force_admit_focus"),
Step("slq11", "saga_role_rise_focus", value: "become_king"),
// Keep parent as remembered partner so focus_partner always returns to the MC.
Step("slq12", "happiness_remember_partner"),
// Lover (focus other, partner = parent).
Step("slq13", "pick_unit", asset: "human", value: "other"),
Step("slq14", "focus", asset: "human"),
Step("slq15", "happiness_bond_lovers"),
Step("slq16", "focus_partner"),
// Three fresh children: spawn → pick last spawn → setParent1(parent).
Step("slq17", "spawn", asset: "human", count: 1),
Step("slq18", "pick_unit", asset: "human"),
Step("slq19", "relationship_set_parent"),
Step("slq20", "focus_partner"),
Step("slq21", "spawn", asset: "human", count: 1),
Step("slq22", "pick_unit", asset: "human"),
Step("slq23", "relationship_set_parent"),
Step("slq24", "focus_partner"),
Step("slq25", "spawn", asset: "human", count: 1),
Step("slq26", "pick_unit", asset: "human"),
Step("slq27", "relationship_set_parent"),
Step("slq28", "focus_partner"),
// War foe: fresh spawn; harness stamps WarJoin onto remembered parent too.
Step("slq29", "spawn", asset: "human", count: 1),
Step("slq30", "pick_unit", asset: "human"),
Step("slq31", "saga_memory_record_focus", asset: "War", value: "North vs South",
expect: "harness_war_legacy", label: "North"),
// Resolve so Crown/Role stays At stake and War lands in Legacy as a closed beat.
Step("slq31b", "saga_memory_resolve_war", value: "harness_war_legacy"),
Step("slq32", "focus_partner"),
Step("slq33", "saga_select_tab", value: "saga"),
Step("slq34", "assert", expect: "saga_stake_contains", value: "Crown|kingdom|Became king"),
Step("slq35", "assert", expect: "saga_legacy_contains", value: "War"),
Step("slq36", "assert", expect: "saga_legacy_contains", value: "children"),
// saga_legacy_quality already rejects raw "Child Name" prose (and "children" must not trip it).
Step("slq38", "assert", expect: "saga_legacy_quality"),
Step("slq39", "screenshot", value: "saga-legacy-quality"),
Step("slq90", "fast_timing", value: "false"),
Step("slq99", "snapshot"),
};
}
/// Live roster role rise updates Identity and the current supported stake.
private static List SagaStakeFromRoleLive()
{
return new List
{
Step("srl0", "dismiss_windows"),
Step("srl1", "wait_world"),
Step("srl2", "set_setting", expect: "enabled", value: "true"),
Step("srl3", "fast_timing", value: "true"),
Step("srl4", "spawn", asset: "human", count: 1),
Step("srl5", "spectator", value: "off"),
Step("srl6", "spectator", value: "on"),
Step("srl7", "interest_saga_clear"),
Step("srl8", "pick_unit", asset: "human"),
Step("srl9", "focus", asset: "human"),
Step("srl10", "saga_force_admit_focus"),
Step("srl13", "saga_role_rise_focus", value: "become_clan_chief"),
Step("srl14", "assert", expect: "saga_memory_fact_focus", value: "RoleChange"),
Step("srl15", "assert", expect: "saga_stake_contains", value: "Clan Chief"),
Step("srl90", "fast_timing", value: "false"),
Step("srl99", "snapshot"),
};
}
///
/// Session reset clears roster + memory even when World.world reference is unchanged.
/// Same-map spectator toggle must not wipe.
///
private static List SagaSessionReset()
{
return new List
{
Step("ssr0", "dismiss_windows"),
Step("ssr1", "wait_world"),
Step("ssr2", "set_setting", expect: "enabled", value: "true"),
Step("ssr3", "fast_timing", value: "true"),
Step("ssr4", "spawn", asset: "human", count: 2),
Step("ssr5", "spectator", value: "off"),
Step("ssr6", "spectator", value: "on"),
Step("ssr7", "interest_saga_clear"),
Step("ssr8", "pick_unit", asset: "human"),
Step("ssr9", "focus", asset: "human"),
Step("ssr10", "saga_force_admit_focus"),
Step("ssr11", "saga_memory_record_focus", asset: "Kill", value: "Session foe"),
Step("ssr12", "assert", expect: "saga_roster_count", value: "1", label: "min"),
Step("ssr13", "assert", expect: "saga_memory_life_count", value: "1", label: "min"),
Step("ssr14", "spectator", value: "off"),
Step("ssr15", "spectator", value: "on"),
Step("ssr16", "assert", expect: "saga_roster_count", value: "1", label: "min"),
Step("ssr17", "assert", expect: "saga_memory_life_count", value: "1", label: "min"),
Step("ssr18", "saga_session_reset"),
Step("ssr19", "assert", expect: "saga_roster_count", value: "0"),
// Live feeds may immediately record new-world facts; reset proves the old
// focused-life sentinel is gone instead of requiring global count == 0.
Step("ssr20", "assert", expect: "no_bad"),
Step("ssr90", "fast_timing", value: "false"),
Step("ssr99", "snapshot"),
};
}
private static List SagaRelationCacheRevision()
{
return new List
{
Step("srcr0", "dismiss_windows"),
Step("srcr1", "wait_world"),
Step("srcr2", "spawn", asset: "human", count: 1),
Step("srcr3", "pick_unit", asset: "human"),
Step("srcr4", "focus", asset: "human"),
Step("srcr5", "saga_relation_cache_revision_probe"),
Step("srcr6", "assert", expect: "no_bad"),
Step("srcr99", "snapshot"),
};
}
private static List SagaCastStableSnapshot()
{
return new List
{
Step("scds0", "dismiss_windows"),
Step("scds1", "wait_world"),
Step("scds2", "set_setting", expect: "enabled", value: "true"),
Step("scds3", "spawn", asset: "human", count: 2),
Step("scds4", "spectator", value: "off"),
Step("scds5", "spectator", value: "on"),
Step("scds6", "interest_saga_clear"),
Step("scds7", "pick_unit", asset: "human"),
Step("scds8", "happiness_remember_partner"),
Step("scds9", "pick_unit", asset: "human", value: "other"),
Step("scds10", "focus", asset: "human"),
Step("scds11", "saga_force_admit_focus"),
Step("scds12", "saga_memory_record_focus", asset: "child"),
Step("scds13", "assert", expect: "saga_cast_contains", value: "Child"),
Step("scds14", "saga_memory_clear_only"),
Step("scds15", "assert", expect: "saga_cast_contains", value: "Child"),
Step("scds16", "saga_memory_record_focus", asset: "child"),
Step("scds17", "assert", expect: "saga_memory_has_focus", value: "true"),
Step("scds18", "saga_cast_stability_probe"),
Step("scds19", "saga_show_hover_first"),
Step("scds20", "assert", expect: "saga_panel_bound"),
Step("scds21", "assert", expect: "saga_panel_cast_relations", value: "1"),
Step("scds22", "assert", expect: "saga_cast_contains", value: "Child"),
Step("scds23", "assert", expect: "no_bad"),
Step("scds99", "snapshot"),
};
}
private static List SagaLiveChildReverse()
{
return new List
{
Step("slcr0", "dismiss_windows"),
Step("slcr1", "wait_world"),
Step("slcr2", "set_setting", expect: "enabled", value: "true"),
Step("slcr3", "spawn", asset: "human", count: 2),
Step("slcr4", "spectator", value: "off"),
Step("slcr5", "spectator", value: "on"),
Step("slcr6", "interest_saga_clear"),
Step("slcr7", "pick_unit", asset: "human"),
Step("slcr8", "happiness_remember_partner"),
Step("slcr9", "pick_unit", asset: "human", value: "other"),
Step("slcr10", "focus", asset: "human"),
Step("slcr11", "relationship_set_parent", value: "parent2"),
Step("slcr12", "focus_partner"),
Step("slcr13", "saga_force_admit_focus"),
Step("slcr14", "assert", expect: "saga_cast_contains", value: "Child"),
Step("slcr15", "saga_memory_clear_only"),
Step("slcr16", "narrative_story_clear_only"),
Step("slcr17", "assert", expect: "saga_cast_contains", value: "Child"),
Step("slcr18", "assert", expect: "no_bad"),
Step("slcr99", "snapshot"),
};
}
/// Character beat caption: live MC identity, prose bans, hover keeps beat.
private static List CaptionCharacterBeat()
{
return new List
{
Step("ccb0", "dismiss_windows"),
Step("ccb1", "wait_world"),
Step("ccb2", "set_setting", expect: "enabled", value: "true"),
Step("ccb3", "fast_timing", value: "true"),
Step("ccb4", "spawn", asset: "human", count: 2),
Step("ccb5", "spectator", value: "off"),
Step("ccb6", "spectator", value: "on"),
Step("ccb7", "interest_saga_clear"),
Step("ccb8", "pick_unit", asset: "human"),
Step("ccb9", "focus", asset: "human"),
Step("ccb9b", "status_apply", asset: "human", value: "invincible"),
Step("ccb10", "saga_force_admit_focus"),
Step("ccb11", "saga_role_rise_focus", value: "become_king"),
Step("ccb12", "saga_rail_refresh"),
Step("ccb13", "interest_force_session", asset: "human", label: "{a} is fighting Waaaf", tier: "Action",
expect: "ccb_fight", value: "lead=event;evt=70;force=true"),
Step("ccb14", "wait", value: "0.3"),
// The simulated role rise is historical evidence only; the live roster refresh may
// correctly replace it with a stronger current identity (for example singleton).
Step("ccb15", "assert", expect: "caption_mc_identity_or_species"),
Step("ccb16", "assert", expect: "caption_identity_not_in_label"),
Step("ccb17", "assert", expect: "caption_prose_no_jargon"),
Step("ccb18", "saga_memory_record_focus", asset: "War", value: "Northreach", expect: "harness_cap_war"),
Step("ccb19", "interest_force_session", asset: "human", label: "{a} patrols the border", tier: "Action",
expect: "ccb_quiet", value: "lead=event;evt=180;force=true"),
Step("ccb20", "wait", value: "0.3"),
Step("ccb21", "assert", expect: "caption_context_stake"),
// Soft tip must omit kill / crowning biography hitch; war stake may remain.
Step("ccb21b", "saga_memory_record_focus", asset: "Kill", value: "Named foe"),
Step("ccb21c", "interest_force_session", asset: "human", label: "{a} mates with Waaaf", tier: "Action",
expect: "ccb_mate", value: "lead=event;evt=180;force=true"),
Step("ccb21d", "wait", value: "0.3"),
Step("ccb21e", "assert", expect: "caption_context_omits", value: "Slew|Became Pack|Became Clan"),
Step("ccb21f", "assert", expect: "caption_context_stake"),
Step("ccb21g", "interest_force_session", asset: "human", label: "{a} dreams pleasantly", tier: "Action",
expect: "ccb_dream", value: "lead=event;evt=180;force=true"),
Step("ccb21h", "wait", value: "0.3"),
Step("ccb21i", "assert", expect: "caption_context_omits", value: "Slew|Became Pack|Became Clan|Founded"),
Step("ccb21j", "caption_capture_beat"),
Step("ccb22", "saga_show_hover_first"),
Step("ccb23", "assert", expect: "caption_hover_keeps_beat"),
Step("ccb23b", "assert", expect: "saga_panel_cast_first"),
Step("ccb23c", "assert", expect: "saga_panel_fixed_size"),
Step("ccb24", "saga_hide_hover"),
Step("ccb25", "saga_hover_tick_away"),
Step("ccb26", "spawn", asset: "sheep", count: 1),
Step("ccb27", "pick_unit", asset: "sheep"),
Step("ccb28", "focus", asset: "sheep"),
Step("ccb29", "interest_force_session", asset: "sheep", label: "{a} grazes", tier: "Action",
expect: "ccb_sheep", value: "lead=event;evt=40;force=true"),
Step("ccb30", "wait", value: "0.3"),
Step("ccb31", "assert", expect: "caption_nobody_species"),
Step("ccb90", "fast_timing", value: "false"),
Step("ccb99", "snapshot"),
};
}
private static List CaptionStatusBannerLive()
{
return new List
{
Step("csbl0", "dismiss_windows"),
Step("csbl1", "wait_world"),
Step("csbl2", "set_setting", expect: "enabled", value: "true"),
Step("csbl3", "spawn", asset: "human", count: 1),
Step("csbl4", "spectator", value: "off"),
Step("csbl5", "spectator", value: "on"),
Step("csbl6", "interest_saga_clear"),
Step("csbl7", "pick_unit", asset: "human"),
Step("csbl8", "focus", asset: "human"),
Step("csbl9", "saga_force_admit_focus"),
Step("csbl10", "interest_force_session", asset: "human", label: "Quiet watch",
tier: "Curiosity", expect: "csbl_quiet"),
Step("csbl11", "caption_status_banner_probe"),
Step("csbl12", "spectator", value: "on"),
Step("csbl13", "assert", expect: "no_bad"),
Step("csbl99", "snapshot"),
};
}
/// SagaProse cast/stake voice across bond, foe, plot partner classes.
private static List CaptionProseVoice()
{
return new List
{
Step("cpv0", "dismiss_windows"),
Step("cpv1", "wait_world"),
Step("cpv2", "set_setting", expect: "enabled", value: "true"),
Step("cpv3", "fast_timing", value: "true"),
Step("cpv4", "spawn", asset: "human", count: 2),
Step("cpv5", "spectator", value: "off"),
Step("cpv6", "spectator", value: "on"),
Step("cpv7", "interest_saga_clear"),
Step("cpv8", "pick_unit", asset: "human"),
Step("cpv9", "happiness_remember_partner"),
Step("cpv10", "pick_unit", asset: "human", value: "other"),
Step("cpv11", "focus", asset: "human"),
Step("cpv12", "saga_force_admit_focus"),
Step("cpv13", "happiness_bond_lovers"),
Step("cpv14", "assert", expect: "saga_cast_contains", value: "Lover"),
Step("cpv15", "assert", expect: "caption_prose_casing_cast", value: "Lover|Best Friend"),
Step("cpv16", "saga_note_encounters", value: "3"),
Step("cpv17", "assert", expect: "saga_rival_focus", value: "true"),
Step("cpv18", "assert", expect: "saga_cast_contains", value: "Old Foe|War Enemy|Blood Foe|Lover"),
Step("cpv19", "assert", expect: "caption_prose_no_jargon"),
Step("cpv20", "saga_memory_record_focus", asset: "Plot", value: "coup", expect: "harness_cpv_plot"),
Step("cpv21", "assert", expect: "saga_cast_contains", value: "Plot Partner|Lover|Old Foe"),
Step("cpv22", "assert", expect: "saga_stake_contains", value: "Still tangled|Still at war|Feud|Bound|Lover|Plot"),
Step("cpv90", "fast_timing", value: "false"),
Step("cpv99", "snapshot"),
};
}
/// Hover acquires read-pause; camera owner stays fixed until release.
private static List SagaHoverReadPause()
{
return new List
{
Step("shp0", "dismiss_windows"),
Step("shp1", "wait_world"),
Step("shp2", "set_setting", expect: "enabled", value: "true"),
Step("shp3", "fast_timing", value: "true"),
Step("shp4", "spawn", asset: "human", count: 2),
Step("shp5", "spectator", value: "off"),
Step("shp6", "spectator", value: "on"),
Step("shp7", "interest_saga_clear"),
Step("shp8", "pick_unit", asset: "human"),
Step("shp9", "focus", asset: "human"),
Step("shp10", "saga_force_admit_focus"),
Step("shp11", "interest_force_session", asset: "human", label: "PauseHold",
tier: "Action", value: "lead=event;evt=70;force=true;ttl=30"),
Step("shp12", "assert", expect: "tip_contains", value: "PauseHold"),
Step("shp13", "saga_show_hover_first"),
Step("shp14", "assert", expect: "saga_read_pause", value: "true"),
Step("shp15", "interest_inject", asset: "human", label: "ShouldNotCut", tier: "Action",
expect: "shp_cut", value: "lead=event;evt=95;force=false;ttl=30;pick=other"),
Step("shp16", "director_run", wait: 1.0f),
Step("shp17", "assert", expect: "tip_contains", value: "PauseHold"),
Step("shp18", "assert", expect: "tip_not_contains", value: "ShouldNotCut"),
Step("shp19", "saga_hide_hover"),
Step("shp20", "saga_hover_tick_away"),
Step("shp21", "assert", expect: "saga_read_pause", value: "false"),
Step("shp90", "fast_timing", value: "false"),
Step("shp99", "snapshot"),
};
}
/// Multi-species notables fill distinct roster slots.
private static List SagaRosterDiversity()
{
return new List
{
Step("srd0", "dismiss_windows"),
Step("srd1", "wait_world"),
Step("srd2", "set_setting", expect: "enabled", value: "true"),
Step("srd3", "fast_timing", value: "true"),
Step("srd4", "spawn", asset: "human", count: 1),
Step("srd5", "spawn", asset: "elf", count: 1),
Step("srd6", "spawn", asset: "dwarf", count: 1),
Step("srd7", "spectator", value: "off"),
Step("srd8", "spectator", value: "on"),
Step("srd9", "interest_saga_clear"),
Step("srd10", "pick_unit", asset: "human"),
Step("srd11", "focus", asset: "human"),
Step("srd12", "saga_force_admit_focus"),
Step("srd13", "pick_unit", asset: "elf"),
Step("srd14", "focus", asset: "elf"),
Step("srd15", "saga_force_admit_focus"),
Step("srd16", "pick_unit", asset: "dwarf"),
Step("srd17", "focus", asset: "dwarf"),
Step("srd18", "saga_force_admit_focus"),
Step("srd19", "assert", expect: "saga_roster_count", value: "3", label: "min"),
Step("srd90", "fast_timing", value: "false"),
Step("srd99", "snapshot"),
};
}
/// Dead MC leaves the living roster; a new living unit can refill the slot.
private static List SagaReplaceOnDeath()
{
return new List
{
Step("srdth0", "dismiss_windows"),
Step("srdth1", "wait_world"),
Step("srdth2", "set_setting", expect: "enabled", value: "true"),
Step("srdth3", "fast_timing", value: "true"),
// Single MC so auto-refill cannot mask the death prune.
Step("srdth4", "spawn", asset: "human", count: 1),
Step("srdth5", "spectator", value: "off"),
Step("srdth6", "spectator", value: "on"),
Step("srdth7", "pick_unit", asset: "human"),
Step("srdth8", "focus", asset: "human"),
Step("srdth9", "interest_saga_clear"),
Step("srdth10", "saga_force_admit_focus"),
Step("srdth11", "assert", expect: "saga_roster_count", value: "1"),
Step("srdth12", "kill_focus"),
Step("srdth13", "wait", wait: 0.3f),
Step("srdth14", "director_run", wait: 0.8f),
Step("srdth15", "assert", expect: "saga_roster_count", value: "0"),
Step("srdth16", "spawn", asset: "human", count: 1),
Step("srdth17", "pick_unit", asset: "human"),
Step("srdth18", "focus", asset: "human"),
Step("srdth19", "saga_force_admit_focus"),
Step("srdth20", "assert", expect: "saga_roster_count", value: "1"),
Step("srdth90", "fast_timing", value: "false"),
Step("srdth99", "snapshot"),
};
}
///
/// Extended roles: singleton dragon overview + explicit ordinary-character admission.
///
private static List SagaAdmitRoles()
{
return new List
{
Step("sar0", "dismiss_windows"),
Step("sar1", "wait_world"),
Step("sar2", "set_setting", expect: "enabled", value: "true"),
Step("sar3", "fast_timing", value: "true"),
Step("sar4", "spawn", asset: "dragon", count: 1),
Step("sar5", "spawn", asset: "human", count: 1),
Step("sar6", "spectator", value: "off"),
Step("sar7", "spectator", value: "on"),
Step("sar8", "interest_saga_clear"),
Step("sar9", "pick_unit", asset: "dragon"),
Step("sar10", "focus", asset: "dragon"),
// Real admission path: special/rare identity opens the challenger gate.
Step("sar11", "saga_consider_admit_focus"),
Step("sar12", "assert", expect: "saga_has_focus"),
Step("sar13", "saga_rail_refresh"),
Step("sar14", "assert", expect: "saga_focus_admission", value: "Singleton|SpecialCreature|RareCreature"),
// Harness admission proves an ordinary human can occupy the ensemble.
Step("sar20", "pick_unit", asset: "human"),
Step("sar21", "focus", asset: "human"),
Step("sar21b", "assert", expect: "saga_focus_admission_eligible", value: "false"),
Step("sar22", "saga_force_admit_focus"),
Step("sar23", "assert", expect: "saga_has_focus"),
Step("sar24", "assert", expect: "saga_roster_count", value: "2", label: "min"),
Step("sar90", "fast_timing", value: "false"),
Step("sar99", "snapshot"),
};
}
/// Full Cap: Prefer pin survives; a new admitted character displaces the weakest non-pin.
private static List SagaReplaceWeaker()
{
return new List
{
Step("srw0", "dismiss_windows"),
Step("srw1", "wait_world"),
Step("srw2", "set_setting", expect: "enabled", value: "true"),
Step("srw3", "fast_timing", value: "true"),
Step("srw4", "spawn", asset: "human", count: 11),
Step("srw5", "spectator", value: "off"),
Step("srw6", "spectator", value: "on"),
Step("srw7", "interest_saga_clear"),
// Fill Cap with force-admits.
Step("srw10", "pick_unit", asset: "human"),
Step("srw11", "focus", asset: "human"),
Step("srw12", "saga_force_admit_focus"),
Step("srw13", "saga_prefer_focus"),
Step("srw14", "assert", expect: "saga_prefer_focus_on"),
Step("srw20", "pick_unit", asset: "human", value: "other"),
Step("srw21", "focus", asset: "human"),
Step("srw22", "saga_force_admit_focus"),
Step("srw23", "pick_unit", asset: "human", value: "other"),
Step("srw24", "focus", asset: "human"),
Step("srw25", "saga_force_admit_focus"),
Step("srw26", "pick_unit", asset: "human", value: "other"),
Step("srw27", "focus", asset: "human"),
Step("srw28", "saga_force_admit_focus"),
Step("srw29", "pick_unit", asset: "human", value: "other"),
Step("srw30", "focus", asset: "human"),
Step("srw31", "saga_force_admit_focus"),
Step("srw32", "pick_unit", asset: "human", value: "other"),
Step("srw33", "focus", asset: "human"),
Step("srw34", "saga_force_admit_focus"),
Step("srw35", "pick_unit", asset: "human", value: "other"),
Step("srw36", "focus", asset: "human"),
Step("srw37", "saga_force_admit_focus"),
Step("srw38", "pick_unit", asset: "human", value: "other"),
Step("srw39", "focus", asset: "human"),
Step("srw40", "saga_force_admit_focus"),
Step("srw41", "pick_unit", asset: "human", value: "other"),
Step("srw42", "focus", asset: "human"),
Step("srw43", "saga_force_admit_focus"),
Step("srw44", "pick_unit", asset: "human", value: "other"),
Step("srw45", "focus", asset: "human"),
Step("srw46", "saga_force_admit_focus"),
Step("srw50", "assert", expect: "saga_roster_count", value: "5"),
Step("srw51", "saga_weaken_nonprefer"),
// Another human challenges the full five-character ensemble.
Step("srw60", "pick_unit", asset: "human", value: "other"),
Step("srw61", "focus", asset: "human"),
Step("srw62", "saga_force_admit_focus"),
Step("srw63", "assert", expect: "saga_has_focus"),
Step("srw64", "assert", expect: "saga_roster_count", value: "5"),
// Prefer pin still present; challenger focus itself is not Prefer.
Step("srw70", "assert", expect: "saga_prefer_focus_on", value: "false"),
Step("srw71", "assert", expect: "saga_prefer_count", value: "1", label: "min"),
Step("srw90", "fast_timing", value: "false"),
Step("srw99", "snapshot"),
};
}
/// Active rail chrome tracks camera follow, not Prefer alone.
private static List SagaRailActiveHighlight()
{
return new List
{
Step("sra0", "dismiss_windows"),
Step("sra1", "wait_world"),
Step("sra2", "set_setting", expect: "enabled", value: "true"),
Step("sra3", "fast_timing", value: "true"),
Step("sra4", "spawn", asset: "human", count: 2),
Step("sra5", "spectator", value: "off"),
Step("sra6", "spectator", value: "on"),
Step("sra7", "interest_saga_clear"),
Step("sra8", "pick_unit", asset: "human"),
Step("sra9", "focus", asset: "human"),
Step("sra10", "saga_force_admit_focus"),
Step("sra11", "saga_prefer_focus"),
Step("sra12", "saga_rail_refresh"),
Step("sra13", "assert", expect: "saga_rail_active_highlight", value: "true"),
Step("sra14", "assert", expect: "saga_rail_prefer_pip", value: "true"),
Step("sra20", "pick_unit", asset: "human", value: "other"),
Step("sra21", "focus", asset: "human"),
Step("sra22", "saga_force_admit_focus"),
Step("sra23", "saga_rail_refresh"),
// Follow moved off Prefer'd MC - Active highlight should track the new follow.
Step("sra24", "assert", expect: "saga_rail_active_highlight", value: "true"),
Step("sra25", "assert", expect: "saga_rail_prefer_pip", value: "true"),
Step("sra25b", "assert", expect: "saga_rail_cached_faces", value: "2", label: "min"),
Step("sra25c", "assert", expect: "saga_rail_fallback_cache_isolation"),
Step("sra90", "fast_timing", value: "false"),
Step("sra99", "snapshot"),
};
}
///
/// Tall and short creature glyphs retain animation frames, then the full living
/// species catalog is checked under the same extraction policy.
///
private static List SagaRailLiveAnimalFrame()
{
return new List
{
Step("srl0", "dismiss_windows"),
Step("srl1", "wait_world"),
Step("srl2", "set_setting", expect: "enabled", value: "true"),
Step("srl3", "fast_timing", value: "true"),
Step("srl4", "spawn", asset: "cat", count: 1),
Step("srl5", "spectator", value: "off"),
Step("srl6", "spectator", value: "on"),
Step("srl7", "interest_saga_clear"),
Step("srl8", "pick_unit", asset: "cat"),
Step("srl9", "focus", asset: "cat"),
Step("srl10", "saga_force_admit_focus"),
Step("srl11", "saga_rail_refresh"),
Step("srl12", "assert", expect: "saga_rail_focus_live_frame_distinct"),
Step("srl13", "screenshot", value: "saga-rail-cat-live"),
Step("srl14", "spawn", asset: "fox", count: 1),
Step("srl15", "pick_unit", asset: "fox"),
Step("srl16", "focus", asset: "fox"),
Step("srl17", "saga_force_admit_focus"),
Step("srl18", "saga_rail_refresh"),
Step("srl19", "assert", expect: "saga_rail_focus_live_frame_distinct"),
Step("srl20", "screenshot", value: "saga-rail-fox-live"),
Step("srl21", "saga_rail_species_frame_sweep"),
Step("srl90", "fast_timing", value: "false"),
Step("srl99", "snapshot"),
};
}
/// Prefer toggle flips on then off with immediate pip state.
private static List SagaRailPreferClick()
{
return new List
{
Step("srp0", "dismiss_windows"),
Step("srp1", "wait_world"),
Step("srp2", "set_setting", expect: "enabled", value: "true"),
Step("srp3", "fast_timing", value: "true"),
Step("srp4", "spawn", asset: "human", count: 1),
Step("srp5", "spectator", value: "off"),
Step("srp6", "spectator", value: "on"),
Step("srp7", "interest_saga_clear"),
Step("srp8", "pick_unit", asset: "human"),
Step("srp9", "focus", asset: "human"),
Step("srp10", "saga_force_admit_focus"),
Step("srp11", "saga_prefer_focus"),
Step("srp12", "saga_rail_refresh"),
Step("srp13", "assert", expect: "saga_prefer_focus_on"),
Step("srp14", "assert", expect: "saga_rail_prefer_pip", value: "true"),
Step("srp20", "saga_prefer_focus"),
Step("srp21", "saga_rail_refresh"),
Step("srp22", "assert", expect: "saga_prefer_focus_on", value: "false"),
Step("srp23", "assert", expect: "saga_rail_prefer_pip", value: "false"),
Step("srp90", "fast_timing", value: "false"),
Step("srp99", "snapshot"),
};
}
///
/// During soft-fill quiet, Pack and stun/invincible crumbs do not win; combat still can.
///
private static List SagaSoftFillNoPack()
{
return new List
{
Step("ssf0", "dismiss_windows"),
Step("ssf1", "wait_world"),
Step("ssf2", "set_setting", expect: "enabled", value: "true"),
Step("ssf3", "fast_timing", value: "true"),
Step("ssf4", "spawn", asset: "human", count: 2),
Step("ssf5", "spectator", value: "off"),
Step("ssf6", "spectator", value: "on"),
Step("ssf7", "pick_unit", asset: "human"),
Step("ssf8", "focus", asset: "human"),
Step("ssf9", "interest_end_session"),
Step("ssf10", "soft_fill_quiet_arm"),
Step("ssf11", "assert", expect: "soft_fill_quiet", value: "true"),
Step("ssf20", "interest_expire_pending", value: ""),
Step("ssf21", "interest_inject", asset: "human", label: "Pack - QuietBlock (3) · gathering",
tier: "Action", expect: "pack_soft",
value: "lead=event;evt=70;char=5;force=false;ttl=60"),
Step("ssf22", "interest_inject", asset: "human", label: "QuietStun",
tier: "Action", expect: "stunned",
value: "lead=event;evt=68;char=5;force=false;ttl=60;pick=other"),
Step("ssf23", "interest_inject", asset: "human", label: "QuietInvincible",
tier: "Action", expect: "invincible",
value: "lead=event;evt=66;char=5;force=false;ttl=60"),
Step("ssf24", "director_run", wait: 1.2f),
Step("ssf25", "assert", expect: "tip_not_contains", value: "Pack -"),
Step("ssf26", "assert", expect: "tip_not_contains", value: "QuietStun"),
Step("ssf27", "assert", expect: "tip_not_contains", value: "QuietInvincible"),
Step("ssf30", "interest_combat_session", asset: "human", expect: "ssf_combat"),
Step("ssf31", "director_run", wait: 0.8f),
Step("ssf32", "assert", expect: "tip_matches_any", value: "Duel -|fighting|Skirmish -"),
Step("ssf90", "fast_timing", value: "false"),
Step("ssf99", "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("wfs10", "interest_story_clear"),
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"),
// Same-kingdom Mass peer must not steal the War tip (soak War↔Mass thrash).
// Headcounts may refresh; tip must stay War - … (not Mass/Battle).
Step("wfs60", "remember_tip"),
Step("wfs61", "war_inject_theater_mass", value: "8:6"),
Step("wfs62", "director_run", wait: 1.0f),
Step("wfs63", "war_maintain_focus"),
Step("wfs64", "assert", expect: "tip_matches_any", value: "War -"),
Step("wfs65", "assert", expect: "tip_not_contains", value: "Mass -"),
Step("wfs66", "assert", expect: "tip_not_contains", value: "Battle -"),
Step("wfs67", "director_run", wait: 0.8f),
Step("wfs68", "assert", expect: "tip_matches_any", value: "War -"),
Step("wfs69", "assert", expect: "tip_not_contains", value: "Mass -"),
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("pcs60b", "assert", expect: "decision_worthy", value: "try_to_start_new_civilization", label: "false"),
Step("pcs60c", "assert", expect: "decision_worthy", value: "build_civ_city_here", label: "false"),
Step("pcs60d", "assert", expect: "decision_worthy", value: "king_check_new_city_foundation", label: "false"),
Step("pcs60e", "assert", expect: "decision_worthy", value: "claim_land", 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