Add officer portrait defaults
This commit is contained in:
@@ -103,7 +103,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
||||
- Campaign completion screen.
|
||||
- New campaign save reset.
|
||||
- Victory and defeat result overlay.
|
||||
- Dialogue portrait slot with optional image loading and speaker-initial fallback.
|
||||
- Dialogue portrait slot with officer default image paths, optional per-line overrides, and speaker-initial fallback.
|
||||
- Placeholder menu/battle BGM plus UI, movement, attack, tactic, item, victory, and defeat SFX.
|
||||
- Floating combat text for damage, recovery, misses, support effects, level-ups, and promotions.
|
||||
- Short unit slide animation for board movement.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "Cao Cao",
|
||||
"faction": "wei",
|
||||
"class_id": "hero",
|
||||
"portrait": "res://art/portraits/cao_cao.png",
|
||||
"level": 1,
|
||||
"exp": 0,
|
||||
"base": { "hp": 38, "mp": 10, "atk": 10, "def": 7, "int": 12, "agi": 8 },
|
||||
@@ -13,6 +14,7 @@
|
||||
"name": "Xiahou Dun",
|
||||
"faction": "wei",
|
||||
"class_id": "cavalry",
|
||||
"portrait": "res://art/portraits/xiahou_dun.png",
|
||||
"level": 1,
|
||||
"exp": 0,
|
||||
"base": { "hp": 40, "mp": 4, "atk": 11, "def": 6, "int": 5, "agi": 9 },
|
||||
@@ -23,6 +25,7 @@
|
||||
"name": "Xiahou Yuan",
|
||||
"faction": "wei",
|
||||
"class_id": "archer",
|
||||
"portrait": "res://art/portraits/xiahou_yuan.png",
|
||||
"level": 1,
|
||||
"exp": 0,
|
||||
"base": { "hp": 32, "mp": 4, "atk": 9, "def": 5, "int": 6, "agi": 12 },
|
||||
@@ -33,6 +36,7 @@
|
||||
"name": "Cao Ren",
|
||||
"faction": "wei",
|
||||
"class_id": "infantry",
|
||||
"portrait": "res://art/portraits/cao_ren.png",
|
||||
"level": 1,
|
||||
"exp": 0,
|
||||
"base": { "hp": 36, "mp": 3, "atk": 10, "def": 8, "int": 5, "agi": 7 },
|
||||
@@ -43,6 +47,7 @@
|
||||
"name": "Dian Wei",
|
||||
"faction": "wei",
|
||||
"class_id": "warrior",
|
||||
"portrait": "res://art/portraits/dian_wei.png",
|
||||
"level": 3,
|
||||
"exp": 0,
|
||||
"base": { "hp": 42, "mp": 2, "atk": 13, "def": 8, "int": 3, "agi": 7 },
|
||||
@@ -53,6 +58,7 @@
|
||||
"name": "Guo Jia",
|
||||
"faction": "wei",
|
||||
"class_id": "strategist",
|
||||
"portrait": "res://art/portraits/guo_jia.png",
|
||||
"level": 8,
|
||||
"exp": 0,
|
||||
"base": { "hp": 30, "mp": 18, "atk": 5, "def": 5, "int": 14, "agi": 9 },
|
||||
@@ -63,6 +69,7 @@
|
||||
"name": "Zhang He",
|
||||
"faction": "wei",
|
||||
"class_id": "elite_cavalry",
|
||||
"portrait": "res://art/portraits/zhang_he.png",
|
||||
"level": 12,
|
||||
"exp": 0,
|
||||
"base": { "hp": 48, "mp": 6, "atk": 15, "def": 10, "int": 7, "agi": 14 },
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
## Data Files
|
||||
|
||||
- `data/defs/classes.json`: Class stats, movement types, skill access.
|
||||
- `data/defs/officers.json`: Named officer base stats and growth.
|
||||
- `data/defs/officers.json`: Named officer base stats, growth, and default portrait paths.
|
||||
- `data/defs/items.json`: Weapons, armor, accessories, and consumables.
|
||||
- `data/defs/skills.json`: MP tactic definitions for damage, healing, target rules, and range.
|
||||
- `data/defs/terrain.json`: Terrain colors, defense, avoid, and movement costs.
|
||||
|
||||
@@ -392,11 +392,11 @@ Conditions may include `after_event` to stay inactive until a one-shot event has
|
||||
|
||||
`unit_reaches_tile` checks living deployed units against a zero-based `pos` coordinate. Use `officer_ids` for persistent named officers or `unit_ids` for scenario-specific units; the validator requires one of those filters so destination objectives do not accidentally trigger from any unit. Victory destination cells are drawn as objective markers on the battle map.
|
||||
|
||||
`post_battle_dialogue` plays once after victory and before the victory result panel opens. Lines may be plain strings or objects with `speaker`, non-empty `text`, and optional `portrait` resource paths such as `res://art/portraits/cao_cao.png`. If no usable portrait is loaded, the battle scene falls back to a speaker-initial panel.
|
||||
`post_battle_dialogue` plays once after victory and before the victory result panel opens. Lines may be plain strings or objects with `speaker`, non-empty `text`, and optional `portrait` resource paths such as `res://art/portraits/cao_cao.png`. If `portrait` is omitted, `BattleState` uses a matching officer definition's default portrait path when the line's `speaker` matches the officer `name`. If no usable portrait is loaded, the battle scene falls back to a speaker-initial panel.
|
||||
|
||||
`post_battle_choices` are shown on the victory result panel after first-time scenario rewards are applied. Each choice needs a unique stable lowercase `id`, a non-empty `label`, and a `set_flags` object whose keys are stable lowercase flag ids. Choices may also grant positive `gold`, known `items`, `join_officers`, and `leave_officers`. When rewards are saved but the player has not selected a choice yet, `CampaignState.pending_post_battle_choice_scenario_id` records that scenario id so reloading the game returns to the victory choice panel instead of skipping the branch. The selected choice is saved to `CampaignState.flags`, `CampaignState.applied_post_battle_choices`, and campaign membership only when the player presses its result-panel button; completed-scenario replays do not show choices again. Save-load migration for older saves uses flags as a conservative choice signal, so avoid designing multiple choices in one scenario that can all match the same saved flag state.
|
||||
|
||||
Scenario `events` support `battle_start`, `battle_begin`, `turn_start`, and movement-triggered `unit_reaches_tile` triggers. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. Actions currently include `log`, `dialogue`, `set_objective`, `spawn_deployment`, and `spawn_deployments`.
|
||||
Scenario `events` support `battle_start`, `battle_begin`, `turn_start`, and movement-triggered `unit_reaches_tile` triggers. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. Actions currently include `log`, `dialogue`, `set_objective`, `spawn_deployment`, and `spawn_deployments`. Event `dialogue` actions use the same dialogue line format and portrait default rules as `post_battle_dialogue`.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
## Milestone 5: Presentation
|
||||
|
||||
- Portrait pipeline. Dialogue lines can carry optional portrait image paths, with speaker-initial fallback when art is missing.
|
||||
- Portrait pipeline. Officer definitions can provide default portrait paths, dialogue lines can override them, and missing art falls back to speaker initials.
|
||||
- Unit sprites and animations. Board movement now has a short slide animation, and board units show low-HP warning rings.
|
||||
- Battle effects. First floating combat text now covers damage, recovery, misses, support effects, level-ups, and promotions. Hover target preview badges summarize attacks, tactics, and items.
|
||||
- Music and sound. Basic placeholder BGM and SFX now play for menus, battle flow, unit actions, tactics, items, and result states.
|
||||
|
||||
@@ -2492,10 +2492,14 @@ func _normalize_dialogue_line(line) -> Dictionary:
|
||||
var text := str(line.get("text", ""))
|
||||
if text.is_empty():
|
||||
return {}
|
||||
var speaker := str(line.get("speaker", ""))
|
||||
var portrait := str(line.get("portrait", ""))
|
||||
if portrait.is_empty():
|
||||
portrait = data_catalog.get_portrait_for_speaker(speaker)
|
||||
return {
|
||||
"speaker": str(line.get("speaker", "")),
|
||||
"speaker": speaker,
|
||||
"text": text,
|
||||
"portrait": str(line.get("portrait", ""))
|
||||
"portrait": portrait
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@@ -110,6 +110,17 @@ func get_class_def(class_id: String) -> Dictionary:
|
||||
return _get_dict(classes, class_id).duplicate(true)
|
||||
|
||||
|
||||
func get_portrait_for_speaker(speaker_name: String) -> String:
|
||||
var normalized_speaker := _normalized_speaker_name(speaker_name)
|
||||
if normalized_speaker.is_empty():
|
||||
return ""
|
||||
for officer_id in officers.keys():
|
||||
var officer := _get_dict(officers, officer_id)
|
||||
if _normalized_speaker_name(str(officer.get("name", ""))) == normalized_speaker:
|
||||
return str(officer.get("portrait", ""))
|
||||
return ""
|
||||
|
||||
|
||||
func _load_json_object(path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(path):
|
||||
push_error("Missing data file: %s" % path)
|
||||
@@ -132,6 +143,10 @@ func _get_dict(source: Dictionary, key) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
func _normalized_speaker_name(value: String) -> String:
|
||||
return value.strip_edges().to_lower()
|
||||
|
||||
|
||||
func _merged_equipment(base_equipment, override_equipment) -> Dictionary:
|
||||
var result := {}
|
||||
if typeof(base_equipment) == TYPE_DICTIONARY:
|
||||
|
||||
@@ -101,6 +101,7 @@ $validSkillKinds = @("damage", "heal", "support")
|
||||
$validSkillTargets = @("enemy", "ally", "self", "any")
|
||||
$validSkillStats = @("atk", "def", "int", "agi")
|
||||
$validSkillEffectTypes = @("stat_bonus")
|
||||
$validPortraitPathPattern = '^res://.+\.(png|jpg|jpeg|webp)$'
|
||||
$knownFlagIds = New-Object System.Collections.Generic.HashSet[string]
|
||||
$knownFlagValueKinds = @{}
|
||||
$joinedOfficerIds = New-Object System.Collections.Generic.HashSet[string]
|
||||
@@ -1204,17 +1205,19 @@ function Check-Dialogue-Line($Line, [string]$Context) {
|
||||
Fail "$Context has an empty dialogue text."
|
||||
}
|
||||
if (Has-Prop $Line "portrait") {
|
||||
$portrait = Get-Prop $Line "portrait" ""
|
||||
if ($portrait -is [System.Array] -or $portrait -is [System.Management.Automation.PSCustomObject]) {
|
||||
Fail "$Context portrait must be a string resource path."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace([string]$portrait)) {
|
||||
Fail "$Context has an empty portrait path."
|
||||
}
|
||||
$portraitText = [string]$portrait
|
||||
if ($portraitText -notmatch '^res://.+\.(png|jpg|jpeg|webp)$') {
|
||||
Fail "$Context portrait must be a res:// image path."
|
||||
}
|
||||
Check-Portrait-Path (Get-Prop $Line "portrait" "") "$Context portrait"
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1269,9 +1272,21 @@ 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) {
|
||||
@@ -1357,6 +1372,20 @@ foreach ($scenario in $campaign.scenarios) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user