diff --git a/README.md b/README.md index 6dce05c..4e9e1c8 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Damage forecast for selected-unit attacks. - Terrain defense and avoid affect physical attack damage and hit chance. - Min/max attack ranges and counterattack forecasts. -- MP tactics with low/high damage options, healing options, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare action locks, a skill list menu, previews, and target overlays. -- Campaign inventory consumables with an item menu, HP/MP recovery, poison/seal/snare cure previews, and target overlay. +- MP tactics with low/high damage options, healing 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. - 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. @@ -114,7 +114,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Floating combat text for damage, recovery, misses, support effects, poison ticks, action locks, level-ups, and promotions. - Short unit slide animation for board movement. - Hover target preview badges for attacks, tactics, and items. -- Low-HP board unit warning rings, HP bar color states, and board status pips for support, debuff, poison, seal, and snare effects. +- Low-HP board unit warning rings, HP bar color states, and board status pips for support, debuff, poison, seal, snare, and disarm effects. - Data-driven scenario setup through JSON definitions and deployments. ## Run diff --git a/data/defs/classes.json b/data/defs/classes.json index da87b6f..8f768ff 100644 --- a/data/defs/classes.json +++ b/data/defs/classes.json @@ -160,7 +160,7 @@ "move_type": "foot", "move": 4, "attack_range": [1, 1], - "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "march_order", "focus_order", "disrupt_order", "hamper_order", "feint_order", "poison_mist", "seal_tactic", "snare_tactic"], + "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "march_order", "focus_order", "disrupt_order", "hamper_order", "feint_order", "disarm_tactic", "poison_mist", "seal_tactic", "snare_tactic"], "growth": { "hp": "C", "mp": "A", "atk": "E", "def": "D", "int": "A", "agi": "C" }, "base_bonus": { "hp": -2, "mp": 8, "atk": -2, "def": -1, "int": 2 }, "equipment_slots": { @@ -176,7 +176,7 @@ "move_type": "foot", "move": 4, "attack_range": [1, 1], - "skills": ["spark", "fire_tactic", "mend", "great_mend", "guard_order", "rally_order", "march_order", "focus_order", "disrupt_order", "hamper_order", "feint_order", "poison_mist", "seal_tactic", "snare_tactic"], + "skills": ["spark", "fire_tactic", "mend", "great_mend", "guard_order", "rally_order", "march_order", "focus_order", "disrupt_order", "hamper_order", "feint_order", "disarm_tactic", "poison_mist", "seal_tactic", "snare_tactic"], "growth": { "hp": "B", "mp": "A", "atk": "E", "def": "C", "int": "A", "agi": "C" }, "base_bonus": { "hp": 2, "mp": 6, "atk": -2, "def": 1, "int": 2 }, "equipment_slots": { diff --git a/data/defs/items.json b/data/defs/items.json index 8df4801..1a205c8 100644 --- a/data/defs/items.json +++ b/data/defs/items.json @@ -118,7 +118,8 @@ "effects": [ { "type": "cure_status", "status": "poison" }, { "type": "cure_status", "status": "seal" }, - { "type": "cure_status", "status": "snare" } + { "type": "cure_status", "status": "snare" }, + { "type": "cure_status", "status": "disarm" } ], "price": 140 }, diff --git a/data/defs/skills.json b/data/defs/skills.json index 672832f..4fce814 100644 --- a/data/defs/skills.json +++ b/data/defs/skills.json @@ -128,6 +128,18 @@ { "type": "stat_bonus", "stat": "agi", "amount": -6, "duration_turns": 2 } ] }, + "disarm_tactic": { + "name": "Disarm Tactic", + "kind": "support", + "target": "enemy", + "mp_cost": 5, + "range": [1, 3], + "power": 0, + "stat": "int", + "effects": [ + { "type": "action_lock", "status": "disarm", "action": "attack", "duration_turns": 2 } + ] + }, "poison_mist": { "name": "Poison Mist", "kind": "support", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 240a72f..7b88238 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -56,8 +56,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. 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 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 or cure matching poison/seal/snare statuses, 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, 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 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 or cure matching poison/seal/snare/disarm statuses, 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, 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 status pips from battle state fields, hover previews are rendered as target badges from existing `BattleState` forecast APIs, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, and snare 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. +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, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, and disarm 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. The `New Campaign` button clears `user://campaign_save.json`, resets campaign state to the first scenario, and reloads the opening briefing. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 22c1d5a..2555200 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -242,12 +242,12 @@ Skills define MP tactics. A class, officer, or scenario deployment can add skill } ``` -Support skills use an `effects` array. `stat_bonus` uses `stat` in `atk`, `def`, `int`, `agi`, or `move`, a non-zero `amount`, and positive `duration_turns`. Positive amounts are buffs and negative amounts are debuffs; both are battle-only and expire when the target's team begins enough future phases. Move bonuses alter the affected unit's pathfinding range while active, and AGI bonuses or penalties alter physical hit and avoid forecasts. `damage_over_time` uses a stable `status`, positive `amount`, and positive `duration_turns`; it ticks at the start of the target team's phase before expiration and can defeat units without awarding delayed defeat EXP. `action_lock` uses a stable `status`, an `action` of `skill` or `move`, and positive `duration_turns`; it blocks the affected unit from selecting or casting tactics, or from moving, while active. +Support skills use an `effects` array. `stat_bonus` uses `stat` in `atk`, `def`, `int`, `agi`, or `move`, a non-zero `amount`, and positive `duration_turns`. Positive amounts are buffs and negative amounts are debuffs; both are battle-only and expire when the target's team begins enough future phases. Move bonuses alter the affected unit's pathfinding range while active, and AGI bonuses or penalties alter physical hit and avoid forecasts. `damage_over_time` uses a stable `status`, positive `amount`, and positive `duration_turns`; it ticks at the start of the target team's phase before expiration and can defeat units without awarding delayed defeat EXP. `action_lock` uses a stable `status`, an `action` of `skill`, `move`, or `attack`, and positive `duration_turns`; it blocks the affected unit from selecting or casting tactics, moving, or making physical attacks and counterattacks while active. ```json { - "snare_tactic": { - "name": "Snare Tactic", + "disarm_tactic": { + "name": "Disarm Tactic", "kind": "support", "target": "enemy", "mp_cost": 5, @@ -255,13 +255,13 @@ Support skills use an `effects` array. `stat_bonus` uses `stat` in `atk`, `def`, "power": 0, "stat": "int", "effects": [ - { "type": "action_lock", "status": "snare", "action": "move", "duration_turns": 2 } + { "type": "action_lock", "status": "disarm", "action": "attack", "duration_turns": 2 } ] } } ``` -The current single-target tactic set includes `spark`, `fire_tactic`, `blaze`, `mend`, `great_mend`, `guard_order`, `rally_order`, `march_order`, `focus_order`, `disrupt_order`, `hamper_order`, `feint_order`, `poison_mist`, `seal_tactic`, and `snare_tactic`. Hero classes get low-cost Spark alongside Fire/Mend and ally and enemy orders, strategist classes also get Poison Mist, Seal Tactic, and Snare Tactic, promoted command/advisor classes add Great Mend, and late scenario deployments can add stronger pressure skills such as Blaze without changing their base class. +The current single-target tactic set includes `spark`, `fire_tactic`, `blaze`, `mend`, `great_mend`, `guard_order`, `rally_order`, `march_order`, `focus_order`, `disrupt_order`, `hamper_order`, `feint_order`, `disarm_tactic`, `poison_mist`, `seal_tactic`, and `snare_tactic`. Hero classes get low-cost Spark alongside Fire/Mend and ally and enemy orders, strategist classes also get Disarm Tactic, Poison Mist, Seal Tactic, and Snare Tactic, promoted command/advisor classes add Great Mend, and late scenario deployments can add stronger pressure skills such as Blaze without changing their base class. ## Terrain @@ -315,7 +315,8 @@ Consumables use `kind: "consumable"` with effect records. `heal_hp` restores HP, "effects": [ { "type": "cure_status", "status": "poison" }, { "type": "cure_status", "status": "seal" }, - { "type": "cure_status", "status": "snare" } + { "type": "cure_status", "status": "snare" }, + { "type": "cure_status", "status": "disarm" } ], "price": 140 } diff --git a/docs/GODOT_4_6_MIGRATION.md b/docs/GODOT_4_6_MIGRATION.md index 6576a5b..67692b0 100644 --- a/docs/GODOT_4_6_MIGRATION.md +++ b/docs/GODOT_4_6_MIGRATION.md @@ -51,8 +51,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readines - Poison Mist applies poison, poison ticks at the target team's phase start, and poison defeat immediately ends the battle when it satisfies a victory or defeat condition. - Seal Tactic applies seal, prevents the affected unit from using tactics during its next action phase, and disables the tactic button with a sealed forecast/badge. - Snare Tactic applies snare, prevents the affected unit from moving during its next action phase, and still leaves attacks, tactics, items, and wait available. -- Support, debuff, poison, seal, and snare status pips appear on affected board units and disappear when those effects expire or are cured. -- Antidote can cure poison, Panacea can cure poison, seal, or snare, and neither consumes stock when used on a target without a matching status. +- Disarm Tactic applies disarm, prevents physical attacks and counterattacks, and still leaves movement, tactics, items, and wait available. +- Support, debuff, poison, seal, snare, and disarm status pips appear on affected board units and disappear when those effects expire or are cured. +- Antidote can cure poison, Panacea can cure poison, seal, snare, or disarm, and neither consumes stock when used on a target without a matching status. - Enemy turn advances and AI acts. - Victory and defeat panels still appear. - Post-battle dialogue and post-battle choices still block progression until a choice is saved. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 08cd0d8..1044398 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -32,8 +32,8 @@ - Weapon, armor, and accessory slots. Basic stat bonuses exist. - Equipment rewards can be stored, displayed, and swapped from the battle HUD. - Terrain movement, defense, and avoid modifiers affect combat. -- Skills, tactics, and MP. Multiple single-target damage/heal tactics, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare action locks, a skill list menu with range/effect hints, and enemy tactic AI exist. -- Consumable item use. Basic global inventory, Bean HP recovery, Wine MP recovery, Antidote poison cure, Panacea poison/seal/snare cure, item menu, preview, and target overlay exist. +- Skills, tactics, and MP. Multiple single-target damage/heal tactics, ally support buffs, enemy debuffs, movement and accuracy/evasion modifiers, poison damage-over-time, seal/snare/disarm action locks, a skill list menu with range/effect hints, and enemy tactic AI exist. +- Consumable item use. Basic global inventory, Bean HP recovery, Wine MP recovery, Antidote poison cure, Panacea poison/seal/snare/disarm cure, item menu, preview, and target overlay exist. - Counterattacks exist. Battle-only support, debuff, damage-over-time, and first action-lock status effects exist; richer ailment/status families are still planned. - Battle EXP, level-ups, class promotion, and campaign roster persistence exist. @@ -132,7 +132,7 @@ ## Milestone 5: Presentation - 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 plus small status pips for active support, debuff, poison, seal, and snare effects. +- 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. - Battle effects. First floating combat text now covers damage, recovery, misses, support effects, poison ticks, 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/replay overview, and dialogue lines can choose left or right portrait placement. diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index 0d5d889..653ca82 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -586,6 +586,9 @@ func try_attack_selected(target_id: String) -> bool: return false if attacker.get("acted", false): return false + if _unit_has_action_lock(attacker, "attack"): + _emit_log("%s cannot attack while disarmed." % attacker["name"]) + return false if attacker.get("team", "") == target.get("team", ""): return false if not _is_in_attack_range(attacker, target["pos"]): @@ -696,14 +699,15 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary: if attacker.get("team", "") == target.get("team", ""): return {} - var damage := calculate_damage(attacker, target) + var attack_locked := _unit_has_action_lock(attacker, "attack") + var damage := 0 if attack_locked else calculate_damage(attacker, target) var target_hp_after: int = maxi(0, int(target["hp"]) - damage) var counter_damage := 0 var attacker_hp_after := int(attacker["hp"]) - var hit_chance := calculate_hit_chance(attacker, target) + var hit_chance := 0 if attack_locked else calculate_hit_chance(attacker, target) var counter_hit_chance := 0 var counter_in_range := false - if target_hp_after > 0 or hit_chance < 100: + if not attack_locked and (target_hp_after > 0 or hit_chance < 100): counter_in_range = _is_in_attack_range(target, attacker["pos"]) if counter_in_range: counter_damage = calculate_damage(target, attacker) @@ -716,7 +720,8 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary: "hit_chance": hit_chance, "target_hp_after": target_hp_after, "would_defeat": int(target["hp"]) - damage <= 0, - "in_range": _is_in_attack_range(attacker, target["pos"]), + "in_range": not attack_locked and _is_in_attack_range(attacker, target["pos"]), + "attack_locked": attack_locked, "counter_in_range": counter_in_range, "counter_damage": counter_damage, "counter_hit_chance": counter_hit_chance, @@ -1065,6 +1070,8 @@ func _action_lock_display_name(action: String) -> String: return "tactics" if action == "move": return "movement" + if action == "attack": + return "attacks" return "action" @@ -1179,6 +1186,8 @@ func get_attack_cells(unit_id: String) -> Array[Vector2i]: return [] if unit.get("team", "") == TEAM_PLAYER and not unit.get("deployed", true): return [] + if _unit_has_action_lock(unit, "attack"): + return [] return _attack_cells_from(unit, unit["pos"]) @@ -1692,6 +1701,8 @@ func _attack_cells_from(unit: Dictionary, origin: Vector2i) -> Array[Vector2i]: func _is_in_attack_range(attacker: Dictionary, target_cell: Vector2i) -> bool: + if _unit_has_action_lock(attacker, "attack"): + return false return _attack_cells_from(attacker, attacker["pos"]).has(target_cell) @@ -2262,6 +2273,8 @@ func _unit_action_threat_score(unit: Dictionary, action: String) -> int: if int(unit.get("range", 1)) <= 1: move_score += 10 return move_score + if action == "attack": + return _effective_stat(unit, "atk") * 4 + int(unit.get("range", 1)) * 4 return 0 diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b7e4b35..85a06c8 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -1063,6 +1063,8 @@ func _unit_status_marker_color(marker_kind: String) -> Color: return Color(0.44, 0.86, 1.0) if marker_kind == "snare": return Color(1.0, 0.64, 0.22) + if marker_kind == "disarm": + return Color(1.0, 0.48, 0.42) return Color(0.72, 0.86, 1.0) @@ -1123,6 +1125,8 @@ func _floating_text_color(kind: String) -> Color: return Color(0.44, 0.86, 1.0) if kind == "snare": return Color(1.0, 0.64, 0.22) + if kind == "disarm": + return Color(1.0, 0.48, 0.42) if kind == "miss" or kind == "fade": return Color(0.82, 0.84, 0.90) if kind == "defeat": @@ -1182,6 +1186,8 @@ func _attack_target_preview_badge(selected: Dictionary, target: Dictionary) -> D if preview.is_empty(): return {} + if bool(preview.get("attack_locked", false)): + return _make_target_preview_badge("DISARMED", "disarm") if not bool(preview.get("in_range", false)): return _make_target_preview_badge("OUT", "invalid") @@ -1214,7 +1220,7 @@ func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Di return _make_target_preview_badge("+%d HP" % heal, "heal") if kind == "support": var effect_text := str(preview.get("effect_text", "Support")) - var badge_kind := "snare" if effect_text.contains("Snare") else ("seal" if effect_text.contains("Seal") else ("poison" if effect_text.contains("Poison") else ("debuff" if effect_text.contains("-") else "support"))) + var badge_kind := "disarm" if effect_text.contains("Disarm") else ("snare" if effect_text.contains("Snare") else ("seal" if effect_text.contains("Seal") else ("poison" if effect_text.contains("Poison") else ("debuff" if effect_text.contains("-") else "support")))) return _make_target_preview_badge(_compact_support_preview_text(effect_text), badge_kind) var result_text := "KO" if bool(preview.get("would_defeat", false)) else "-%d" % int(preview.get("damage", 0)) @@ -1293,6 +1299,8 @@ func _target_preview_badge_color(kind: String) -> Color: return Color(0.44, 0.86, 1.0) if kind == "snare": return Color(1.0, 0.64, 0.22) + if kind == "disarm": + return Color(1.0, 0.48, 0.42) return Color(0.78, 0.80, 0.86) @@ -1461,6 +1469,10 @@ func _update_forecast() -> void: forecast_label.text = "Forecast: -" return + if bool(preview.get("attack_locked", false)): + forecast_label.text = "Forecast: disarmed.\nCannot use physical attacks." + return + var range_text := "ready" if preview["in_range"] else "out of range" var defeat_text := " Defeat" if preview["would_defeat"] else "" var counter_text := "" @@ -3401,6 +3413,8 @@ func _action_lock_effect_label(action: String) -> String: return "tactics" if action == "move": return "movement" + if action == "attack": + return "attacks" return "action" diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index 7372398..7a2ac32 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -799,8 +799,8 @@ function Check-Skill-Definitions() { Fail "Skill $skillId action_lock effect must have a stable status." } $action = [string](Get-Prop $effect "action" "") - if ($action -ne "skill" -and $action -ne "move") { - Fail "Skill $skillId action_lock effect currently supports only action=skill or action=move." + if ($action -ne "skill" -and $action -ne "move" -and $action -ne "attack") { + Fail "Skill $skillId action_lock effect currently supports only action=skill, action=move, or action=attack." } if ([int](Get-Prop $effect "duration_turns" 1) -le 0) { Fail "Skill $skillId action_lock effect must have positive duration_turns."