Add event gold pickups
This commit is contained in:
@@ -98,7 +98,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
||||
- Scenario post-battle dialogue can play before the victory result panel.
|
||||
- Basic battle EXP, level-ups, and core first-tier class promotion routes.
|
||||
- Victory results split rewards, roster changes, saved growth, campaign status, and pending choices into readable sections.
|
||||
- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, unit-defeat reactions, `after_event` prerequisites, objective updates with HUD notices, inventory pickups, reinforcements, scripted unit withdrawals, and AI target-priority shifts.
|
||||
- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, unit-defeat reactions, `after_event` prerequisites, objective updates with HUD notices, item and gold pickups, reinforcements, scripted unit withdrawals, and AI target-priority shifts.
|
||||
- Scenario briefing before battle with a campaign battle header and objective summary.
|
||||
- Victory rewards and consumed inventory saved into a campaign state.
|
||||
- Scenario-defined victory and defeat conditions.
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
]
|
||||
},
|
||||
{ "type": "log", "text": "Usable Wuchao stores are seized before the granaries burn." },
|
||||
{ "type": "grant_gold", "amount": 200, "text": "Wuchao's captured pay chest yields 200 gold." },
|
||||
{
|
||||
"type": "grant_items",
|
||||
"items": [
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
8. Player ends turn.
|
||||
9. Enemy AI moves, picks physical attacks from damage, defeat, priority, and weapon-effectiveness scores, and can cast single-target or area damage, healing, support, debuff, movement-modifier, accuracy/evasion-modifier, damage-over-time, or action-lock 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, movement-triggered, and unit-defeat scenario events run as phases change, units enter marked cells, or units fall, can wait for prerequisite events, and may update objectives with HUD notices, grant inventory pickups, deploy reinforcements, withdraw units from the battlefield, or change AI target priorities.
|
||||
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`.
|
||||
11. Turn-start, movement-triggered, and unit-defeat scenario events run as phases change, units enter marked cells, or units fall, can wait for prerequisite events, and may update objectives with HUD notices, grant item or gold pickups, deploy reinforcements, withdraw units from the battlefield, or change AI target priorities.
|
||||
12. On first-time victory, `CampaignState` snapshots player progression, battle inventory, and battle-only gold pickups, 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 splits rewards, roster changes, saved player growth, campaign status, and pending choices into sections.
|
||||
15. If the scenario defines post-battle choices, the result panel waits for the player to select one, previews choice rewards including officer join/leave effects, then saves the selected flags and optional choice rewards before enabling the next battle.
|
||||
@@ -57,8 +57,8 @@
|
||||
|
||||
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 and can load completed or current battles without mutating the save. Briefings preview first-clear rewards and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts 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, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. 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, and can be expressed as one `pos` or a multi-cell `cells` list. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP or cure matching poison/seal/snare/disarm statuses, and in-battle item use, event pickup grants, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up 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. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, save writes, current-scenario advancement, or finite-stock purchases; pre-battle Shop, Armory, Roster, Formation, and Save 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. Briefings preview first-clear rewards and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts 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, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. 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, and can be expressed as one `pos` or a multi-cell `cells` list. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP or cure matching poison/seal/snare/disarm statuses, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, 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. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, event gold pickups, save writes, current-scenario advancement, or finite-stock purchases; pre-battle Shop, Armory, Roster, Formation, and Save 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, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, can place the portrait on the left or right side of that panel, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures for the selected unit or hovered unit, board units render low-HP warning rings, HP bar color states, and active support/debuff/poison/seal/snare/disarm status pips from battle state fields, hover previews are rendered as target badges from existing `BattleState` forecast APIs, hover unit info shows class, movement type, AGI, movement, range, and equipped gear, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item pickup effects use distinct feedback, `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.
|
||||
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, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, can place the portrait on the left or right side of that panel, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures for the selected unit or hovered unit, board units render low-HP warning rings, HP bar color states, and active support/debuff/poison/seal/snare/disarm status pips from battle state fields, hover previews are rendered as target badges from existing `BattleState` forecast APIs, hover unit info shows class, movement type, AGI, movement, range, and equipped gear, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `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.
|
||||
|
||||
The `New Campaign` button clears `user://campaign_save.json`, resets campaign state to the first scenario, and reloads the opening briefing.
|
||||
|
||||
@@ -465,7 +465,7 @@ Conditions may include `after_event` to stay inactive until a one-shot event has
|
||||
|
||||
`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`, movement-triggered `unit_reaches_tile`, and defeat-triggered `unit_defeated` triggers. Each event needs a stable lowercase `id` that is unique within the scenario; condition and event `after_event` gates can only reference those explicit ids. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell or any zero-based entry in `cells`; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. `unit_defeated` events run after a deployed unit is marked defeated and before final victory/defeat checks; use `team`, `unit_ids`, or `officer_ids` to limit which fallen unit can trigger the event. Event `when` blocks may include `after_event` to wait until an earlier one-shot scenario beat has fired, which is useful for dialogue or follow-up pressure that only makes sense after a destination, ambush, gate, fire event, or named-unit defeat. Actions currently include `log`, `dialogue`, `set_objective`, `grant_item`, `grant_items`, `spawn_deployment`, `spawn_deployments`, `withdraw_unit`, `withdraw_units`, and `set_ai_target_priority`. `set_objective` updates visible objective text, emits concrete log lines for changed victory/defeat text, and raises an objective-update signal so the battle scene can refresh the HUD and show a short notice. `set_ai_target_priority` accepts `unit_id` or `unit_ids`, a `priority` from 0 to 20, optional `text`, and optional `silent`; it changes enemy AI target scoring immediately and, unless silent, logs the change plus shows target-focus floating text. `withdraw_unit` accepts `unit_id` or `id`, while `withdraw_units` accepts a `unit_ids` array; living deployed targets leave the battlefield without being marked defeated, so they are removed from map occupancy and living-unit counts while protected-unit defeat checks still distinguish withdrawal from death. Event `dialogue` actions use the same dialogue line format, portrait default rules, and optional `side` placement as `post_battle_dialogue`. Event item grants add to the battle inventory immediately; they persist to the campaign save only when the battle is won.
|
||||
Scenario `events` support `battle_start`, `battle_begin`, `turn_start`, movement-triggered `unit_reaches_tile`, and defeat-triggered `unit_defeated` triggers. Each event needs a stable lowercase `id` that is unique within the scenario; condition and event `after_event` gates can only reference those explicit ids. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell or any zero-based entry in `cells`; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. `unit_defeated` events run after a deployed unit is marked defeated and before final victory/defeat checks; use `team`, `unit_ids`, or `officer_ids` to limit which fallen unit can trigger the event. Event `when` blocks may include `after_event` to wait until an earlier one-shot scenario beat has fired, which is useful for dialogue or follow-up pressure that only makes sense after a destination, ambush, gate, fire event, or named-unit defeat. Actions currently include `log`, `dialogue`, `set_objective`, `grant_item`, `grant_items`, `grant_gold`, `spawn_deployment`, `spawn_deployments`, `withdraw_unit`, `withdraw_units`, and `set_ai_target_priority`. `set_objective` updates visible objective text, emits concrete log lines for changed victory/defeat text, and raises an objective-update signal so the battle scene can refresh the HUD and show a short notice. `grant_gold` accepts a positive `amount` and optional non-empty `text`, adds battle-only gold, and persists to the campaign save only on first-time victory. `set_ai_target_priority` accepts `unit_id` or `unit_ids`, a `priority` from 0 to 20, optional `text`, and optional `silent`; it changes enemy AI target scoring immediately and, unless silent, logs the change plus shows target-focus floating text. `withdraw_unit` accepts `unit_id` or `id`, while `withdraw_units` accepts a `unit_ids` array; living deployed targets leave the battlefield without being marked defeated, so they are removed from map occupancy and living-unit counts while protected-unit defeat checks still distinguish withdrawal from death. Event `dialogue` actions use the same dialogue line format, portrait default rules, and optional `side` placement as `post_battle_dialogue`. Event item grants add to the battle inventory immediately; event gold grants add to the battle reward pool. Both persist to the campaign save only when the battle is won.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -565,6 +565,19 @@ Movement-triggered pickups can use `grant_item` for one item or `grant_items` fo
|
||||
}
|
||||
```
|
||||
|
||||
Movement-triggered gold caches can use `grant_gold` with a positive `amount`. The gold is shown immediately through log and floating feedback, but it stays battle-only until victory commits the battle result.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "captured_pay_chest",
|
||||
"once": true,
|
||||
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [14, 4] },
|
||||
"actions": [
|
||||
{ "type": "grant_gold", "amount": 200, "text": "The captured pay chest yields 200 gold." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`briefing.conditional_lines`, `shop.conditional_items`, and `when.campaign_flags` all use exact flag matching. When present, every listed flag must match the saved campaign flag value for that block or event to apply. Use separate blocks or events for OR-style branches. The validator requires referenced flags to have been introduced by an earlier `post_battle_choices.set_flags` entry in campaign order.
|
||||
|
||||
Event-spawned units use the same compact deployment shape as scenario starting units. Spawn attempts are skipped if the unit id already exists, the target cell is occupied, the cell is outside the map, or the unit cannot stand on that terrain. Reinforcements that arrive during their own team's phase wait until their next phase before acting.
|
||||
@@ -577,7 +590,7 @@ Event-spawned units use the same compact deployment shape as scenario starting u
|
||||
|
||||
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, event item pickups, equipment swaps, and unequipped gear returned to stock 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.
|
||||
During a battle, item consumption, event item pickups, equipment swaps, and unequipped gear returned to stock affect `BattleState.battle_inventory`, while event gold pickups accumulate in a separate battle-only reward pool. `CampaignState.apply_battle_result()` commits that inventory snapshot and battle gold 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.
|
||||
|
||||
`BattleState` records player-only `progression_events` when a unit levels up or promotes. `CampaignState.apply_battle_result()` includes those events in the victory summary only when the first-time victory save succeeds, so completed-scenario replays and failed saves do not present unsaved growth as campaign progress. The result panel presents saved rewards, roster changes, growth, campaign status, and pending choices as separate sections; failed saves explicitly show rewards as not applied.
|
||||
|
||||
@@ -587,4 +600,4 @@ Before a battle starts, `BattleScene` can buy priced items through `CampaignStat
|
||||
|
||||
When every campaign scenario id is present in `completed_scenarios`, the battle scene shows a campaign completion panel. Starting a new campaign clears the automatic save file and resets `current_scenario_id` to `start_scenario`; the manual checkpoint is left alone until overwritten from the Save menu.
|
||||
|
||||
Run `tools/validate_data.ps1` after data edits to check campaign paths, chapter ranges, map dimensions, terrain keys, class/officer/item/skill references, skill area shapes, skill status/action-lock effect shapes, item effect names and cure statuses, officer and deployment portraits, officer and deployment equipment slot compatibility including accessory types and weapon effectiveness fields, promotion routes and promotion equipment compatibility, condition/event names, event `after_event` references, event trigger unit/officer filters, event item grants, event withdrawal unit ids, event AI target-priority unit ids, condition unit references, destination `pos`/`cells` coordinates, campaign flag references, joined-officer gates, briefing line blocks, deployment ids, deployment bounds, impassable spawn cells, shop stock, roster rules, formation cells, post-battle dialogue, post-battle choices, and reward item ids.
|
||||
Run `tools/validate_data.ps1` after data edits to check campaign paths, chapter ranges, map dimensions, terrain keys, class/officer/item/skill references, skill area shapes, skill status/action-lock effect shapes, item effect names and cure statuses, officer and deployment portraits, officer and deployment equipment slot compatibility including accessory types and weapon effectiveness fields, promotion routes and promotion equipment compatibility, condition/event names, event `after_event` references, event trigger unit/officer filters, event item/gold grants, event withdrawal unit ids, event AI target-priority unit ids, condition unit references, destination `pos`/`cells` coordinates, campaign flag references, joined-officer gates, briefing line blocks, deployment ids, deployment bounds, impassable spawn cells, shop stock, roster rules, formation cells, post-battle dialogue, post-battle choices, and reward item ids.
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
- Mid-battle map/action events have a first action system with movement ambushes and scripted unit withdrawals.
|
||||
- Opening battle dialogue exists through event actions.
|
||||
- Post-battle dialogue exists before the victory result panel.
|
||||
- Reinforcements, scripted unit withdrawals, objective changes with HUD/log feedback, and inventory pickup events exist in a basic form.
|
||||
- Reinforcements, scripted unit withdrawals, objective changes with HUD/log feedback, and item/gold pickup events exist in a basic form.
|
||||
- Post-battle reward save, campaign choice flags, first flag-driven briefing/shop/event branches, sectioned result summaries, and choice reward previews exist in a basic form. Richer reward screens and wider branch support are still planned.
|
||||
|
||||
## Milestone 4: Campaign Progression
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
- Portrait pipeline. Officer definitions can provide default portrait paths, dialogue lines can override them, battle HUD thumbnails reuse them for selected or hovered units, missing art falls back to speaker initials, and the first seven AI-generated photorealistic officer portraits are in `art/portraits`.
|
||||
- Unit sprites and animations. Board movement now has a short slide animation, and board units show low-HP warning rings plus small status pips for active support, debuff, poison, seal, snare, and disarm effects, with HUD summaries showing remaining phases.
|
||||
- Battle effects. First floating combat text now covers damage, recovery, misses, support effects, poison ticks, item pickups, AI target-focus changes, level-ups, and promotions. Hover target preview badges summarize attacks, tactics, and items, and the board can overlay enemy threat ranges with physical damage and hostile tactic hints.
|
||||
- Battle effects. First floating combat text now covers damage, recovery, misses, support effects, poison ticks, item and gold pickups, AI target-focus changes, level-ups, and promotions. Hover target preview badges summarize attacks, tactics, and items, and the board can overlay enemy threat ranges with physical damage and hostile tactic hints.
|
||||
- Music and sound. Basic placeholder BGM and SFX now play for menus, battle flow, unit actions, tactics, items, and result states.
|
||||
- Chapter UI, objective presentation, and visual novel scenes. Pre-battle briefing now uses campaign chapter ranges for chapter-aware numbering, has a chapter progress/replay overview plus objective/progress/risk tracker, active battle HUD shows the same live objective panel, and dialogue lines can choose left or right portrait placement in an expanded portrait dialogue panel with progress and previous-line controls.
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ var battle_events: Array[Dictionary] = []
|
||||
var fired_event_ids := {}
|
||||
var roster_overrides_snapshot := {}
|
||||
var battle_inventory := {}
|
||||
var battle_gold_reward := 0
|
||||
var campaign_flags := {}
|
||||
var campaign_joined_officers := {}
|
||||
var progression_events: Array[Dictionary] = []
|
||||
@@ -170,6 +171,7 @@ func _apply_battle_data(data: Dictionary, roster_overrides := {}) -> void:
|
||||
if typeof(event) == TYPE_DICTIONARY:
|
||||
battle_events.append(event)
|
||||
fired_event_ids.clear()
|
||||
battle_gold_reward = 0
|
||||
progression_events.clear()
|
||||
|
||||
var map_data: Dictionary = data.get("map", {})
|
||||
@@ -1824,6 +1826,10 @@ func set_inventory_snapshot(inventory_snapshot: Dictionary) -> void:
|
||||
_notify_changed()
|
||||
|
||||
|
||||
func get_battle_gold_reward() -> int:
|
||||
return max(0, battle_gold_reward)
|
||||
|
||||
|
||||
func get_terrain_key(cell: Vector2i) -> String:
|
||||
if not is_inside(cell):
|
||||
return "G"
|
||||
@@ -3774,6 +3780,8 @@ func _execute_event_action(action: Dictionary, trigger_unit: Dictionary = {}) ->
|
||||
_grant_event_item(action, trigger_unit)
|
||||
elif action_type == "grant_items":
|
||||
_grant_event_items(action.get("items", []), trigger_unit)
|
||||
elif action_type == "grant_gold":
|
||||
_grant_event_gold(action, trigger_unit)
|
||||
elif action_type == "spawn_deployment":
|
||||
_spawn_event_deployment(action.get("deployment", {}))
|
||||
elif action_type == "spawn_deployments":
|
||||
@@ -3828,6 +3836,21 @@ func _grant_event_item(action: Dictionary, trigger_unit: Dictionary = {}) -> boo
|
||||
return true
|
||||
|
||||
|
||||
func _grant_event_gold(action: Dictionary, trigger_unit: Dictionary = {}) -> bool:
|
||||
var amount := maxi(0, int(action.get("amount", action.get("gold", 0))))
|
||||
if amount <= 0:
|
||||
push_error("Skipping event gold grant with invalid amount in %s." % battle_id)
|
||||
return false
|
||||
battle_gold_reward += amount
|
||||
var text := str(action.get("text", "")).strip_edges()
|
||||
if text.is_empty():
|
||||
text = "Received %d gold." % amount
|
||||
_emit_log(text)
|
||||
if not trigger_unit.is_empty() and bool(trigger_unit.get("alive", false)):
|
||||
_emit_combat_feedback(trigger_unit, "+%dG" % amount, "gold")
|
||||
return true
|
||||
|
||||
|
||||
func _set_event_ai_target_priority(action: Dictionary) -> void:
|
||||
var unit_ids := _event_action_unit_ids(action)
|
||||
if unit_ids.is_empty():
|
||||
|
||||
@@ -278,7 +278,7 @@ func apply_battle_result(battle_state) -> Dictionary:
|
||||
inventory = battle_state.get_inventory_snapshot()
|
||||
|
||||
completed_scenarios.append(battle_state.battle_id)
|
||||
reward_gold = int(rewards.get("gold", 0))
|
||||
reward_gold = int(rewards.get("gold", 0)) + int(battle_state.get_battle_gold_reward())
|
||||
gold += reward_gold
|
||||
|
||||
for item_id in rewards.get("items", []):
|
||||
|
||||
105
tools/smoke_event_gold_pickups.gd
Normal file
105
tools/smoke_event_gold_pickups.gd
Normal file
@@ -0,0 +1,105 @@
|
||||
extends SceneTree
|
||||
|
||||
const BattleStateScript := preload("res://scripts/core/battle_state.gd")
|
||||
const CampaignStateScript := preload("res://scripts/core/campaign_state.gd")
|
||||
|
||||
const CAMPAIGN_PATH := "res://data/campaign/campaign.json"
|
||||
const SAVE_PATH := "user://campaign_save.json"
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var failures: Array[String] = []
|
||||
var save_existed := FileAccess.file_exists(SAVE_PATH)
|
||||
var save_text := FileAccess.get_file_as_string(SAVE_PATH) if save_existed else ""
|
||||
|
||||
_check_wuchao_gold_pickup_persists_on_victory(failures)
|
||||
_restore_save(save_existed, save_text, failures)
|
||||
|
||||
if failures.is_empty():
|
||||
print("event gold pickups smoke ok")
|
||||
quit(0)
|
||||
return
|
||||
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _check_wuchao_gold_pickup_persists_on_victory(failures: Array[String]) -> void:
|
||||
var campaign_state = CampaignStateScript.new()
|
||||
if not campaign_state.load_campaign(CAMPAIGN_PATH):
|
||||
failures.append("could not load campaign data")
|
||||
return
|
||||
campaign_state.start_new(campaign_state.get_start_scenario_id())
|
||||
|
||||
var state = BattleStateScript.new()
|
||||
if not state.load_battle(
|
||||
campaign_state.get_scenario_path("013_wuchao_raid"),
|
||||
campaign_state.get_roster_overrides(),
|
||||
campaign_state.get_inventory_snapshot(),
|
||||
campaign_state.get_flags_snapshot(),
|
||||
campaign_state.get_joined_officers_snapshot()
|
||||
):
|
||||
failures.append("could not load Wuchao raid")
|
||||
return
|
||||
|
||||
var logs: Array[String] = []
|
||||
var feedback: Array[String] = []
|
||||
state.log_added.connect(func(message: String) -> void:
|
||||
logs.append(message)
|
||||
)
|
||||
state.combat_feedback_requested.connect(func(unit_id: String, text: String, kind: String) -> void:
|
||||
feedback.append("%s|%s|%s" % [unit_id, text, kind])
|
||||
)
|
||||
|
||||
var cao_cao: Dictionary = state.get_unit("cao_cao_ch13")
|
||||
if cao_cao.is_empty():
|
||||
failures.append("missing Cao Cao deployment in Wuchao raid")
|
||||
return
|
||||
|
||||
cao_cao["pos"] = Vector2i(14, 4)
|
||||
state._run_events("unit_reaches_tile", "player", 1, cao_cao)
|
||||
|
||||
if state.get_battle_gold_reward() != 200:
|
||||
failures.append("Wuchao depot should add 200 battle gold, got %d" % state.get_battle_gold_reward())
|
||||
if not logs.has("Wuchao's captured pay chest yields 200 gold."):
|
||||
failures.append("Wuchao gold pickup log missing")
|
||||
if not feedback.has("cao_cao_ch13|+200G|gold"):
|
||||
failures.append("Wuchao gold pickup feedback missing")
|
||||
|
||||
var active_result: Dictionary = campaign_state.apply_battle_result(state)
|
||||
if bool(active_result.get("saved", false)):
|
||||
failures.append("active battle should not save event gold before victory")
|
||||
if campaign_state.gold != 0:
|
||||
failures.append("campaign gold should stay unchanged before victory, got %d" % campaign_state.gold)
|
||||
|
||||
state.battle_status = "victory"
|
||||
var result: Dictionary = campaign_state.apply_battle_result(state)
|
||||
if not bool(result.get("saved", false)):
|
||||
failures.append("campaign save should succeed after Wuchao victory")
|
||||
if int(result.get("gold", 0)) != 2300:
|
||||
failures.append("Wuchao result should combine base and event gold, got %d" % int(result.get("gold", 0)))
|
||||
if campaign_state.gold != 2300:
|
||||
failures.append("campaign gold should include event gold after victory, got %d" % campaign_state.gold)
|
||||
|
||||
var before_replay_gold := int(campaign_state.gold)
|
||||
var replay_result: Dictionary = campaign_state.apply_battle_result(state)
|
||||
if not bool(replay_result.get("already_completed", false)):
|
||||
failures.append("second Wuchao apply should be treated as completed replay")
|
||||
if campaign_state.gold != before_replay_gold:
|
||||
failures.append("completed replay should not duplicate event gold")
|
||||
|
||||
|
||||
func _restore_save(save_existed: bool, save_text: String, failures: Array[String]) -> void:
|
||||
if save_existed:
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
failures.append("could not restore previous campaign save")
|
||||
return
|
||||
file.store_string(save_text)
|
||||
return
|
||||
|
||||
if FileAccess.file_exists(SAVE_PATH):
|
||||
var error := DirAccess.remove_absolute(ProjectSettings.globalize_path(SAVE_PATH))
|
||||
if error != OK:
|
||||
failures.append("could not remove temporary campaign save")
|
||||
@@ -93,7 +93,7 @@ $validConditionTypes = @(
|
||||
"any"
|
||||
)
|
||||
$validTriggers = @("battle_start", "battle_begin", "turn_start", "unit_reaches_tile", "unit_defeated")
|
||||
$validActions = @("log", "dialogue", "set_objective", "grant_item", "grant_items", "spawn_deployment", "spawn_deployments", "withdraw_unit", "withdraw_units", "set_ai_target_priority")
|
||||
$validActions = @("log", "dialogue", "set_objective", "grant_item", "grant_items", "grant_gold", "spawn_deployment", "spawn_deployments", "withdraw_unit", "withdraw_units", "set_ai_target_priority")
|
||||
$validEffects = @("heal_hp", "heal_mp", "cure_status")
|
||||
$validItemKinds = @("weapon", "armor", "accessory", "consumable")
|
||||
$validItemRarities = @("common", "named")
|
||||
@@ -621,6 +621,22 @@ function Check-Event-Grant-Item($Entry, [string]$Context) {
|
||||
}
|
||||
}
|
||||
|
||||
function Check-Event-Grant-Gold($Action, [string]$Context) {
|
||||
if (-not (Has-Prop $Action "amount")) {
|
||||
Fail "$Context must set amount."
|
||||
}
|
||||
$amountValue = Get-Prop $Action "amount" 0
|
||||
if (-not ($amountValue -is [int] -or $amountValue -is [long])) {
|
||||
Fail "$Context amount must be an integer."
|
||||
}
|
||||
if ([int]$amountValue -le 0) {
|
||||
Fail "$Context amount must be positive."
|
||||
}
|
||||
if ((Has-Prop $Action "text") -and [string]::IsNullOrWhiteSpace([string](Get-Prop $Action "text" ""))) {
|
||||
Fail "$Context text must not be empty."
|
||||
}
|
||||
}
|
||||
|
||||
function Check-Event-Withdraw-Unit-Id([string]$UnitId, $KnownUnitIds, [string]$Context) {
|
||||
if ([string]::IsNullOrWhiteSpace($UnitId)) {
|
||||
Fail "$Context has empty unit id."
|
||||
@@ -1926,6 +1942,9 @@ foreach ($scenario in $campaign.scenarios) {
|
||||
Check-Event-Grant-Item $entry "Scenario $scenarioId event $eventId grant_items"
|
||||
}
|
||||
}
|
||||
if ($actionType -eq "grant_gold") {
|
||||
Check-Event-Grant-Gold $action "Scenario $scenarioId event $eventId grant_gold"
|
||||
}
|
||||
if ($actionType -eq "withdraw_unit") {
|
||||
$unitId = [string](Get-Prop $action "unit_id" (Get-Prop $action "id" ""))
|
||||
Check-Event-Withdraw-Unit-Id $unitId $knownUnitIds "Scenario $scenarioId event $eventId withdraw_unit"
|
||||
|
||||
Reference in New Issue
Block a user