diff --git a/README.md b/README.md index 62d5440..c432cb8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Move, attack, wait, end turn. - Automatic end-turn prompt appears after all controllable allies finish acting, using a styled command-complete confirmation. - Ally, enemy, and objective phase changes raise compact command notices with generated visual icon chips in the active battle HUD. -- Enemy AI with movement, damage-aware physical attacks, multiple MP tactic choices, and scenario target priorities that events can retune mid-battle. +- Enemy AI with movement, damage-aware physical attacks, multiple MP tactic choices, scenario target priorities, and sentry-awake state that events can retune mid-battle. - Hover tile and unit info with class, movement type, AGI, movement, range, and equipped gear. - Active battle unit list opens from the top toolbar with ally/enemy tabs, portrait or sprite rows, compact HP/status/zone badges, and hover combat details. - Enemy threat range overlay with tile-level threat source, physical damage hints, and hostile tactic hints. @@ -119,7 +119,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, unit-defeat reactions, `after_event` prerequisites, objective updates with HUD notices, item and gold pickups, reinforcements, scripted unit withdrawals, and AI target-priority shifts. +- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, unit-defeat reactions, `after_event` prerequisites, objective updates with HUD notices, item and gold pickups, reinforcements, scripted unit withdrawals, AI sentry-awake shifts, and AI target-priority shifts. - 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/001_yellow_turbans.json b/data/scenarios/001_yellow_turbans.json index cde5b09..86e143a 100644 --- a/data/scenarios/001_yellow_turbans.json +++ b/data/scenarios/001_yellow_turbans.json @@ -766,6 +766,20 @@ "type": "log", "text": "조조군이 마을 앞 유인선에 닿았다." }, + { + "type": "set_ai_awake", + "unit_ids": [ + "yellow_turban_5", + "yellow_turban_9" + ], + "text": "황건 앞줄이 유인선 쪽으로 끌려 나왔다." + }, + { + "type": "set_ai_target_priority", + "unit_id": "xiahou_dun", + "priority": 6, + "text": "황건 앞줄이 하후돈의 선봉을 노린다." + }, { "type": "dialogue", "lines": [ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ed72f85..848fde1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -44,7 +44,7 @@ 9. Player ends turn. 10. 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. 11. 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. -12. Turn-start, movement-triggered, and unit-defeat scenario events run as phases change, units enter marked cells, or units fall, can wait for prerequisite events, and may update objectives with HUD notices, grant item or gold pickups, deploy reinforcements, withdraw units from the battlefield, or change AI target priorities. +12. Turn-start, movement-triggered, and unit-defeat scenario events run as phases change, units enter marked cells, or units fall, can wait for prerequisite events, and may update objectives with HUD notices, grant item or gold pickups, deploy reinforcements, withdraw units from the battlefield, wake or quiet AI sentries, or change AI target priorities. 13. On first-time victory, `CampaignState` snapshots player progression, battle inventory, and battle-only gold pickups, applies rewards once, advances `current_scenario_id`, and writes `user://campaign_save.json`. 14. If the scenario defines post-battle dialogue, it plays once before the victory result panel opens. 15. The victory result panel splits rewards, roster changes, saved player growth, campaign status, and pending choices into sections. @@ -60,7 +60,7 @@ Presentation preferences are separate from campaign progression. `BattleScene` r 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, can expose `briefing.camp_conversations` through a selectable Talk menu with `briefing.camp_dialogue` as a fallback, can filter those conversations by saved campaign flags, can claim saved one-time camp conversation supply effects through `CampaignState` and refresh the loaded battle inventory copy, 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, and `shop.merchant` lines are presentation-only camp flavor. 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. Camp conversation supply claims are also campaign transactions: they save a per-scenario conversation claim ledger and inventory immediately, then refresh the already-loaded battle inventory. 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, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. 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, and can be expressed as one `pos` or a multi-cell `cells` list; gated victory conditions can also expose the `unit_reaches_tile` event that unlocks them as the next visible objective marker. Rewarding movement events that grant items or gold can expose optional battlefield side markers and Korean tile-info labels through `when.label`, without being treated as victory objectives. 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; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP, cure matching poison/seal/snare/disarm statuses, or cleanse negative stat debuffs, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, 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, event gold pickups, save writes, current-scenario advancement, finite-stock purchases, or camp conversation supply claims; 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, can expose `briefing.camp_conversations` through a selectable Talk menu with `briefing.camp_dialogue` as a fallback, can filter those conversations by saved campaign flags, can claim saved one-time camp conversation supply effects through `CampaignState` and refresh the loaded battle inventory copy, 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, and `shop.merchant` lines are presentation-only camp flavor. 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. Camp conversation supply claims are also campaign transactions: they save a per-scenario conversation claim ledger and inventory immediately, then refresh the already-loaded battle inventory. 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, `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure, and `set_ai_awake` events can wake or quiet selected sentries for lure-line pacing without changing their permanent guard/sentry definitions. 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, and can be expressed as one `pos` or a multi-cell `cells` list; gated victory conditions can also expose the `unit_reaches_tile` event that unlocks them as the next visible objective marker. Rewarding movement events that grant items or gold can expose optional battlefield side markers and Korean tile-info labels through `when.label`, without being treated as victory objectives. 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; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI sentry-awake shifts, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP, cure matching poison/seal/snare/disarm statuses, or cleanse negative stat debuffs, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, 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, event gold pickups, save writes, current-scenario advancement, finite-stock purchases, or camp conversation supply claims; 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, core panels prefer generated lacquer, scroll, jade, and command-seal `StyleBoxTexture` frames with flat ink/wood/jade styles as fallbacks, text buttons prefer generated lacquer, jade-hover, cinnabar-pressed, and disabled-ink `StyleBoxTexture` surfaces, icon-only buttons prefer separate compact bronze/jade generated `StyleBoxTexture` surfaces, and flat styles remain fallbacks, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, keep the portrait slot fixed even for narration lines, mark speaking side through text alignment, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit, uses generated class crest icons for role badges, and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, generated terrain texture tiles, generated terrain feature overlays for connected roads, water, villages, castles, and wasteland accents, generated tactical tile markers for move, attack, selection, target, objective, and recovery overlays, connected edge blends, generated map badge icons for objective, recovery, supply, gold, tactic, and side-event markers, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts with generated class crest markers, team rings, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, 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 draft SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c11c1cc..1d36987 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, unit defeat reactions, event prerequisites, objective-change notices, and AI target-priority shifts. +- Scenario events exist for battle start, battle begin, turn start, movement-triggered map tiles, unit defeat reactions, event prerequisites, objective-change notices, AI sentry-awake shifts, and AI target-priority shifts. - 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 d2d4392..3447662 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -4620,6 +4620,8 @@ func _execute_event_action(action: Dictionary, trigger_unit: Dictionary = {}) -> return for unit_id in unit_ids: _withdraw_event_unit(str(unit_id)) + elif action_type == "set_ai_awake": + _set_event_ai_awake(action) elif action_type == "set_ai_target_priority": _set_event_ai_target_priority(action) @@ -4690,6 +4692,35 @@ func _set_event_ai_target_priority(action: Dictionary) -> void: _notify_changed() +func _set_event_ai_awake(action: Dictionary) -> void: + var unit_ids := _event_action_unit_ids(action) + if unit_ids.is_empty(): + push_error("Skipping AI awake event with no unit ids in %s." % battle_id) + return + var awake := bool(action.get("awake", true)) + var changed_units: Array[Dictionary] = [] + for unit_id in unit_ids: + var unit := get_unit(unit_id) + if unit.is_empty(): + push_error("Skipping unknown AI awake unit %s in %s." % [unit_id, battle_id]) + continue + var was_awake := bool(unit.get("ai_awake", false)) + unit["ai_awake"] = awake + if was_awake != awake: + changed_units.append(unit) + if changed_units.is_empty(): + return + if not bool(action.get("silent", false)): + var text := str(action.get("text", "")).strip_edges() + if text.is_empty(): + text = "적 초소병의 움직임이 바뀌었다." if awake else "적 초소병이 다시 경계 위치에 머문다." + _emit_log(text) + for unit in changed_units: + if bool(unit.get("alive", false)) and bool(unit.get("deployed", true)): + _emit_combat_feedback(unit, "경계" if awake else "대기", "priority") + _notify_changed() + + func _event_action_unit_ids(action: Dictionary) -> Array[String]: var result: Array[String] = [] if action.has("unit_ids") and typeof(action["unit_ids"]) == TYPE_ARRAY: diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 1323922..76c886b 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -28,6 +28,7 @@ func _init() -> void: _check_event_dialogue_side_passthrough(failures) _check_opening_battle_camp_context(failures) _check_opening_battle_tactical_spacing(failures) + _check_opening_battle_lure_line_ai_reaction(failures) _check_opening_battle_post_battle_bridge(failures) _check_sishui_gate_camp_context(failures) _check_opening_battle_event_dialogue_structure(failures) @@ -711,6 +712,28 @@ func _check_opening_battle_tactical_spacing(failures: Array[String]) -> void: scene.free() +func _check_opening_battle_lure_line_ai_reaction(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("could not load opening battle for lure line AI reaction") + return + var cao_cao: Dictionary = state.get_unit("cao_cao") + cao_cao["pos"] = Vector2i(4, 6) + state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, cao_cao) + if not state.fired_event_ids.has("first_lure_line"): + failures.append("opening lure line should fire when a player reaches the marked forward cells") + for unit_id in ["yellow_turban_5", "yellow_turban_9"]: + var sentry: Dictionary = state.get_unit(unit_id) + if not bool(sentry.get("ai_awake", false)): + failures.append("opening lure line should wake front sentry %s without waiting for its activation turn" % unit_id) + var rear_archer: Dictionary = state.get_unit("yellow_turban_3") + if bool(rear_archer.get("ai_awake", false)): + failures.append("opening lure line should not wake the castle-side archer wave") + var xiahou_dun: Dictionary = state.get_unit("xiahou_dun") + if int(xiahou_dun.get("ai_target_priority", 0)) < 6: + failures.append("opening lure line should make Xiahou Dun a visible vanguard lure target") + + func _check_opening_battle_post_battle_bridge(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index c519c6f..a44e186 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -93,7 +93,7 @@ $validConditionTypes = @( "any" ) $validTriggers = @("battle_start", "battle_begin", "turn_start", "unit_reaches_tile", "unit_defeated") -$validActions = @("log", "dialogue", "set_objective", "grant_item", "grant_items", "grant_gold", "spawn_deployment", "spawn_deployments", "withdraw_unit", "withdraw_units", "set_ai_target_priority") +$validActions = @("log", "dialogue", "set_objective", "grant_item", "grant_items", "grant_gold", "spawn_deployment", "spawn_deployments", "withdraw_unit", "withdraw_units", "set_ai_awake", "set_ai_target_priority") $validEffects = @("heal_hp", "heal_mp", "cure_status", "cleanse_debuffs") $validItemKinds = @("weapon", "armor", "accessory", "consumable") $validItemRarities = @("common", "named") @@ -793,10 +793,10 @@ function Check-Event-Unit-Ids($Action, $KnownUnitIds, [string]$Context) { } function Check-Event-Ai-Target-Priority($Action, $KnownUnitIds, [string]$Context) { - Check-Event-Unit-Ids $Action $KnownUnitIds $Context - if (-not (Has-Prop $Action "priority")) { - Fail "$Context must set priority." - } + Check-Event-Unit-Ids $Action $KnownUnitIds $Context + if (-not (Has-Prop $Action "priority")) { + Fail "$Context must set priority." + } $priorityValue = Get-Prop $Action "priority" 0 if (-not ($priorityValue -is [int] -or $priorityValue -is [long])) { Fail "$Context priority must be an integer." @@ -808,9 +808,22 @@ function Check-Event-Ai-Target-Priority($Action, $KnownUnitIds, [string]$Context if ((Has-Prop $Action "text") -and [string]::IsNullOrWhiteSpace([string](Get-Prop $Action "text" ""))) { Fail "$Context text must not be empty." } - if ((Has-Prop $Action "silent") -and -not ((Get-Prop $Action "silent" $false) -is [bool])) { - Fail "$Context silent must be boolean." - } + if ((Has-Prop $Action "silent") -and -not ((Get-Prop $Action "silent" $false) -is [bool])) { + Fail "$Context silent must be boolean." + } +} + +function Check-Event-Ai-Awake($Action, $KnownUnitIds, [string]$Context) { + Check-Event-Unit-Ids $Action $KnownUnitIds $Context + if ((Has-Prop $Action "awake") -and -not ((Get-Prop $Action "awake" $true) -is [bool])) { + Fail "$Context awake must be boolean." + } + if ((Has-Prop $Action "text") -and [string]::IsNullOrWhiteSpace([string](Get-Prop $Action "text" ""))) { + Fail "$Context text must not be empty." + } + if ((Has-Prop $Action "silent") -and -not ((Get-Prop $Action "silent" $false) -is [bool])) { + Fail "$Context silent must be boolean." + } } function Check-Class-Equipment-Slots() { @@ -2217,12 +2230,15 @@ foreach ($scenario in $campaign.scenarios) { $unitId = [string](Get-Prop $action "unit_id" (Get-Prop $action "id" "")) Check-Event-Withdraw-Unit-Id $unitId $knownUnitIds "Scenario $scenarioId event $eventId withdraw_unit" } - if ($actionType -eq "withdraw_units") { - Check-Event-Withdraw-Units (Get-Prop $action "unit_ids" $null) $knownUnitIds "Scenario $scenarioId event $eventId withdraw_units" - } - if ($actionType -eq "set_ai_target_priority") { - Check-Event-Ai-Target-Priority $action $knownUnitIds "Scenario $scenarioId event $eventId set_ai_target_priority" - } + if ($actionType -eq "withdraw_units") { + Check-Event-Withdraw-Units (Get-Prop $action "unit_ids" $null) $knownUnitIds "Scenario $scenarioId event $eventId withdraw_units" + } + if ($actionType -eq "set_ai_awake") { + Check-Event-Ai-Awake $action $knownUnitIds "Scenario $scenarioId event $eventId set_ai_awake" + } + if ($actionType -eq "set_ai_target_priority") { + Check-Event-Ai-Target-Priority $action $knownUnitIds "Scenario $scenarioId event $eventId set_ai_target_priority" + } } }