Files
heros/tools/validate_data.ps1

2306 lines
100 KiB
PowerShell

$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $PSScriptRoot
function Read-Json($RelativePath) {
$path = Join-Path $Root $RelativePath
if (-not (Test-Path $path)) {
throw "Missing file: $RelativePath"
}
return Get-Content $path -Raw -Encoding UTF8 | ConvertFrom-Json
}
function Has-Prop($Object, [string]$Name) {
if ($null -eq $Object) {
return $false
}
return $null -ne ($Object.PSObject.Properties | Where-Object { $_.Name -eq $Name })
}
function Get-Prop($Object, [string]$Name, $Default = $null) {
if (Has-Prop $Object $Name) {
return $Object.$Name
}
return $Default
}
function Get-Names($Object) {
return @($Object.PSObject.Properties.Name)
}
function Fail($Message) {
throw $Message
}
function Check-Readable-Korean-Text([string]$Text, [string]$Context, [switch]$RequireHangul) {
if ([string]::IsNullOrWhiteSpace($Text)) {
Fail "$Context must not be empty."
}
$replacementCharacter = [string][char]0xFFFD
if ($Text.Contains($replacementCharacter)) {
Fail "$Context contains a replacement character; check UTF-8 encoding."
}
if ($Text -match '\p{IsCJKUnifiedIdeographs}') {
Fail "$Context contains Hanja/CJK ideographs; use Korean-facing Hangul text."
}
$needsHangul = $RequireHangul -or $script:CurrentScenarioRequiresHangul
if ($needsHangul -and $Text -notmatch '\p{IsHangulSyllables}') {
Fail "$Context should contain readable Hangul text."
}
}
function Check-Raw-Skills-Arrays() {
$relativePaths = @(
"data\defs\classes.json",
"data\defs\officers.json"
)
$scenarioDir = Join-Path $Root "data\scenarios"
foreach ($file in Get-ChildItem -Path $scenarioDir -Filter "*.json") {
$relativePaths += "data\scenarios\$($file.Name)"
}
foreach ($relativePath in $relativePaths) {
$path = Join-Path $Root $relativePath
$text = Get-Content $path -Raw
if ($text -match '"skills"\s*:\s*"') {
Fail "$relativePath has skills as a string; use an array."
}
}
}
function Get-Flag-Value-Kind($Value) {
if ($Value -is [bool]) {
return "bool"
}
if ($Value -is [string]) {
return "string"
}
if ($Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
return "number"
}
return "other"
}
function Terrain-At($Rows, [int]$X, [int]$Y) {
return [string]$Rows[$Y].Substring($X, 1)
}
$campaign = Read-Json "data\campaign\campaign.json"
$classes = Read-Json "data\defs\classes.json"
$items = Read-Json "data\defs\items.json"
$officers = Read-Json "data\defs\officers.json"
$skills = Read-Json "data\defs\skills.json"
$terrain = Read-Json "data\defs\terrain.json"
$classIds = Get-Names $classes
$itemIds = Get-Names $items
$officerIds = Get-Names $officers
$skillIds = Get-Names $skills
$terrainKeys = Get-Names $terrain
$script:CurrentScenarioRequiresHangul = $false
$validConditionTypes = @(
"all_units_defeated",
"all_enemies_defeated",
"all_players_defeated",
"any_unit_defeated",
"any_officer_defeated",
"unit_reaches_tile",
"turn_limit",
"turn_reached",
"all",
"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_awake", "set_ai_target_priority")
$validEffects = @("heal_hp", "heal_mp", "cure_status", "cleanse_debuffs")
$validItemKinds = @("weapon", "armor", "accessory", "consumable")
$validItemRarities = @("common", "named")
$validBonusStats = @("hp", "mp", "atk", "def", "int", "agi")
$validSkillKinds = @("damage", "heal", "support")
$validSkillTargets = @("enemy", "ally", "self", "any")
$validSkillStats = @("atk", "def", "int", "agi")
$validStatusStats = @("atk", "def", "int", "agi", "move")
$validSkillEffectTypes = @("stat_bonus", "damage_over_time", "action_lock")
$validSkillAreaShapes = @("single", "diamond")
$validDialogueSides = @("left", "right")
$validCampConversationGroups = @("officer", "topic")
$validCampConversationEffectTypes = @("grant_item")
$validPortraitPathPattern = '^res://.+\.(png|jpg|jpeg|webp)$'
$minimumPortraitDimension = 512
$portraitImageInspectorReady = $false
try {
Add-Type -AssemblyName System.Drawing -ErrorAction Stop
$portraitImageInspectorReady = $true
} catch {
$portraitImageInspectorReady = $false
}
$validMoveTypes = New-Object System.Collections.Generic.HashSet[string]
$knownFlagIds = New-Object System.Collections.Generic.HashSet[string]
$knownFlagValueKinds = @{}
$joinedOfficerIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($classProp in $classes.PSObject.Properties) {
$moveType = [string](Get-Prop $classProp.Value "move_type" "foot")
if (-not [string]::IsNullOrWhiteSpace($moveType)) {
$validMoveTypes.Add($moveType) | Out-Null
}
}
foreach ($terrainProp in $terrain.PSObject.Properties) {
$moveCost = Get-Prop $terrainProp.Value "move_cost" $null
if ($null -eq $moveCost) {
continue
}
foreach ($moveCostProp in $moveCost.PSObject.Properties) {
$moveType = [string]$moveCostProp.Name
if (-not [string]::IsNullOrWhiteSpace($moveType)) {
$validMoveTypes.Add($moveType) | Out-Null
}
}
}
foreach ($officerIdValue in (Get-Prop $campaign "initial_joined_officers" @())) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Campaign initial_joined_officers has an empty officer id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Campaign initial_joined_officers references unknown officer: $officerId"
}
if (-not $joinedOfficerIds.Add($officerId)) {
Fail "Campaign initial_joined_officers has duplicate officer: $officerId"
}
}
$campaignScenarioIds = @()
$campaignScenarioIdSet = New-Object System.Collections.Generic.HashSet[string]
foreach ($scenario in @($campaign.scenarios)) {
if ($scenario -is [System.Array] -or $null -eq $scenario) {
Fail "Campaign has a malformed scenario entry."
}
$scenarioId = [string](Get-Prop $scenario "id" "")
$scenarioTitle = [string](Get-Prop $scenario "title" "")
$scenarioPath = [string](Get-Prop $scenario "path" "")
if ([string]::IsNullOrWhiteSpace($scenarioId)) {
Fail "Campaign has a scenario with an empty id."
}
if (-not ($scenarioId -match '^[a-z0-9_]+$')) {
Fail "Campaign scenario has unstable id: $scenarioId"
}
if ([string]::IsNullOrWhiteSpace($scenarioTitle)) {
Fail "Campaign scenario $scenarioId has an empty title."
}
Check-Readable-Korean-Text $scenarioTitle "Campaign scenario $scenarioId title" -RequireHangul
if ([string]::IsNullOrWhiteSpace($scenarioPath)) {
Fail "Campaign scenario $scenarioId has an empty path."
}
if (-not $campaignScenarioIdSet.Add($scenarioId)) {
Fail "Campaign has duplicate scenario id: $scenarioId"
}
$campaignScenarioIds += $scenarioId
}
if ($campaignScenarioIds.Count -le 0) {
Fail "Campaign has no scenarios."
}
if (-not $campaignScenarioIdSet.Contains([string]$campaign.start_scenario)) {
Fail "Campaign start_scenario references unknown scenario: $($campaign.start_scenario)"
}
if (Has-Prop $campaign "chapters") {
$chaptersValue = Get-Prop $campaign "chapters" $null
if ($chaptersValue -isnot [System.Array]) {
Fail "Campaign chapters must be an array."
}
if (@($chaptersValue).Count -le 0) {
Fail "Campaign chapters must not be empty."
}
$chapterIds = New-Object System.Collections.Generic.HashSet[string]
$coveredScenarioIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($chapter in @($chaptersValue)) {
if ($chapter -is [System.Array] -or $null -eq $chapter) {
Fail "Campaign has a malformed chapter entry."
}
$chapterId = [string](Get-Prop $chapter "id" "")
$chapterTitle = [string](Get-Prop $chapter "title" "")
$startScenario = [string](Get-Prop $chapter "start_scenario" "")
$endScenario = [string](Get-Prop $chapter "end_scenario" "")
if ([string]::IsNullOrWhiteSpace($chapterId)) {
Fail "Campaign chapter has an empty id."
}
if (-not ($chapterId -match '^[a-z0-9_]+$')) {
Fail "Campaign chapter has unstable id: $chapterId"
}
if (-not $chapterIds.Add($chapterId)) {
Fail "Campaign has duplicate chapter id: $chapterId"
}
if ([string]::IsNullOrWhiteSpace($chapterTitle)) {
Fail "Campaign chapter $chapterId has an empty title."
}
Check-Readable-Korean-Text $chapterTitle "Campaign chapter $chapterId title" -RequireHangul
if (-not $campaignScenarioIdSet.Contains($startScenario)) {
Fail "Campaign chapter $chapterId references unknown start_scenario: $startScenario"
}
if (-not $campaignScenarioIdSet.Contains($endScenario)) {
Fail "Campaign chapter $chapterId references unknown end_scenario: $endScenario"
}
$startIndex = [Array]::IndexOf($campaignScenarioIds, $startScenario)
$endIndex = [Array]::IndexOf($campaignScenarioIds, $endScenario)
if ($startIndex -lt 0 -or $endIndex -lt 0 -or $endIndex -lt $startIndex) {
Fail "Campaign chapter $chapterId has an invalid scenario range."
}
for ($index = $startIndex; $index -le $endIndex; $index++) {
$coveredScenarioId = [string]$campaignScenarioIds[$index]
if (-not $coveredScenarioIds.Add($coveredScenarioId)) {
Fail "Campaign chapter $chapterId overlaps scenario: $coveredScenarioId"
}
}
}
foreach ($scenarioId in $campaignScenarioIds) {
if (-not $coveredScenarioIds.Contains($scenarioId)) {
Fail "Campaign chapters do not cover scenario: $scenarioId"
}
}
}
function Check-Unit-Reaches-Cells($Source, [string]$Context, [int]$Width, [int]$Height, $Rows) {
$hasPos = Has-Prop $Source "pos"
$hasCells = Has-Prop $Source "cells"
if ($hasPos -and $hasCells) {
Fail "$Context must use either pos or cells, not both."
}
if (-not $hasPos -and -not $hasCells) {
Fail "$Context is missing pos or cells."
}
$cellEntries = @()
if ($hasPos) {
$cellEntries += ,$Source.pos
} else {
if (-not ($Source.cells -is [System.Array])) {
Fail "$Context cells must be an array."
}
$cellEntries = @($Source.cells)
if ($cellEntries.Count -le 0) {
Fail "$Context cells must not be empty."
}
}
$seenCells = New-Object System.Collections.Generic.HashSet[string]
foreach ($entry in $cellEntries) {
$cell = Resolve-Cell $entry $Context
$x = [int]$cell.X
$y = [int]$cell.Y
if ($Width -ge 0 -and $Height -ge 0 -and ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height)) {
Fail "$Context is outside map at [$x,$y]."
}
if ($null -ne $Rows) {
$terrainKey = Terrain-At $Rows $x $y
if ([int]$terrain.$terrainKey.move_cost.foot -ge 99 -and [int]$terrain.$terrainKey.move_cost.mounted -ge 99 -and [int]$terrain.$terrainKey.move_cost.archer -ge 99) {
Fail "$Context targets impassable terrain at [$x,$y]."
}
}
$key = "$x,$y"
if (-not $seenCells.Add($key)) {
Fail "$Context has duplicate cell [$x,$y]."
}
}
}
function Check-Condition($Condition, [string]$ScenarioId, $EventIds, [int]$Width = -1, [int]$Height = -1, $Rows = $null, $KnownUnitIds = $null) {
if ($Condition -is [System.Array]) {
foreach ($entry in $Condition) {
Check-Condition $entry $ScenarioId $EventIds $Width $Height $Rows $KnownUnitIds
}
return
}
if (-not (Has-Prop $Condition "type")) {
Fail "Scenario $ScenarioId has a malformed condition."
}
$type = [string]$Condition.type
if (-not $validConditionTypes.Contains($type)) {
Fail "Scenario $ScenarioId has unknown condition type: $type"
}
if ((Has-Prop $Condition "after_event") -and (-not $EventIds.Contains([string]$Condition.after_event))) {
Fail "Scenario $ScenarioId condition references missing event: $($Condition.after_event)"
}
if (($type -eq "all" -or $type -eq "any") -and (Has-Prop $Condition "conditions")) {
Check-Condition $Condition.conditions $ScenarioId $EventIds $Width $Height $Rows $KnownUnitIds
}
if ($type -eq "any_unit_defeated") {
$unitIdsValue = @()
if (Has-Prop $Condition "unit_ids") {
$unitIdsValue = @($Condition.unit_ids)
}
if ($unitIdsValue.Count -le 0) {
Fail "Scenario $ScenarioId any_unit_defeated needs unit_ids."
}
foreach ($unitIdValue in $unitIdsValue) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId any_unit_defeated has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId any_unit_defeated references unknown unit: $unitId"
}
}
}
if ($type -eq "unit_reaches_tile") {
Check-Unit-Reaches-Cells $Condition "Scenario $ScenarioId unit_reaches_tile condition" $Width $Height $Rows
$unitIdsValue = $null
if (Has-Prop $Condition "unit_ids") {
$unitIdsValue = $Condition.unit_ids
}
if ($null -ne $unitIdsValue -and -not ($unitIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile unit_ids must be an array."
}
if ($null -ne $unitIdsValue) {
foreach ($unitIdValue in @($unitIdsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId unit_reaches_tile has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId unit_reaches_tile references unknown unit: $unitId"
}
}
}
$officerIdsValue = $null
if (Has-Prop $Condition "officer_ids") {
$officerIdsValue = $Condition.officer_ids
if ($null -ne $officerIdsValue -and -not ($officerIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile officer_ids must be an array."
}
foreach ($officerIdValue in @($officerIdsValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId) -or (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId unit_reaches_tile references unknown officer: $officerId"
}
}
}
if (($null -eq $unitIdsValue -or @($unitIdsValue).Count -le 0) -and ((-not (Has-Prop $Condition "officer_ids")) -or @($officerIdsValue).Count -le 0)) {
Fail "Scenario $ScenarioId unit_reaches_tile needs unit_ids or officer_ids."
}
$team = [string](Get-Prop $Condition "team" "")
if (-not [string]::IsNullOrWhiteSpace($team) -and $team -ne "player" -and $team -ne "enemy") {
Fail "Scenario $ScenarioId unit_reaches_tile has unknown team: $team"
}
}
}
function Check-Unit-Reaches-Event-When($When, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $KnownUnitIds) {
Check-Unit-Reaches-Cells $When "Scenario $ScenarioId unit_reaches_tile event" $Width $Height $Rows
$unitIdsValue = $null
if (Has-Prop $When "unit_ids") {
$unitIdsValue = $When.unit_ids
if ($null -ne $unitIdsValue -and -not ($unitIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile event unit_ids must be an array."
}
foreach ($unitIdValue in @($unitIdsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId unit_reaches_tile event has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId unit_reaches_tile event references unknown unit: $unitId"
}
}
}
$officerIdsValue = $null
if (Has-Prop $When "officer_ids") {
$officerIdsValue = $When.officer_ids
if ($null -ne $officerIdsValue -and -not ($officerIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_reaches_tile event officer_ids must be an array."
}
foreach ($officerIdValue in @($officerIdsValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId) -or (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId unit_reaches_tile event references unknown officer: $officerId"
}
}
}
$team = [string](Get-Prop $When "team" "")
if (-not [string]::IsNullOrWhiteSpace($team) -and $team -ne "player" -and $team -ne "enemy") {
Fail "Scenario $ScenarioId unit_reaches_tile event has unknown team: $team"
}
$unitIdCount = 0
if ($null -ne $unitIdsValue) {
$unitIdCount = @($unitIdsValue).Count
}
$officerIdCount = 0
if ($null -ne $officerIdsValue) {
$officerIdCount = @($officerIdsValue).Count
}
if ([string]::IsNullOrWhiteSpace($team) -and $unitIdCount -le 0 -and $officerIdCount -le 0) {
Fail "Scenario $ScenarioId unit_reaches_tile event needs team, unit_ids, or officer_ids."
}
}
function Check-Unit-Defeated-Event-When($When, [string]$ScenarioId, $KnownUnitIds) {
$unitIdsValue = $null
if (Has-Prop $When "unit_ids") {
$unitIdsValue = $When.unit_ids
if ($null -ne $unitIdsValue -and -not ($unitIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_defeated event unit_ids must be an array."
}
foreach ($unitIdValue in @($unitIdsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId unit_defeated event has empty unit id."
}
if ($null -ne $KnownUnitIds -and (-not $KnownUnitIds.Contains($unitId))) {
Fail "Scenario $ScenarioId unit_defeated event references unknown unit: $unitId"
}
}
}
$officerIdsValue = $null
if (Has-Prop $When "officer_ids") {
$officerIdsValue = $When.officer_ids
if ($null -ne $officerIdsValue -and -not ($officerIdsValue -is [System.Array])) {
Fail "Scenario $ScenarioId unit_defeated event officer_ids must be an array."
}
foreach ($officerIdValue in @($officerIdsValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId) -or (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId unit_defeated event references unknown officer: $officerId"
}
}
}
$team = [string](Get-Prop $When "team" "")
if (-not [string]::IsNullOrWhiteSpace($team) -and $team -ne "player" -and $team -ne "enemy") {
Fail "Scenario $ScenarioId unit_defeated event has unknown team: $team"
}
$unitIdCount = 0
if ($null -ne $unitIdsValue) {
$unitIdCount = @($unitIdsValue).Count
}
$officerIdCount = 0
if ($null -ne $officerIdsValue) {
$officerIdCount = @($officerIdsValue).Count
}
if ([string]::IsNullOrWhiteSpace($team) -and $unitIdCount -le 0 -and $officerIdCount -le 0) {
Fail "Scenario $ScenarioId unit_defeated event needs team, unit_ids, or officer_ids."
}
}
function Resolve-Class-Id($Deployment, [string]$ScenarioId) {
$classId = [string](Get-Prop $Deployment "class_id" "")
if (-not [string]::IsNullOrWhiteSpace($classId)) {
return $classId
}
$officerId = [string](Get-Prop $Deployment "officer_id" "")
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $($Deployment.unit_id) has no class_id or officer_id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId deployment $($Deployment.unit_id) references unknown officer: $officerId"
}
return [string]$officers.$officerId.class_id
}
function Check-Deployment($Deployment, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $SeenIds) {
if (-not (Has-Prop $Deployment "unit_id")) {
Fail "Scenario $ScenarioId has a deployment without unit_id."
}
$unitId = [string]$Deployment.unit_id
if ($SeenIds.Contains($unitId)) {
Fail "Scenario $ScenarioId has duplicate deployment id: $unitId"
}
$SeenIds.Add($unitId) | Out-Null
$team = [string](Get-Prop $Deployment "team" "enemy")
$officerId = [string](Get-Prop $Deployment "officer_id" "")
if (-not [string]::IsNullOrWhiteSpace($officerId) -and (-not $officerIds.Contains($officerId))) {
Fail "Scenario $ScenarioId deployment $unitId references unknown officer: $officerId"
}
if (Has-Prop $Deployment "portrait") {
Check-Portrait-Path (Get-Prop $Deployment "portrait" "") "Scenario $ScenarioId deployment $unitId portrait"
}
if (Has-Prop $Deployment "sprite") {
Check-Portrait-Path (Get-Prop $Deployment "sprite" "") "Scenario $ScenarioId deployment $unitId sprite"
}
if (Has-Prop $Deployment "requires_joined") {
if (-not ((Get-Prop $Deployment "requires_joined" $false) -is [bool])) {
Fail "Scenario $ScenarioId deployment $unitId requires_joined must be boolean."
}
if ([bool](Get-Prop $Deployment "requires_joined" $false)) {
if ($team -ne "player") {
Fail "Scenario $ScenarioId deployment $unitId requires_joined is only valid for player deployments."
}
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $unitId requires_joined needs officer_id."
}
}
}
if ((Has-Prop $Deployment "controllable") -and (-not ((Get-Prop $Deployment "controllable" $true) -is [bool]))) {
Fail "Scenario $ScenarioId deployment $unitId controllable must be boolean."
}
if ((Has-Prop $Deployment "persist_progression") -and (-not ((Get-Prop $Deployment "persist_progression" $true) -is [bool]))) {
Fail "Scenario $ScenarioId deployment $unitId persist_progression must be boolean."
}
if (Has-Prop $Deployment "ai_target_priority") {
$priorityValue = Get-Prop $Deployment "ai_target_priority" 0
if (-not ($priorityValue -is [int] -or $priorityValue -is [long])) {
Fail "Scenario $ScenarioId deployment $unitId ai_target_priority must be an integer."
}
$priority = [int]$priorityValue
if ($priority -lt 0 -or $priority -gt 20) {
Fail "Scenario $ScenarioId deployment $unitId ai_target_priority must be between 0 and 20."
}
}
$aiBehavior = [string](Get-Prop $Deployment "ai_behavior" "")
if (-not [string]::IsNullOrWhiteSpace($aiBehavior)) {
if ($aiBehavior -notin @("guard", "sentry")) {
Fail "Scenario $ScenarioId deployment $unitId has unknown ai_behavior: $aiBehavior"
}
if ($team -ne "enemy") {
Fail "Scenario $ScenarioId deployment $unitId ai_behavior $aiBehavior is only valid for enemy deployments."
}
}
if ((Has-Prop $Deployment "ai_guard_anchor") -and $aiBehavior -ne "guard") {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_anchor needs ai_behavior guard."
}
if ((Has-Prop $Deployment "ai_guard_radius") -and $aiBehavior -ne "guard") {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_radius needs ai_behavior guard."
}
if ((Has-Prop $Deployment "ai_sentry_anchor") -and $aiBehavior -ne "sentry") {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_anchor needs ai_behavior sentry."
}
if ((Has-Prop $Deployment "ai_sentry_radius") -and $aiBehavior -ne "sentry") {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_radius needs ai_behavior sentry."
}
if ((Has-Prop $Deployment "ai_sentry_activation_turn") -and $aiBehavior -ne "sentry") {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_activation_turn needs ai_behavior sentry."
}
if (Has-Prop $Deployment "ai_guard_radius") {
$guardRadiusValue = Get-Prop $Deployment "ai_guard_radius" 0
if (-not ($guardRadiusValue -is [int] -or $guardRadiusValue -is [long])) {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_radius must be an integer."
}
$guardRadius = [int]$guardRadiusValue
if ($guardRadius -lt 0 -or $guardRadius -gt 12) {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_radius must be between 0 and 12."
}
}
if (Has-Prop $Deployment "ai_sentry_radius") {
$sentryRadiusValue = Get-Prop $Deployment "ai_sentry_radius" 0
if (-not ($sentryRadiusValue -is [int] -or $sentryRadiusValue -is [long])) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_radius must be an integer."
}
$sentryRadius = [int]$sentryRadiusValue
if ($sentryRadius -lt 0 -or $sentryRadius -gt 12) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_radius must be between 0 and 12."
}
}
if (Has-Prop $Deployment "ai_sentry_activation_turn") {
$sentryActivationTurnValue = Get-Prop $Deployment "ai_sentry_activation_turn" 0
if (-not ($sentryActivationTurnValue -is [int] -or $sentryActivationTurnValue -is [long])) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_activation_turn must be an integer."
}
$sentryActivationTurn = [int]$sentryActivationTurnValue
if ($sentryActivationTurn -lt 0 -or $sentryActivationTurn -gt 99) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_activation_turn must be between 0 and 99."
}
}
$classId = Resolve-Class-Id $Deployment $ScenarioId
if (-not $classIds.Contains($classId)) {
Fail "Scenario $ScenarioId deployment $unitId references unknown class: $classId"
}
$classDef = $classes.$classId
Check-Skill-Refs (Get-Prop $Deployment "skills" @()) "Scenario $ScenarioId deployment $unitId"
Check-Equipment-Loadout (Get-Prop $Deployment "equipment" $null) $classDef "Scenario $ScenarioId deployment $unitId"
$pos = Get-Prop $Deployment "pos" @()
if ($pos.Count -lt 2) {
Fail "Scenario $ScenarioId deployment $unitId has malformed pos."
}
$x = [int]$pos[0]
$y = [int]$pos[1]
if ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height) {
Fail "Scenario $ScenarioId deployment $unitId is outside map at [$x,$y]."
}
$terrainKey = Terrain-At $Rows $x $y
$moveType = [string](Get-Prop $Deployment "move_type" (Get-Prop $classDef "move_type" "foot"))
$moveCost = $terrain.$terrainKey.move_cost.$moveType
if ($null -eq $moveCost) {
$moveCost = $terrain.$terrainKey.move_cost.foot
}
if ([int]$moveCost -ge 99) {
Fail "Scenario $ScenarioId deployment $unitId starts on impassable terrain."
}
if (Has-Prop $Deployment "ai_guard_anchor") {
$anchor = Get-Prop $Deployment "ai_guard_anchor" @()
if ($anchor.Count -lt 2) {
Fail "Scenario $ScenarioId deployment $unitId has malformed ai_guard_anchor."
}
$anchorX = [int]$anchor[0]
$anchorY = [int]$anchor[1]
if ($anchorX -lt 0 -or $anchorY -lt 0 -or $anchorX -ge $Width -or $anchorY -ge $Height) {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_anchor is outside map at [$anchorX,$anchorY]."
}
$anchorTerrainKey = Terrain-At $Rows $anchorX $anchorY
$anchorMoveCost = $terrain.$anchorTerrainKey.move_cost.$moveType
if ($null -eq $anchorMoveCost) {
$anchorMoveCost = $terrain.$anchorTerrainKey.move_cost.foot
}
if ([int]$anchorMoveCost -ge 99) {
Fail "Scenario $ScenarioId deployment $unitId ai_guard_anchor is on impassable terrain."
}
}
if (Has-Prop $Deployment "ai_sentry_anchor") {
$sentryAnchor = Get-Prop $Deployment "ai_sentry_anchor" @()
if ($sentryAnchor.Count -lt 2) {
Fail "Scenario $ScenarioId deployment $unitId has malformed ai_sentry_anchor."
}
$sentryAnchorX = [int]$sentryAnchor[0]
$sentryAnchorY = [int]$sentryAnchor[1]
if ($sentryAnchorX -lt 0 -or $sentryAnchorY -lt 0 -or $sentryAnchorX -ge $Width -or $sentryAnchorY -ge $Height) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_anchor is outside map at [$sentryAnchorX,$sentryAnchorY]."
}
$sentryAnchorTerrainKey = Terrain-At $Rows $sentryAnchorX $sentryAnchorY
$sentryAnchorMoveCost = $terrain.$sentryAnchorTerrainKey.move_cost.$moveType
if ($null -eq $sentryAnchorMoveCost) {
$sentryAnchorMoveCost = $terrain.$sentryAnchorTerrainKey.move_cost.foot
}
if ([int]$sentryAnchorMoveCost -ge 99) {
Fail "Scenario $ScenarioId deployment $unitId ai_sentry_anchor is on impassable terrain."
}
}
}
function Add-Deployment-Unit-Id($Deployment, $Result) {
$unitId = [string](Get-Prop $Deployment "unit_id" (Get-Prop $Deployment "id" ""))
if (-not [string]::IsNullOrWhiteSpace($unitId)) {
$Result.Add($unitId) | Out-Null
}
}
function Collect-Scenario-Unit-Ids($ScenarioData) {
$result = New-Object System.Collections.Generic.HashSet[string]
foreach ($deployment in (Get-Prop $ScenarioData "deployments" @())) {
Add-Deployment-Unit-Id $deployment $result
}
foreach ($event in (Get-Prop $ScenarioData "events" @())) {
foreach ($action in (Get-Prop $event "actions" @())) {
$actionType = [string](Get-Prop $action "type" "")
if ($actionType -eq "spawn_deployment") {
Add-Deployment-Unit-Id $action.deployment $result
}
if ($actionType -eq "spawn_deployments") {
foreach ($deployment in (Get-Prop $action "deployments" @())) {
Add-Deployment-Unit-Id $deployment $result
}
}
}
}
return $result
}
function Find-Item-Def([string]$ItemId) {
$prop = $items.PSObject.Properties | Where-Object { $_.Name -eq $ItemId } | Select-Object -First 1
if ($null -eq $prop) {
return $null
}
return $prop.Value
}
function Resolve-Event-Grant-Item-Id($Entry, [string]$Context) {
if ($Entry -is [string]) {
return [string]$Entry
}
if ($null -eq $Entry -or $Entry -is [System.Array]) {
Fail "$Context has malformed item grant."
}
return [string](Get-Prop $Entry "item_id" (Get-Prop $Entry "id" ""))
}
function Check-Event-Grant-Item($Entry, [string]$Context) {
$itemId = Resolve-Event-Grant-Item-Id $Entry $Context
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "$Context has empty item id."
}
if (-not $itemIds.Contains($itemId)) {
Fail "$Context references unknown item: $itemId"
}
if (-not ($Entry -is [string]) -and (Has-Prop $Entry "count")) {
$count = [int](Get-Prop $Entry "count" 0)
if ($count -le 0) {
Fail "$Context has invalid count for item $itemId."
}
}
}
function Check-Event-Grant-Gold($Action, [string]$Context) {
if (-not (Has-Prop $Action "amount")) {
Fail "$Context must set amount."
}
$amountValue = Get-Prop $Action "amount" 0
if (-not ($amountValue -is [int] -or $amountValue -is [long])) {
Fail "$Context amount must be an integer."
}
if ([int]$amountValue -le 0) {
Fail "$Context amount must be positive."
}
if ((Has-Prop $Action "text") -and [string]::IsNullOrWhiteSpace([string](Get-Prop $Action "text" ""))) {
Fail "$Context text must not be empty."
}
}
function Check-Event-Withdraw-Unit-Id([string]$UnitId, $KnownUnitIds, [string]$Context) {
if ([string]::IsNullOrWhiteSpace($UnitId)) {
Fail "$Context has empty unit id."
}
if (-not $KnownUnitIds.Contains($UnitId)) {
Fail "$Context references unknown unit: $UnitId"
}
}
function Check-Event-Withdraw-Units($UnitIds, $KnownUnitIds, [string]$Context) {
if ($null -eq $UnitIds -or -not ($UnitIds -is [System.Array])) {
Fail "$Context unit_ids must be an array."
}
if (@($UnitIds).Count -le 0) {
Fail "$Context unit_ids must not be empty."
}
$seenWithdrawals = New-Object System.Collections.Generic.HashSet[string]
foreach ($unitIdValue in @($UnitIds)) {
$unitId = [string]$unitIdValue
Check-Event-Withdraw-Unit-Id $unitId $KnownUnitIds $Context
if (-not $seenWithdrawals.Add($unitId)) {
Fail "$Context has duplicate unit: $unitId"
}
}
}
function Check-Event-Unit-Ids($Action, $KnownUnitIds, [string]$Context) {
$unitIds = @()
if (Has-Prop $Action "unit_ids") {
if (-not ($Action.unit_ids -is [System.Array])) {
Fail "$Context unit_ids must be an array."
}
$unitIds = @($Action.unit_ids)
} else {
$unitId = [string](Get-Prop $Action "unit_id" (Get-Prop $Action "id" ""))
if (-not [string]::IsNullOrWhiteSpace($unitId)) {
$unitIds = @($unitId)
}
}
if ($unitIds.Count -le 0) {
Fail "$Context needs unit_id or unit_ids."
}
$seenUnitIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($unitIdValue in $unitIds) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "$Context has empty unit id."
}
if (-not $KnownUnitIds.Contains($unitId)) {
Fail "$Context references unknown unit: $unitId"
}
if (-not $seenUnitIds.Add($unitId)) {
Fail "$Context has duplicate unit: $unitId"
}
}
}
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."
}
$priorityValue = Get-Prop $Action "priority" 0
if (-not ($priorityValue -is [int] -or $priorityValue -is [long])) {
Fail "$Context priority must be an integer."
}
$priority = [int]$priorityValue
if ($priority -lt 0 -or $priority -gt 20) {
Fail "$Context priority must be between 0 and 20."
}
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-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() {
$weaponTypes = @()
$armorTypes = @()
$accessoryTypes = @()
foreach ($itemProp in $items.PSObject.Properties) {
$item = $itemProp.Value
$kind = [string](Get-Prop $item "kind" "")
if ($kind -eq "weapon") {
$weaponTypes += [string](Get-Prop $item "weapon_type" "")
} elseif ($kind -eq "armor") {
$armorTypes += [string](Get-Prop $item "armor_type" "")
} elseif ($kind -eq "accessory") {
$accessoryTypes += [string](Get-Prop $item "accessory_type" "accessory")
}
}
foreach ($classProp in $classes.PSObject.Properties) {
$classId = $classProp.Name
$slots = Get-Prop $classProp.Value "equipment_slots" $null
if ($null -eq $slots) {
continue
}
foreach ($weaponType in (Get-Prop $slots "weapon" @())) {
if (-not $weaponTypes.Contains([string]$weaponType)) {
Fail "Class $classId allows unknown weapon type: $weaponType"
}
}
foreach ($armorType in (Get-Prop $slots "armor" @())) {
if (-not $armorTypes.Contains([string]$armorType)) {
Fail "Class $classId allows unknown armor type: $armorType"
}
}
foreach ($accessoryType in (Get-Prop $slots "accessory" @())) {
if (-not $accessoryTypes.Contains([string]$accessoryType)) {
Fail "Class $classId allows unknown accessory type: $accessoryType"
}
}
}
}
function Check-Class-Promotions() {
foreach ($classProp in $classes.PSObject.Properties) {
$classId = [string]$classProp.Name
$promotion = Get-Prop $classProp.Value "promotion" $null
if ($null -eq $promotion) {
continue
}
if ($promotion -is [string] -or $promotion -is [System.Array]) {
Fail "Class $classId promotion must be an object."
}
$level = [int](Get-Prop $promotion "level" 0)
if ($level -le 1) {
Fail "Class $classId promotion has invalid level."
}
$targetClassId = [string](Get-Prop $promotion "to" "")
if ([string]::IsNullOrWhiteSpace($targetClassId) -or (-not $classIds.Contains($targetClassId))) {
Fail "Class $classId promotes to unknown class: $targetClassId"
}
if ($targetClassId -eq $classId) {
Fail "Class $classId cannot promote to itself."
}
$targetClass = Get-Prop $classes $targetClassId $null
$currentTier = [int](Get-Prop $classProp.Value "tier" 1)
$targetTier = [int](Get-Prop $targetClass "tier" $currentTier)
if ($targetTier -le $currentTier) {
Fail "Class $classId promotion target must have higher tier: $targetClassId"
}
$sourceSlots = Get-Prop $classProp.Value "equipment_slots" $null
$targetSlots = Get-Prop $targetClass "equipment_slots" $null
foreach ($slot in @("weapon", "armor", "accessory")) {
$sourceAllowed = @()
if ($null -ne $sourceSlots) {
$sourceAllowed = @(Get-Prop $sourceSlots $slot @())
}
$targetAllowed = @()
if ($null -ne $targetSlots) {
$targetAllowed = @(Get-Prop $targetSlots $slot @())
}
foreach ($allowed in $sourceAllowed) {
if (-not $targetAllowed.Contains([string]$allowed)) {
Fail "Class $classId promotion target $targetClassId must keep $slot type: $allowed"
}
}
}
}
}
function Check-Equipment-Loadout($Equipment, $ClassDef, [string]$Context) {
$slots = Get-Prop $classDef "equipment_slots" $null
if ($null -eq $Equipment) {
return
}
if ($Equipment -is [string] -or $Equipment -is [System.Array]) {
Fail "$Context equipment must be an object."
}
foreach ($slot in $Equipment.PSObject.Properties) {
$slotName = [string]$slot.Name
$itemId = [string]$slot.Value
if ([string]::IsNullOrWhiteSpace($itemId)) {
continue
}
if (-not @("weapon", "armor", "accessory").Contains($slotName)) {
Fail "$Context has unknown equipment slot: $slotName"
}
$item = Find-Item-Def $itemId
if ($null -eq $item) {
Fail "$Context references unknown equipment item: $itemId"
}
$kind = [string](Get-Prop $item "kind" "")
if ($slotName -eq "weapon") {
if ($kind -ne "weapon") {
Fail "$Context has non-weapon item $itemId in weapon slot."
}
$weaponType = [string](Get-Prop $item "weapon_type" "")
if ($null -ne $slots -and (-not @((Get-Prop $slots "weapon" @())).Contains($weaponType))) {
Fail "$Context cannot equip weapon type $weaponType."
}
} elseif ($slotName -eq "armor") {
if ($kind -ne "armor") {
Fail "$Context has non-armor item $itemId in armor slot."
}
$armorType = [string](Get-Prop $item "armor_type" "")
if ($null -ne $slots -and (-not @((Get-Prop $slots "armor" @())).Contains($armorType))) {
Fail "$Context cannot equip armor type $armorType."
}
} elseif ($slotName -eq "accessory") {
if ($kind -ne "accessory") {
Fail "$Context has non-accessory item $itemId in accessory slot."
}
$accessoryType = [string](Get-Prop $item "accessory_type" "accessory")
if ($null -ne $slots -and (-not @((Get-Prop $slots "accessory" @())).Contains($accessoryType))) {
Fail "$Context cannot equip accessory type $accessoryType."
}
}
}
}
function Check-Officer-Equipment([string]$OfficerId, $Officer) {
$classId = [string](Get-Prop $Officer "class_id" "")
if ([string]::IsNullOrWhiteSpace($classId) -or (-not $classIds.Contains($classId))) {
Fail "Officer $OfficerId references unknown class: $classId"
}
Check-Equipment-Loadout (Get-Prop $Officer "equipment" $null) $classes.$classId "Officer $OfficerId"
}
function Check-Item-Effects() {
foreach ($itemProp in $items.PSObject.Properties) {
$itemId = $itemProp.Name
$item = $itemProp.Value
$kind = [string](Get-Prop $item "kind" "")
if (Has-Prop $item "icon") {
$iconPath = [string](Get-Prop $item "icon" "")
Check-Portrait-Path $iconPath "Item $itemId icon"
Check-Item-Icon-Cutout $iconPath "Item $itemId icon"
}
if (-not $validItemKinds.Contains($kind)) {
Fail "Item $itemId has unknown kind: $kind"
}
if ([int](Get-Prop $item "price" 0) -lt 0) {
Fail "Item $itemId has negative price."
}
if (Has-Prop $item "rarity") {
$rarity = [string](Get-Prop $item "rarity" "")
if (-not $validItemRarities.Contains($rarity)) {
Fail "Item $itemId has unknown rarity: $rarity"
}
}
if ($kind -eq "weapon") {
if (-not (Has-Prop $item "weapon_type")) {
Fail "Weapon item $itemId is missing weapon_type."
}
Check-Skill-Range (Get-Prop $item "range" 1) "Item $itemId"
$hasEffectiveTypes = Has-Prop $item "effective_vs_move_types"
$hasEffectiveBonus = Has-Prop $item "effective_bonus_damage"
if ($hasEffectiveTypes -or $hasEffectiveBonus) {
if (-not $hasEffectiveTypes -or -not $hasEffectiveBonus) {
Fail "Weapon item $itemId effectiveness requires effective_vs_move_types and effective_bonus_damage."
}
$effectiveTypes = @((Get-Prop $item "effective_vs_move_types" @()))
if ($effectiveTypes.Count -eq 0) {
Fail "Weapon item $itemId effective_vs_move_types must be a non-empty array."
}
$seenEffectiveTypes = New-Object System.Collections.Generic.HashSet[string]
foreach ($moveTypeValue in $effectiveTypes) {
$moveType = [string]$moveTypeValue
if ([string]::IsNullOrWhiteSpace($moveType) -or -not ($moveType -match '^[a-z0-9_]+$')) {
Fail "Weapon item $itemId has unstable effective move type: $moveType"
}
if (-not $validMoveTypes.Contains($moveType)) {
Fail "Weapon item $itemId references unknown effective move type: $moveType"
}
if (-not $seenEffectiveTypes.Add($moveType)) {
Fail "Weapon item $itemId duplicates effective move type: $moveType"
}
}
$effectiveBonus = Get-Prop $item "effective_bonus_damage" 0
if (-not ($effectiveBonus -is [int] -or $effectiveBonus -is [long])) {
Fail "Weapon item $itemId effective_bonus_damage must be an integer."
}
$effectiveBonusValue = [int]$effectiveBonus
if ($effectiveBonusValue -lt 1 -or $effectiveBonusValue -gt 5) {
Fail "Weapon item $itemId effective_bonus_damage must be between 1 and 5."
}
}
}
if ($kind -ne "weapon") {
if (Has-Prop $item "effective_vs_move_types") {
Fail "Item $itemId has effectiveness move types but is not a weapon."
}
if (Has-Prop $item "effective_bonus_damage") {
Fail "Item $itemId has effectiveness bonus but is not a weapon."
}
}
if ($kind -eq "armor" -and (-not (Has-Prop $item "armor_type"))) {
Fail "Armor item $itemId is missing armor_type."
}
if ($kind -eq "accessory") {
$accessoryType = [string](Get-Prop $item "accessory_type" "")
if ([string]::IsNullOrWhiteSpace($accessoryType)) {
Fail "Accessory item $itemId is missing accessory_type."
}
}
if ($kind -eq "consumable") {
if ([int](Get-Prop $item "uses" 0) -le 0) {
Fail "Consumable item $itemId must have positive uses."
}
$target = [string](Get-Prop $item "target" "ally")
if (-not $validSkillTargets.Contains($target)) {
Fail "Consumable item $itemId has unknown target: $target"
}
Check-Skill-Range (Get-Prop $item "range" @(0, 1)) "Item $itemId"
}
$bonuses = Get-Prop $item "bonuses" $null
if ($null -ne $bonuses) {
foreach ($bonus in $bonuses.PSObject.Properties) {
if (-not $validBonusStats.Contains([string]$bonus.Name)) {
Fail "Item $itemId has unsupported bonus stat: $($bonus.Name)"
}
}
}
foreach ($effect in (Get-Prop $item "effects" @())) {
if (-not (Has-Prop $effect "type")) {
Fail "Item $itemId has malformed effect."
}
if (-not $validEffects.Contains([string]$effect.type)) {
Fail "Item $itemId has unknown effect: $($effect.type)"
}
$effectType = [string]$effect.type
if ($effectType -eq "heal_hp" -or $effectType -eq "heal_mp") {
if ([int](Get-Prop $effect "amount" 0) -le 0) {
Fail "Item $itemId effect $effectType must have positive amount."
}
} elseif ($effectType -eq "cure_status") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Item $itemId cure_status effect must have a stable status."
}
}
}
}
}
function Check-Terrain-Definitions() {
foreach ($terrainProp in $terrain.PSObject.Properties) {
$terrainKey = [string]$terrainProp.Name
$terrainDef = $terrainProp.Value
$defense = [int](Get-Prop $terrainDef "defense" 0)
if ($defense -lt 0) {
Fail "Terrain $terrainKey has negative defense."
}
$avoid = [int](Get-Prop $terrainDef "avoid" 0)
if ($avoid -lt 0 -or $avoid -gt 100) {
Fail "Terrain $terrainKey avoid must be between 0 and 100."
}
$heal = [int](Get-Prop $terrainDef "heal" 0)
if ($heal -lt 0 -or $heal -gt 99) {
Fail "Terrain $terrainKey heal must be between 0 and 99."
}
}
}
function Check-Skill-Range($Value, [string]$Context) {
if ($Value -is [System.Array]) {
if ($Value.Count -lt 1 -or $Value.Count -gt 2) {
Fail "$Context has malformed range."
}
$minRange = [int]$Value[0]
$maxRange = [int]$Value[$Value.Count - 1]
if ($minRange -lt 0 -or $maxRange -lt $minRange) {
Fail "$Context has invalid range."
}
return
}
$range = [int]$Value
if ($range -lt 0) {
Fail "$Context has invalid range."
}
}
function Check-Skill-Definitions() {
foreach ($skillProp in $skills.PSObject.Properties) {
$skillId = $skillProp.Name
$skill = $skillProp.Value
$kind = [string](Get-Prop $skill "kind" "")
if (-not $validSkillKinds.Contains($kind)) {
Fail "Skill $skillId has unknown kind: $kind"
}
$target = [string](Get-Prop $skill "target" "")
if (-not $validSkillTargets.Contains($target)) {
Fail "Skill $skillId has unknown target: $target"
}
Check-Skill-Range (Get-Prop $skill "range" 1) "Skill $skillId"
$areaShape = [string](Get-Prop $skill "area_shape" "single")
if (-not $validSkillAreaShapes.Contains($areaShape)) {
Fail "Skill $skillId has unsupported area_shape: $areaShape"
}
$areaRadius = [int](Get-Prop $skill "area_radius" 0)
if ($areaRadius -lt 0) {
Fail "Skill $skillId has invalid area_radius."
}
if ($areaShape -eq "single" -and $areaRadius -gt 0) {
Fail "Skill $skillId area_radius requires a non-single area_shape."
}
if ($areaShape -ne "single" -and $areaRadius -le 0) {
Fail "Skill $skillId area_shape requires a positive area_radius."
}
if ([int](Get-Prop $skill "mp_cost" 0) -lt 0) {
Fail "Skill $skillId has negative mp_cost."
}
if ([int](Get-Prop $skill "power" 0) -lt 0) {
Fail "Skill $skillId has negative power."
}
$stat = [string](Get-Prop $skill "stat" "int")
if (-not $validSkillStats.Contains($stat)) {
Fail "Skill $skillId references unsupported stat: $stat"
}
$effects = Get-Prop $skill "effects" @()
if ($kind -eq "support" -and @($effects).Count -eq 0) {
Fail "Support skill $skillId must define at least one effect."
}
foreach ($effect in @($effects)) {
if ($effect -isnot [pscustomobject]) {
Fail "Skill $skillId has malformed effect."
continue
}
$effectType = [string](Get-Prop $effect "type" "")
if (-not $validSkillEffectTypes.Contains($effectType)) {
Fail "Skill $skillId has unknown effect: $effectType"
}
if ($effectType -eq "stat_bonus") {
$effectStat = [string](Get-Prop $effect "stat" "")
if (-not $validStatusStats.Contains($effectStat)) {
Fail "Skill $skillId effect references unsupported stat: $effectStat"
}
if ([int](Get-Prop $effect "amount" 0) -eq 0) {
Fail "Skill $skillId stat_bonus effect must have non-zero amount."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId stat_bonus effect must have positive duration_turns."
}
} elseif ($effectType -eq "damage_over_time") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Skill $skillId damage_over_time effect must have a stable status."
}
if ([int](Get-Prop $effect "amount" 0) -le 0) {
Fail "Skill $skillId damage_over_time effect must have positive amount."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId damage_over_time effect must have positive duration_turns."
}
} elseif ($effectType -eq "action_lock") {
$status = [string](Get-Prop $effect "status" "")
if ([string]::IsNullOrWhiteSpace($status) -or -not ($status -match '^[a-z0-9_]+$')) {
Fail "Skill $skillId action_lock effect must have a stable status."
}
$action = [string](Get-Prop $effect "action" "")
if ($action -ne "skill" -and $action -ne "move" -and $action -ne "attack") {
Fail "Skill $skillId action_lock effect currently supports only action=skill, action=move, or action=attack."
}
if ([int](Get-Prop $effect "duration_turns" 1) -le 0) {
Fail "Skill $skillId action_lock effect must have positive duration_turns."
}
}
}
}
}
function Check-Skill-Refs($Value, [string]$Context) {
if ($null -eq $Value) {
return
}
if ($Value -is [pscustomobject]) {
Fail "$Context skills must be an array."
}
foreach ($skillId in @($Value)) {
if (-not $skillIds.Contains([string]$skillId)) {
Fail "$Context references unknown skill: $skillId"
}
}
}
function Resolve-Shop-Item-Id($Entry, [string]$Context) {
if ($Entry -is [string]) {
return [string]$Entry
}
if (Has-Prop $Entry "id") {
return [string]$Entry.id
}
Fail "$Context has a malformed shop item entry."
}
function Check-Shop-Stock-Value($Value, [string]$Context) {
$isInteger = (
$Value -is [byte] -or
$Value -is [int16] -or
$Value -is [int] -or
$Value -is [long]
)
if (-not $isInteger -or [long]$Value -lt 0) {
Fail "$Context stock must be a non-negative integer."
}
}
function Check-Shop-Entry-Stock($Entry, [string]$ItemId, [string]$Context) {
if (-not (Has-Prop $Entry "stock")) {
return
}
Check-Shop-Stock-Value (Get-Prop $Entry "stock" $null) "$Context item $ItemId"
}
function Check-Shop-Stock-Block($Stock, $AllowedItemIds, [string]$Context) {
if ($null -eq $Stock -or $Stock -is [System.Array] -or $Stock -is [string] -or $Stock -is [ValueType]) {
Fail "$Context stock must be an object."
}
foreach ($prop in @($Stock.PSObject.Properties)) {
$itemId = [string]$prop.Name
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "$Context stock has an empty item id."
}
if (-not $AllowedItemIds.Contains($itemId)) {
Fail "$Context stock references item outside that shop branch: $itemId"
}
Check-Shop-Stock-Value $prop.Value "$Context stock $itemId"
}
}
function Check-Shop($Shop, [string]$ScenarioId) {
if ($null -eq $Shop) {
return
}
if (($Shop -is [System.Array]) -or ($Shop -is [string])) {
Fail "Scenario $ScenarioId shop must be an object."
}
if (-not (Has-Prop $Shop "items")) {
Fail "Scenario $ScenarioId shop is missing items."
}
$shopItemsValue = $Shop.items
if (-not ($shopItemsValue -is [System.Array])) {
Fail "Scenario $ScenarioId shop items must be an array."
}
$shopItems = @($shopItemsValue)
$seenShopItems = New-Object System.Collections.Generic.HashSet[string]
foreach ($entry in $shopItems) {
$itemId = Resolve-Shop-Item-Id $entry "Scenario $ScenarioId shop"
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "Scenario $ScenarioId shop has an empty item id."
}
if (-not $seenShopItems.Add($itemId)) {
Fail "Scenario $ScenarioId shop has duplicate item: $itemId"
}
if (-not $itemIds.Contains($itemId)) {
Fail "Scenario $ScenarioId shop references unknown item: $itemId"
}
Check-Shop-Entry-Stock $entry $itemId "Scenario $ScenarioId shop"
$item = Find-Item-Def $itemId
if ([int](Get-Prop $item "price" 0) -le 0) {
Fail "Scenario $ScenarioId shop item $itemId has no positive price."
}
}
if (Has-Prop $Shop "stock") {
Check-Shop-Stock-Block $Shop.stock $seenShopItems "Scenario $ScenarioId shop"
}
if (Has-Prop $Shop "merchant") {
$merchant = Get-Prop $Shop "merchant" $null
if ($null -eq $merchant -or $merchant -is [string] -or $merchant -is [System.Array]) {
Fail "Scenario $ScenarioId shop merchant must be an object."
}
$merchantName = [string](Get-Prop $merchant "name" "Merchant")
if ([string]::IsNullOrWhiteSpace($merchantName)) {
Fail "Scenario $ScenarioId shop merchant has an empty name."
}
Check-Readable-Korean-Text $merchantName "Scenario $ScenarioId shop merchant name"
$merchantLines = Get-Prop $merchant "lines" $null
if ($null -eq $merchantLines -or -not ($merchantLines -is [System.Array])) {
Fail "Scenario $ScenarioId shop merchant must have line array."
}
foreach ($merchantLine in @($merchantLines)) {
if ([string]::IsNullOrWhiteSpace([string]$merchantLine)) {
Fail "Scenario $ScenarioId shop merchant has an empty line."
}
Check-Readable-Korean-Text ([string]$merchantLine) "Scenario $ScenarioId shop merchant line"
}
}
$conditionalItems = Get-Prop $Shop "conditional_items" @()
foreach ($block in @($conditionalItems)) {
if ($block -is [string] -or $block -is [System.Array]) {
Fail "Scenario $ScenarioId shop has malformed conditional_items entry."
}
$blockFlags = Get-Prop $block "campaign_flags" $null
if ($null -eq $blockFlags -or @($blockFlags.PSObject.Properties).Count -le 0) {
Fail "Scenario $ScenarioId shop conditional_items entry must have campaign_flags."
}
Check-Required-Flags $blockFlags $ScenarioId "shop conditional_items"
$blockItems = Get-Prop $block "items" $null
if ($null -eq $blockItems) {
Fail "Scenario $ScenarioId shop conditional_items entry must have item array."
}
$seenConditionalItems = New-Object System.Collections.Generic.HashSet[string]
foreach ($baseItem in $shopItems) {
$seenConditionalItems.Add((Resolve-Shop-Item-Id $baseItem "Scenario $ScenarioId shop")) | Out-Null
}
foreach ($entry in @($blockItems)) {
$itemId = Resolve-Shop-Item-Id $entry "Scenario $ScenarioId shop conditional_items"
if ([string]::IsNullOrWhiteSpace($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items has an empty item id."
}
if (-not $seenConditionalItems.Add($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items duplicates item already in that branch: $itemId"
}
if (-not $itemIds.Contains($itemId)) {
Fail "Scenario $ScenarioId shop conditional_items references unknown item: $itemId"
}
Check-Shop-Entry-Stock $entry $itemId "Scenario $ScenarioId shop conditional_items"
$item = Find-Item-Def $itemId
if ([int](Get-Prop $item "price" 0) -le 0) {
Fail "Scenario $ScenarioId shop conditional item $itemId has no positive price."
}
}
if (Has-Prop $block "stock") {
Check-Shop-Stock-Block $block.stock $seenConditionalItems "Scenario $ScenarioId shop conditional_items"
}
}
}
function Resolve-Cell($Cell, [string]$Context) {
if (-not ($Cell -is [System.Array]) -or $Cell.Count -lt 2) {
Fail "$Context has a malformed cell."
}
return @{ X = [int]$Cell[0]; Y = [int]$Cell[1] }
}
function Cell-Key([int]$X, [int]$Y) {
return "$X,$Y"
}
function Check-Formation($Formation, [string]$ScenarioId, [int]$Width, [int]$Height, $Rows, $Deployments) {
if ($null -eq $Formation) {
return
}
if (($Formation -is [System.Array]) -or ($Formation -is [string])) {
Fail "Scenario $ScenarioId formation must be an object."
}
if (-not (Has-Prop $Formation "cells")) {
Fail "Scenario $ScenarioId formation is missing cells."
}
$cellsValue = $Formation.cells
if (-not ($cellsValue -is [System.Array])) {
Fail "Scenario $ScenarioId formation cells must be an array."
}
$formationKeys = New-Object System.Collections.Generic.HashSet[string]
foreach ($cellEntry in @($cellsValue)) {
$cell = Resolve-Cell $cellEntry "Scenario $ScenarioId formation"
$x = [int]$cell.X
$y = [int]$cell.Y
if ($x -lt 0 -or $y -lt 0 -or $x -ge $Width -or $y -ge $Height) {
Fail "Scenario $ScenarioId formation cell is outside map at [$x,$y]."
}
if (-not $formationKeys.Add((Cell-Key $x $y))) {
Fail "Scenario $ScenarioId formation has duplicate cell: [$x,$y]"
}
$terrainKey = Terrain-At $Rows $x $y
if ([int]$terrain.$terrainKey.move_cost.foot -ge 99 -and [int]$terrain.$terrainKey.move_cost.mounted -ge 99 -and [int]$terrain.$terrainKey.move_cost.archer -ge 99) {
Fail "Scenario $ScenarioId formation cell [$x,$y] is impassable for normal units."
}
}
$playerCount = 0
foreach ($deployment in $Deployments) {
$team = [string](Get-Prop $deployment "team" "enemy")
$pos = Get-Prop $deployment "pos" @()
if ($pos.Count -lt 2) {
continue
}
$x = [int]$pos[0]
$y = [int]$pos[1]
$key = Cell-Key $x $y
if ($team -eq "player") {
$playerCount += 1
if (-not $formationKeys.Contains($key)) {
Fail "Scenario $ScenarioId player deployment $($deployment.unit_id) starts outside formation cells."
}
} elseif ($formationKeys.Contains($key)) {
Fail "Scenario $ScenarioId formation overlaps enemy deployment at [$x,$y]."
}
}
if ($formationKeys.Count -lt $playerCount) {
Fail "Scenario $ScenarioId formation has fewer cells than player deployments."
}
}
function Check-Roster($Roster, [string]$ScenarioId, $Deployments) {
if ($null -eq $Roster) {
return
}
if (($Roster -is [System.Array]) -or ($Roster -is [string])) {
Fail "Scenario $ScenarioId roster must be an object."
}
$playerOfficerIds = New-Object System.Collections.Generic.HashSet[string]
$playerUnitIds = New-Object System.Collections.Generic.HashSet[string]
$officerToUnitIds = @{}
$playerCount = 0
foreach ($deployment in $Deployments) {
if ([string](Get-Prop $deployment "team" "enemy") -ne "player") {
continue
}
$playerCount += 1
$unitId = [string](Get-Prop $deployment "unit_id" (Get-Prop $deployment "id" ""))
if (-not [string]::IsNullOrWhiteSpace($unitId)) {
$playerUnitIds.Add($unitId) | Out-Null
}
$officerId = [string](Get-Prop $deployment "officer_id" "")
if (-not [string]::IsNullOrWhiteSpace($officerId)) {
$playerOfficerIds.Add($officerId) | Out-Null
if (-not $officerToUnitIds.ContainsKey($officerId)) {
$officerToUnitIds[$officerId] = @()
}
$officerToUnitIds[$officerId] += $unitId
}
}
if ($playerCount -le 0) {
Fail "Scenario $ScenarioId roster has no player deployments."
}
$maxUnits = [int](Get-Prop $Roster "max_units" $playerCount)
if ($maxUnits -le 0 -or $maxUnits -gt $playerCount) {
Fail "Scenario $ScenarioId roster max_units must be between 1 and player deployment count."
}
$requiredValue = @()
if (Has-Prop $Roster "required_officers") {
$requiredValue = $Roster.required_officers
}
if (-not ($requiredValue -is [System.Array])) {
Fail "Scenario $ScenarioId roster required_officers must be an array."
}
$required = New-Object System.Collections.Generic.HashSet[string]
$requiredDeploymentUnitIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($officerIdValue in @($requiredValue)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId roster has an empty required officer."
}
if (-not $required.Add($officerId)) {
Fail "Scenario $ScenarioId roster has duplicate required officer: $officerId"
}
if (-not $playerOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId roster requires officer not in player deployments: $officerId"
}
foreach ($unitId in @($officerToUnitIds[$officerId])) {
if (-not [string]::IsNullOrWhiteSpace([string]$unitId)) {
$requiredDeploymentUnitIds.Add([string]$unitId) | Out-Null
}
}
}
$requiredUnitsValue = @()
if (Has-Prop $Roster "required_units") {
$requiredUnitsValue = $Roster.required_units
}
if (-not ($requiredUnitsValue -is [System.Array])) {
Fail "Scenario $ScenarioId roster required_units must be an array."
}
$requiredUnits = New-Object System.Collections.Generic.HashSet[string]
foreach ($unitIdValue in @($requiredUnitsValue)) {
$unitId = [string]$unitIdValue
if ([string]::IsNullOrWhiteSpace($unitId)) {
Fail "Scenario $ScenarioId roster has an empty required unit."
}
if (-not $requiredUnits.Add($unitId)) {
Fail "Scenario $ScenarioId roster has duplicate required unit: $unitId"
}
if (-not $playerUnitIds.Contains($unitId)) {
Fail "Scenario $ScenarioId roster requires unit not in player deployments: $unitId"
}
$requiredDeploymentUnitIds.Add($unitId) | Out-Null
}
if ($requiredDeploymentUnitIds.Count -gt $maxUnits) {
Fail "Scenario $ScenarioId roster requires more units than max_units."
}
}
function Check-Joined-Deployment-Requirements($Deployments, [string]$ScenarioId) {
foreach ($deployment in $Deployments) {
if ([string](Get-Prop $deployment "team" "enemy") -ne "player") {
continue
}
if (-not [bool](Get-Prop $deployment "requires_joined" $false)) {
continue
}
$officerId = [string](Get-Prop $deployment "officer_id" "")
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId deployment $($deployment.unit_id) requires joined officer but has no officer_id."
}
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId deployment $($deployment.unit_id) requires officer before they join: $officerId"
}
}
}
function Check-Required-Officers-Joined($Roster, $Conditions, [string]$ScenarioId) {
if ($null -ne $Roster -and (Has-Prop $Roster "required_officers")) {
foreach ($officerIdValue in @($Roster.required_officers)) {
$officerId = [string]$officerIdValue
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId roster requires officer before they join: $officerId"
}
}
}
$conditionRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Conditions -and (Has-Prop $Conditions "defeat")) {
Collect-Required-Player-Officers $Conditions.defeat $conditionRequired
}
foreach ($officerId in $conditionRequired) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId defeat condition references officer before they join: $officerId"
}
}
}
function Collect-Required-Player-Officers($Condition, $Result) {
if ($Condition -is [System.Array]) {
foreach ($entry in $Condition) {
Collect-Required-Player-Officers $entry $Result
}
return
}
if (-not (Has-Prop $Condition "type")) {
return
}
$type = [string]$Condition.type
if ($type -eq "any_officer_defeated" -and [string](Get-Prop $Condition "team" "") -eq "player") {
foreach ($officerId in (Get-Prop $Condition "officer_ids" @())) {
$Result.Add([string]$officerId) | Out-Null
}
}
if (($type -eq "all" -or $type -eq "any") -and (Has-Prop $Condition "conditions")) {
Collect-Required-Player-Officers $Condition.conditions $Result
}
}
function Check-Roster-Condition-Consistency($Roster, $Conditions, [string]$ScenarioId) {
$conditionRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Conditions -and (Has-Prop $Conditions "defeat")) {
Collect-Required-Player-Officers $Conditions.defeat $conditionRequired
}
if ($conditionRequired.Count -le 0) {
return
}
$rosterRequired = New-Object System.Collections.Generic.HashSet[string]
if ($null -ne $Roster -and (Has-Prop $Roster "required_officers")) {
foreach ($officerIdValue in @($Roster.required_officers)) {
$rosterRequired.Add([string]$officerIdValue) | Out-Null
}
}
foreach ($officerId in $conditionRequired) {
if (-not $rosterRequired.Contains($officerId)) {
Fail "Scenario $ScenarioId defeat condition officer must be required in roster: $officerId"
}
}
}
function Check-Officer-Transition-List($Value, [string]$ScenarioId, [string]$Context) {
$result = New-Object System.Collections.Generic.HashSet[string]
if ($null -eq $Value) {
return ,$result
}
foreach ($officerIdValue in @($Value)) {
$officerId = [string]$officerIdValue
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId $Context has empty officer id."
}
if (-not $officerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId $Context references unknown officer: $officerId"
}
if (-not $result.Add($officerId)) {
Fail "Scenario $ScenarioId $Context has duplicate officer: $officerId"
}
}
return ,$result
}
function Check-Post-Battle-Choices($Choices, [string]$ScenarioId) {
if ($null -eq $Choices) {
return
}
if (-not ($Choices -is [System.Array])) {
Fail "Scenario $ScenarioId post_battle_choices must be an array."
}
$seenChoices = New-Object System.Collections.Generic.HashSet[string]
foreach ($choice in @($Choices)) {
if ($choice -is [string] -or $choice -is [System.Array]) {
Fail "Scenario $ScenarioId has malformed post-battle choice."
}
$choiceId = [string](Get-Prop $choice "id" "")
if ([string]::IsNullOrWhiteSpace($choiceId)) {
Fail "Scenario $ScenarioId post-battle choice has empty id."
}
if (-not ($choiceId -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId post-battle choice has unstable id: $choiceId"
}
if (-not $seenChoices.Add($choiceId)) {
Fail "Scenario $ScenarioId has duplicate post-battle choice: $choiceId"
}
if ([string]::IsNullOrWhiteSpace([string](Get-Prop $choice "label" ""))) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has empty label."
}
if ([int](Get-Prop $choice "gold" 0) -lt 0) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has negative gold."
}
$choiceItems = @()
if (Has-Prop $choice "items") {
$choiceItems = @($choice.items)
}
foreach ($itemId in @($choiceItems)) {
if (-not $itemIds.Contains([string]$itemId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId grants unknown item: $itemId"
}
}
$choiceJoins = Check-Officer-Transition-List (Get-Prop $choice "join_officers" $null) $ScenarioId "post-battle choice $choiceId join_officers"
$choiceLeaves = Check-Officer-Transition-List (Get-Prop $choice "leave_officers" $null) $ScenarioId "post-battle choice $choiceId leave_officers"
foreach ($officerId in $choiceJoins) {
if ($choiceLeaves.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId both joins and leaves officer: $officerId"
}
if ($joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId joins already joined officer: $officerId"
}
}
foreach ($officerId in $choiceLeaves) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId leaves officer before they join: $officerId"
}
}
$setFlags = Get-Prop $choice "set_flags" $null
if ($null -eq $setFlags) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has no set_flags."
}
if ($setFlags -is [string] -or $setFlags -is [System.Array]) {
Fail "Scenario $ScenarioId post-battle choice $choiceId set_flags must be an object."
}
foreach ($flag in $setFlags.PSObject.Properties) {
if ([string]::IsNullOrWhiteSpace([string]$flag.Name)) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has empty flag name."
}
if (-not ([string]$flag.Name -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId post-battle choice $choiceId has unstable flag name: $($flag.Name)"
}
if ($null -eq $flag.Value -or $flag.Value -is [System.Array] -or $flag.Value -is [pscustomobject]) {
Fail "Scenario $ScenarioId post-battle choice $choiceId flag $($flag.Name) must be scalar."
}
$flagName = [string]$flag.Name
$flagKind = Get-Flag-Value-Kind $flag.Value
if ($knownFlagValueKinds.ContainsKey($flagName) -and $knownFlagValueKinds[$flagName] -ne $flagKind) {
Fail "Scenario $ScenarioId post-battle choice $choiceId changes flag type: $flagName"
}
$knownFlagIds.Add($flagName) | Out-Null
$knownFlagValueKinds[$flagName] = $flagKind
}
}
}
function Check-Event-Ids($Events, [string]$ScenarioId) {
$eventIds = New-Object System.Collections.Generic.HashSet[string]
if ($null -eq $Events) {
return ,$eventIds
}
if (-not ($Events -is [System.Array])) {
Fail "Scenario $ScenarioId events must be an array."
}
foreach ($event in @($Events)) {
if ($event -is [string] -or $event -is [System.Array]) {
Fail "Scenario $ScenarioId has malformed event."
}
if (-not (Has-Prop $event "id")) {
Fail "Scenario $ScenarioId event has empty id."
}
$eventId = [string](Get-Prop $event "id" "")
if ([string]::IsNullOrWhiteSpace($eventId)) {
Fail "Scenario $ScenarioId event has empty id."
}
if (-not ($eventId -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId event has unstable id: $eventId"
}
if (-not $eventIds.Add($eventId)) {
Fail "Scenario $ScenarioId has duplicate event id: $eventId"
}
}
return ,$eventIds
}
function Check-Required-Flags($RequiredFlags, [string]$ScenarioId, [string]$Context) {
if ($null -eq $RequiredFlags) {
return
}
if ($RequiredFlags -is [string] -or $RequiredFlags -is [System.Array]) {
Fail "Scenario $ScenarioId $Context campaign_flags must be an object."
}
foreach ($flag in $RequiredFlags.PSObject.Properties) {
$flagName = [string]$flag.Name
if ([string]::IsNullOrWhiteSpace($flagName)) {
Fail "Scenario $ScenarioId $Context has empty required flag name."
}
if (-not ($flagName -match "^[a-z0-9_]+$")) {
Fail "Scenario $ScenarioId $Context has unstable required flag name: $flagName"
}
if (-not $knownFlagIds.Contains($flagName)) {
Fail "Scenario $ScenarioId $Context requires unknown campaign flag: $flagName"
}
if ($null -eq $flag.Value -or $flag.Value -is [System.Array] -or $flag.Value -is [pscustomobject]) {
Fail "Scenario $ScenarioId $Context required flag $flagName must be scalar."
}
$flagKind = Get-Flag-Value-Kind $flag.Value
if ($knownFlagValueKinds.ContainsKey($flagName) -and $knownFlagValueKinds[$flagName] -ne $flagKind) {
Fail "Scenario $ScenarioId $Context required flag $flagName has type $flagKind but was defined as $($knownFlagValueKinds[$flagName])."
}
}
}
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
}
if ($Briefing -is [string] -or $Briefing -is [System.Array]) {
Fail "Scenario $ScenarioId briefing must be an object."
}
if (Has-Prop $Briefing "title") {
Check-Readable-Korean-Text ([string](Get-Prop $Briefing "title" "")) "Scenario $ScenarioId briefing title"
}
if (Has-Prop $Briefing "location") {
Check-Readable-Korean-Text ([string](Get-Prop $Briefing "location" "")) "Scenario $ScenarioId briefing location"
}
$lines = Get-Prop $Briefing "lines" @()
foreach ($line in @($lines)) {
if ([string]::IsNullOrWhiteSpace([string]$line)) {
Fail "Scenario $ScenarioId briefing has an empty line."
}
Check-Readable-Korean-Text ([string]$line) "Scenario $ScenarioId briefing line"
}
foreach ($line in @((Get-Prop $Briefing "camp_dialogue" @()))) {
Check-Dialogue-Line $line "Scenario $ScenarioId briefing camp_dialogue"
}
Check-Camp-Conversations (Get-Prop $Briefing "camp_conversations" $null) $ScenarioId
$conditionalLines = Get-Prop $Briefing "conditional_lines" @()
foreach ($block in @($conditionalLines)) {
if ($block -is [string] -or $block -is [System.Array]) {
Fail "Scenario $ScenarioId briefing has malformed conditional_lines entry."
}
$blockFlags = Get-Prop $block "campaign_flags" $null
if ($null -eq $blockFlags -or @($blockFlags.PSObject.Properties).Count -le 0) {
Fail "Scenario $ScenarioId briefing conditional_lines entry must have campaign_flags."
}
Check-Required-Flags $blockFlags $ScenarioId "briefing conditional_lines"
$blockLines = Get-Prop $block "lines" $null
if ($null -eq $blockLines) {
Fail "Scenario $ScenarioId briefing conditional_lines entry must have line array."
}
foreach ($line in @($blockLines)) {
if ([string]::IsNullOrWhiteSpace([string]$line)) {
Fail "Scenario $ScenarioId briefing conditional_lines has an empty line."
}
Check-Readable-Korean-Text ([string]$line) "Scenario $ScenarioId briefing conditional line"
}
}
}
function Check-Camp-Conversations($Conversations, [string]$ScenarioId) {
if ($null -eq $Conversations) {
return
}
if (-not ($Conversations -is [System.Array])) {
Fail "Scenario $ScenarioId briefing camp_conversations must be an array."
}
$seenConversationIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($conversation in @($Conversations)) {
if ($conversation -is [string] -or $conversation -is [System.Array]) {
Fail "Scenario $ScenarioId briefing camp_conversations has malformed entry."
}
$conversationId = [string](Get-Prop $conversation "id" "")
if ([string]::IsNullOrWhiteSpace($conversationId)) {
Fail "Scenario $ScenarioId briefing camp_conversations has an empty id."
}
if (-not ($conversationId -match '^[a-z0-9_]+$')) {
Fail "Scenario $ScenarioId briefing camp_conversations has unstable id: $conversationId"
}
if (-not $seenConversationIds.Add($conversationId)) {
Fail "Scenario $ScenarioId briefing camp_conversations has duplicate id: $conversationId"
}
$label = [string](Get-Prop $conversation "label" "")
if ([string]::IsNullOrWhiteSpace($label)) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId has an empty label."
}
Check-Readable-Korean-Text $label "Scenario $ScenarioId briefing camp_conversations $conversationId label"
if (Has-Prop $conversation "summary") {
Check-Readable-Korean-Text ([string](Get-Prop $conversation "summary" "")) "Scenario $ScenarioId briefing camp_conversations $conversationId summary"
}
$group = ([string](Get-Prop $conversation "group" "topic")).Trim().ToLowerInvariant()
if (-not $validCampConversationGroups.Contains($group)) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId has unknown group: $group"
}
$officerId = [string](Get-Prop $conversation "officer_id" "")
if ($group -eq "officer") {
if ([string]::IsNullOrWhiteSpace($officerId)) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId needs officer_id."
}
if (-not $officerIds.Contains($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
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."
}
foreach ($conversationLine in @($conversationLines)) {
Check-Dialogue-Line $conversationLine "Scenario $ScenarioId briefing camp_conversations $conversationId"
}
if (Has-Prop $conversation "effects") {
$rawEffects = Get-Prop $conversation "effects" $null
$effects = @($rawEffects)
if ($null -eq $rawEffects -or $effects.Count -le 0) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId effects must be an array."
}
foreach ($effect in $effects) {
if ($effect -is [string] -or $effect -is [System.Array]) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId has malformed effect."
}
$effectType = [string](Get-Prop $effect "type" "")
if (-not $validCampConversationEffectTypes.Contains($effectType)) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId has unknown effect: $effectType"
}
if ($effectType -eq "grant_item") {
Check-Event-Grant-Item $effect "Scenario $ScenarioId briefing camp_conversations $conversationId grant_item"
}
if ((Has-Prop $effect "text") -and [string]::IsNullOrWhiteSpace([string](Get-Prop $effect "text" ""))) {
Fail "Scenario $ScenarioId briefing camp_conversations $conversationId effect text must not be empty."
}
if (Has-Prop $effect "text") {
Check-Readable-Korean-Text ([string](Get-Prop $effect "text" "")) "Scenario $ScenarioId briefing camp_conversations $conversationId effect text"
}
}
}
}
}
function Check-Dialogue-Line($Line, [string]$Context) {
if ($Line -is [string]) {
if ([string]::IsNullOrWhiteSpace([string]$Line)) {
Fail "$Context has an empty dialogue line."
}
Check-Readable-Korean-Text ([string]$Line) "$Context dialogue line"
return
}
if ($Line -is [System.Array] -or -not (Has-Prop $Line "text")) {
Fail "$Context has malformed dialogue line."
}
if ([string]::IsNullOrWhiteSpace([string](Get-Prop $Line "text" ""))) {
Fail "$Context has an empty dialogue text."
}
Check-Readable-Korean-Text ([string](Get-Prop $Line "text" "")) "$Context dialogue text"
if (Has-Prop $Line "display_speaker") {
Check-Readable-Korean-Text ([string](Get-Prop $Line "display_speaker" "")) "$Context display speaker"
}
if (Has-Prop $Line "portrait") {
Check-Portrait-Path (Get-Prop $Line "portrait" "") "$Context portrait"
}
if (Has-Prop $Line "side") {
$side = ([string](Get-Prop $Line "side" "")).Trim().ToLowerInvariant()
if (-not $validDialogueSides.Contains($side)) {
Fail "$Context has unknown dialogue side: $side"
}
}
}
function Check-Portrait-Path($Portrait, [string]$Context) {
if ($Portrait -is [System.Array] -or $Portrait -is [System.Management.Automation.PSCustomObject]) {
Fail "$Context must be a string resource path."
}
if ([string]::IsNullOrWhiteSpace([string]$Portrait)) {
Fail "$Context must not be empty."
}
if ([string]$Portrait -notmatch $validPortraitPathPattern) {
Fail "$Context must be a res:// image path."
}
$relativePath = ([string]$Portrait).Substring("res://".Length)
$rootPath = [System.IO.Path]::GetFullPath($Root)
$absolutePath = [System.IO.Path]::GetFullPath((Join-Path $rootPath $relativePath))
$trimChars = @([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
$rootPathWithSeparator = $rootPath.TrimEnd($trimChars) + [System.IO.Path]::DirectorySeparatorChar
$insideRoot = $absolutePath.Equals($rootPath, [System.StringComparison]::OrdinalIgnoreCase) -or $absolutePath.StartsWith($rootPathWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)
if (-not $insideRoot) {
Fail "$Context must stay inside the project: $Portrait"
}
if (-not (Test-Path -LiteralPath $absolutePath -PathType Leaf)) {
Fail "$Context references missing file: $Portrait"
}
Check-Portrait-Image $absolutePath ([string]$Portrait) $Context
if ($relativePath -match '^art[\\/]+portraits[\\/]+.+\.png$') {
Check-Alpha-Cutout-File $absolutePath ([string]$Portrait) $Context
}
}
function Check-Portrait-Image([string]$AbsolutePath, [string]$Portrait, [string]$Context) {
if (-not $portraitImageInspectorReady) {
Fail "$Context cannot inspect portrait image metadata: $Portrait"
}
$image = $null
$width = 0
$height = 0
try {
$image = [System.Drawing.Image]::FromFile($AbsolutePath)
$width = [int]$image.Width
$height = [int]$image.Height
} catch {
Fail "$Context references an unreadable image file: $Portrait"
} finally {
if ($null -ne $image) {
$image.Dispose()
}
}
if ($width -lt $minimumPortraitDimension -or $height -lt $minimumPortraitDimension) {
Fail "$Context image is too small: $Portrait ($width x $height, minimum ${minimumPortraitDimension}x${minimumPortraitDimension})"
}
}
function Check-Alpha-Cutout-File([string]$AbsolutePath, [string]$DisplayPath, [string]$Context) {
$bitmap = $null
try {
$bitmap = [System.Drawing.Bitmap]::FromFile($AbsolutePath)
if (-not [System.Drawing.Image]::IsAlphaPixelFormat($bitmap.PixelFormat)) {
Fail "$Context must be an alpha PNG cutout: $DisplayPath"
}
$cornerPixels = @(
$bitmap.GetPixel(0, 0),
$bitmap.GetPixel($bitmap.Width - 1, 0),
$bitmap.GetPixel(0, $bitmap.Height - 1),
$bitmap.GetPixel($bitmap.Width - 1, $bitmap.Height - 1)
)
foreach ($pixel in $cornerPixels) {
if ($pixel.A -gt 3) {
Fail "$Context must have transparent corners: $DisplayPath"
}
}
$transparentSamples = 0
$totalSamples = 0
for ($y = 0; $y -lt $bitmap.Height; $y += 4) {
for ($x = 0; $x -lt $bitmap.Width; $x += 4) {
$totalSamples += 1
if ($bitmap.GetPixel($x, $y).A -le 3) {
$transparentSamples += 1
}
}
}
if ($transparentSamples -lt [int]($totalSamples * 0.20)) {
Fail "$Context must have substantial transparent background: $DisplayPath ($transparentSamples/$totalSamples sampled pixels)"
}
} catch {
if ($_.Exception.Message -like "$Context*") {
throw
}
Fail "$Context could not inspect alpha cutout data: $DisplayPath"
} finally {
if ($null -ne $bitmap) {
$bitmap.Dispose()
}
}
}
function Check-Item-Icon-Cutout([string]$Icon, [string]$Context) {
$relativePath = $Icon.Substring("res://".Length)
$absolutePath = [System.IO.Path]::GetFullPath((Join-Path ([System.IO.Path]::GetFullPath($Root)) $relativePath))
Check-Alpha-Cutout-File $absolutePath $Icon $Context
}
function Check-Post-Battle-Dialogue($Dialogue, [string]$ScenarioId) {
if ($null -eq $Dialogue) {
return
}
$lines = @($Dialogue)
if ($lines.Count -le 0) {
Fail "Scenario $ScenarioId post_battle_dialogue must not be empty."
}
foreach ($line in $lines) {
Check-Dialogue-Line $line "Scenario $ScenarioId post_battle_dialogue"
}
}
function Check-Join-Officer-Rewards($Rewards, [string]$ScenarioId) {
if ($null -eq $Rewards) {
return
}
$joinOfficers = Check-Officer-Transition-List (Get-Prop $Rewards "join_officers" $null) $ScenarioId "rewards join_officers"
$leaveOfficers = Check-Officer-Transition-List (Get-Prop $Rewards "leave_officers" $null) $ScenarioId "rewards leave_officers"
foreach ($officerId in $joinOfficers) {
if ($leaveOfficers.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards both joins and leaves officer: $officerId"
}
if ($joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards joins already joined officer: $officerId"
}
}
foreach ($officerId in $leaveOfficers) {
if (-not $joinedOfficerIds.Contains($officerId)) {
Fail "Scenario $ScenarioId rewards leaves officer before they join: $officerId"
}
}
foreach ($officerId in $joinOfficers) {
$joinedOfficerIds.Add($officerId) | Out-Null
}
foreach ($officerId in $leaveOfficers) {
$joinedOfficerIds.Remove($officerId) | Out-Null
}
}
Check-Raw-Skills-Arrays
Check-Item-Effects
Check-Terrain-Definitions
Check-Skill-Definitions
Check-Class-Equipment-Slots
Check-Class-Promotions
foreach ($classProp in $classes.PSObject.Properties) {
Check-Skill-Refs (Get-Prop $classProp.Value "skills" @()) "Class $($classProp.Name)"
if (Has-Prop $classProp.Value "sprite") {
Check-Portrait-Path (Get-Prop $classProp.Value "sprite" "") "Class $($classProp.Name) sprite"
}
if (Has-Prop $classProp.Value "enemy_sprite") {
Check-Portrait-Path (Get-Prop $classProp.Value "enemy_sprite" "") "Class $($classProp.Name) enemy_sprite"
}
}
$officerNames = New-Object System.Collections.Generic.HashSet[string]
foreach ($officerProp in $officers.PSObject.Properties) {
$officerName = [string](Get-Prop $officerProp.Value "name" "")
if ([string]::IsNullOrWhiteSpace($officerName)) {
Fail "Officer $($officerProp.Name) has empty name."
}
$normalizedOfficerName = $officerName.Trim().ToLowerInvariant()
if (-not $officerNames.Add($normalizedOfficerName)) {
Fail "Officer $($officerProp.Name) has duplicate name: $officerName"
}
Check-Skill-Refs (Get-Prop $officerProp.Value "skills" @()) "Officer $($officerProp.Name)"
Check-Officer-Equipment $officerProp.Name $officerProp.Value
if (Has-Prop $officerProp.Value "portrait") {
Check-Portrait-Path (Get-Prop $officerProp.Value "portrait" "") "Officer $($officerProp.Name) portrait"
}
}
foreach ($scenario in $campaign.scenarios) {
$scenarioPath = ([string]$scenario.path).Replace("res://", "")
$scenarioData = Read-Json $scenarioPath
$scenarioId = [string]$scenarioData.id
$script:CurrentScenarioRequiresHangul = $scenarioId -match '^00[1-9]_'
if ($scenarioId -ne [string]$scenario.id) {
Fail "Campaign id $($scenario.id) does not match scenario file id $scenarioId."
}
Check-Readable-Korean-Text ([string](Get-Prop $scenarioData "name" "")) "Scenario $scenarioId name"
if (Has-Prop $scenarioData "objectives") {
Check-Readable-Korean-Text ([string](Get-Prop $scenarioData.objectives "victory" "")) "Scenario $scenarioId victory objective"
Check-Readable-Korean-Text ([string](Get-Prop $scenarioData.objectives "defeat" "")) "Scenario $scenarioId defeat objective"
}
$map = $scenarioData.map
if (Has-Prop $map "background") {
Check-Portrait-Path (Get-Prop $map "background" "") "Scenario $scenarioId map background"
}
$width = [int]$map.width
$height = [int]$map.height
$rows = @($map.terrain)
if ($rows.Count -ne $height) {
Fail "Scenario $scenarioId map height mismatch."
}
foreach ($row in $rows) {
if ([string]$row.Length -ne [string]$width) {
Fail "Scenario $scenarioId map row width mismatch: $row"
}
foreach ($char in $row.ToCharArray()) {
if (-not $terrainKeys.Contains([string]$char)) {
Fail "Scenario $scenarioId uses unknown terrain key: $char"
}
}
}
$eventIds = Check-Event-Ids (Get-Prop $scenarioData "events" $null) $scenarioId
$knownUnitIds = Collect-Scenario-Unit-Ids $scenarioData
if (Has-Prop $scenarioData "conditions") {
if (Has-Prop $scenarioData.conditions "victory") {
Check-Condition $scenarioData.conditions.victory $scenarioId $eventIds $width $height $rows $knownUnitIds
}
if (Has-Prop $scenarioData.conditions "defeat") {
Check-Condition $scenarioData.conditions.defeat $scenarioId $eventIds $width $height $rows $knownUnitIds
}
}
$seenIds = New-Object System.Collections.Generic.HashSet[string]
foreach ($deployment in $scenarioData.deployments) {
Check-Deployment $deployment $scenarioId $width $height $rows $seenIds
}
Check-Briefing (Get-Prop $scenarioData "briefing" $null) $scenarioId
Check-Shop (Get-Prop $scenarioData "shop" $null) $scenarioId
Check-Joined-Deployment-Requirements $scenarioData.deployments $scenarioId
Check-Roster (Get-Prop $scenarioData "roster" $null) $scenarioId $scenarioData.deployments
Check-Required-Officers-Joined (Get-Prop $scenarioData "roster" $null) (Get-Prop $scenarioData "conditions" $null) $scenarioId
Check-Roster-Condition-Consistency (Get-Prop $scenarioData "roster" $null) (Get-Prop $scenarioData "conditions" $null) $scenarioId
Check-Formation (Get-Prop $scenarioData "formation" $null) $scenarioId $width $height $rows $scenarioData.deployments
foreach ($event in (Get-Prop $scenarioData "events" @())) {
$eventId = [string](Get-Prop $event "id" "event")
$when = Get-Prop $event "when" $null
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)) {
Fail "Scenario $scenarioId has unknown trigger: $trigger"
}
if ($trigger -eq "unit_reaches_tile") {
Check-Unit-Reaches-Event-When $when $scenarioId $width $height $rows $knownUnitIds
}
if ($trigger -eq "unit_defeated") {
Check-Unit-Defeated-Event-When $when $scenarioId $knownUnitIds
}
foreach ($action in (Get-Prop $event "actions" @())) {
$actionType = [string]$action.type
if (-not $validActions.Contains($actionType)) {
Fail "Scenario $scenarioId has unknown event action: $actionType"
}
if ($actionType -eq "spawn_deployment") {
Check-Deployment $action.deployment $scenarioId $width $height $rows $seenIds
}
if ($actionType -eq "spawn_deployments") {
foreach ($deployment in $action.deployments) {
Check-Deployment $deployment $scenarioId $width $height $rows $seenIds
}
}
if ($actionType -eq "dialogue") {
if (Has-Prop $action "lines") {
$rawLines = Get-Prop $action "lines" $null
$lines = @($rawLines)
if ($null -eq $rawLines -or $lines.Count -le 0) {
Fail "Scenario $scenarioId event $eventId dialogue action has malformed lines."
}
foreach ($line in $lines) {
Check-Dialogue-Line $line "Scenario $scenarioId event $eventId dialogue"
}
} else {
Check-Dialogue-Line $action "Scenario $scenarioId event $eventId dialogue"
}
}
if ($actionType -eq "grant_item") {
Check-Event-Grant-Item $action "Scenario $scenarioId event $eventId grant_item"
}
if ($actionType -eq "grant_items") {
$rawItems = Get-Prop $action "items" $null
$grantItems = @($rawItems)
if ($null -eq $rawItems -or $grantItems.Count -le 0) {
Fail "Scenario $scenarioId event $eventId grant_items action has malformed items."
}
foreach ($entry in $grantItems) {
Check-Event-Grant-Item $entry "Scenario $scenarioId event $eventId grant_items"
}
}
if ($actionType -eq "grant_gold") {
Check-Event-Grant-Gold $action "Scenario $scenarioId event $eventId grant_gold"
}
if ($actionType -eq "withdraw_unit") {
$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_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"
}
}
}
foreach ($itemId in (Get-Prop (Get-Prop $scenarioData "rewards" @{}) "items" @())) {
if (-not $itemIds.Contains([string]$itemId)) {
Fail "Scenario $scenarioId rewards unknown item: $itemId"
}
}
Check-Join-Officer-Rewards (Get-Prop $scenarioData "rewards" $null) $scenarioId
Check-Post-Battle-Dialogue (Get-Prop $scenarioData "post_battle_dialogue" $null) $scenarioId
Check-Post-Battle-Choices (Get-Prop $scenarioData "post_battle_choices" $null) $scenarioId
}
"data validation ok"