From c062fbc8323fef0822da20321b2920a6c9271e73 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 11:58:47 +0900 Subject: [PATCH] Add named equipment rewards --- README.md | 1 + data/defs/items.json | 45 ++++++++++++++++++++++++ data/scenarios/012_guandu_showdown.json | 2 +- data/scenarios/013_wuchao_raid.json | 2 +- data/scenarios/014_cangting_pursuit.json | 2 +- data/scenarios/015_ye_campaign.json | 2 +- docs/ARCHITECTURE.md | 2 +- docs/DATA_MODEL.md | 4 +-- docs/GODOT_4_6_MIGRATION.md | 1 + docs/ROADMAP.md | 2 +- scripts/scenes/battle_scene.gd | 10 ++++++ tools/validate_data.ps1 | 7 ++++ 12 files changed, 72 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7241f79..bd5641b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - MP tactics with low/high damage options, area damage/healing/status options, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare/disarm action locks, a skill list menu, previews, and target overlays. - Campaign inventory consumables with an item menu, HP/MP recovery, poison/seal/snare/disarm cure previews, and target overlay. - Weapon, armor, and accessory rewards can be stored, displayed, and equipped from the battle HUD; some weapons deal bonus physical damage against matching move types. +- First named equipment rewards exist in the northern campaign arc and show a `Named` tag in shop, Armory, and equipment menus. - Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory. - Pre-battle Armory equipment changes save immediately and sync roster equipment plus inventory stock. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits. diff --git a/data/defs/items.json b/data/defs/items.json index 5166cb0..e7665e1 100644 --- a/data/defs/items.json +++ b/data/defs/items.json @@ -101,6 +101,51 @@ "effects": [], "price": 900 }, + "yitian_sword": { + "name": "Yitian Sword", + "kind": "weapon", + "rarity": "named", + "weapon_type": "sword", + "range": [1, 1], + "bonuses": { "atk": 7, "int": 1 }, + "effective_vs_move_types": ["foot"], + "effective_bonus_damage": 2, + "effects": [], + "price": 1300 + }, + "dragon_spear": { + "name": "Dragon Spear", + "kind": "weapon", + "rarity": "named", + "weapon_type": "spear", + "range": [1, 1], + "bonuses": { "atk": 7, "agi": 1 }, + "effective_vs_move_types": ["mounted"], + "effective_bonus_damage": 4, + "effects": [], + "price": 1400 + }, + "wind_chaser_bow": { + "name": "Wind Chaser Bow", + "kind": "weapon", + "rarity": "named", + "weapon_type": "bow", + "range": [2, 2], + "bonuses": { "atk": 5, "agi": 2 }, + "effective_vs_move_types": ["water"], + "effective_bonus_damage": 3, + "effects": [], + "price": 1200 + }, + "black_tortoise_robe": { + "name": "Black Tortoise Robe", + "kind": "armor", + "rarity": "named", + "armor_type": "robe", + "bonuses": { "mp": 2, "def": 3, "int": 1 }, + "effects": [], + "price": 1100 + }, "bean": { "name": "Bean", "kind": "consumable", diff --git a/data/scenarios/012_guandu_showdown.json b/data/scenarios/012_guandu_showdown.json index 0fa0288..5c25524 100644 --- a/data/scenarios/012_guandu_showdown.json +++ b/data/scenarios/012_guandu_showdown.json @@ -367,7 +367,7 @@ ], "rewards": { "gold": 1900, - "items": ["bean", "wine", "imperial_seal"] + "items": ["bean", "wine", "imperial_seal", "yitian_sword"] }, "post_battle_dialogue": [ { "speaker": "Xiahou Dun", "text": "Yuan Shao's first great push is broken, but his camps still fill the horizon." }, diff --git a/data/scenarios/013_wuchao_raid.json b/data/scenarios/013_wuchao_raid.json index 868e7d7..a0cd5b4 100644 --- a/data/scenarios/013_wuchao_raid.json +++ b/data/scenarios/013_wuchao_raid.json @@ -312,7 +312,7 @@ ], "rewards": { "gold": 2100, - "items": ["bean", "wine", "war_drum"] + "items": ["bean", "wine", "war_drum", "black_tortoise_robe"] }, "post_battle_dialogue": [ { "speaker": "Xiahou Dun", "text": "Wuchao burns. Yuan Shao's army will wake hungry." }, diff --git a/data/scenarios/014_cangting_pursuit.json b/data/scenarios/014_cangting_pursuit.json index 2b7974b..5a0676a 100644 --- a/data/scenarios/014_cangting_pursuit.json +++ b/data/scenarios/014_cangting_pursuit.json @@ -344,7 +344,7 @@ ], "rewards": { "gold": 2300, - "items": ["bean", "wine", "iron_armor"], + "items": ["bean", "wine", "iron_armor", "dragon_spear"], "join_officers": ["zhang_he"] }, "post_battle_dialogue": [ diff --git a/data/scenarios/015_ye_campaign.json b/data/scenarios/015_ye_campaign.json index 7495fd0..97fd363 100644 --- a/data/scenarios/015_ye_campaign.json +++ b/data/scenarios/015_ye_campaign.json @@ -351,7 +351,7 @@ ], "rewards": { "gold": 2500, - "items": ["bean", "wine", "war_axe"] + "items": ["bean", "wine", "war_axe", "wind_chaser_bow"] }, "post_battle_dialogue": [ { "speaker": "Zhang He", "text": "Ye's outer defense is broken. The heirs will blame each other before they blame us." }, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 27a8f68..d350967 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 and can load completed or current battles without mutating the save. 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. 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 and pickup events 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, item pickups, and reinforcements from saved campaign choices. 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. 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. 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. 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 and pickup events 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, item pickups, and reinforcements from saved campaign choices. 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. 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, 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, 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, and item pickup effects use distinct floating feedback colors, `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. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index be3a7ba..c94acda 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -302,7 +302,7 @@ Physical hit chance is `90 + attacker AGI - target AGI - terrain avoid`, clamped ## Items -Items start as static bonuses and consumable effects. Equipment leveling should later live on save-game item instances, not base item definitions. Consumables and unequipped weapon, armor, and accessory rewards are stored as campaign inventory counts and copied into the battle state when a scenario loads. +Items start as static bonuses and consumable effects. Equipment leveling should later live on save-game item instances, not base item definitions. Consumables and unequipped weapon, armor, and accessory rewards are stored as campaign inventory counts and copied into the battle state when a scenario loads. Equipment can optionally set `rarity: "named"` to mark story-tier rewards while still using the same stackable inventory model as other equipment. ```json { @@ -318,7 +318,7 @@ Items start as static bonuses and consumable effects. Equipment leveling should } ``` -Weapon `range` also uses `[min, max]`. When class and equipment both provide range, the runtime unit keeps the lowest minimum and highest maximum. Weapons can optionally define `effective_vs_move_types` plus `effective_bonus_damage` to add a small physical damage bonus during combat against targets with a matching runtime `move_type`; this is shown in attack forecasts, combat logs, shop stock, and equipment menus, but it does not mutate saved unit stats. The first examples are spear vs mounted, bow vs water, and axe vs foot. Accessories use `kind: "accessory"` with an `accessory_type` checked against the class `equipment_slots.accessory` list; `war_drum` and `imperial_seal` are the first branch-choice accessory rewards. Current equipment inventory is stackable stock only; unique equipment instances, durability, and enhancement are planned later. `war_axe` is a stronger axe reward for the Warrior/Bandit weapon family. +Weapon `range` also uses `[min, max]`. When class and equipment both provide range, the runtime unit keeps the lowest minimum and highest maximum. Weapons can optionally define `effective_vs_move_types` plus `effective_bonus_damage` to add a small physical damage bonus during combat against targets with a matching runtime `move_type`; this is shown in attack forecasts, combat logs, shop stock, and equipment menus, but it does not mutate saved unit stats. The first examples are spear vs mounted, bow vs water, and axe vs foot. Accessories use `kind: "accessory"` with an `accessory_type` checked against the class `equipment_slots.accessory` list; `war_drum` and `imperial_seal` are the first branch-choice accessory rewards. Current equipment inventory is stackable stock only; unique equipment instances, durability, and enhancement are planned later. `war_axe` is a stronger axe reward for the Warrior/Bandit weapon family. The first named rewards are `yitian_sword`, `dragon_spear`, `wind_chaser_bow`, and `black_tortoise_robe`; their `rarity` tag is presentation-only and does not enforce uniqueness. Consumables use `kind: "consumable"` with effect records. `heal_hp` restores HP, `heal_mp` restores MP, and `cure_status` removes active battle-only effects with a matching `status` from a valid target; `Bean`, `Wine`, `Antidote`, and `Panacea` are the first examples. Item use consumes the acting unit's action and only commits to the save file on victory. Weapons, armor, and accessories can be granted through `rewards.items`; they are shown in inventory, can be equipped through the Equip menu before the unit moves or acts, and are not usable from the battle item menu. diff --git a/docs/GODOT_4_6_MIGRATION.md b/docs/GODOT_4_6_MIGRATION.md index 66410b3..6f5d6cc 100644 --- a/docs/GODOT_4_6_MIGRATION.md +++ b/docs/GODOT_4_6_MIGRATION.md @@ -46,6 +46,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readines - Finite-stock Shop entries show remaining counts and disable once sold out. - Manual checkpoint save/load restores the expected current briefing without script errors. - A player unit can move, attack, wait, use a tactic, use an item, and equip compatible gear; weapon effectiveness appears in forecasts and equipment/shop text when applicable. +- Named equipment rewards such as Yitian Sword and Dragon Spear validate, appear with a `Named` equipment-menu tag, and remain ordinary stackable inventory stock until unique item instances are implemented. - Hovering an occupied tile shows the unit's class, movement type, AGI, movement, range, and equipped weapon/armor/accessory, with empty slots shown as `-`. - Movement-triggered supply cache events grant battle inventory items with pickup feedback, and the items persist only after victory. - The Threat toggle or `Y` key shows enemy physical and hostile tactic reach, hovered threatened tiles name the threatening units, and occupied player tiles show physical damage/hit estimates plus hostile tactic damage or status hints. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1dc2424..551fa30 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -141,5 +141,5 @@ ## Milestone 6: Content Expansion - Recreate a long campaign structure chapter by chapter. -- Add historical officers, unique classes, and named equipment. +- Add historical officers, unique classes, and named equipment. First named equipment reward items exist in the northern campaign arc. - Balance maps through repeated playtesting. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b98edd1..aab53ee 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -3863,6 +3863,9 @@ func _equipment_item_id(equipment: Dictionary, slot: String) -> String: func _format_equipment_bonus_text(item: Dictionary) -> String: var parts := [] + var rarity_text := _format_equipment_rarity_text(item) + if not rarity_text.is_empty(): + parts.append(rarity_text) var bonuses: Dictionary = item.get("bonuses", {}) for stat in ["hp", "mp", "atk", "def", "int", "agi"]: var amount := int(bonuses.get(stat, 0)) @@ -3878,6 +3881,13 @@ func _format_equipment_bonus_text(item: Dictionary) -> String: return _join_strings(parts, ", ") +func _format_equipment_rarity_text(item: Dictionary) -> String: + var rarity := str(item.get("rarity", "")) + if rarity == "named": + return "Named" + return "" + + func _format_equipment_effectiveness_text(item: Dictionary) -> String: if str(item.get("kind", "")) != "weapon": return "" diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index f697bb9..491775e 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -96,6 +96,7 @@ $validTriggers = @("battle_start", "battle_begin", "turn_start", "unit_reaches_t $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") @@ -703,6 +704,12 @@ function Check-Item-Effects() { 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."