Add chapter battle replay entries
This commit is contained in:
@@ -22,7 +22,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
||||
- Pre-battle Roster can move optional officers between sortie and reserve within scenario limits.
|
||||
- Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells.
|
||||
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, and victory/defeat summary.
|
||||
- Pre-battle Chapters overview shows story-arc progress and the current battle.
|
||||
- Pre-battle Chapters overview shows story-arc progress and can open completed or current battles.
|
||||
- Escort scenarios can include required, non-controllable protected units.
|
||||
- Campaign saves joined officers, future deployments can require prior recruitment, and choices can make officers leave.
|
||||
- Completed saves reconcile officer join/leave rewards on load without replaying gold or items.
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
9. Enemy AI moves, attacks, and can cast damage or healing tactics through the same combat and skill resolvers.
|
||||
10. Battle checks scenario-defined defeat conditions first, including commander loss and turn limits, then victory conditions such as enemy defeat or reaching a marked destination tile.
|
||||
11. Turn-start and movement-triggered scenario events run as phases change or units enter marked cells, and may update objectives or deploy reinforcements.
|
||||
12. On victory, `CampaignState` snapshots player progression and the battle inventory, applies rewards once, advances `current_scenario_id`, and writes `user://campaign_save.json`.
|
||||
12. On first-time victory, `CampaignState` snapshots player progression and the battle inventory, applies rewards once, advances `current_scenario_id`, and writes `user://campaign_save.json`.
|
||||
13. If the scenario defines post-battle dialogue, it plays once before the victory result panel opens.
|
||||
14. The victory result panel summarizes rewards plus saved player level-ups and promotions.
|
||||
15. If the scenario defines post-battle choices, the result panel waits for the player to select one, then saves the selected flags and optional choice rewards before enabling the next battle.
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
Joined officers gate whether `requires_joined` player deployments are loaded at all. Rewards and post-battle choices can add or remove joined officers without deleting their saved roster snapshot, so a later rejoin can preserve progression. When a save loads, `CampaignState` scans completed scenarios in campaign order and reapplies only officer join/leave transitions from rewards and the saved `applied_post_battle_choices` ledger, allowing added recruitment rewards to reach older saves without replaying gold or items. Old saves without the ledger can backfill a choice id only when exactly one completed scenario choice matches saved flags. Roster entries are keyed by `officer_id` when available, so future scenarios can deploy the same joined officer under different scenario-specific `unit_id` values. Saved progression overlays final battle stats for now; a later equipment refactor should split base stats from equipment bonuses before adding item leveling or stat recalculation. New battles currently start player units healed to their saved maximum HP/MP. Skill access is carried in roster snapshots, with class defaults still applied for old saves that do not include skills. Promoted class ids are saved in the roster, and the next battle rehydrates class-derived movement, growth, skills, and range from the saved class id before equipment range is recalculated.
|
||||
|
||||
Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress without mutating the save. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks. Shop purchases and 50% sell-back are campaign transactions: they update saved gold and unequipped inventory immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes fire on entry rather than from units already standing on a marker. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, and reinforcements from saved campaign choices. Consumable use can restore HP or MP, and in-battle item use plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Already completed scenarios can be replayed without granting duplicate rewards, choices, or replay inventory consumption; pre-battle Shop, Armory, Roster, and Formation are disabled on completed-scenario replays to avoid side effects.
|
||||
Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks. Shop purchases and 50% sell-back are campaign transactions: they update saved gold and unequipped inventory immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes fire on entry rather than from units already standing on a marker. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, and reinforcements from saved campaign choices. Consumable use can restore HP or MP, and in-battle item use plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, save writes, or current-scenario advancement; pre-battle Shop, Armory, Roster, and Formation are disabled on completed-scenario replays to avoid side effects.
|
||||
|
||||
The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing and objective text, active battles use battle BGM, dialogue lines can show optional portrait textures or speaker-initial fallback panels and can place the portrait on the left or right side of the dialogue panel, board units render low-HP warning rings and HP bar color states from battle state fields, hover previews are rendered as target badges from existing `BattleState` forecast APIs, log/result hooks trigger placeholder SFX, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules.
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ The campaign file defines chapter ranges, scenario order, resource paths, and of
|
||||
}
|
||||
```
|
||||
|
||||
`chapters` are display metadata for campaign presentation. Each chapter has a stable lowercase `id`, non-empty `title`, and inclusive `start_scenario`/`end_scenario` ids from the linear scenario order. When chapters are present, the validator requires the ranges to cover every campaign scenario exactly once, without overlaps or gaps. `CampaignState` uses them for briefing headers, chapter-local battle numbering, and pre-battle chapter progress overview entries; save files still store scenario ids, not chapter ids.
|
||||
`chapters` are display metadata for campaign presentation. Each chapter has a stable lowercase `id`, non-empty `title`, and inclusive `start_scenario`/`end_scenario` ids from the linear scenario order. When chapters are present, the validator requires the ranges to cover every campaign scenario exactly once, without overlaps or gaps. `CampaignState` uses them for briefing headers, chapter-local battle numbering, and pre-battle chapter progress/replay overview entries; save files still store scenario ids, not chapter ids.
|
||||
|
||||
## Officers
|
||||
|
||||
@@ -477,7 +477,7 @@ Event-spawned units use the same compact deployment shape as scenario starting u
|
||||
|
||||
`scripts/core/campaign_state.gd` writes `user://campaign_save.json` with `save_version`, completed scenarios, gold, inventory counts, joined officers, pending post-battle choice scenario id, applied post-battle choice ids, campaign flags, and player roster progression. Roster snapshots include level, EXP, stats, class id/name, class-derived movement/range/growth fields, skills, and equipment. Rewards, joined/left officers, and post-battle choices are guarded by completed scenario id so restarting a finished battle does not duplicate gold, items, membership changes, or branch decisions. On load, completed-scenario officer join/leave transitions are reconciled without replaying gold or item rewards, which lets new officer rewards added to older completed scenarios become available to existing saves.
|
||||
|
||||
After victory, `CampaignState` advances `current_scenario_id` to the next entry in `data/campaign/campaign.json`. The battle scene passes `CampaignState.get_roster_overrides()`, `CampaignState.get_inventory_snapshot()`, `CampaignState.get_flags_snapshot()`, and `CampaignState.get_joined_officers_snapshot()` into `BattleState.load_battle()`, so officers can keep level/EXP/stat progression, equipped gear, consumable/equipment stock counts, recruitment gates, and branch flags into the next scenario.
|
||||
After a first-time victory, `CampaignState` advances `current_scenario_id` to the next entry in `data/campaign/campaign.json`. Completed-scenario replays return a result summary without rewriting the save or moving `current_scenario_id`. The battle scene passes `CampaignState.get_roster_overrides()`, `CampaignState.get_inventory_snapshot()`, `CampaignState.get_flags_snapshot()`, and `CampaignState.get_joined_officers_snapshot()` into `BattleState.load_battle()`, so officers can keep level/EXP/stat progression, equipped gear, consumable/equipment stock counts, recruitment gates, and branch flags into the next scenario.
|
||||
|
||||
During a battle, item consumption and equipment swaps affect `BattleState.battle_inventory`. `CampaignState.apply_battle_result()` commits that inventory snapshot only for a first-time victory, then adds the victory rewards. Equipped gear is stored in the player roster snapshot together with the current derived stats; a later stat-model refactor should split base stats, growth, and equipment bonuses.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readines
|
||||
- Main scene starts without script errors.
|
||||
- Current campaign save loads or a new campaign starts cleanly.
|
||||
- Briefing opens and can enter battle.
|
||||
- Chapters, Shop, Armory, Roster, and Formation open and close.
|
||||
- Chapters, Shop, Armory, Roster, and Formation open and close; completed or current Chapter entries can open a battle briefing.
|
||||
- A player unit can move, attack, wait, use a tactic, use an item, and equip compatible gear.
|
||||
- Enemy turn advances and AI acts.
|
||||
- Victory and defeat panels still appear.
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
- Party roster persistence has a first save-file skeleton.
|
||||
- Level, experience, and equipment persistence.
|
||||
- Chapter overview exists in pre-battle briefing. Chapter selection and manual save/load are still planned.
|
||||
- Chapter overview exists in pre-battle briefing and can open completed or current battles. Manual save/load is still planned.
|
||||
- Linear sixty-seven-scenario campaign order exists with chapter ranges for story arcs.
|
||||
- Save reset exists.
|
||||
- Victory rewards, consumable counts, equipment stock, and equipped gear persist.
|
||||
@@ -134,7 +134,7 @@
|
||||
- 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.
|
||||
- Chapter UI and visual novel scenes. Pre-battle briefing now uses campaign chapter ranges for chapter-aware numbering, has a chapter progress overview, and dialogue lines can choose left or right portrait placement.
|
||||
- Chapter UI and visual novel scenes. Pre-battle briefing now uses campaign chapter ranges for chapter-aware numbering, has a chapter progress/replay overview, and dialogue lines can choose left or right portrait placement.
|
||||
|
||||
## Milestone 6: Content Expansion
|
||||
|
||||
|
||||
@@ -204,6 +204,25 @@ func apply_battle_result(battle_state) -> Dictionary:
|
||||
var already_completed := completed_scenarios.has(battle_state.battle_id)
|
||||
var rewards := battle_state.get_rewards()
|
||||
var post_battle_choices := battle_state.get_post_battle_choices()
|
||||
if already_completed:
|
||||
var return_scenario_id := current_scenario_id if not is_campaign_complete() else ""
|
||||
return {
|
||||
"saved": true,
|
||||
"rewards_applied": false,
|
||||
"already_completed": true,
|
||||
"gold": 0,
|
||||
"items": [],
|
||||
"joined_officers": [],
|
||||
"left_officers": [],
|
||||
"progression_events": [],
|
||||
"next_scenario_id": return_scenario_id,
|
||||
"next_scenario_title": get_scenario_title(return_scenario_id),
|
||||
"scenario_id": battle_state.battle_id,
|
||||
"campaign_complete": return_scenario_id.is_empty(),
|
||||
"post_battle_choices": [],
|
||||
"choice_applied": true,
|
||||
"choice_label": ""
|
||||
}
|
||||
var previous_completed := completed_scenarios.duplicate()
|
||||
var previous_gold := gold
|
||||
var previous_inventory := inventory.duplicate(true)
|
||||
@@ -217,22 +236,20 @@ func apply_battle_result(battle_state) -> Dictionary:
|
||||
var reward_gold := 0
|
||||
var next_scenario_id := get_next_scenario_id(battle_state.battle_id)
|
||||
|
||||
if not already_completed:
|
||||
roster = battle_state.get_player_roster_snapshot()
|
||||
inventory = battle_state.get_inventory_snapshot()
|
||||
roster = battle_state.get_player_roster_snapshot()
|
||||
inventory = battle_state.get_inventory_snapshot()
|
||||
|
||||
if not already_completed:
|
||||
completed_scenarios.append(battle_state.battle_id)
|
||||
reward_gold = int(rewards.get("gold", 0))
|
||||
gold += reward_gold
|
||||
completed_scenarios.append(battle_state.battle_id)
|
||||
reward_gold = int(rewards.get("gold", 0))
|
||||
gold += reward_gold
|
||||
|
||||
for item_id in rewards.get("items", []):
|
||||
var key := str(item_id)
|
||||
inventory[key] = int(inventory.get(key, 0)) + 1
|
||||
rewarded_items.append(key)
|
||||
joined_reward_officers = _apply_join_officers(rewards.get("join_officers", []))
|
||||
left_reward_officers = _apply_leave_officers(rewards.get("leave_officers", []))
|
||||
pending_post_battle_choice_scenario_id = battle_state.battle_id if not post_battle_choices.is_empty() else ""
|
||||
for item_id in rewards.get("items", []):
|
||||
var key := str(item_id)
|
||||
inventory[key] = int(inventory.get(key, 0)) + 1
|
||||
rewarded_items.append(key)
|
||||
joined_reward_officers = _apply_join_officers(rewards.get("join_officers", []))
|
||||
left_reward_officers = _apply_leave_officers(rewards.get("leave_officers", []))
|
||||
pending_post_battle_choice_scenario_id = battle_state.battle_id if not post_battle_choices.is_empty() else ""
|
||||
|
||||
if not next_scenario_id.is_empty():
|
||||
current_scenario_id = next_scenario_id
|
||||
@@ -259,19 +276,19 @@ func apply_battle_result(battle_state) -> Dictionary:
|
||||
|
||||
return {
|
||||
"saved": saved,
|
||||
"rewards_applied": not already_completed and saved,
|
||||
"already_completed": already_completed,
|
||||
"rewards_applied": saved,
|
||||
"already_completed": false,
|
||||
"gold": reward_gold,
|
||||
"items": rewarded_items,
|
||||
"joined_officers": joined_reward_officers,
|
||||
"left_officers": left_reward_officers,
|
||||
"progression_events": battle_state.get_progression_events() if saved and not already_completed else [],
|
||||
"progression_events": battle_state.get_progression_events() if saved else [],
|
||||
"next_scenario_id": next_scenario_id,
|
||||
"next_scenario_title": get_scenario_title(next_scenario_id),
|
||||
"scenario_id": battle_state.battle_id,
|
||||
"campaign_complete": next_scenario_id.is_empty(),
|
||||
"post_battle_choices": post_battle_choices if saved and not already_completed else [],
|
||||
"choice_applied": not saved or already_completed or post_battle_choices.is_empty(),
|
||||
"post_battle_choices": post_battle_choices if saved else [],
|
||||
"choice_applied": not saved or post_battle_choices.is_empty(),
|
||||
"choice_label": ""
|
||||
}
|
||||
|
||||
@@ -658,12 +675,24 @@ func get_chapter_progress_entries() -> Array:
|
||||
var completed_count := 0
|
||||
var current_in_chapter := false
|
||||
var current_scenario_title := ""
|
||||
var scenarios := []
|
||||
for scenario_id in scenario_ids:
|
||||
var completed := completed_scenarios.has(scenario_id)
|
||||
var current := scenario_id == current_scenario_id
|
||||
if completed_scenarios.has(scenario_id):
|
||||
completed_count += 1
|
||||
if scenario_id == current_scenario_id:
|
||||
if current:
|
||||
current_in_chapter = true
|
||||
current_scenario_title = get_scenario_title(scenario_id)
|
||||
scenarios.append({
|
||||
"id": scenario_id,
|
||||
"title": get_scenario_title(scenario_id),
|
||||
"position": get_scenario_position_in_chapter(scenario_id),
|
||||
"total": scenario_ids.size(),
|
||||
"completed": completed,
|
||||
"current": current,
|
||||
"available": completed or current
|
||||
})
|
||||
entries.append({
|
||||
"id": chapter_id,
|
||||
"title": str(chapter_titles.get(chapter_id, chapter_id)),
|
||||
@@ -672,7 +701,8 @@ func get_chapter_progress_entries() -> Array:
|
||||
"completed": completed_count,
|
||||
"total": scenario_ids.size(),
|
||||
"current": current_in_chapter,
|
||||
"current_scenario_title": current_scenario_title
|
||||
"current_scenario_title": current_scenario_title,
|
||||
"scenarios": scenarios
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
@@ -2045,6 +2045,48 @@ func _rebuild_chapter_menu() -> void:
|
||||
chapter_label.custom_minimum_size = Vector2(560, 0)
|
||||
chapter_label.text = line
|
||||
chapter_list.add_child(chapter_label)
|
||||
for scenario in entry.get("scenarios", []):
|
||||
_add_chapter_scenario_entry(scenario)
|
||||
|
||||
|
||||
func _add_chapter_scenario_entry(scenario: Dictionary) -> void:
|
||||
var scenario_id := str(scenario.get("id", ""))
|
||||
var title := str(scenario.get("title", scenario_id))
|
||||
var position := int(scenario.get("position", 0))
|
||||
var total := int(scenario.get("total", 0))
|
||||
var completed := bool(scenario.get("completed", false))
|
||||
var current := bool(scenario.get("current", false))
|
||||
var available := bool(scenario.get("available", false))
|
||||
var status := "Current" if current else ("Replay" if completed else "Upcoming")
|
||||
var text := " %02d/%02d %s [%s]" % [position, total, title, status]
|
||||
if available:
|
||||
var scenario_button := Button.new()
|
||||
scenario_button.custom_minimum_size = Vector2(560, 0)
|
||||
scenario_button.text = text
|
||||
scenario_button.pressed.connect(_on_chapter_scenario_pressed.bind(scenario_id))
|
||||
chapter_list.add_child(scenario_button)
|
||||
else:
|
||||
var scenario_label := Label.new()
|
||||
scenario_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
scenario_label.custom_minimum_size = Vector2(560, 0)
|
||||
scenario_label.text = text
|
||||
chapter_list.add_child(scenario_label)
|
||||
|
||||
|
||||
func _on_chapter_scenario_pressed(scenario_id: String) -> void:
|
||||
if battle_started or briefing_panel == null or not briefing_panel.visible:
|
||||
return
|
||||
if scenario_id.is_empty():
|
||||
return
|
||||
if not campaign_state.is_scenario_completed(scenario_id) and scenario_id != campaign_state.current_scenario_id:
|
||||
_play_ui_cancel()
|
||||
return
|
||||
_play_ui_confirm()
|
||||
if log_box != null:
|
||||
log_box.clear()
|
||||
_load_scenario(scenario_id)
|
||||
_show_briefing()
|
||||
_update_hud()
|
||||
|
||||
|
||||
func _on_shop_pressed() -> void:
|
||||
@@ -2691,7 +2733,7 @@ func _format_battle_result_summary() -> String:
|
||||
pending_text += "\nNext: %s" % pending_next
|
||||
return pending_text
|
||||
if battle_result_summary.get("already_completed", false):
|
||||
var already_saved := "Campaign saved." if battle_result_summary.get("saved", false) else "Campaign save failed."
|
||||
var already_saved := "Campaign unchanged." if battle_result_summary.get("saved", false) else "Campaign replay failed."
|
||||
var already_next := str(battle_result_summary.get("next_scenario_title", ""))
|
||||
if already_next.is_empty():
|
||||
return "Rewards already claimed.\n%s" % already_saved
|
||||
|
||||
Reference in New Issue
Block a user