diff --git a/README.md b/README.md index d57ce95..a6fc005 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Scenario post-battle dialogue can play before the victory result panel. - Basic battle EXP, level-ups, and core first-tier class promotion routes. - Victory results split rewards, roster changes, saved growth, campaign status, and pending choices into readable sections. -- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, objective updates with HUD notices, inventory pickups, reinforcements, and scripted unit withdrawals. +- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, `after_event` prerequisites, objective updates with HUD notices, inventory pickups, reinforcements, and scripted unit withdrawals. - Scenario briefing before battle with a campaign battle header and objective summary. - Victory rewards and consumed inventory saved into a campaign state. - Scenario-defined victory and defeat conditions. diff --git a/data/scenarios/013_wuchao_raid.json b/data/scenarios/013_wuchao_raid.json index a0cd5b4..1979c1d 100644 --- a/data/scenarios/013_wuchao_raid.json +++ b/data/scenarios/013_wuchao_raid.json @@ -298,7 +298,7 @@ { "id": "turn_6_granary_collapse", "once": true, - "when": { "type": "turn_start", "team": "player", "turn": 6 }, + "when": { "type": "turn_start", "team": "player", "turn": 6, "after_event": "wuchao_depot_burned" }, "actions": [ { "type": "dialogue", diff --git a/data/scenarios/017_ye_surrender.json b/data/scenarios/017_ye_surrender.json index f4fafc4..564eaca 100644 --- a/data/scenarios/017_ye_surrender.json +++ b/data/scenarios/017_ye_surrender.json @@ -305,7 +305,7 @@ { "id": "turn_7_city_surrenders", "once": true, - "when": { "type": "turn_start", "team": "player", "turn": 7 }, + "when": { "type": "turn_start", "team": "player", "turn": 7, "after_event": "surrender_signal_sent" }, "actions": [ { "type": "dialogue", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 504f4ad..013c1b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -43,7 +43,7 @@ 8. Player ends turn. 9. Enemy AI moves, picks physical attacks from damage, defeat, priority, and weapon-effectiveness scores, and can cast single-target or area damage, healing, support, debuff, movement-modifier, accuracy/evasion-modifier, damage-over-time, or action-lock tactics through the same combat and skill resolvers. 10. Battle checks scenario-defined defeat conditions first, including commander loss and turn limits, then victory conditions such as enemy defeat or reaching a marked destination tile. -11. Turn-start and movement-triggered scenario events run as phases change or units enter marked cells, and may update objectives with HUD notices, grant inventory pickups, deploy reinforcements, or withdraw units from the battlefield. +11. Turn-start and movement-triggered scenario events run as phases change or units enter marked cells, can wait for prerequisite events, and may update objectives with HUD notices, grant inventory pickups, deploy reinforcements, or withdraw units from the battlefield. 12. On first-time victory, `CampaignState` snapshots player progression and the battle inventory, applies rewards once, advances `current_scenario_id`, and writes `user://campaign_save.json`. 13. If the scenario defines post-battle dialogue, it plays once before the victory result panel opens. 14. The victory result panel splits rewards, roster changes, saved player growth, campaign status, and pending choices into sections. @@ -57,7 +57,7 @@ Joined officers gate whether `requires_joined` player deployments are loaded at all. Rewards and post-battle choices can add or remove joined officers without deleting their saved roster snapshot, so a later rejoin can preserve progression. When a save loads, `CampaignState` scans completed scenarios in campaign order and reapplies only officer join/leave transitions from rewards and the saved `applied_post_battle_choices` ledger, allowing added recruitment rewards to reach older saves without replaying gold or items. Old saves without the ledger can backfill a choice id only when exactly one completed scenario choice matches saved flags. Roster entries are keyed by `officer_id` when available, so future scenarios can deploy the same joined officer under different scenario-specific `unit_id` values. Saved progression overlays final battle stats for now; a later equipment refactor should split base stats from equipment bonuses before adding item leveling or stat recalculation. New battles currently start player units healed to their saved maximum HP/MP. Skill access is carried in roster snapshots, with class defaults still applied for old saves that do not include skills. Promoted class ids are saved in the roster, and the next battle rehydrates class-derived movement, growth, skills, and range from the saved class id before equipment range is recalculated. -Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Briefings preview first-clear rewards and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item pickups, and reinforcements from saved campaign choices. Consumable use can restore HP or MP or cure matching poison/seal/snare/disarm statuses, and in-battle item use, event pickup grants, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items, or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, save writes, current-scenario advancement, or finite-stock purchases; pre-battle Shop, Armory, Roster, Formation, and Save are disabled on completed-scenario replays to avoid side effects. +Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Briefings preview first-clear rewards and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item pickups, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP or cure matching poison/seal/snare/disarm statuses, and in-battle item use, event pickup grants, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items, or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, save writes, current-scenario advancement, or finite-stock purchases; pre-battle Shop, Armory, Roster, Formation, and Save are disabled on completed-scenario replays to avoid side effects. The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, can place the portrait on the left or right side of that panel, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures for the selected unit or hovered unit, board units render low-HP warning rings, HP bar color states, and active support/debuff/poison/seal/snare/disarm status pips from battle state fields, hover previews are rendered as target badges from existing `BattleState` forecast APIs, hover unit info shows class, movement type, AGI, movement, range, and equipped gear, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, and item pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 310120b..30552d6 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -465,7 +465,18 @@ Conditions may include `after_event` to stay inactive until a one-shot event has `post_battle_choices` are shown on the victory result panel after first-time scenario rewards are applied. Each choice needs a unique stable lowercase `id`, a non-empty `label`, and a `set_flags` object whose keys are stable lowercase flag ids. Choices may also grant positive `gold`, known `items`, `join_officers`, and `leave_officers`. When rewards are saved but the player has not selected a choice yet, `CampaignState.pending_post_battle_choice_scenario_id` records that scenario id so reloading the game returns to the victory choice panel instead of skipping the branch. The selected choice is saved to `CampaignState.flags`, `CampaignState.applied_post_battle_choices`, and campaign membership only when the player presses its result-panel button; completed-scenario replays do not show choices again. Save-load migration for older saves uses flags as a conservative choice signal, so avoid designing multiple choices in one scenario that can all match the same saved flag state. -Scenario `events` support `battle_start`, `battle_begin`, `turn_start`, and movement-triggered `unit_reaches_tile` triggers. Each event needs a stable lowercase `id` that is unique within the scenario; `after_event` conditions can only reference those explicit ids. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. Actions currently include `log`, `dialogue`, `set_objective`, `grant_item`, `grant_items`, `spawn_deployment`, `spawn_deployments`, `withdraw_unit`, and `withdraw_units`. `set_objective` updates visible objective text, emits concrete log lines for changed victory/defeat text, and raises an objective-update signal so the battle scene can refresh the HUD and show a short notice. `withdraw_unit` accepts `unit_id` or `id`, while `withdraw_units` accepts a `unit_ids` array; living deployed targets leave the battlefield without being marked defeated, so they are removed from map occupancy and living-unit counts while protected-unit defeat checks still distinguish withdrawal from death. Event `dialogue` actions use the same dialogue line format, portrait default rules, and optional `side` placement as `post_battle_dialogue`. Event item grants add to the battle inventory immediately; they persist to the campaign save only when the battle is won. +Scenario `events` support `battle_start`, `battle_begin`, `turn_start`, and movement-triggered `unit_reaches_tile` triggers. Each event needs a stable lowercase `id` that is unique within the scenario; condition and event `after_event` gates can only reference those explicit ids. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. Event `when` blocks may include `after_event` to wait until an earlier one-shot scenario beat has fired, which is useful for dialogue or follow-up pressure that only makes sense after a destination, ambush, gate, or fire event. Actions currently include `log`, `dialogue`, `set_objective`, `grant_item`, `grant_items`, `spawn_deployment`, `spawn_deployments`, `withdraw_unit`, and `withdraw_units`. `set_objective` updates visible objective text, emits concrete log lines for changed victory/defeat text, and raises an objective-update signal so the battle scene can refresh the HUD and show a short notice. `withdraw_unit` accepts `unit_id` or `id`, while `withdraw_units` accepts a `unit_ids` array; living deployed targets leave the battlefield without being marked defeated, so they are removed from map occupancy and living-unit counts while protected-unit defeat checks still distinguish withdrawal from death. Event `dialogue` actions use the same dialogue line format, portrait default rules, and optional `side` placement as `post_battle_dialogue`. Event item grants add to the battle inventory immediately; they persist to the campaign save only when the battle is won. + +```json +{ + "id": "turn_6_granary_collapse", + "once": true, + "when": { "type": "turn_start", "team": "player", "turn": 6, "after_event": "wuchao_depot_burned" }, + "actions": [ + { "type": "dialogue", "lines": [{ "speaker": "Guo Jia", "text": "Smoke is rising above the camp." }] } + ] +} +``` ```json { @@ -564,4 +575,4 @@ Before a battle starts, `BattleScene` can buy priced items through `CampaignStat When every campaign scenario id is present in `completed_scenarios`, the battle scene shows a campaign completion panel. Starting a new campaign clears the automatic save file and resets `current_scenario_id` to `start_scenario`; the manual checkpoint is left alone until overwritten from the Save menu. -Run `tools/validate_data.ps1` after data edits to check campaign paths, chapter ranges, map dimensions, terrain keys, class/officer/item/skill references, skill area shapes, skill status/action-lock effect shapes, item effect names and cure statuses, officer and deployment portraits, officer and deployment equipment slot compatibility including accessory types and weapon effectiveness fields, promotion routes and promotion equipment compatibility, condition/event names, event item grants, event withdrawal unit ids, condition unit references, destination condition coordinates, campaign flag references, joined-officer gates, briefing line blocks, deployment ids, deployment bounds, impassable spawn cells, shop stock, roster rules, formation cells, post-battle dialogue, post-battle choices, and reward item ids. +Run `tools/validate_data.ps1` after data edits to check campaign paths, chapter ranges, map dimensions, terrain keys, class/officer/item/skill references, skill area shapes, skill status/action-lock effect shapes, item effect names and cure statuses, officer and deployment portraits, officer and deployment equipment slot compatibility including accessory types and weapon effectiveness fields, promotion routes and promotion equipment compatibility, condition/event names, event `after_event` references, event item grants, event withdrawal unit ids, condition unit references, destination condition coordinates, campaign flag references, joined-officer gates, briefing line blocks, deployment ids, deployment bounds, impassable spawn cells, shop stock, roster rules, formation cells, post-battle dialogue, post-battle choices, and reward item ids. diff --git a/docs/GODOT_4_6_MIGRATION.md b/docs/GODOT_4_6_MIGRATION.md index d4d7297..8c2522c 100644 --- a/docs/GODOT_4_6_MIGRATION.md +++ b/docs/GODOT_4_6_MIGRATION.md @@ -49,6 +49,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readines - Named equipment rewards such as Yitian Sword and Dragon Spear validate, appear with compact rarity tags in reward/inventory/equipment text, and remain ordinary stackable inventory stock until unique item instances are implemented. - Hovering an occupied tile shows the unit's class, movement type, AGI, movement, range, equipped weapon/armor/accessory, and a HUD portrait thumbnail when no unit is selected, with empty gear slots shown as `-`. - The battle HUD and briefing objective panels split objective, progress, and risk details, including pending unlocks, destination reach status, defeated enemy counts, named protected-unit or commander safety, and turns remaining. +- Scenario event `when.after_event` prerequisites keep follow-up events from firing before their one-shot setup event. - `set_objective` scenario events show concrete objective-update logs, play a UI confirmation sound, refresh the HUD immediately, and briefly show the objective notice panel. - `withdraw_unit` and `withdraw_units` scenario events remove living targets from the battlefield without marking them defeated. - Movement-triggered supply cache events grant battle inventory items with pickup feedback, and the items persist only after victory. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index abd83ce..8900998 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -42,7 +42,7 @@ ## Milestone 3: Scenario Layer - Pre-battle briefing exists with a campaign chapter header, battle header, objective summary, and condition tracker. Full dialogue scenes are still planned. -- Scenario events exist for battle start, battle begin, turn start, movement-triggered map tiles, and objective-change notices. +- Scenario events exist for battle start, battle begin, turn start, movement-triggered map tiles, event prerequisites, and objective-change notices. - Mid-battle map/action events have a first action system with movement ambushes and scripted unit withdrawals. - Opening battle dialogue exists through event actions. - Post-battle dialogue exists before the victory result panel. diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index 4f8e6ca..137b71f 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -3585,6 +3585,8 @@ func _run_events(trigger_type: String, team := "", turn := -1, trigger_unit: Dic continue if trigger_type == "unit_reaches_tile" and not _unit_reaches_event_tile(trigger_unit, when): continue + if not _event_gate_open(when): + continue if not _campaign_flags_match(when.get("campaign_flags", {})): continue _execute_event_actions(event.get("actions", []), trigger_unit) @@ -3604,6 +3606,13 @@ func _unit_reaches_event_tile(unit: Dictionary, when: Dictionary) -> bool: return _unit_matches_condition_ids(unit, when.get("unit_ids", []), when.get("officer_ids", [])) +func _event_gate_open(when: Dictionary) -> bool: + var after_event := str(when.get("after_event", "")) + if after_event.is_empty(): + return true + return fired_event_ids.has(after_event) + + func _campaign_flags_match(required_flags) -> bool: if typeof(required_flags) != TYPE_DICTIONARY: return true diff --git a/tools/smoke_event_after_event.gd b/tools/smoke_event_after_event.gd new file mode 100644 index 0000000..5e4521e --- /dev/null +++ b/tools/smoke_event_after_event.gd @@ -0,0 +1,104 @@ +extends SceneTree + +const BattleStateScript := preload("res://scripts/core/battle_state.gd") + + +func _init() -> void: + var failures: Array[String] = [] + _check_manual_gate(failures) + _check_same_pass_gate(failures) + + if failures.is_empty(): + print("event after_event smoke ok") + quit(0) + return + + for failure in failures: + push_error(failure) + quit(1) + + +func _check_manual_gate(failures: Array[String]) -> void: + var state = _loaded_state(failures, "manual gate") + if state == null: + return + var logs := [] + state.log_added.connect(func(message: String) -> void: + logs.append(message) + ) + state.fired_event_ids.clear() + state.battle_events.clear() + state.battle_events.append({ + "id": "dependent_report", + "once": true, + "when": { + "type": "turn_start", + "team": "player", + "turn": 1, + "after_event": "signal_ready" + }, + "actions": [ + {"type": "log", "text": "Dependent event fired."} + ] + }) + + state._run_events("turn_start", "player", 1) + if logs.has("Dependent event fired."): + failures.append("after_event dependent fired before prerequisite") + + state.fired_event_ids["signal_ready"] = true + state._run_events("turn_start", "player", 1) + if not logs.has("Dependent event fired."): + failures.append("after_event dependent did not fire after prerequisite") + + logs.clear() + state._run_events("turn_start", "player", 1) + if logs.has("Dependent event fired."): + failures.append("after_event dependent repeated despite once flag") + + +func _check_same_pass_gate(failures: Array[String]) -> void: + var state = _loaded_state(failures, "same pass gate") + if state == null: + return + var logs := [] + state.log_added.connect(func(message: String) -> void: + logs.append(message) + ) + state.fired_event_ids.clear() + state.battle_events.clear() + state.battle_events.append({ + "id": "signal_ready", + "once": true, + "when": {"type": "turn_start", "team": "player", "turn": 1}, + "actions": [ + {"type": "log", "text": "Signal ready."} + ] + }) + state.battle_events.append({ + "id": "dependent_report", + "once": true, + "when": { + "type": "turn_start", + "team": "player", + "turn": 1, + "after_event": "signal_ready" + }, + "actions": [ + {"type": "log", "text": "Dependent event fired."} + ] + }) + + state._run_events("turn_start", "player", 1) + if not logs.has("Signal ready."): + failures.append("same-pass prerequisite did not fire") + if not logs.has("Dependent event fired."): + failures.append("same-pass dependent did not fire after prerequisite") + + +func _loaded_state(failures: Array[String], label: String): + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("%s could not load smoke scenario" % label) + return null + return state diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index 637576b..8836b80 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -1502,6 +1502,25 @@ function Check-Required-Flags($RequiredFlags, [string]$ScenarioId, [string]$Cont } } +function Check-Event-After-Event($When, [string]$ScenarioId, [string]$EventId, $EventIds) { + if (-not (Has-Prop $When "after_event")) { + return + } + $afterEvent = [string](Get-Prop $When "after_event" "") + if ([string]::IsNullOrWhiteSpace($afterEvent)) { + Fail "Scenario $ScenarioId event $EventId has empty after_event." + } + if (-not ($afterEvent -match "^[a-z0-9_]+$")) { + Fail "Scenario $ScenarioId event $EventId has unstable after_event: $afterEvent" + } + if ($afterEvent -eq $EventId) { + Fail "Scenario $ScenarioId event $EventId cannot depend on itself." + } + if (-not $EventIds.Contains($afterEvent)) { + Fail "Scenario $ScenarioId event $EventId references missing after_event: $afterEvent" + } +} + function Check-Briefing($Briefing, [string]$ScenarioId) { if ($null -eq $Briefing) { return @@ -1735,6 +1754,7 @@ foreach ($scenario in $campaign.scenarios) { if ($null -eq $when -or $when -is [string] -or $when -is [System.Array]) { Fail "Scenario $scenarioId event $eventId has malformed when." } + Check-Event-After-Event $when $scenarioId $eventId $eventIds Check-Required-Flags (Get-Prop $when "campaign_flags" $null) $scenarioId "event $eventId" $trigger = [string]$event.when.type if (-not $validTriggers.Contains($trigger)) {