Add branch-aware camp conversations

This commit is contained in:
2026-06-18 18:16:38 +09:00
parent 87e75ea018
commit 4bd18a3f53
8 changed files with 155 additions and 10 deletions

View File

@@ -20,7 +20,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Weapon, armor, and accessory rewards can be stored, displayed, compared, equipped, and unequipped from the battle HUD; some weapons deal bonus physical damage against matching move types. - Weapon, armor, and accessory rewards can be stored, displayed, compared, equipped, and unequipped from the battle HUD; some weapons deal bonus physical damage against matching move types.
- First named equipment rewards exist in the northern campaign arc and show a `[Named]` or `Named` tag in reward, inventory, shop, Armory, and equipment menus. - First named equipment rewards exist in the northern campaign arc and show a `[Named]` or `Named` tag in reward, inventory, shop, Armory, and equipment menus.
- Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory. - Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory.
- Pre-battle Talk can list scenario camp conversations by officer or topic, grant one-time pre-battle supplies, and shop stock can include merchant flavor lines before a battle. - Pre-battle Talk can list scenario camp conversations by officer or topic, branch them from saved campaign choices, grant one-time pre-battle supplies, and shop stock can include merchant flavor lines before a battle.
- Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock. - Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock.
- Pre-battle Roster can move optional officers between sortie and reserve within scenario limits. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits.
- Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells. - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells.

View File

@@ -33,6 +33,72 @@
"Scouts from Sishui report suspicious movement in the trees. Expect an ambush and prepare accordingly." "Scouts from Sishui report suspicious movement in the trees. Expect an ambush and prepare accordingly."
] ]
} }
],
"camp_conversations": [
{
"id": "xingyang_war_council",
"group": "topic",
"label": "Xingyang War Council",
"summary": "Review the narrow road before the advance.",
"lines": [
{ "speaker": "Cao Cao", "side": "left", "text": "Dong Zhuo retreats with the court in his grip. If Xu Rong bars the road, we break him quickly." },
{ "speaker": "Xiahou Dun", "side": "right", "text": "The forest line is too quiet. I would rather meet steel in the open than courtesy from those trees." }
]
},
{
"id": "xingyang_forced_march",
"campaign_flags": { "pursued_dong_zhuo": true },
"group": "officer",
"officer_id": "cao_cao",
"label": "Cao Cao - Forced March",
"speaker": "Cao Cao",
"summary": "Consider the cost of chasing Dong Zhuo without rest.",
"lines": [
{ "speaker": "Cao Cao", "side": "left", "text": "The coalition will count banners until Dong Zhuo is beyond reach. We chose the road while they chose their chairs." },
{ "speaker": "Xiahou Dun", "side": "right", "text": "The men are tired, but their blood is up. Let Xu Rong stand in the road if he wants to be first." }
]
},
{
"id": "xingyang_hasty_stores",
"campaign_flags": { "pursued_dong_zhuo": true },
"group": "topic",
"label": "Hasty Stores",
"summary": "Open the last wagon packed during the pursuit.",
"lines": [
{ "speaker": "Xiahou Yuan", "side": "right", "text": "We left Sishui at speed, but I kept one small wagon close to the vanguard." },
{ "speaker": "Cao Cao", "side": "left", "text": "Good. Issue it before the road narrows; tired soldiers need steadier hands." }
],
"effects": [
{ "type": "grant_item", "item_id": "wine", "count": 1, "text": "The hasty stores add Wine to the field supplies." }
]
},
{
"id": "xingyang_scout_screen",
"campaign_flags": { "regrouped_after_sishui": true },
"group": "officer",
"officer_id": "cao_ren",
"label": "Cao Ren - Scout Screen",
"speaker": "Cao Ren",
"summary": "Review the warning signs around the forest road.",
"lines": [
{ "speaker": "Cao Ren", "side": "right", "text": "The scouts found cut branches near the forest edge. Xu Rong wants us to enter blind." },
{ "speaker": "Cao Cao", "side": "left", "text": "Then we thank him for showing us where to look. Keep the line compact until the trap reveals itself." }
]
},
{
"id": "xingyang_sishui_supply_train",
"campaign_flags": { "regrouped_after_sishui": true },
"group": "topic",
"label": "Sishui Supply Train",
"summary": "Distribute the supplies gathered during the regroup.",
"lines": [
{ "speaker": "Camp Merchant", "side": "right", "text": "The delay at Sishui let me sort the physician's chest. Take this before the forest road." },
{ "speaker": "Xiahou Yuan", "side": "left", "text": "If Xu Rong brought poison or snares, better to spend the remedy before men start cursing my scouting." }
],
"effects": [
{ "type": "grant_item", "item_id": "panacea", "count": 1, "text": "The Sishui supply train adds a Panacea to the field supplies." }
]
}
] ]
}, },
"shop": { "shop": {

File diff suppressed because one or more lines are too long

View File

@@ -488,7 +488,7 @@ Scenario deployments may override `move_type` for special units. For example, a
`objectives` are display text. `conditions` drive the actual victory and defeat checks. Defeat is evaluated before victory, matching Cao Cao Zhuan-style commander-loss rules. If `conditions` is omitted, battles fall back to defeating all enemies for victory and losing all player units for defeat. `objectives` are display text. `conditions` drive the actual victory and defeat checks. Defeat is evaluated before victory, matching Cao Cao Zhuan-style commander-loss rules. If `conditions` is omitted, battles fall back to defeating all enemies for victory and losing all player units for defeat.
`briefing.camp_conversations` drives the pre-battle Talk menu. Each entry needs a stable lowercase `id`, a non-empty `label`, a `group` of `officer` or `topic`, and non-empty dialogue `lines`. Officer conversations also need an `officer_id` that exists in `officers.json`; `BattleState` normalizes their dialogue lines so speaker portraits can be filled from officer defaults. Optional `effects` can currently include `grant_item` entries with an `item_id`, optional positive `count`, and optional non-empty `text`. Camp conversation supplies are claimed once per scenario/conversation, saved immediately to campaign inventory through `CampaignState`, and copied into the already-loaded battle inventory; completed-scenario replays cannot claim them. If `camp_conversations` is absent, the older `briefing.camp_dialogue` array is still exposed as one fallback War Council conversation. `briefing.camp_conversations` drives the pre-battle Talk menu. Each entry needs a stable lowercase `id`, a non-empty `label`, a `group` of `officer` or `topic`, and non-empty dialogue `lines`. Officer conversations also need an `officer_id` that exists in `officers.json`; `BattleState` normalizes their dialogue lines so speaker portraits can be filled from officer defaults. Optional `campaign_flags`, such as `"campaign_flags": { "regrouped_after_sishui": true }`, can hide a conversation unless saved flags match exactly. Optional `effects` can currently include `grant_item` entries with an `item_id`, optional positive `count`, and optional non-empty `text`. Camp conversation supplies are claimed once per scenario/conversation, saved immediately to campaign inventory through `CampaignState`, and copied into the already-loaded battle inventory; completed-scenario replays cannot claim them. If `camp_conversations` is absent, the older `briefing.camp_dialogue` array is still exposed as one fallback War Council conversation.
`BattleState` also exposes condition progress and defeat-risk text for presentation. The active HUD and briefing panels split objective, progress, and risk details while showing defeated enemy counts, destination reach status, named commander or protected-unit safety, and turns remaining from the same `conditions` data. Conditions gated by `after_event` stay marked as pending; when the referenced event is a `unit_reaches_tile` or `turn_start` trigger, the tracker can surface that prerequisite as the next visible objective, and `unit_reaches_tile` prerequisites can also draw objective markers while the gated condition is still locked. `BattleState` also exposes condition progress and defeat-risk text for presentation. The active HUD and briefing panels split objective, progress, and risk details while showing defeated enemy counts, destination reach status, named commander or protected-unit safety, and turns remaining from the same `conditions` data. Conditions gated by `after_event` stay marked as pending; when the referenced event is a `unit_reaches_tile` or `turn_start` trigger, the tracker can surface that prerequisite as the next visible objective, and `unit_reaches_tile` prerequisites can also draw objective markers while the gated condition is still locked.
@@ -632,7 +632,7 @@ Movement-triggered gold caches can use `grant_gold` with a positive `amount`. Th
} }
``` ```
`briefing.conditional_lines`, `shop.conditional_items`, and `when.campaign_flags` all use exact flag matching. When present, every listed flag must match the saved campaign flag value for that block or event to apply. Use separate blocks or events for OR-style branches. The validator requires referenced flags to have been introduced by an earlier `post_battle_choices.set_flags` entry in campaign order. `briefing.conditional_lines`, `briefing.camp_conversations[].campaign_flags`, `shop.conditional_items`, and `when.campaign_flags` all use exact flag matching. When present, every listed flag must match the saved campaign flag value for that block, conversation, or event to apply. Use separate blocks, conversations, or events for OR-style branches. The validator requires referenced flags to have been introduced by an earlier `post_battle_choices.set_flags` entry in campaign order.
Event-spawned units use the same compact deployment shape as scenario starting units. Spawn attempts are skipped if the unit id already exists, the target cell is occupied, the cell is outside the map, or the unit cannot stand on that terrain. Reinforcements that arrive during their own team's phase wait until their next phase before acting. Event-spawned units use the same compact deployment shape as scenario starting units. Spawn attempts are skipped if the unit id already exists, the target cell is occupied, the cell is outside the map, or the unit cannot stand on that terrain. Reinforcements that arrive during their own team's phase wait until their next phase before acting.
@@ -650,8 +650,8 @@ During a battle, item consumption, event item pickups, equipment swaps, and uneq
Physical attacks grant normal attack/counter EXP only on hit after the attack/counter exchange resolves. A missed physical attack or counter grants a small miss EXP value, while defeat bonuses still require the attack to hit and defeat the target. Physical attacks grant normal attack/counter EXP only on hit after the attack/counter exchange resolves. A missed physical attack or counter grants a small miss EXP value, while defeat bonuses still require the attack to hit and defeat the target.
Before a battle starts, `BattleScene` can buy priced items through `CampaignState.try_buy_item()` and sell unequipped inventory through `CampaignState.try_sell_item()`. Successful shop transactions update campaign gold, inventory, and finite-stock purchase counts in the save file immediately, then call `BattleState.set_inventory_snapshot()` so the upcoming battle sees the new stock. Pre-battle Talk can claim `briefing.camp_conversations[].effects` through `CampaignState.try_claim_camp_conversation_effects()`, which saves the supply reward and conversation ledger immediately before refreshing the loaded battle inventory copy. Pre-battle Armory changes call `CampaignState.try_save_prebattle_loadout()`, which merges the currently deployed roster snapshot into the saved roster and saves the adjusted inventory in the same operation. Pre-battle Roster calls `BattleState.try_set_unit_deployed()` to toggle optional player units while enforcing required officers and sortie limits. Pre-battle Formation calls `BattleState.try_set_prebattle_formation()`, which can move a player unit to an open formation cell or swap two player units inside the formation area. Roster and Formation choices are battle setup state, so the manual checkpoint stores campaign progression and saved loadout state but not unsaved sortie or starting-cell edits. Matching `briefing.conditional_lines` and `shop.conditional_items` are merged while the scenario loads, before the briefing panel opens. During battle, item previews can show HP/MP recovery, status cures, and debuff cleanses, and no-effect item use is rejected without consuming stock. `post_battle_dialogue` is rendered by the battle scene after victory and before rewards/choices are shown in the result panel. Post-battle choices call `CampaignState.try_apply_post_battle_choice()`, which saves selected flags and rolls back flags, gold, and inventory if the save write fails. Before a battle starts, `BattleScene` can buy priced items through `CampaignState.try_buy_item()` and sell unequipped inventory through `CampaignState.try_sell_item()`. Successful shop transactions update campaign gold, inventory, and finite-stock purchase counts in the save file immediately, then call `BattleState.set_inventory_snapshot()` so the upcoming battle sees the new stock. Pre-battle Talk can claim `briefing.camp_conversations[].effects` through `CampaignState.try_claim_camp_conversation_effects()`, which saves the supply reward and conversation ledger immediately before refreshing the loaded battle inventory copy. Pre-battle Armory changes call `CampaignState.try_save_prebattle_loadout()`, which merges the currently deployed roster snapshot into the saved roster and saves the adjusted inventory in the same operation. Pre-battle Roster calls `BattleState.try_set_unit_deployed()` to toggle optional player units while enforcing required officers and sortie limits. Pre-battle Formation calls `BattleState.try_set_prebattle_formation()`, which can move a player unit to an open formation cell or swap two player units inside the formation area. Roster and Formation choices are battle setup state, so the manual checkpoint stores campaign progression and saved loadout state but not unsaved sortie or starting-cell edits. Matching `briefing.conditional_lines`, `briefing.camp_conversations[].campaign_flags`, and `shop.conditional_items` are resolved while the scenario loads, before the briefing panel opens. During battle, item previews can show HP/MP recovery, status cures, and debuff cleanses, and no-effect item use is rejected without consuming stock. `post_battle_dialogue` is rendered by the battle scene after victory and before rewards/choices are shown in the result panel. Post-battle choices call `CampaignState.try_apply_post_battle_choice()`, which saves selected flags and rolls back flags, gold, and inventory if the save write fails.
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. 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, cure statuses, debuff cleanse effects, class sprites, item icons, map backgrounds, officer and deployment portraits/sprites, 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 trigger unit/officer filters, event item/gold grants, event withdrawal unit ids, event AI target-priority unit ids, condition unit references, destination `pos`/`cells` coordinates, campaign flag references, joined-officer gates, briefing line blocks, camp dialogue, camp conversations and camp conversation effects, deployment ids, deployment bounds, impassable spawn cells, shop stock, shop merchant lines, 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, cure statuses, debuff cleanse effects, class sprites, item icons, map backgrounds, officer and deployment portraits/sprites, 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 trigger unit/officer filters, event item/gold grants, event withdrawal unit ids, event AI target-priority unit ids, condition unit references, destination `pos`/`cells` coordinates, campaign flag references, joined-officer gates, briefing line blocks, camp dialogue, camp conversations, camp conversation campaign flags and camp conversation effects, deployment ids, deployment bounds, impassable spawn cells, shop stock, shop merchant lines, roster rules, formation cells, post-battle dialogue, post-battle choices, and reward item ids.

View File

@@ -46,7 +46,7 @@
- Mid-battle map/action events have a first action system with movement ambushes and scripted unit withdrawals. - Mid-battle map/action events have a first action system with movement ambushes and scripted unit withdrawals.
- Opening battle dialogue exists through event actions. - Opening battle dialogue exists through event actions.
- Post-battle dialogue exists before the victory result panel. - Post-battle dialogue exists before the victory result panel.
- Pre-battle camp conversations can be selected from the briefing Talk menu by officer or topic, can grant one-time pre-battle supplies, and shops can show merchant flavor lines. - Pre-battle camp conversations can be selected from the briefing Talk menu by officer or topic, can branch from saved campaign choices, can grant one-time pre-battle supplies, and shops can show merchant flavor lines.
- Reinforcements, scripted unit withdrawals, objective changes with HUD/log feedback, and item/gold pickup events exist in a basic form. - Reinforcements, scripted unit withdrawals, objective changes with HUD/log feedback, and item/gold pickup events exist in a basic form.
- Post-battle reward save, campaign choice flags, first flag-driven briefing/shop/event branches, sectioned result summaries, and choice reward previews exist in a basic form. Richer reward screens and wider branch support are still planned. - Post-battle reward save, campaign choice flags, first flag-driven briefing/shop/event branches, sectioned result summaries, and choice reward previews exist in a basic form. Richer reward screens and wider branch support are still planned.

View File

@@ -257,6 +257,10 @@ func _normalized_camp_conversations(source) -> Array:
for entry in source: for entry in source:
if typeof(entry) != TYPE_DICTIONARY: if typeof(entry) != TYPE_DICTIONARY:
continue continue
if entry.has("campaign_flags"):
var required_flags = entry.get("campaign_flags", {})
if typeof(required_flags) != TYPE_DICTIONARY or not _campaign_flags_match(required_flags):
continue
var conversation_id := str(entry.get("id", "")).strip_edges() var conversation_id := str(entry.get("id", "")).strip_edges()
if conversation_id.is_empty() or seen.has(conversation_id): if conversation_id.is_empty() or seen.has(conversation_id):
continue continue

View File

@@ -6,6 +6,8 @@ const CAMPAIGN_PATH := "res://data/campaign/campaign.json"
const SCENARIO_ID := "001_yellow_turbans" const SCENARIO_ID := "001_yellow_turbans"
const SCENARIO_PATH := "res://data/scenarios/001_yellow_turbans.json" const SCENARIO_PATH := "res://data/scenarios/001_yellow_turbans.json"
const CONVERSATION_ID := "northern_woods_cache" const CONVERSATION_ID := "northern_woods_cache"
const BRANCH_SCENARIO_ID := "003_xingyang_ambush"
const BRANCH_SCENARIO_PATH := "res://data/scenarios/003_xingyang_ambush.json"
const SAVE_PATH := "user://campaign_save.json" const SAVE_PATH := "user://campaign_save.json"
const MANUAL_SAVE_PATH := "user://campaign_manual_save.json" const MANUAL_SAVE_PATH := "user://campaign_manual_save.json"
@@ -19,6 +21,7 @@ func _init() -> void:
_check_shop_and_armory_preserve_claim(failures) _check_shop_and_armory_preserve_claim(failures)
_check_manual_checkpoint_reverts_claim(failures) _check_manual_checkpoint_reverts_claim(failures)
_check_completed_replay_cannot_claim(failures) _check_completed_replay_cannot_claim(failures)
_check_conditional_camp_conversations(failures)
if not _restore_user_file(SAVE_PATH, save_backup): if not _restore_user_file(SAVE_PATH, save_backup):
failures.append("could not restore automatic campaign save") failures.append("could not restore automatic campaign save")
@@ -136,16 +139,81 @@ func _check_completed_replay_cannot_claim(failures: Array[String]) -> void:
scene.free() scene.free()
func _check_conditional_camp_conversations(failures: Array[String]) -> void:
var no_flags_scene = _new_prebattle_scene_for(failures, "conditional camp no flags", BRANCH_SCENARIO_ID, BRANCH_SCENARIO_PATH)
if no_flags_scene != null:
if no_flags_scene._camp_conversation_by_id("xingyang_war_council").is_empty():
failures.append("Xingyang should expose ungated war council conversation")
if not no_flags_scene._camp_conversation_by_id("xingyang_forced_march").is_empty():
failures.append("pursuit camp conversation should be hidden without pursuit flag")
if not no_flags_scene._camp_conversation_by_id("xingyang_scout_screen").is_empty():
failures.append("regroup camp conversation should be hidden without regroup flag")
no_flags_scene.free()
var pursuit_scene = _new_prebattle_scene_for(
failures,
"conditional camp pursuit",
BRANCH_SCENARIO_ID,
BRANCH_SCENARIO_PATH,
{"pursued_dong_zhuo": true}
)
if pursuit_scene != null:
if pursuit_scene._camp_conversation_by_id("xingyang_forced_march").is_empty():
failures.append("pursuit flag should expose forced march conversation")
if pursuit_scene._camp_conversation_by_id("xingyang_hasty_stores").is_empty():
failures.append("pursuit flag should expose hasty stores conversation")
if not pursuit_scene._camp_conversation_by_id("xingyang_scout_screen").is_empty():
failures.append("pursuit flag should not expose regroup scout conversation")
var hasty_stores: Dictionary = pursuit_scene._camp_conversation_by_id("xingyang_hasty_stores")
var before_wine := int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0))
pursuit_scene._apply_camp_conversation_effects(hasty_stores)
if int(pursuit_scene.campaign_state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("pursuit hasty stores should add Wine to campaign inventory")
if int(pursuit_scene.state.get_inventory_snapshot().get("wine", 0)) != before_wine + 1:
failures.append("pursuit hasty stores should refresh battle Wine inventory")
pursuit_scene.free()
var regroup_scene = _new_prebattle_scene_for(
failures,
"conditional camp regroup",
BRANCH_SCENARIO_ID,
BRANCH_SCENARIO_PATH,
{"regrouped_after_sishui": true}
)
if regroup_scene != null:
if regroup_scene._camp_conversation_by_id("xingyang_scout_screen").is_empty():
failures.append("regroup flag should expose scout screen conversation")
if regroup_scene._camp_conversation_by_id("xingyang_sishui_supply_train").is_empty():
failures.append("regroup flag should expose Sishui supply train conversation")
if not regroup_scene._camp_conversation_by_id("xingyang_forced_march").is_empty():
failures.append("regroup flag should not expose pursuit forced march conversation")
var supply_train: Dictionary = regroup_scene._camp_conversation_by_id("xingyang_sishui_supply_train")
var before_panacea := int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0))
regroup_scene._apply_camp_conversation_effects(supply_train)
if int(regroup_scene.campaign_state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("regroup Sishui supply train should add Panacea to campaign inventory")
if int(regroup_scene.state.get_inventory_snapshot().get("panacea", 0)) != before_panacea + 1:
failures.append("regroup Sishui supply train should refresh battle Panacea inventory")
regroup_scene.free()
func _new_prebattle_scene(failures: Array[String], label: String): func _new_prebattle_scene(failures: Array[String], label: String):
return _new_prebattle_scene_for(failures, label, SCENARIO_ID, SCENARIO_PATH)
func _new_prebattle_scene_for(failures: Array[String], label: String, scenario_id: String, scenario_path: String, flag_overrides := {}):
var scene = BattleSceneScript.new() var scene = BattleSceneScript.new()
if not scene.campaign_state.load_campaign(CAMPAIGN_PATH): if not scene.campaign_state.load_campaign(CAMPAIGN_PATH):
failures.append("%s could not load campaign data" % label) failures.append("%s could not load campaign data" % label)
scene.free() scene.free()
return null return null
scene.campaign_state.start_new(SCENARIO_ID) scene.campaign_state.start_new(scenario_id)
scene.active_scenario_id = SCENARIO_ID if typeof(flag_overrides) == TYPE_DICTIONARY:
for flag_name in flag_overrides.keys():
scene.campaign_state.flags[str(flag_name)] = flag_overrides[flag_name]
scene.active_scenario_id = scenario_id
if not scene.state.load_battle( if not scene.state.load_battle(
SCENARIO_PATH, scenario_path,
scene.campaign_state.get_roster_overrides(), scene.campaign_state.get_roster_overrides(),
scene.campaign_state.get_inventory_snapshot(), scene.campaign_state.get_inventory_snapshot(),
scene.campaign_state.get_flags_snapshot(), scene.campaign_state.get_flags_snapshot(),

View File

@@ -1763,6 +1763,13 @@ function Check-Camp-Conversations($Conversations, [string]$ScenarioId) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId references unknown officer: $officerId" Fail "Scenario $ScenarioId briefing camp_conversations $conversationId references unknown officer: $officerId"
} }
} }
if (Has-Prop $conversation "campaign_flags") {
$conversationFlags = Get-Prop $conversation "campaign_flags" $null
if ($null -eq $conversationFlags -or -not ($conversationFlags -is [pscustomobject]) -or @($conversationFlags.PSObject.Properties).Count -le 0) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId campaign_flags must be a non-empty object."
}
Check-Required-Flags $conversationFlags $ScenarioId "briefing camp_conversations $conversationId"
}
$conversationLines = Get-Prop $conversation "lines" $null $conversationLines = Get-Prop $conversation "lines" $null
if ($null -eq $conversationLines -or -not ($conversationLines -is [System.Array]) -or @($conversationLines).Count -le 0) { if ($null -eq $conversationLines -or -not ($conversationLines -is [System.Array]) -or @($conversationLines).Count -le 0) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId must have non-empty lines." Fail "Scenario $ScenarioId briefing camp_conversations $conversationId must have non-empty lines."