From feae7dd167fae10212f089267554913bb76d8364 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 10:18:07 +0900 Subject: [PATCH] Add movement order tactics --- README.md | 2 +- data/defs/classes.json | 8 ++++---- data/defs/skills.json | 24 ++++++++++++++++++++++++ docs/ARCHITECTURE.md | 4 ++-- docs/DATA_MODEL.md | 6 +++--- docs/GODOT_4_6_MIGRATION.md | 1 + docs/ROADMAP.md | 2 +- scripts/core/battle_state.gd | 6 ++++-- tools/validate_data.ps1 | 3 ++- 9 files changed, 42 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e419538..bc94324 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 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, poison damage-over-time, seal/snare action locks, a skill list menu, previews, and target overlays. +- MP tactics with low/high damage options, healing options, ally support buffs, enemy debuffs, movement buffs/debuffs, 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. - 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. diff --git a/data/defs/classes.json b/data/defs/classes.json index b22fe07..267bf31 100644 --- a/data/defs/classes.json +++ b/data/defs/classes.json @@ -5,7 +5,7 @@ "move_type": "foot", "move": 4, "attack_range": [1, 1], - "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "disrupt_order"], + "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "march_order", "disrupt_order", "hamper_order"], "growth": { "hp": "B", "mp": "B", "atk": "B", "def": "B", "int": "A", "agi": "B" }, "base_bonus": { "hp": 0, "atk": 0, "def": 0 }, "equipment_slots": { @@ -21,7 +21,7 @@ "move_type": "foot", "move": 5, "attack_range": [1, 1], - "skills": ["spark", "fire_tactic", "mend", "great_mend", "guard_order", "rally_order", "disrupt_order"], + "skills": ["spark", "fire_tactic", "mend", "great_mend", "guard_order", "rally_order", "march_order", "disrupt_order", "hamper_order"], "growth": { "hp": "A", "mp": "B", "atk": "A", "def": "B", "int": "A", "agi": "B" }, "base_bonus": { "hp": 4, "atk": 1, "def": 1, "int": 1 }, "equipment_slots": { @@ -160,7 +160,7 @@ "move_type": "foot", "move": 4, "attack_range": [1, 1], - "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "disrupt_order", "poison_mist", "seal_tactic", "snare_tactic"], + "skills": ["spark", "fire_tactic", "mend", "guard_order", "rally_order", "march_order", "disrupt_order", "hamper_order", "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", "disrupt_order", "poison_mist", "seal_tactic", "snare_tactic"], + "skills": ["spark", "fire_tactic", "mend", "great_mend", "guard_order", "rally_order", "march_order", "disrupt_order", "hamper_order", "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/skills.json b/data/defs/skills.json index 93e930c..73a7ec0 100644 --- a/data/defs/skills.json +++ b/data/defs/skills.json @@ -68,6 +68,18 @@ { "type": "stat_bonus", "stat": "atk", "amount": 3, "duration_turns": 1 } ] }, + "march_order": { + "name": "March Order", + "kind": "support", + "target": "ally", + "mp_cost": 5, + "range": [0, 2], + "power": 0, + "stat": "int", + "effects": [ + { "type": "stat_bonus", "stat": "move", "amount": 1, "duration_turns": 1 } + ] + }, "disrupt_order": { "name": "Disrupt Order", "kind": "support", @@ -80,6 +92,18 @@ { "type": "stat_bonus", "stat": "atk", "amount": -3, "duration_turns": 2 } ] }, + "hamper_order": { + "name": "Hamper Order", + "kind": "support", + "target": "enemy", + "mp_cost": 5, + "range": [1, 3], + "power": 0, + "stat": "int", + "effects": [ + { "type": "stat_bonus", "stat": "move", "amount": -1, "duration_turns": 2 } + ] + }, "poison_mist": { "name": "Poison Mist", "kind": "support", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f6fb0f8..ea5a06a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -38,9 +38,9 @@ 4. Show scenario briefing with the campaign chapter header, battle header, objective summary, chapter overview, manual checkpoint menu, and optional pre-battle shop, Armory, Roster, and Formation setup. 5. Player selects a unit. 6. Briefing completion dispatches battle-begin events, including opening dialogue. -7. Unit moves, attacks, chooses a tactic or consumable item from the side menu, changes equipment, counters, gains EXP, levels up, promotes at class thresholds, or waits. Tactic buttons show MP cost, kind, range, and power, support, debuff, damage-over-time, or action-lock effect. Physical attacks roll hit chance from unit agility and target terrain avoid; misses give reduced EXP. The scene plays placeholder SFX, a short movement slide, low-HP warning rings, hover target preview badges, and floating combat text for core action and UI feedback. +7. Unit moves, attacks, chooses a tactic or consumable item from the side menu, changes equipment, counters, gains EXP, levels up, promotes at class thresholds, or waits. Tactic buttons show MP cost, kind, range, and power, support, debuff, movement modifier, damage-over-time, or action-lock effect. Physical attacks roll hit chance from unit agility and target terrain avoid; misses give reduced EXP. The scene plays placeholder SFX, a short movement slide, low-HP warning rings, hover target preview badges, and floating combat text for core action and UI feedback. 8. Player ends turn. -9. Enemy AI moves, attacks, and can cast damage, healing, support, debuff, damage-over-time, or skill-lock tactics through the same combat and skill resolvers. +9. Enemy AI moves, attacks, and can cast damage, healing, support, debuff, movement-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 and movement-triggered scenario events run as phases change or units enter marked cells, and may update objectives or deploy reinforcements. 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`. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index e3c0c5b..8e3cd47 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -242,7 +242,7 @@ 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`, or `agi`, 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. `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. `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. ```json { @@ -261,7 +261,7 @@ Support skills use an `effects` array. `stat_bonus` uses `stat` in `atk`, `def`, } ``` -The current single-target tactic set includes `spark`, `fire_tactic`, `blaze`, `mend`, `great_mend`, `guard_order`, `rally_order`, `disrupt_order`, `poison_mist`, `seal_tactic`, and `snare_tactic`. Hero classes get low-cost Spark alongside Fire/Mend and ally 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`, `disrupt_order`, `hamper_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. ## Terrain @@ -510,7 +510,7 @@ Event-spawned units use the same compact deployment shape as scenario starting u ## Current Implementation -`scripts/core/data_catalog.gd` reads definition files once and `BattleState._apply_battle_data()` accepts either legacy `units` or catalog-backed `deployments`. Runtime units now carry `min_range`, `range`, `skills`, `exp`, `growth`, `growth_bonus`, and battle-only `status_effects` so the battle resolver can handle counterattacks, MP tactics, support buffs, enemy debuffs, damage-over-time statuses, action-lock statuses, EXP, level-ups, and enemy tactic AI without changing scenario files. +`scripts/core/data_catalog.gd` reads definition files once and `BattleState._apply_battle_data()` accepts either legacy `units` or catalog-backed `deployments`. Runtime units now carry `min_range`, `range`, `skills`, `exp`, `growth`, `growth_bonus`, and battle-only `status_effects` so the battle resolver can handle counterattacks, MP tactics, support buffs, enemy debuffs, movement modifiers, damage-over-time statuses, action-lock statuses, EXP, level-ups, and enemy tactic AI without changing scenario files. `scripts/core/campaign_state.gd` writes `user://campaign_save.json` with `save_version`, completed scenarios, gold, inventory counts, joined officers, pending post-battle choice scenario id, applied post-battle choice ids, campaign flags, per-scenario shop purchase counts, and player roster progression. The same snapshot can be copied to the one-slot manual checkpoint at `user://campaign_manual_save.json` from the pre-battle Save menu. Loading that checkpoint restores it into the active automatic save and then reloads the pending choice panel, campaign completion state, or current scenario briefing. Roster snapshots include level, EXP, stats, class id/name, class-derived movement/range/growth fields, skills, and equipment. Rewards, joined/left officers, and post-battle choices are guarded by completed scenario id so restarting a finished battle does not duplicate gold, items, membership changes, branch decisions, or finite shop purchases. On load, completed-scenario officer join/leave transitions are reconciled without replaying gold or item rewards, which lets new officer rewards added to older completed scenarios become available to existing saves. diff --git a/docs/GODOT_4_6_MIGRATION.md b/docs/GODOT_4_6_MIGRATION.md index 864ece8..cc38f5c 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. +- March Order and Hamper Order adjust movement range while active and expire with the normal support/debuff timing. - 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 150cf92..19f1ba5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -32,7 +32,7 @@ - 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, poison damage-over-time, seal/snare action locks, a skill list menu with range/effect hints, and enemy tactic AI exist. +- Skills, tactics, and MP. Multiple single-target damage/heal tactics, ally support buffs, enemy debuffs, movement buffs/debuffs, 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. - 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. diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index 410b83e..0d5d889 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -1047,7 +1047,7 @@ func _status_display_name(status: String) -> String: func _is_supported_status_stat(stat: String) -> bool: - return stat == "atk" or stat == "def" or stat == "int" or stat == "agi" + return stat == "atk" or stat == "def" or stat == "int" or stat == "agi" or stat == "move" func _unit_has_action_lock(unit: Dictionary, action: String) -> bool: @@ -1665,7 +1665,7 @@ func _movement_range_for_unit(unit: Dictionary) -> Array[Vector2i]: continue var new_cost := int(costs[current]) + terrain_cost - if new_cost > int(unit["move"]): + if new_cost > _effective_stat(unit, "move"): continue if not costs.has(next_cell) or new_cost < int(costs[next_cell]): costs[next_cell] = new_cost @@ -2220,6 +2220,8 @@ func _support_ai_score(caster: Dictionary, target: Dictionary, skill: Dictionary func _support_ai_stat_weight(stat: String) -> int: if stat == "atk": return 9 + if stat == "move": + return 8 if stat == "def": return 7 if stat == "int": diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index d64e1b7..7372398 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -100,6 +100,7 @@ $validBonusStats = @("hp", "mp", "atk", "def", "int", "agi") $validSkillKinds = @("damage", "heal", "support") $validSkillTargets = @("enemy", "ally", "self", "any") $validSkillStats = @("atk", "def", "int", "agi") +$validStatusStats = @("atk", "def", "int", "agi", "move") $validSkillEffectTypes = @("stat_bonus", "damage_over_time", "action_lock") $validDialogueSides = @("left", "right") $validPortraitPathPattern = '^res://.+\.(png|jpg|jpeg|webp)$' @@ -772,7 +773,7 @@ function Check-Skill-Definitions() { } if ($effectType -eq "stat_bonus") { $effectStat = [string](Get-Prop $effect "stat" "") - if (-not $validSkillStats.Contains($effectStat)) { + if (-not $validStatusStats.Contains($effectStat)) { Fail "Skill $skillId effect references unsupported stat: $effectStat" } if ([int](Get-Prop $effect "amount" 0) -eq 0) {