1775 lines
75 KiB
PowerShell
1775 lines
75 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 | 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-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
|
|
|
|
$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")
|
|
$validActions = @("log", "dialogue", "set_objective", "grant_item", "grant_items", "spawn_deployment", "spawn_deployments")
|
|
$validEffects = @("heal_hp", "heal_mp", "cure_status")
|
|
$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")
|
|
$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."
|
|
}
|
|
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."
|
|
}
|
|
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-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") {
|
|
if (-not (Has-Prop $Condition "pos")) {
|
|
Fail "Scenario $ScenarioId unit_reaches_tile condition is missing pos."
|
|
}
|
|
$cell = Resolve-Cell $Condition.pos "Scenario $ScenarioId unit_reaches_tile condition"
|
|
$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 "Scenario $ScenarioId unit_reaches_tile condition 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 "Scenario $ScenarioId unit_reaches_tile condition targets impassable terrain at [$x,$y]."
|
|
}
|
|
}
|
|
$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) {
|
|
if (-not (Has-Prop $When "pos")) {
|
|
Fail "Scenario $ScenarioId unit_reaches_tile event is missing pos."
|
|
}
|
|
$cell = Resolve-Cell $When.pos "Scenario $ScenarioId unit_reaches_tile event"
|
|
$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 unit_reaches_tile event is outside map at [$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 unit_reaches_tile event targets impassable terrain at [$x,$y]."
|
|
}
|
|
|
|
$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 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 "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."
|
|
}
|
|
}
|
|
|
|
$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."
|
|
}
|
|
}
|
|
|
|
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-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 (-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."
|
|
}
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
$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-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."
|
|
}
|
|
$lines = Get-Prop $Briefing "lines" @()
|
|
foreach ($line in @($lines)) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$line)) {
|
|
Fail "Scenario $ScenarioId briefing has an empty line."
|
|
}
|
|
}
|
|
|
|
$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."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Check-Dialogue-Line($Line, [string]$Context) {
|
|
if ($Line -is [string]) {
|
|
if ([string]::IsNullOrWhiteSpace([string]$Line)) {
|
|
Fail "$Context has an empty 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."
|
|
}
|
|
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
|
|
}
|
|
|
|
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-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)"
|
|
}
|
|
|
|
$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
|
|
if ($scenarioId -ne [string]$scenario.id) {
|
|
Fail "Campaign id $($scenario.id) does not match scenario file id $scenarioId."
|
|
}
|
|
|
|
$map = $scenarioData.map
|
|
$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-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
|
|
}
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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"
|