Initial Godot tactical RPG prototype
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.godot/
|
||||||
|
.import/
|
||||||
|
export/
|
||||||
|
*.tmp
|
||||||
|
*.translation
|
||||||
|
|
||||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Heroes of Three Kingdoms
|
||||||
|
|
||||||
|
Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Three Kingdoms games. The current editor target is moving to Godot 4.6.
|
||||||
|
|
||||||
|
## Current Slice
|
||||||
|
|
||||||
|
- Seventeen campaign scenarios.
|
||||||
|
- Grid movement.
|
||||||
|
- Unit selection.
|
||||||
|
- Move, attack, wait, end turn.
|
||||||
|
- Enemy AI with movement, attacks, multiple MP tactic choices, and scenario target priorities.
|
||||||
|
- Hover tile and unit info.
|
||||||
|
- 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, a skill list menu, damage/healing previews, and target overlays.
|
||||||
|
- Campaign inventory consumables with an item menu, HP/MP recovery preview, and target overlay.
|
||||||
|
- Weapon, armor, and accessory rewards can be stored, displayed, and equipped from the battle HUD.
|
||||||
|
- Scenario-specific pre-battle shop purchases spend campaign gold, 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.
|
||||||
|
- Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells.
|
||||||
|
- Escort scenarios can include required, non-controllable protected units.
|
||||||
|
- Campaign saves joined officers, future deployments can require prior recruitment, and choices can make officers leave.
|
||||||
|
- Completed saves reconcile officer join/leave rewards on load without replaying gold or items.
|
||||||
|
- Post-battle choices save a scenario-to-choice id ledger for safer future branch migrations.
|
||||||
|
- Reloading during a pending post-battle choice returns to the choice panel instead of skipping the branch.
|
||||||
|
- Qingzhou victory can recruit Dian Wei, backed by a new Warrior class.
|
||||||
|
- Puyang uses Dian Wei as a required joined officer and introduces a Lu Bu branch choice.
|
||||||
|
- Dingtao consumes the Puyang branch flags and rewards a stronger axe.
|
||||||
|
- Wan Castle consumes the imperial escort branch flags and adds a protected Cao Ang escape map.
|
||||||
|
- Xiapi consumes the Wan Castle branch flags and adds a flood-gate siege against Lu Bu.
|
||||||
|
- Xiapi victory recruits Guo Jia, and White Horse starts the northern campaign with a decoy-line battle.
|
||||||
|
- Yan Ford consumes the White Horse branch flags and adds Wen Chou's countercharge before Guandu.
|
||||||
|
- Guandu consumes the Yan Ford branch flags and adds Yuan Shao's main assault.
|
||||||
|
- Wuchao consumes the Guandu branch flags and adds a depot-burning raid.
|
||||||
|
- Cangting consumes the Wuchao branch flags, breaks Yuan Shao's rear guard, and recruits Zhang He.
|
||||||
|
- Ye consumes the Cangting branch flags and lets Zhang He deploy against the Yuan heirs.
|
||||||
|
- Ye Siege consumes the Ye campaign branch flags and stages an inner-gate breach.
|
||||||
|
- Ye Surrender consumes the siege branch flags and settles the city before the Yuan heirs flee north.
|
||||||
|
- Post-battle choices can save campaign flags that branch later briefing text, shop stock, and scenario events.
|
||||||
|
- 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 summarize saved player level-ups and promotions.
|
||||||
|
- Scenario events for battle start, battle-begin dialogue, turn start, movement-triggered ambushes, objective updates, and reinforcements.
|
||||||
|
- Scenario briefing before battle.
|
||||||
|
- Victory rewards and consumed inventory saved into a campaign state.
|
||||||
|
- Scenario-defined victory and defeat conditions.
|
||||||
|
- Destination-style victory conditions with objective map markers.
|
||||||
|
- Protected-unit defeat conditions for escort battles.
|
||||||
|
- Turn-limit defeat conditions with turn-limit HUD display.
|
||||||
|
- Next-battle flow after victory.
|
||||||
|
- Campaign completion screen.
|
||||||
|
- New campaign save reset.
|
||||||
|
- Victory and defeat result overlay.
|
||||||
|
- Data-driven scenario setup through JSON definitions and deployments.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
1. Install Godot 4.
|
||||||
|
2. Open this folder as a Godot project.
|
||||||
|
3. Run the project. The main scene is `res://scenes/battle_scene.tscn`.
|
||||||
|
|
||||||
|
Campaign order lives at `res://data/campaign/campaign.json`. Scenarios live under `res://data/scenarios/`, with reusable officers, classes, items, skills, and terrain definitions under `res://data/defs/`.
|
||||||
|
|
||||||
|
Data files can be checked with `powershell -NoProfile -ExecutionPolicy Bypass -File tools\validate_data.ps1`.
|
||||||
|
|
||||||
|
Godot 4.6 migration readiness can be checked with `powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readiness.ps1`. The migration checklist lives in `docs/GODOT_4_6_MIGRATION.md`.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
The project should grow from a small playable battle into a campaign tactics RPG:
|
||||||
|
|
||||||
|
- Battle system first.
|
||||||
|
- Scenario and event scripting second.
|
||||||
|
- Progression, equipment, and class depth third.
|
||||||
|
- Portraits, cutscenes, and campaign presentation after the core loop is fun.
|
||||||
|
|
||||||
|
The code keeps portraits and other visual assets replaceable so generated original assets can be added later without changing battle rules.
|
||||||
93
data/campaign/campaign.json
Normal file
93
data/campaign/campaign.json
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"id": "cao_cao_campaign",
|
||||||
|
"title": "Cao Cao Campaign",
|
||||||
|
"start_scenario": "001_yellow_turbans",
|
||||||
|
"initial_joined_officers": ["cao_cao", "xiahou_dun"],
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"id": "001_yellow_turbans",
|
||||||
|
"title": "Yingchuan Skirmish",
|
||||||
|
"path": "res://data/scenarios/001_yellow_turbans.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "002_sishui_gate",
|
||||||
|
"title": "Sishui Gate Vanguard",
|
||||||
|
"path": "res://data/scenarios/002_sishui_gate.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "003_xingyang_ambush",
|
||||||
|
"title": "Xingyang Ambush",
|
||||||
|
"path": "res://data/scenarios/003_xingyang_ambush.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "004_qingzhou_campaign",
|
||||||
|
"title": "Qingzhou Campaign",
|
||||||
|
"path": "res://data/scenarios/004_qingzhou_campaign.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "005_puyang_raid",
|
||||||
|
"title": "Puyang Raid",
|
||||||
|
"path": "res://data/scenarios/005_puyang_raid.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "006_dingtao_counterattack",
|
||||||
|
"title": "Dingtao Counterattack",
|
||||||
|
"path": "res://data/scenarios/006_dingtao_counterattack.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "007_xian_emperor_escort",
|
||||||
|
"title": "Xian Emperor Escort",
|
||||||
|
"path": "res://data/scenarios/007_xian_emperor_escort.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "008_wan_castle_escape",
|
||||||
|
"title": "Wan Castle Escape",
|
||||||
|
"path": "res://data/scenarios/008_wan_castle_escape.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "009_xiapi_siege",
|
||||||
|
"title": "Xiapi Siege",
|
||||||
|
"path": "res://data/scenarios/009_xiapi_siege.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "010_white_horse_relief",
|
||||||
|
"title": "White Horse Relief",
|
||||||
|
"path": "res://data/scenarios/010_white_horse_relief.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "011_yan_ford_pursuit",
|
||||||
|
"title": "Yan Ford Pursuit",
|
||||||
|
"path": "res://data/scenarios/011_yan_ford_pursuit.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "012_guandu_showdown",
|
||||||
|
"title": "Guandu Showdown",
|
||||||
|
"path": "res://data/scenarios/012_guandu_showdown.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "013_wuchao_raid",
|
||||||
|
"title": "Wuchao Raid",
|
||||||
|
"path": "res://data/scenarios/013_wuchao_raid.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "014_cangting_pursuit",
|
||||||
|
"title": "Cangting Pursuit",
|
||||||
|
"path": "res://data/scenarios/014_cangting_pursuit.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "015_ye_campaign",
|
||||||
|
"title": "Ye Campaign",
|
||||||
|
"path": "res://data/scenarios/015_ye_campaign.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "016_ye_siege",
|
||||||
|
"title": "Ye Siege",
|
||||||
|
"path": "res://data/scenarios/016_ye_siege.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "017_ye_surrender",
|
||||||
|
"title": "Ye Surrender",
|
||||||
|
"path": "res://data/scenarios/017_ye_surrender.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
203
data/defs/classes.json
Normal file
203
data/defs/classes.json
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"hero": {
|
||||||
|
"name": "Hero",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": ["spark", "fire_tactic", "mend"],
|
||||||
|
"growth": { "hp": "B", "mp": "B", "atk": "B", "def": "B", "int": "A", "agi": "B" },
|
||||||
|
"base_bonus": { "hp": 0, "atk": 0, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["sword"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "commander" }
|
||||||
|
},
|
||||||
|
"commander": {
|
||||||
|
"name": "Commander",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": ["spark", "fire_tactic", "mend", "great_mend"],
|
||||||
|
"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": {
|
||||||
|
"weapon": ["sword"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cavalry": {
|
||||||
|
"name": "Cavalry",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "mounted",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "D", "atk": "A", "def": "B", "int": "D", "agi": "B" },
|
||||||
|
"base_bonus": { "hp": 2, "atk": 0, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["spear", "sword"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "elite_cavalry" }
|
||||||
|
},
|
||||||
|
"elite_cavalry": {
|
||||||
|
"name": "Elite Cavalry",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "mounted",
|
||||||
|
"move": 6,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "D", "atk": "A", "def": "A", "int": "D", "agi": "A" },
|
||||||
|
"base_bonus": { "hp": 5, "atk": 2, "def": 1 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["spear", "sword"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"infantry": {
|
||||||
|
"name": "Infantry",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "B", "mp": "D", "atk": "B", "def": "B", "int": "D", "agi": "C" },
|
||||||
|
"base_bonus": { "hp": 0, "atk": 0, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["sword", "spear"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "guard_captain" }
|
||||||
|
},
|
||||||
|
"guard_captain": {
|
||||||
|
"name": "Guard Captain",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "D", "atk": "B", "def": "A", "int": "D", "agi": "C" },
|
||||||
|
"base_bonus": { "hp": 4, "atk": 1, "def": 2 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["sword", "spear"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warrior": {
|
||||||
|
"name": "Warrior",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "E", "atk": "A", "def": "B", "int": "E", "agi": "C" },
|
||||||
|
"base_bonus": { "hp": 4, "atk": 2, "def": 1, "int": -2 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["axe", "sword"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "champion" }
|
||||||
|
},
|
||||||
|
"champion": {
|
||||||
|
"name": "Champion",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "E", "atk": "A", "def": "A", "int": "E", "agi": "B" },
|
||||||
|
"base_bonus": { "hp": 6, "atk": 2, "def": 2, "int": -2 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["axe", "sword"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"archer": {
|
||||||
|
"name": "Archer",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "archer",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [2, 2],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "C", "mp": "D", "atk": "B", "def": "C", "int": "D", "agi": "A" },
|
||||||
|
"base_bonus": { "hp": 0, "atk": 0, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["bow"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "marksman" }
|
||||||
|
},
|
||||||
|
"marksman": {
|
||||||
|
"name": "Marksman",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "archer",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [2, 2],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "B", "mp": "D", "atk": "A", "def": "C", "int": "D", "agi": "A" },
|
||||||
|
"base_bonus": { "hp": 3, "atk": 2, "agi": 1 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["bow"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"strategist": {
|
||||||
|
"name": "Strategist",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": ["spark", "fire_tactic", "mend"],
|
||||||
|
"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": {
|
||||||
|
"weapon": ["sword"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "military_advisor" }
|
||||||
|
},
|
||||||
|
"military_advisor": {
|
||||||
|
"name": "Military Advisor",
|
||||||
|
"tier": 2,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": ["spark", "fire_tactic", "mend", "great_mend"],
|
||||||
|
"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": {
|
||||||
|
"weapon": ["sword"],
|
||||||
|
"armor": ["robe", "light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bandit": {
|
||||||
|
"name": "Bandit",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "foot",
|
||||||
|
"move": 4,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "B", "mp": "E", "atk": "B", "def": "C", "int": "E", "agi": "B" },
|
||||||
|
"base_bonus": { "hp": 0, "atk": 0, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["sword", "axe"],
|
||||||
|
"armor": ["light_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
data/defs/items.json
Normal file
110
data/defs/items.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"bronze_sword": {
|
||||||
|
"name": "Bronze Sword",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "sword",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 3 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 300
|
||||||
|
},
|
||||||
|
"iron_sword": {
|
||||||
|
"name": "Iron Sword",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "sword",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 5 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 520
|
||||||
|
},
|
||||||
|
"training_spear": {
|
||||||
|
"name": "Training Spear",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "spear",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 4 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 360
|
||||||
|
},
|
||||||
|
"short_bow": {
|
||||||
|
"name": "Short Bow",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "bow",
|
||||||
|
"range": [2, 2],
|
||||||
|
"bonuses": { "atk": 3 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 320
|
||||||
|
},
|
||||||
|
"hand_axe": {
|
||||||
|
"name": "Hand Axe",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "axe",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 4 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 340
|
||||||
|
},
|
||||||
|
"war_axe": {
|
||||||
|
"name": "War Axe",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "axe",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 6 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 680
|
||||||
|
},
|
||||||
|
"cloth_robe": {
|
||||||
|
"name": "Cloth Robe",
|
||||||
|
"kind": "armor",
|
||||||
|
"armor_type": "robe",
|
||||||
|
"bonuses": { "def": 1 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 120
|
||||||
|
},
|
||||||
|
"leather_armor": {
|
||||||
|
"name": "Leather Armor",
|
||||||
|
"kind": "armor",
|
||||||
|
"armor_type": "light_armor",
|
||||||
|
"bonuses": { "def": 1 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 180
|
||||||
|
},
|
||||||
|
"iron_armor": {
|
||||||
|
"name": "Iron Armor",
|
||||||
|
"kind": "armor",
|
||||||
|
"armor_type": "heavy_armor",
|
||||||
|
"bonuses": { "def": 3 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 520
|
||||||
|
},
|
||||||
|
"war_drum": {
|
||||||
|
"name": "War Drum",
|
||||||
|
"kind": "accessory",
|
||||||
|
"accessory_type": "accessory",
|
||||||
|
"bonuses": { "atk": 1, "agi": 1 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 640
|
||||||
|
},
|
||||||
|
"imperial_seal": {
|
||||||
|
"name": "Imperial Seal",
|
||||||
|
"kind": "accessory",
|
||||||
|
"accessory_type": "accessory",
|
||||||
|
"bonuses": { "mp": 4, "int": 1 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 900
|
||||||
|
},
|
||||||
|
"bean": {
|
||||||
|
"name": "Bean",
|
||||||
|
"kind": "consumable",
|
||||||
|
"uses": 1,
|
||||||
|
"effects": [{ "type": "heal_hp", "amount": 20 }],
|
||||||
|
"price": 50
|
||||||
|
},
|
||||||
|
"wine": {
|
||||||
|
"name": "Wine",
|
||||||
|
"kind": "consumable",
|
||||||
|
"uses": 1,
|
||||||
|
"effects": [{ "type": "heal_mp", "amount": 8 }],
|
||||||
|
"price": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
72
data/defs/officers.json
Normal file
72
data/defs/officers.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"cao_cao": {
|
||||||
|
"name": "Cao Cao",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "hero",
|
||||||
|
"level": 1,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 38, "mp": 10, "atk": 10, "def": 7, "int": 12, "agi": 8 },
|
||||||
|
"growth_bonus": { "atk": 1, "int": 1 },
|
||||||
|
"equipment": { "weapon": "bronze_sword", "armor": "cloth_robe", "accessory": null }
|
||||||
|
},
|
||||||
|
"xiahou_dun": {
|
||||||
|
"name": "Xiahou Dun",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"level": 1,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 40, "mp": 4, "atk": 11, "def": 6, "int": 5, "agi": 9 },
|
||||||
|
"growth_bonus": { "hp": 1, "atk": 1 },
|
||||||
|
"equipment": { "weapon": "training_spear", "armor": "leather_armor", "accessory": null }
|
||||||
|
},
|
||||||
|
"xiahou_yuan": {
|
||||||
|
"name": "Xiahou Yuan",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "archer",
|
||||||
|
"level": 1,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 32, "mp": 4, "atk": 9, "def": 5, "int": 6, "agi": 12 },
|
||||||
|
"growth_bonus": { "agi": 1 },
|
||||||
|
"equipment": { "weapon": "short_bow", "armor": "leather_armor", "accessory": null }
|
||||||
|
},
|
||||||
|
"cao_ren": {
|
||||||
|
"name": "Cao Ren",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"level": 1,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 36, "mp": 3, "atk": 10, "def": 8, "int": 5, "agi": 7 },
|
||||||
|
"growth_bonus": { "def": 1 },
|
||||||
|
"equipment": { "weapon": "bronze_sword", "armor": "leather_armor", "accessory": null }
|
||||||
|
},
|
||||||
|
"dian_wei": {
|
||||||
|
"name": "Dian Wei",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "warrior",
|
||||||
|
"level": 3,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 42, "mp": 2, "atk": 13, "def": 8, "int": 3, "agi": 7 },
|
||||||
|
"growth_bonus": { "hp": 1, "atk": 1 },
|
||||||
|
"equipment": { "weapon": "hand_axe", "armor": "leather_armor", "accessory": null }
|
||||||
|
},
|
||||||
|
"guo_jia": {
|
||||||
|
"name": "Guo Jia",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"level": 8,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 30, "mp": 18, "atk": 5, "def": 5, "int": 14, "agi": 9 },
|
||||||
|
"growth_bonus": { "mp": 1, "int": 1 },
|
||||||
|
"equipment": { "weapon": "bronze_sword", "armor": "cloth_robe", "accessory": null }
|
||||||
|
},
|
||||||
|
"zhang_he": {
|
||||||
|
"name": "Zhang He",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"level": 12,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 48, "mp": 6, "atk": 15, "def": 10, "int": 7, "agi": 14 },
|
||||||
|
"growth_bonus": { "atk": 1, "agi": 1 },
|
||||||
|
"equipment": { "weapon": "training_spear", "armor": "iron_armor", "accessory": null }
|
||||||
|
}
|
||||||
|
}
|
||||||
47
data/defs/skills.json
Normal file
47
data/defs/skills.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"spark": {
|
||||||
|
"name": "Spark",
|
||||||
|
"kind": "damage",
|
||||||
|
"target": "enemy",
|
||||||
|
"mp_cost": 2,
|
||||||
|
"range": [1, 2],
|
||||||
|
"power": 5,
|
||||||
|
"stat": "int"
|
||||||
|
},
|
||||||
|
"fire_tactic": {
|
||||||
|
"name": "Fire",
|
||||||
|
"kind": "damage",
|
||||||
|
"target": "enemy",
|
||||||
|
"mp_cost": 4,
|
||||||
|
"range": [1, 3],
|
||||||
|
"power": 8,
|
||||||
|
"stat": "int"
|
||||||
|
},
|
||||||
|
"blaze": {
|
||||||
|
"name": "Blaze",
|
||||||
|
"kind": "damage",
|
||||||
|
"target": "enemy",
|
||||||
|
"mp_cost": 7,
|
||||||
|
"range": [1, 3],
|
||||||
|
"power": 13,
|
||||||
|
"stat": "int"
|
||||||
|
},
|
||||||
|
"mend": {
|
||||||
|
"name": "Mend",
|
||||||
|
"kind": "heal",
|
||||||
|
"target": "ally",
|
||||||
|
"mp_cost": 4,
|
||||||
|
"range": [0, 2],
|
||||||
|
"power": 12,
|
||||||
|
"stat": "int"
|
||||||
|
},
|
||||||
|
"great_mend": {
|
||||||
|
"name": "Great Mend",
|
||||||
|
"kind": "heal",
|
||||||
|
"target": "ally",
|
||||||
|
"mp_cost": 7,
|
||||||
|
"range": [0, 2],
|
||||||
|
"power": 20,
|
||||||
|
"stat": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
data/defs/terrain.json
Normal file
43
data/defs/terrain.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"G": {
|
||||||
|
"id": "plain",
|
||||||
|
"name": "Plain",
|
||||||
|
"color": "#6d9e4f",
|
||||||
|
"defense": 0,
|
||||||
|
"avoid": 0,
|
||||||
|
"move_cost": { "foot": 1, "mounted": 1, "archer": 1, "water": 99 }
|
||||||
|
},
|
||||||
|
"F": {
|
||||||
|
"id": "forest",
|
||||||
|
"name": "Forest",
|
||||||
|
"color": "#33733a",
|
||||||
|
"defense": 2,
|
||||||
|
"avoid": 10,
|
||||||
|
"move_cost": { "foot": 2, "mounted": 3, "archer": 2, "water": 99 }
|
||||||
|
},
|
||||||
|
"H": {
|
||||||
|
"id": "hill",
|
||||||
|
"name": "Hill",
|
||||||
|
"color": "#8c8057",
|
||||||
|
"defense": 1,
|
||||||
|
"avoid": 5,
|
||||||
|
"move_cost": { "foot": 2, "mounted": 3, "archer": 2, "water": 99 }
|
||||||
|
},
|
||||||
|
"R": {
|
||||||
|
"id": "road",
|
||||||
|
"name": "Road",
|
||||||
|
"color": "#a38c6b",
|
||||||
|
"defense": 0,
|
||||||
|
"avoid": 0,
|
||||||
|
"move_cost": { "foot": 1, "mounted": 1, "archer": 1, "water": 99 }
|
||||||
|
},
|
||||||
|
"W": {
|
||||||
|
"id": "water",
|
||||||
|
"name": "Water",
|
||||||
|
"color": "#2e5c9e",
|
||||||
|
"defense": 0,
|
||||||
|
"avoid": 0,
|
||||||
|
"move_cost": { "foot": 99, "mounted": 99, "archer": 99, "water": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
124
data/scenarios/001_yellow_turbans.json
Normal file
124
data/scenarios/001_yellow_turbans.json
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"id": "001_yellow_turbans",
|
||||||
|
"name": "Yingchuan Skirmish",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat all Yellow Turban units.",
|
||||||
|
"defeat": "All allied units are defeated."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy" },
|
||||||
|
"defeat": { "type": "all_units_defeated", "team": "player" }
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Yingchuan Skirmish",
|
||||||
|
"location": "Yingchuan, 184 CE",
|
||||||
|
"lines": [
|
||||||
|
"Yellow Turban rebels have gathered near Yingchuan.",
|
||||||
|
"Cao Cao and Xiahou Dun must break their formation before the revolt spreads."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": ["bean"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [2, 3], [2, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 10,
|
||||||
|
"height": 8,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGGGGGG",
|
||||||
|
"GGGFFFFGGG",
|
||||||
|
"GGGFFFGGGG",
|
||||||
|
"GGGRRGGGGG",
|
||||||
|
"GGGRRGGHHG",
|
||||||
|
"GGGGGGGHHG",
|
||||||
|
"GGGWWGGGGG",
|
||||||
|
"GGGWWGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_1",
|
||||||
|
"name": "Zhang Mancheng",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 1,
|
||||||
|
"pos": [7, 2],
|
||||||
|
"base": { "hp": 34, "atk": 11, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_2",
|
||||||
|
"name": "Peasant Rebel",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 1,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 30, "atk": 10, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_3",
|
||||||
|
"name": "Rebel Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 1,
|
||||||
|
"pos": [7, 6],
|
||||||
|
"base": { "hp": 26, "atk": 9, "def": 4 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao engages the Yellow Turbans." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The rebels are scattered, but their numbers will swell if we hesitate." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then we strike now. I will open a path through their front line." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_warning",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The Yellow Turbans are looking for an opening." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 300,
|
||||||
|
"items": ["bean"],
|
||||||
|
"join_officers": ["xiahou_yuan", "cao_ren"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The rebels are scattered. Yingchuan should breathe easier tonight." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "This is only the first fire. If the court cannot move quickly, we must." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Then call on us for the next campaign. We will ride with you." },
|
||||||
|
{ "speaker": "Cao Ren", "text": "The clan stands ready, my lord." }
|
||||||
|
]
|
||||||
|
}
|
||||||
201
data/scenarios/002_sishui_gate.json
Normal file
201
data/scenarios/002_sishui_gate.json
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"id": "002_sishui_gate",
|
||||||
|
"name": "Sishui Gate Vanguard",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Hua Xiong's vanguard.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 9."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_2_warning" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 9, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Sishui Gate Vanguard",
|
||||||
|
"location": "Sishui Gate, 190 CE",
|
||||||
|
"lines": [
|
||||||
|
"The coalition gathers to challenge Dong Zhuo.",
|
||||||
|
"Cao Cao leads a small vanguard to test Hua Xiong's defenses."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"cloth_robe",
|
||||||
|
"leather_armor",
|
||||||
|
"short_bow",
|
||||||
|
"training_spear"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 3,
|
||||||
|
"required_officers": ["cao_cao"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 12,
|
||||||
|
"height": 9,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGRRGGGGG",
|
||||||
|
"GGGFFRRGHHGG",
|
||||||
|
"GGGFFRRGHHGG",
|
||||||
|
"GGGGGRRGGGGG",
|
||||||
|
"RRRRRRRRRRRR",
|
||||||
|
"GGGGGRRGGGGG",
|
||||||
|
"GGHGGFFGGGGG",
|
||||||
|
"GGHGGFFGGWWG",
|
||||||
|
"GGGGGGGGGWWG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch2",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch2",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch2",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch2",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "hua_xiong_vanguard",
|
||||||
|
"name": "Hua Xiong Vanguard",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [9, 4],
|
||||||
|
"base": { "hp": 44, "atk": 15, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_spearman_1",
|
||||||
|
"name": "Liang Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [8, 3],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_spearman_2",
|
||||||
|
"name": "Liang Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_archer",
|
||||||
|
"name": "Liang Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [10, 2],
|
||||||
|
"base": { "hp": 28, "atk": 10, "def": 4 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The coalition vanguard moves toward Sishui Gate." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Hua Xiong's vanguard is testing the coalition's nerve. We cannot give them ground." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Their archers hold the ridge. I will keep pressure on their flank." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_warning",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Hua Xiong's men tighten their formation." },
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Hua Xiong's vanguard and reinforcements.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 9."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "liang_reinforcement_1",
|
||||||
|
"name": "Liang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 36, "atk": 12, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_reinforcement_2",
|
||||||
|
"name": "Liang Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [11, 5],
|
||||||
|
"base": { "hp": 34, "atk": 11, "def": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "pursue_dong_zhuo",
|
||||||
|
"label": "Pursue Dong Zhuo",
|
||||||
|
"description": "Cao Cao presses the retreating army while Cao Ren secures Sishui.",
|
||||||
|
"leave_officers": ["cao_ren"],
|
||||||
|
"set_flags": { "pursued_dong_zhuo": true, "regrouped_after_sishui": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "regroup_coalition",
|
||||||
|
"label": "Regroup",
|
||||||
|
"description": "The vanguard pauses to gather supplies.",
|
||||||
|
"set_flags": { "pursued_dong_zhuo": false, "regrouped_after_sishui": true }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 500,
|
||||||
|
"items": ["bean"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Sishui Gate is broken, but Dong Zhuo's main force is already moving west." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "The coalition will argue while the enemy escapes. We must decide whether to pursue or regroup." }
|
||||||
|
]
|
||||||
|
}
|
||||||
330
data/scenarios/003_xingyang_ambush.json
Normal file
330
data/scenarios/003_xingyang_ambush.json
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
{
|
||||||
|
"id": "003_xingyang_ambush",
|
||||||
|
"name": "Xingyang Ambush",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Xu Rong's rearguard.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_ambush" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 11, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Xingyang Ambush",
|
||||||
|
"location": "Xingyang, 190 CE",
|
||||||
|
"lines": [
|
||||||
|
"Dong Zhuo abandons Luoyang and retreats west.",
|
||||||
|
"Cao Cao's vanguard turns toward the enemy rearguard near Xingyang.",
|
||||||
|
"The road narrows ahead. Keep the vanguard together and watch the forest line."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_dong_zhuo": true },
|
||||||
|
"lines": [
|
||||||
|
"The rapid pursuit leaves little time for scouting, but may catch Xu Rong before he regroups."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "regrouped_after_sishui": true },
|
||||||
|
"lines": [
|
||||||
|
"Scouts from Sishui report suspicious movement in the trees. Expect an ambush and prepare accordingly."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_dong_zhuo": true },
|
||||||
|
"items": ["hand_axe"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "regrouped_after_sishui": true },
|
||||||
|
"items": ["leather_armor"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 3,
|
||||||
|
"required_officers": ["cao_cao"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 12,
|
||||||
|
"height": 9,
|
||||||
|
"terrain": [
|
||||||
|
"GGGFFRRGGGGG",
|
||||||
|
"GGGFFRRGHHGG",
|
||||||
|
"GGGGGRRGHHGG",
|
||||||
|
"RRRRRRRRRRRR",
|
||||||
|
"GGGGFRRFGGGG",
|
||||||
|
"GGGHFRRFGGGG",
|
||||||
|
"GGGHHRRGFFGG",
|
||||||
|
"GGGGGRRGFFGG",
|
||||||
|
"GGWWGRRGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch3",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch3",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch3",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch3",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xu_rong_rearguard",
|
||||||
|
"name": "Xu Rong Rearguard",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [9, 4],
|
||||||
|
"base": { "hp": 46, "atk": 15, "def": 8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_spearman",
|
||||||
|
"name": "Liang Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [7, 3],
|
||||||
|
"base": { "hp": 36, "atk": 12, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "liang_strategist",
|
||||||
|
"name": "Liang Strategist",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 30, "mp": 14, "atk": 7, "def": 4, "int": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "rearguard_archer",
|
||||||
|
"name": "Rearguard Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [8, 2],
|
||||||
|
"base": { "hp": 30, "atk": 11, "def": 5 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening_pursuit",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start", "campaign_flags": { "pursued_dong_zhuo": true } },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao's rapid pursuit closes on Dong Zhuo's rearguard." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_regroup",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start", "campaign_flags": { "regrouped_after_sishui": true } },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao advances after gathering scouts and supplies at Sishui." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue_pursuit",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin", "campaign_flags": { "pursued_dong_zhuo": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The coalition hesitates while Dong Zhuo escapes. We will not let this chance pass." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The road is too quiet. If they mean to spring a trap, let them try it." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "I will watch the ridges. Their bowmen will favor the high ground." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue_regroup",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin", "campaign_flags": { "regrouped_after_sishui": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The delay at Sishui bought us scouts and fresh supplies. Now we move with clear eyes." },
|
||||||
|
{ "speaker": "Cao Ren", "text": "Our riders report movement in the forest line. Xu Rong may be waiting ahead." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Then we draw them out and break their trap before it closes." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_ambush",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The forest line shifts as Xu Rong's ambush springs." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_ambush_pursuit",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "pursued_dong_zhuo": true } },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Xu Rong's ambush force closes in from the forest." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xu Rong", "text": "Cao Cao took the bait. Seal the road and cut down his vanguard." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "So this was their plan. Hold formation and break through!" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Xu Rong and every ambush unit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ambush_bandit_1",
|
||||||
|
"name": "Ambush Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [5, 1],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ambush_bandit_2",
|
||||||
|
"name": "Ambush Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [5, 7],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ambush_archer",
|
||||||
|
"name": "Ambush Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [10, 6],
|
||||||
|
"base": { "hp": 28, "atk": 11, "def": 4 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_ambush_regroup",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "regrouped_after_sishui": true } },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Scouts expose Xu Rong's ambush before the trap fully closes." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xu Rong", "text": "They found our screen too soon. Attack before Cao Cao can withdraw!" },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Their trap is revealed. Strike the exposed units and keep the road open." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Xu Rong and the exposed ambush units.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ambush_bandit_scouted",
|
||||||
|
"name": "Exposed Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [5, 1],
|
||||||
|
"base": { "hp": 32, "atk": 11, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ambush_archer_scouted",
|
||||||
|
"name": "Exposed Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 3,
|
||||||
|
"pos": [10, 6],
|
||||||
|
"base": { "hp": 26, "atk": 10, "def": 4 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5, "campaign_flags": { "pursued_dong_zhuo": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Their line is stretched. Push through the road before they regroup." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_regroup_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5, "campaign_flags": { "regrouped_after_sishui": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "Their hidden troops are fewer than expected. The scouts earned their pay." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. Keep the pressure on Xu Rong before he reforms the line." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 700,
|
||||||
|
"items": ["bean", "wine", "iron_sword"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Xu Rong's trap failed, but Dong Zhuo has bought himself time." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then we sharpen the army and wait for the next opening." }
|
||||||
|
]
|
||||||
|
}
|
||||||
247
data/scenarios/004_qingzhou_campaign.json
Normal file
247
data/scenarios/004_qingzhou_campaign.json
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
{
|
||||||
|
"id": "004_qingzhou_campaign",
|
||||||
|
"name": "Qingzhou Campaign",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat the Yellow Turban remnant leaders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_bandit_push" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Qingzhou Campaign",
|
||||||
|
"location": "Qingzhou, 192 CE",
|
||||||
|
"lines": [
|
||||||
|
"After the coalition fractures, Cao Cao turns toward Qingzhou.",
|
||||||
|
"Yellow Turban remnants threaten the villages and trade roads.",
|
||||||
|
"Secure the road, defeat the rebel leaders, and gather soldiers for a lasting army."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_dong_zhuo": true },
|
||||||
|
"lines": [
|
||||||
|
"The army is still lean from the rapid pursuit after Sishui, but its veterans are hardened by the ambush at Xingyang."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "regrouped_after_sishui": true },
|
||||||
|
"lines": [
|
||||||
|
"Supplies gathered after Sishui let Cao Cao press into Qingzhou with a steadier line."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 4,
|
||||||
|
"required_officers": ["cao_cao"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [2, 6], [3, 4], [3, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 14,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGGGGGGGGGG",
|
||||||
|
"GGGFFFGGGHHGGG",
|
||||||
|
"GGGFFFGRRHHGGG",
|
||||||
|
"GGGGGGRRRGGGGG",
|
||||||
|
"RRRRRRRRRRRRRR",
|
||||||
|
"GGGGGGRRRGGGGG",
|
||||||
|
"GGHGGGFFFGGGGG",
|
||||||
|
"GGHHGGFFFGGWWG",
|
||||||
|
"GGGGGGGFFGGWWG",
|
||||||
|
"GGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch4",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch4",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch4",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch4",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "qingzhou_chief",
|
||||||
|
"name": "Qingzhou Chief",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 48, "atk": 15, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_spearman_1",
|
||||||
|
"name": "Yellow Turban Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 38, "atk": 13, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_spearman_2",
|
||||||
|
"name": "Yellow Turban Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [9, 5],
|
||||||
|
"base": { "hp": 38, "atk": 13, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_archer",
|
||||||
|
"name": "Yellow Turban Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [10, 2],
|
||||||
|
"base": { "hp": 30, "atk": 11, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_strategist",
|
||||||
|
"name": "Yellow Turban Mystic",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [11, 6],
|
||||||
|
"base": { "hp": 32, "mp": 16, "atk": 7, "def": 4, "int": 11 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao enters Qingzhou to pacify the Yellow Turban remnants." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "A commander needs more than victories. We need men who can hold the land after the banners move on." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then we break these remnants and give the villages a reason to trust our flag." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Their archers have taken the eastern ridge. I will keep them from harassing the road." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_bandit_push",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Hidden Yellow Turban raiders rush from the woods." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Qingzhou Chief", "text": "The villages are ours to feed from. Drive Cao Cao back to the west!" },
|
||||||
|
{ "speaker": "Cao Cao", "text": "They reveal themselves. Hold the road and cut them down in detail." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat the Qingzhou chief and every raider.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "qingzhou_raider_north",
|
||||||
|
"name": "Qingzhou Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [6, 1],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "qingzhou_raider_south",
|
||||||
|
"name": "Qingzhou Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [6, 8],
|
||||||
|
"base": { "hp": 34, "atk": 12, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "qingzhou_raider_archer",
|
||||||
|
"name": "Qingzhou Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 4,
|
||||||
|
"pos": [13, 7],
|
||||||
|
"base": { "hp": 28, "atk": 11, "def": 4 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_dian_wei_rumor",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Scout", "text": "My lord, a fierce warrior has been seen defending villagers east of the ridge." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Remember his name. Men who protect the weak are worth more than plunderers." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 800,
|
||||||
|
"items": ["bean", "hand_axe", "iron_armor"],
|
||||||
|
"join_officers": ["dian_wei"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Dian Wei", "text": "You fought the raiders instead of bargaining with them. I can respect that." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then stand with me, Dian Wei. I need warriors who can guard more than their own fame." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Give me a place in the line, and I will hold it." }
|
||||||
|
]
|
||||||
|
}
|
||||||
267
data/scenarios/005_puyang_raid.json
Normal file
267
data/scenarios/005_puyang_raid.json
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
{
|
||||||
|
"id": "005_puyang_raid",
|
||||||
|
"name": "Puyang Raid",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Lu Bu's forward guard.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_lu_bu_arrives" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 11, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Puyang Raid",
|
||||||
|
"location": "Puyang, 194 CE",
|
||||||
|
"lines": [
|
||||||
|
"Lu Bu has seized momentum in Yan Province.",
|
||||||
|
"Cao Cao launches a night raid near Puyang to break the forward guard before the main cavalry can gather.",
|
||||||
|
"Dian Wei joins the front line as Cao Cao's personal shield."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"iron_armor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 4,
|
||||||
|
"required_officers": ["cao_cao", "dian_wei"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 14,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGG",
|
||||||
|
"GGGFFHHGGGFFGG",
|
||||||
|
"GGGFFRRRRRFFGG",
|
||||||
|
"GGGGGRGGGRGGGG",
|
||||||
|
"RRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGRGGGG",
|
||||||
|
"GGFFFRRRRRFFGG",
|
||||||
|
"GGFFFHHGGGFFGG",
|
||||||
|
"GGGGGHHGGGWWGG",
|
||||||
|
"GGGGGGGGGGWWGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch5",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch5",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch5",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch5",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch5",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_liao_vanguard",
|
||||||
|
"name": "Zhang Liao Vanguard",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [10, 4],
|
||||||
|
"base": { "hp": 48, "atk": 16, "def": 8, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "gao_shun_guard",
|
||||||
|
"name": "Gao Shun Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 44, "atk": 14, "def": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "puyang_cavalry_1",
|
||||||
|
"name": "Puyang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 38, "atk": 13, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "puyang_cavalry_2",
|
||||||
|
"name": "Puyang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [11, 5],
|
||||||
|
"base": { "hp": 38, "atk": 13, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "puyang_archer",
|
||||||
|
"name": "Puyang Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [10, 2],
|
||||||
|
"base": { "hp": 30, "atk": 12, "def": 5 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao's raiding force slips toward Puyang under cover of darkness." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Lu Bu's cavalry is dangerous once it gathers speed. We strike before the whole camp wakes." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Stay behind me, my lord. Any rider who reaches you answers to my axe." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Good. We cut open their guard and withdraw before dawn." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_alarm",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Puyang's forward camp sounds the alarm." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang Liao", "text": "Cao Cao comes in the night. Riders, close the road and pin him here." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "The alarm is raised. Break their line before Lu Bu himself arrives." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_lu_bu_arrives",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Lu Bu's banner appears beyond the eastern road." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Lu Bu", "text": "Cao Cao dares raid Puyang? Then let him see how fast my riders wake." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "That one has a loud voice. I will hold the road while you finish the guard." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Zhang Liao's guard and the arriving riders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_banner",
|
||||||
|
"name": "Lu Bu Banner Guard",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [13, 4],
|
||||||
|
"base": { "hp": 54, "atk": 18, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_rider_north",
|
||||||
|
"name": "Lu Bu Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 38, "atk": 14, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_rider_south",
|
||||||
|
"name": "Lu Bu Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 5,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 38, "atk": 14, "def": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Dian Wei", "text": "Their riders hit hard, but their line bends. Give the word and I will split it." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Press them. We leave Puyang with their vanguard broken." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 900,
|
||||||
|
"items": ["bean", "wine", "iron_sword"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Lu Bu's men still hold Puyang, but their forward guard will remember tonight." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "They came fast. Next time, let them come closer." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "We have measured their strength. Now we choose whether to fortify Yan Province or strike again." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "fortify_yan_province",
|
||||||
|
"label": "Fortify Yan Province",
|
||||||
|
"description": "Secure supply roads before another clash with Lu Bu.",
|
||||||
|
"set_flags": { "fortified_yan_province": true, "pressed_lu_bu": false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "press_lu_bu",
|
||||||
|
"label": "Press Lu Bu",
|
||||||
|
"description": "Keep pressure on Puyang while Lu Bu's guard is shaken.",
|
||||||
|
"set_flags": { "fortified_yan_province": false, "pressed_lu_bu": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
305
data/scenarios/006_dingtao_counterattack.json
Normal file
305
data/scenarios/006_dingtao_counterattack.json
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
{
|
||||||
|
"id": "006_dingtao_counterattack",
|
||||||
|
"name": "Dingtao Counterattack",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Lu Bu's counterattack force.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_counterattack" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Dingtao Counterattack",
|
||||||
|
"location": "Dingtao, 194 CE",
|
||||||
|
"lines": [
|
||||||
|
"After the raid at Puyang, Lu Bu moves to punish Cao Cao's army near Dingtao.",
|
||||||
|
"The field is broken by ridges and roads. Keep the line tight and blunt the cavalry charge."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_yan_province": true },
|
||||||
|
"lines": [
|
||||||
|
"Fortified supply roads give Cao Cao time to choose the battlefield and prepare reserves."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_lu_bu": true },
|
||||||
|
"lines": [
|
||||||
|
"Cao Cao's pressure has kept Lu Bu off balance, but the army enters Dingtao with little rest."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_yan_province": true },
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_lu_bu": true },
|
||||||
|
"items": ["war_axe"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 4,
|
||||||
|
"required_officers": ["cao_cao", "dian_wei"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [2, 6], [3, 4], [3, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 14,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGGGGGGGGGG",
|
||||||
|
"GGGFFGGGHHGGGG",
|
||||||
|
"GGGFFRRRHHGGGG",
|
||||||
|
"GGGGGRRRGGGGGG",
|
||||||
|
"RRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRRRGGGGGG",
|
||||||
|
"GGHHGFFFGRRGGG",
|
||||||
|
"GGHHGFFFGGGWWG",
|
||||||
|
"GGGGGGFFGGGWWG",
|
||||||
|
"GGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch6",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch6",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch6",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch6",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch6",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_counterattack",
|
||||||
|
"name": "Lu Bu Counterattack",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [10, 4],
|
||||||
|
"base": { "hp": 58, "atk": 19, "def": 10, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_liao_pursuer",
|
||||||
|
"name": "Zhang Liao Pursuer",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 46, "atk": 16, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "gao_shun_line",
|
||||||
|
"name": "Gao Shun Line",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [9, 5],
|
||||||
|
"base": { "hp": 46, "atk": 15, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dingtao_archer",
|
||||||
|
"name": "Dingtao Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [11, 2],
|
||||||
|
"base": { "hp": 32, "atk": 12, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dingtao_strategist",
|
||||||
|
"name": "Dingtao Strategist",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [11, 6],
|
||||||
|
"base": { "hp": 34, "mp": 18, "atk": 7, "def": 5, "int": 12 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Lu Bu's counterattack sweeps toward Dingtao." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Lu Bu wants a swift decision. We deny him speed and break his vanguard." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Let the riders come. A road is narrow when I stand in it." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The center road is their best charge lane. We hold there, then counter." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_fortified_reserve",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "fortified_yan_province": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Scout", "text": "The eastern riders are moving exactly where the reserve scouts predicted." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. Let them spend their charge against prepared ground." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "screened_lu_bu_rider",
|
||||||
|
"name": "Screened Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [12, 3],
|
||||||
|
"base": { "hp": 38, "atk": 14, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "screened_lu_bu_archer",
|
||||||
|
"name": "Screened Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 30, "atk": 12, "def": 5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_pressed_lu_bu_charge",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "pressed_lu_bu": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Lu Bu", "text": "Cao Cao chased too far. Trample his tired line!" },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Tired men still swing hard when cornered." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "fierce_lu_bu_rider",
|
||||||
|
"name": "Fierce Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "fierce_lu_bu_guard",
|
||||||
|
"name": "Fierce Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [13, 4],
|
||||||
|
"base": { "hp": 42, "atk": 14, "def": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "fierce_lu_bu_rider_south",
|
||||||
|
"name": "Fierce Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 6,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_counterattack",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Lu Bu commits his second wave." },
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Lu Bu's counterattack and every second-wave unit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_hold_center",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Their charge is spent. Now strike at the officers holding the line together." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "I have their archers marked. One clear shot can open the road." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1000,
|
||||||
|
"items": ["bean", "wine", "war_axe"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Lu Bu's charge is powerful, but not invincible." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "His riders bleed like anyone else." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then Yan Province is not lost. We keep fighting." }
|
||||||
|
]
|
||||||
|
}
|
||||||
319
data/scenarios/007_xian_emperor_escort.json
Normal file
319
data/scenarios/007_xian_emperor_escort.json
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
{
|
||||||
|
"id": "007_xian_emperor_escort",
|
||||||
|
"name": "Xian Emperor Escort",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Move Cao Cao to the eastern road marker.",
|
||||||
|
"defeat": "Cao Cao or the imperial envoy is defeated, or the battle reaches Turn 11."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": {
|
||||||
|
"type": "unit_reaches_tile",
|
||||||
|
"team": "player",
|
||||||
|
"officer_ids": ["cao_cao"],
|
||||||
|
"pos": [13, 4]
|
||||||
|
},
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "any_unit_defeated", "unit_ids": ["imperial_envoy_ch7"] },
|
||||||
|
{ "type": "turn_reached", "turn": 11, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Xian Emperor Escort",
|
||||||
|
"location": "Luoyang Road, 196 CE",
|
||||||
|
"lines": [
|
||||||
|
"The imperial carriage has slipped out of the ruined capital under pressure from rival warlords.",
|
||||||
|
"Cao Cao must reach the eastern road marker and secure the escort route before the remnants close in."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_yan_province": true },
|
||||||
|
"lines": [
|
||||||
|
"The fortified roads in Yan Province have kept supplies moving, giving the escort column a steadier pace."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_lu_bu": true },
|
||||||
|
"lines": [
|
||||||
|
"The army arrives quickly after pressing Lu Bu, but the forced march leaves little time to rest."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_yan_province": true },
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_lu_bu": true },
|
||||||
|
"items": ["war_axe"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 5,
|
||||||
|
"required_officers": ["cao_cao"],
|
||||||
|
"required_units": ["imperial_envoy_ch7"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGGGGGGGGGGG",
|
||||||
|
"GGGFFGGGHHGGGGG",
|
||||||
|
"GGGFFRRRHHGGGGG",
|
||||||
|
"GGGGGRRRGGFFFGG",
|
||||||
|
"RRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRRRGGFFFGG",
|
||||||
|
"GGHHGFFFGGGRRGG",
|
||||||
|
"GGHHGFFFGGGWWGG",
|
||||||
|
"GGGGGGFFGGGWWGG",
|
||||||
|
"GGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch7",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "imperial_envoy_ch7",
|
||||||
|
"name": "Imperial Envoy",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "player",
|
||||||
|
"level": 1,
|
||||||
|
"controllable": false,
|
||||||
|
"persist_progression": false,
|
||||||
|
"ai_target_priority": 6,
|
||||||
|
"pos": [1, 5],
|
||||||
|
"base": { "hp": 28, "mp": 0, "atk": 1, "def": 4, "int": 8, "agi": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch7",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch7",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch7",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch7",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "li_jue_vanguard",
|
||||||
|
"name": "Li Jue Vanguard",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [9, 4],
|
||||||
|
"base": { "hp": 48, "atk": 16, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_si_guard",
|
||||||
|
"name": "Guo Si Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [10, 5],
|
||||||
|
"base": { "hp": 48, "atk": 15, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "escort_road_archer",
|
||||||
|
"name": "Road Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 34, "atk": 13, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "escort_hill_bandit",
|
||||||
|
"name": "Hill Bandit",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [8, 2],
|
||||||
|
"base": { "hp": 42, "atk": 14, "def": 7 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "escort_strategist",
|
||||||
|
"name": "Court Remnant",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 36, "mp": 20, "atk": 7, "def": 6, "int": 13 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The imperial escort road lies beyond the enemy vanguard." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Do not chase every remnant. Our task is the road ahead." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then we punch through the center and keep you moving." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "I will keep their blades away from the envoy." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_rear_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Scout", "text": "Enemy riders are circling behind us!" },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then the envoy cannot linger. Forward." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "rear_pressure_rider",
|
||||||
|
"name": "Rear Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [4, 1],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 7, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "rear_pressure_bandit",
|
||||||
|
"name": "Rear Bandit",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [5, 7],
|
||||||
|
"base": { "hp": 40, "atk": 14, "def": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "midroad_ambush",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "pos": [7, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Imperial Envoy", "text": "There are banners moving in the northern trees!" },
|
||||||
|
{ "speaker": "Cao Cao", "text": "So the road was bait. Keep the envoy covered and break through." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "midroad_ambush_guard",
|
||||||
|
"name": "Ambush Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [7, 2],
|
||||||
|
"base": { "hp": 42, "atk": 14, "def": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "midroad_ambush_archer",
|
||||||
|
"name": "Ambush Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [8, 6],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 6 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_5_road_marker",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 5 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "The eastern road marker is in sight. Cao Cao only needs a clear lane." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. Hold the flanks and do not let the rear pressure break our pace." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1100,
|
||||||
|
"items": ["bean", "iron_armor"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The Son of Heaven is safe on the road. That changes every banner in the realm." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then every rival will look at us differently after today." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Let them. Authority is also a battlefield." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "secure_imperial_court",
|
||||||
|
"label": "Secure the Court",
|
||||||
|
"description": "Cao Cao focuses on stabilizing the imperial court.",
|
||||||
|
"set_flags": { "secured_imperial_court": true },
|
||||||
|
"gold": 200,
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pursue_li_jue_remnants",
|
||||||
|
"label": "Pursue Remnants",
|
||||||
|
"description": "The army presses the scattered warlord remnants.",
|
||||||
|
"set_flags": { "pursued_li_jue_remnants": true },
|
||||||
|
"items": ["war_drum", "training_spear", "bean", "wine"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
401
data/scenarios/008_wan_castle_escape.json
Normal file
401
data/scenarios/008_wan_castle_escape.json
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
{
|
||||||
|
"id": "008_wan_castle_escape",
|
||||||
|
"name": "Wan Castle Escape",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Move Cao Cao to the western road marker.",
|
||||||
|
"defeat": "Cao Cao or Cao Ang is defeated, or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": {
|
||||||
|
"type": "unit_reaches_tile",
|
||||||
|
"team": "player",
|
||||||
|
"officer_ids": ["cao_cao"],
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "any_unit_defeated", "unit_ids": ["cao_ang_ch8"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Wan Castle Escape",
|
||||||
|
"location": "Wancheng, 197 CE",
|
||||||
|
"lines": [
|
||||||
|
"Zhang Xiu welcomes Cao Cao into Wan Castle, but Jia Xu's counsel turns the banquet into a trap.",
|
||||||
|
"The eastern gate is collapsing behind the army. Escort Cao Ang and move Cao Cao to the western road marker before the ambush closes."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_imperial_court": true },
|
||||||
|
"lines": [
|
||||||
|
"The imperial court's seal has opened a few guarded roads, giving Cao Cao one narrow escape lane through the west."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_li_jue_remnants": true },
|
||||||
|
"lines": [
|
||||||
|
"The army's hard pursuit after the imperial escort keeps the vanguard sharp, but Zhang Xiu's scouts know Cao Cao's marching habits."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_imperial_court": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_li_jue_remnants": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 5,
|
||||||
|
"required_officers": ["cao_cao", "dian_wei"],
|
||||||
|
"required_units": ["cao_ang_ch8"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[10, 3], [10, 4], [10, 5], [11, 3], [11, 4], [11, 5], [12, 3], [12, 4], [12, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGFFFFG",
|
||||||
|
"GGFFGHHGGGFFFFG",
|
||||||
|
"GGFFRRRRRRGGGGG",
|
||||||
|
"GGGGRGGGGGRRRGG",
|
||||||
|
"RRRRRRRRRRRRRRR",
|
||||||
|
"GGGGRGGGGGRRRGG",
|
||||||
|
"GGFFRRRGGGFFFFG",
|
||||||
|
"GGFFGHHGGGFFFFG",
|
||||||
|
"GGGGGHHGGGWWGGG",
|
||||||
|
"GGGGGGGGGGWWGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch8",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [12, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ang_ch8",
|
||||||
|
"name": "Cao Ang",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "player",
|
||||||
|
"level": 4,
|
||||||
|
"controllable": false,
|
||||||
|
"persist_progression": false,
|
||||||
|
"ai_target_priority": 5,
|
||||||
|
"pos": [12, 5],
|
||||||
|
"base": { "hp": 32, "mp": 2, "atk": 9, "def": 6, "int": 6, "agi": 8 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch8",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [11, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch8",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [12, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch8",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [11, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch8",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [11, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_xiu_vanguard",
|
||||||
|
"name": "Zhang Xiu Vanguard",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [6, 4],
|
||||||
|
"base": { "hp": 52, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wan_castle_guard_north",
|
||||||
|
"name": "Wan Castle Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [5, 3],
|
||||||
|
"base": { "hp": 46, "atk": 15, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wan_castle_guard_south",
|
||||||
|
"name": "Wan Castle Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [5, 5],
|
||||||
|
"base": { "hp": 46, "atk": 15, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "jia_xu_strategist",
|
||||||
|
"name": "Jia Xu Strategist",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [4, 6],
|
||||||
|
"skills": ["blaze"],
|
||||||
|
"base": { "hp": 38, "mp": 22, "atk": 7, "def": 6, "int": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wan_castle_archer",
|
||||||
|
"name": "Wan Castle Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [6, 2],
|
||||||
|
"base": { "hp": 34, "atk": 13, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "southern_ambusher",
|
||||||
|
"name": "Southern Ambusher",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [7, 7],
|
||||||
|
"base": { "hp": 42, "atk": 15, "def": 7 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Wan Castle's gates close behind Cao Cao's column." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The music stopped too cleanly. This is Jia Xu's work." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Then I hold the gate while you cut west." },
|
||||||
|
{ "speaker": "Cao Ang", "text": "Father, the road is narrow. I will stay close." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_gate_closes",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang Xiu", "text": "Close the eastern gate. Cao Cao leaves Wan Castle only through our blades." },
|
||||||
|
{ "speaker": "Jia Xu", "text": "Do not chase the guards. Strike the road and separate him from his son." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Break through the western road and move Cao Cao to the escape marker.",
|
||||||
|
"defeat": "Cao Cao or Cao Ang is defeated, or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "east_gate_rider_north",
|
||||||
|
"name": "East Gate Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [13, 2],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 8, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "east_gate_rider_south",
|
||||||
|
"name": "East Gate Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [13, 6],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 8, "agi": 10 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "court_escape_lane",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3, "campaign_flags": { "secured_imperial_court": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Imperial Guard", "text": "The court seal opens the west postern. We can delay their riders for a moment." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then one moment is enough. Keep the road open." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployment",
|
||||||
|
"deployment": {
|
||||||
|
"unit_id": "imperial_guard_support_ch8",
|
||||||
|
"name": "Imperial Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "player",
|
||||||
|
"level": 6,
|
||||||
|
"controllable": false,
|
||||||
|
"persist_progression": false,
|
||||||
|
"pos": [9, 4],
|
||||||
|
"base": { "hp": 34, "atk": 10, "def": 9, "int": 6, "agi": 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "remnant_scout_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "pursued_li_jue_remnants": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Wan Scout", "text": "That is the same vanguard that chased Li Jue's remnants. Cut off their old marching path." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "They know our pace. Then we change it by force." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "remnant_scout_rider",
|
||||||
|
"name": "Remnant Scout Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [10, 1],
|
||||||
|
"base": { "hp": 38, "atk": 14, "def": 7, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "remnant_scout_archer",
|
||||||
|
"name": "Remnant Scout Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [10, 7],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "midroad_trap",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "pos": [8, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ang", "text": "Archers in the trees!" },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Stay behind the shields. I will make them aim at me." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "midroad_wan_archer_north",
|
||||||
|
"name": "Wan Ambush Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [8, 2],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "midroad_wan_archer_south",
|
||||||
|
"name": "Wan Ambush Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [8, 6],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_last_push",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The western road is close. Do not spend lives chasing revenge inside these walls." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Then I spend steel instead. Move." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1200,
|
||||||
|
"items": ["bean", "wine", "iron_sword"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Cao Ang", "text": "The road is clear. I thought Wan Castle would swallow us." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "It tried. Its teeth broke first." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "A trap survived is still a lesson. We will remember Jia Xu." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "hold_wan_gate",
|
||||||
|
"label": "Hold the Gate",
|
||||||
|
"description": "Dian Wei holds the rear long enough to recover armor and baggage.",
|
||||||
|
"set_flags": { "held_wan_gate": true, "swift_wan_escape": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swift_wan_withdrawal",
|
||||||
|
"label": "Swift Withdrawal",
|
||||||
|
"description": "Cao Cao leaves the baggage behind to keep the army intact.",
|
||||||
|
"set_flags": { "held_wan_gate": false, "swift_wan_escape": true },
|
||||||
|
"gold": 250,
|
||||||
|
"items": ["bean", "wine", "short_bow"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
369
data/scenarios/009_xiapi_siege.json
Normal file
369
data/scenarios/009_xiapi_siege.json
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
{
|
||||||
|
"id": "009_xiapi_siege",
|
||||||
|
"name": "Xiapi Siege",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Hold through the flood-gate operation, then defeat Lu Bu's army.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_flood_gates" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 13, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Xiapi Siege",
|
||||||
|
"location": "Xiapi, 198 CE",
|
||||||
|
"lines": [
|
||||||
|
"Lu Bu has withdrawn behind Xiapi's wet walls, trusting his cavalry and Chen Gong's counsel to break the siege.",
|
||||||
|
"Cao Cao orders the river channels opened. Survive the countercharge until the flood-gate signals confirm that Lu Bu's army has been forced into the open."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "held_wan_gate": true },
|
||||||
|
"lines": [
|
||||||
|
"The baggage recovered at Wan Castle keeps the siege line supplied with heavier shields and dry bowstrings."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "swift_wan_escape": true },
|
||||||
|
"lines": [
|
||||||
|
"The army reached Xiapi quickly after leaving Wan Castle behind, but some siege stores are still catching up."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "held_wan_gate": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "swift_wan_escape": true },
|
||||||
|
"items": ["war_axe"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 5,
|
||||||
|
"required_officers": ["cao_cao", "dian_wei"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGWWGGG",
|
||||||
|
"GGGFFHHGGGWWGGG",
|
||||||
|
"GGGFFRRRRGGGGGG",
|
||||||
|
"GGGGGRRRGGFFFGG",
|
||||||
|
"RRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRRRGGFFFGG",
|
||||||
|
"GGHHGFFFGGGRRGG",
|
||||||
|
"GGHHGFFFGGGWWGG",
|
||||||
|
"GGGGGGFFGGGWWGG",
|
||||||
|
"GGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch9",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch9",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch9",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch9",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch9",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_xiapi",
|
||||||
|
"name": "Lu Bu",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 64, "atk": 21, "def": 11, "agi": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "chen_gong_strategist",
|
||||||
|
"name": "Chen Gong",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [10, 6],
|
||||||
|
"skills": ["blaze"],
|
||||||
|
"base": { "hp": 40, "mp": 24, "atk": 7, "def": 6, "int": 15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "gao_shun_trap_line",
|
||||||
|
"name": "Gao Shun Line",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [9, 5],
|
||||||
|
"base": { "hp": 50, "atk": 16, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiapi_rider_north",
|
||||||
|
"name": "Xiapi Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 42, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiapi_rider_south",
|
||||||
|
"name": "Xiapi Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [10, 5],
|
||||||
|
"base": { "hp": 42, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiapi_wall_archer",
|
||||||
|
"name": "Wall Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 34, "atk": 13, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiapi_gate_guard",
|
||||||
|
"name": "Gate Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 46, "atk": 15, "def": 10 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The siege lines tighten around Xiapi." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Lu Bu trusts his speed even behind walls. We will make the ground choose against him." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The channels are ready. We only need to hold the line until the signal rises." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Then I hold the road. Let the horsemen find me first." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_lu_bu_charge",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Lu Bu", "text": "Cao Cao hides behind ditches and mud. Riders, break the siege line before the water rises!" },
|
||||||
|
{ "speaker": "Chen Gong", "text": "Strike the commander if the line bends. The flood plan dies with him." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_flank_rider_north",
|
||||||
|
"name": "Flank Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [6, 1],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "lu_bu_flank_rider_south",
|
||||||
|
"name": "Flank Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [6, 8],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_flood_gates",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The flood-gate signal rises around Xiapi." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Scout", "text": "The water is moving. Xiapi's outer detachments are falling back from the low roads!" },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. Now Lu Bu must fight where we can reach him." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Lu Bu, Chen Gong, and the remaining Xiapi defenders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "flooded_gate_guard",
|
||||||
|
"name": "Sluice Gate Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [8, 4],
|
||||||
|
"base": { "hp": 42, "atk": 14, "def": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "flooded_wall_archer",
|
||||||
|
"name": "Sluice Wall Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [8, 6],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wan_gate_baggage_support",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 4, "campaign_flags": { "held_wan_gate": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "The baggage wagons from Wan have reached the siege line. The reserves are steady." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then press forward carefully. We can afford patience." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swift_wan_counterraid",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 4, "campaign_flags": { "swift_wan_escape": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Chen Gong", "text": "Cao Cao came quickly, but speed thins a siege line. Strike the loose wagons." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Their raiders found the supply road. I will cut them off." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployment",
|
||||||
|
"deployment": {
|
||||||
|
"unit_id": "supply_road_raider",
|
||||||
|
"name": "Supply Road Raider",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 8,
|
||||||
|
"pos": [4, 7],
|
||||||
|
"base": { "hp": 40, "atk": 14, "def": 7, "agi": 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_lu_bu_cornered",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "His cavalry has nowhere clean to run. Close the ring and do not let Chen Gong buy another hour." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Then the road ends here." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1400,
|
||||||
|
"items": ["bean", "wine", "war_axe"],
|
||||||
|
"join_officers": ["guo_jia"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Xiapi is broken. Lu Bu's riders will not trouble Yan Province again." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Fast horses drown the same as slow ones." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Lu Bu's fall leaves Yuan Shao as the real measure of the north." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Speed without judgment is only a longer fall. We turn now from survival to rule." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "integrate_xu_province",
|
||||||
|
"label": "Integrate Xu Province",
|
||||||
|
"description": "Cao Cao places officials to secure the newly won territory.",
|
||||||
|
"set_flags": { "integrated_xu_province": true, "pressed_northern_campaign": false },
|
||||||
|
"gold": 200,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "press_northern_campaign",
|
||||||
|
"label": "Press North",
|
||||||
|
"description": "The army prepares to turn its momentum toward the northern rivals.",
|
||||||
|
"set_flags": { "integrated_xu_province": false, "pressed_northern_campaign": true },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["wine", "war_drum"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
364
data/scenarios/010_white_horse_relief.json
Normal file
364
data/scenarios/010_white_horse_relief.json
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
{
|
||||||
|
"id": "010_white_horse_relief",
|
||||||
|
"name": "White Horse Relief",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Yan Liang's vanguard after the decoy line is sprung.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_decoy_line" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "White Horse Relief",
|
||||||
|
"location": "White Horse, 200 CE",
|
||||||
|
"lines": [
|
||||||
|
"Yuan Shao's forward army presses toward White Horse with Yan Liang at its point.",
|
||||||
|
"Guo Jia proposes a decoy line: draw the vanguard down the road, then strike once its wings stretch too far."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "integrated_xu_province": true },
|
||||||
|
"lines": [
|
||||||
|
"Officials left in Xu Province keep the rear quiet, letting Cao Cao commit steadier reserves to the northern front."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_northern_campaign": true },
|
||||||
|
"lines": [
|
||||||
|
"The army marches quickly from Xiapi, hoping speed can strike Yuan Shao's vanguard before it settles."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "integrated_xu_province": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_northern_campaign": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 6,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 3], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 15,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGGHHGGGGGGG",
|
||||||
|
"GGFFGGHHGGGFFGG",
|
||||||
|
"GGFFRRRRRRGFFGG",
|
||||||
|
"GGGGGRGGGRGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGRGGGGG",
|
||||||
|
"GGHHGRRRGGFFFGG",
|
||||||
|
"GGHHGFFFGGFFFGG",
|
||||||
|
"GGGGGFFFGGWWGGG",
|
||||||
|
"GGGGGGGGGGWWGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch10",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch10",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch10",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch10",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch10",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch10",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yan_liang_vanguard",
|
||||||
|
"name": "Yan Liang Vanguard",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 62, "atk": 20, "def": 11, "agi": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shao_spear_line",
|
||||||
|
"name": "Yuan Spear Line",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [9, 4],
|
||||||
|
"base": { "hp": 50, "atk": 16, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shao_guard",
|
||||||
|
"name": "Yuan Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [10, 5],
|
||||||
|
"base": { "hp": 48, "atk": 15, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "white_horse_archer_north",
|
||||||
|
"name": "White Horse Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [10, 2],
|
||||||
|
"base": { "hp": 34, "atk": 14, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "white_horse_archer_south",
|
||||||
|
"name": "White Horse Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 34, "atk": 14, "def": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shao_advisor",
|
||||||
|
"name": "Yuan Advisor",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [12, 5],
|
||||||
|
"skills": ["blaze"],
|
||||||
|
"base": { "hp": 38, "mp": 22, "atk": 6, "def": 6, "int": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Yuan Shao's vanguard advances toward White Horse." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Yan Liang is brave enough to follow a retreating banner. That is the door we open." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then the center yields just enough. When his wings stretch, we cut them." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "A false retreat still needs men willing to look beaten." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_yan_liang_push",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yan Liang", "text": "Cao Cao's line bends already. Drive straight down the road!" },
|
||||||
|
{ "speaker": "Guo Jia", "text": "He takes the bait. Hold until the second signal." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "yan_liang_rider_north",
|
||||||
|
"name": "Yan Liang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [8, 2],
|
||||||
|
"base": { "hp": 42, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yan_liang_rider_south",
|
||||||
|
"name": "Yan Liang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [8, 6],
|
||||||
|
"base": { "hp": 42, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_decoy_line",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Signal the counterstroke. Yan Liang has outrun his supports." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "The vanguard is isolated. Strike the horse first, then the spears." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Yan Liang and every isolated vanguard unit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "decoy_cutoff_guard",
|
||||||
|
"name": "Cutoff Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [7, 4],
|
||||||
|
"base": { "hp": 44, "atk": 15, "def": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "decoy_cutoff_archer",
|
||||||
|
"name": "Cutoff Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [7, 5],
|
||||||
|
"base": { "hp": 32, "atk": 13, "def": 5 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xu_province_reserve",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 4, "campaign_flags": { "integrated_xu_province": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "Xu Province's roads are quiet. The reserve wagons have arrived on time." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Order and grain win battles before blades touch." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "northern_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 4, "campaign_flags": { "pressed_northern_campaign": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Scout", "text": "Cao Cao came quickly. Send the rear riders before his men settle!" },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "More riders from the east. They are trying to punish our speed." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployment",
|
||||||
|
"deployment": {
|
||||||
|
"unit_id": "yuan_rear_rider",
|
||||||
|
"name": "Yuan Rear Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 9,
|
||||||
|
"pos": [5, 8],
|
||||||
|
"base": { "hp": 40, "atk": 15, "def": 8, "agi": 11 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_finish_vanguard",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Yan Liang's formation is no longer an army. It is a collection of frightened men." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then finish this cleanly. White Horse must not remain under threat." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1500,
|
||||||
|
"items": ["bean", "wine", "iron_armor"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Yan Liang's charge broke exactly where Guo Jia said it would." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Bravery is useful. Predictable bravery is a road sign." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "White Horse is relieved. Now Yuan Shao must answer us with more than numbers." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "fortify_white_horse",
|
||||||
|
"label": "Fortify White Horse",
|
||||||
|
"description": "Cao Cao strengthens the crossing before the main Guandu confrontation.",
|
||||||
|
"set_flags": { "fortified_white_horse": true, "raided_yuan_supplies": false },
|
||||||
|
"gold": 150,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "raid_yuan_supplies",
|
||||||
|
"label": "Raid Supplies",
|
||||||
|
"description": "The army uses the vanguard's collapse to harass Yuan Shao's supply train.",
|
||||||
|
"set_flags": { "fortified_white_horse": false, "raided_yuan_supplies": true },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["wine", "training_spear"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
356
data/scenarios/011_yan_ford_pursuit.json
Normal file
356
data/scenarios/011_yan_ford_pursuit.json
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
{
|
||||||
|
"id": "011_yan_ford_pursuit",
|
||||||
|
"name": "Yan Ford Pursuit",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Wen Chou's countercharge after his riders commit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_2_wen_chou_charge" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Yan Ford Pursuit",
|
||||||
|
"location": "Yan Ford, 200 CE",
|
||||||
|
"lines": [
|
||||||
|
"Yan Liang's defeat shakes Yuan Shao's forward army, but Wen Chou rides to restore its nerve.",
|
||||||
|
"Cao Cao moves toward Yan Ford, where the road narrows between wooded banks and the river crossing."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_white_horse": true },
|
||||||
|
"lines": [
|
||||||
|
"The reinforced White Horse crossing slows Yuan Shao's messengers, buying time to arrange a tighter pursuit line."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "raided_yuan_supplies": true },
|
||||||
|
"lines": [
|
||||||
|
"Raids against Yuan Shao's baggage train leave Wen Chou's riders angry and hungry, but still eager to strike."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "fortified_white_horse": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "raided_yuan_supplies": true },
|
||||||
|
"items": ["war_axe"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 6,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 3], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 16,
|
||||||
|
"height": 10,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGHHGGGGGGGGGG",
|
||||||
|
"GGFFHHGGGFFGGGGG",
|
||||||
|
"GGRRRRRRRGGGHHGG",
|
||||||
|
"GGGGGGRGGGRRGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGGRGGGRRGGGG",
|
||||||
|
"GGHHGFFFGGWWGGGG",
|
||||||
|
"GGHHGFFFGGWWGGGG",
|
||||||
|
"GGGGGGFFGGWWGGGG",
|
||||||
|
"GGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch11",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch11",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch11",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch11",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch11",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch11",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wen_chou_vanguard",
|
||||||
|
"name": "Wen Chou Vanguard",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [12, 4],
|
||||||
|
"base": { "hp": 66, "atk": 21, "def": 12, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yan_ford_bridge_guard",
|
||||||
|
"name": "Yan Ford Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [10, 4],
|
||||||
|
"base": { "hp": 54, "atk": 17, "def": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_spear_north",
|
||||||
|
"name": "Yuan Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [10, 3],
|
||||||
|
"base": { "hp": 48, "atk": 16, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_spear_south",
|
||||||
|
"name": "Yuan Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [10, 5],
|
||||||
|
"base": { "hp": 48, "atk": 16, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yan_ford_archer_north",
|
||||||
|
"name": "Yan Ford Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 38, "atk": 15, "def": 7, "agi": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yan_ford_archer_south",
|
||||||
|
"name": "Yan Ford Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 38, "atk": 15, "def": 7, "agi": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_field_advisor",
|
||||||
|
"name": "Yuan Field Advisor",
|
||||||
|
"class_id": "strategist",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [13, 5],
|
||||||
|
"skills": ["blaze", "mend"],
|
||||||
|
"base": { "hp": 40, "mp": 24, "atk": 6, "def": 6, "int": 15 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Wen Chou's riders gather near Yan Ford." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Wen Chou will not accept Yan Liang's defeat as a warning. He will treat it as an insult." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then leave him a road wide enough to charge and narrow enough to close." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "The ford bends the field for us. I will watch the wooded banks." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_wen_chou_charge",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Wen Chou", "text": "Yan Liang fell because he was alone. I bring enough horsemen to trample Cao Cao's tricks." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Good. Pride has supplied the second wing for us." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "wen_chou_rider_north",
|
||||||
|
"name": "Wen Chou Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [14, 3],
|
||||||
|
"base": { "hp": 44, "atk": 16, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wen_chou_rider_south",
|
||||||
|
"name": "Wen Chou Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [14, 5],
|
||||||
|
"base": { "hp": 44, "atk": 16, "def": 9, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ford_ambush",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [9, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Guard", "text": "Cao Cao has reached the ford. Close the bank behind him!" },
|
||||||
|
{ "speaker": "Dian Wei", "text": "They picked the wrong bank to close." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ford_bank_guard",
|
||||||
|
"name": "Ford Bank Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [8, 3],
|
||||||
|
"base": { "hp": 46, "atk": 16, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ford_bank_archer",
|
||||||
|
"name": "Ford Bank Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 10,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 34, "atk": 14, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "white_horse_fortified",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3, "campaign_flags": { "fortified_white_horse": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "The fortified crossing behind us holds. Wen Chou cannot expect quick help from White Horse." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then this is his battle alone." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "yuan_supplies_raided",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3, "campaign_flags": { "raided_yuan_supplies": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Field Advisor", "text": "The supply carts are still scattered from Cao Cao's raid. The riders are charging hungry." },
|
||||||
|
{ "speaker": "Wen Chou", "text": "Hunger sharpens men. Forward!" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_close_ford",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "The ford is nearly sealed. Wen Chou's courage cannot carry him back across it." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Finish the countercharge and prepare the Guandu line." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1650,
|
||||||
|
"items": ["bean", "wine", "war_axe"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Wen Chou's riders scattered at the ford. Yuan Shao has lost two blades in two strokes." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "His numbers remain, but their certainty is damaged. That matters before Guandu." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we choose where the next certainty breaks." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "secure_guandu_line",
|
||||||
|
"label": "Secure Guandu Line",
|
||||||
|
"description": "Cao Cao strengthens the main defensive camps before Yuan Shao commits his full army.",
|
||||||
|
"set_flags": { "secured_guandu_line": true, "scouted_wuchao_depots": false },
|
||||||
|
"gold": 200,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scout_wuchao_depots",
|
||||||
|
"label": "Scout Wuchao",
|
||||||
|
"description": "Scouts follow the broken baggage routes toward Yuan Shao's supply depots.",
|
||||||
|
"set_flags": { "secured_guandu_line": false, "scouted_wuchao_depots": true },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["wine", "war_drum"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
395
data/scenarios/012_guandu_showdown.json
Normal file
395
data/scenarios/012_guandu_showdown.json
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
{
|
||||||
|
"id": "012_guandu_showdown",
|
||||||
|
"name": "Guandu Showdown",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Yuan Shao's main assault after the full line commits.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_4_yuan_main_assault" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 13, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Guandu Showdown",
|
||||||
|
"location": "Guandu, 200 CE",
|
||||||
|
"lines": [
|
||||||
|
"Yuan Shao's vast army presses into Guandu, confident that numbers will crush Cao Cao's thin line.",
|
||||||
|
"Guo Jia urges patience: hold the camps, expose the rhythm of Yuan Shao's command, and look for the supply wound."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_guandu_line": true },
|
||||||
|
"lines": [
|
||||||
|
"The defensive camps prepared after Yan Ford give the front line room to bend without breaking."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "scouted_wuchao_depots": true },
|
||||||
|
"lines": [
|
||||||
|
"Scouts from Yan Ford report unusually heavy traffic toward Wuchao. Yuan Shao's grain may be less protected than his banners."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_guandu_line": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "scouted_wuchao_depots": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 6,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 3], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 17,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGHHGGGG",
|
||||||
|
"GGFFGHHGGFFHHGFFG",
|
||||||
|
"GGFFRRRRRRRRRFFGG",
|
||||||
|
"GGGGGRGGGGRGGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGGRGGGGGG",
|
||||||
|
"GGHHGFFFGGWWWGGGG",
|
||||||
|
"GGHHGFFFGGWWWGGGG",
|
||||||
|
"GGGGGGFFGGWWWGGGG",
|
||||||
|
"GGFFGGGGGGGGGFFGG",
|
||||||
|
"GGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch12",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch12",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch12",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch12",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch12",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch12",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shao_main_body",
|
||||||
|
"name": "Yuan Shao Main Body",
|
||||||
|
"class_id": "commander",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [14, 4],
|
||||||
|
"base": { "hp": 70, "mp": 16, "atk": 19, "def": 13, "int": 13, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ju_shou_advisor",
|
||||||
|
"name": "Ju Shou Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [13, 5],
|
||||||
|
"skills": ["blaze", "great_mend"],
|
||||||
|
"base": { "hp": 46, "mp": 32, "atk": 7, "def": 8, "int": 17, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_he_vanguard",
|
||||||
|
"name": "Zhang He Vanguard",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 60, "atk": 20, "def": 12, "agi": 15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_center_guard",
|
||||||
|
"name": "Yuan Center Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [10, 4],
|
||||||
|
"base": { "hp": 56, "atk": 17, "def": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_north_guard",
|
||||||
|
"name": "Yuan North Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 50, "atk": 16, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_south_guard",
|
||||||
|
"name": "Yuan South Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [11, 5],
|
||||||
|
"base": { "hp": 50, "atk": 16, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guandu_archer_north",
|
||||||
|
"name": "Guandu Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guandu_archer_south",
|
||||||
|
"name": "Guandu Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [13, 6],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Yuan Shao's main army bears down on Guandu." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Yuan Shao brings more banners than the field can hold." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Then each banner must be fed, signaled, and protected. A large army has larger seams." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Hold the front until those seams show. I understand." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_2_siege_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Shao", "text": "Press the camps from both wings. Cao Cao cannot answer everywhere." },
|
||||||
|
{ "speaker": "Ju Shou", "text": "Steady pressure, my lord. Do not let their small line choose the tempo." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "secured_line_holds",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3, "campaign_flags": { "secured_guandu_line": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "The camps we reinforced are holding. Their first push has spent itself on timber and earth." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. Let Yuan Shao pay for every step." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wuchao_traces_found",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 3, "campaign_flags": { "scouted_wuchao_depots": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "The supply marks match the Yan Ford reports. Wuchao is feeding this entire assault." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then this battle is not only a wall. It is a lantern pointing toward their granary." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_4_yuan_main_assault",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 4 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Yuan Shao's main body and every committed assault unit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "The center still stands. Send the rear cavalry in; break them before they settle." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Now the main weight is committed. If we endure this, Yuan Shao must expose his stores." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_rear_cavalry",
|
||||||
|
"name": "Yuan Rear Cavalry",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [15, 4],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_north_crossbow",
|
||||||
|
"name": "Yuan Crossbow",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [16, 3],
|
||||||
|
"base": { "hp": 36, "atk": 15, "def": 6, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_south_crossbow",
|
||||||
|
"name": "Yuan Crossbow",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [16, 5],
|
||||||
|
"base": { "hp": 36, "atk": 15, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "center_countertrap",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [9, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Guard", "text": "Cao Cao steps into the center! Close ranks around him." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Around him? Then you are around me too." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_center_spearman",
|
||||||
|
"name": "Yuan Center Spearman",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [8, 3],
|
||||||
|
"base": { "hp": 48, "atk": 16, "def": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_center_archer",
|
||||||
|
"name": "Yuan Center Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 11,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 36, "atk": 15, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_7_guandu_holds",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 7 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Their assault no longer moves as one army. It moves as arguments." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then answer each argument with steel. Guandu will hold." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 1900,
|
||||||
|
"items": ["bean", "wine", "imperial_seal"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Yuan Shao's first great push is broken, but his camps still fill the horizon." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "The horizon eats grain. Wuchao is now more important than the banners before us." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we decide whether to strike the grain or strengthen the wall before the next tide." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "prepare_wuchao_raid",
|
||||||
|
"label": "Prepare Wuchao Raid",
|
||||||
|
"description": "Cao Cao commits scouts and fast troops toward Yuan Shao's exposed granaries.",
|
||||||
|
"set_flags": { "prepared_wuchao_raid": true, "strengthened_guandu_defense": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["wine", "war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "strengthen_guandu_defense",
|
||||||
|
"label": "Strengthen Guandu",
|
||||||
|
"description": "The army reinforces the camps before planning a deeper strike.",
|
||||||
|
"set_flags": { "prepared_wuchao_raid": false, "strengthened_guandu_defense": true },
|
||||||
|
"gold": 250,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
340
data/scenarios/013_wuchao_raid.json
Normal file
340
data/scenarios/013_wuchao_raid.json
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
{
|
||||||
|
"id": "013_wuchao_raid",
|
||||||
|
"name": "Wuchao Raid",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Reach Wuchao's depot with Cao Cao, then defeat the remaining defenders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "wuchao_depot_burned" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 12, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Wuchao Raid",
|
||||||
|
"location": "Wuchao, 200 CE",
|
||||||
|
"lines": [
|
||||||
|
"The Guandu line holds, but Yuan Shao's army will not collapse while Wuchao feeds it.",
|
||||||
|
"Cao Cao leads a night raid toward the granaries, counting on speed, fire, and confusion."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "prepared_wuchao_raid": true },
|
||||||
|
"lines": [
|
||||||
|
"Scouts placed after the Guandu assault mark a narrow path through the outer camp."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "strengthened_guandu_defense": true },
|
||||||
|
"lines": [
|
||||||
|
"The reinforced Guandu camps can withstand a longer absence, but the raid must still finish before dawn."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "prepared_wuchao_raid": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "strengthened_guandu_defense": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 6,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 3], [3, 4]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 17,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGGGGG",
|
||||||
|
"GGFFGHHGGGFFGGGGG",
|
||||||
|
"GGFFRRRRRRRRGGFFG",
|
||||||
|
"GGGGGRGGGGRRGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRG",
|
||||||
|
"GGGGGRGGGGRRGGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGGG",
|
||||||
|
"GGGGGGFFGGHHHGGGG",
|
||||||
|
"GGFFGGGGGGHHHFFGG",
|
||||||
|
"GGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch13",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch13",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch13",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch13",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch13",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch13",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "chunyu_qiong_guard",
|
||||||
|
"name": "Chunyu Qiong Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [13, 4],
|
||||||
|
"base": { "hp": 64, "atk": 19, "def": 15, "agi": 9 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_granary_guard",
|
||||||
|
"name": "Granary Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [12, 4],
|
||||||
|
"base": { "hp": 52, "atk": 17, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_north_archer",
|
||||||
|
"name": "Wuchao Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_south_archer",
|
||||||
|
"name": "Wuchao Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [12, 6],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_camp_advisor",
|
||||||
|
"name": "Camp Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [14, 5],
|
||||||
|
"skills": ["blaze", "mend"],
|
||||||
|
"base": { "hp": 44, "mp": 28, "atk": 7, "def": 7, "int": 16, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_patrol_north",
|
||||||
|
"name": "Wuchao Patrol",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_patrol_south",
|
||||||
|
"name": "Wuchao Patrol",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [9, 5],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao's raiding force approaches Wuchao under cover of darkness." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "If Wuchao burns, Yuan Shao's numbers become mouths instead of soldiers." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Strike the granary first. The defenders will fight harder once they see the flame." },
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Then we give them fire before they give us formation." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "prepared_path",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "prepared_wuchao_raid": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "The scout marks are where they should be. The northern patrol is slower to answer." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we spend that advantage before dawn spends it for us." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "guandu_line_secure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "strengthened_guandu_defense": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Ren", "text": "The Guandu camps remain firm behind us. We can commit fully to the raid." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "A strong wall is useful because it lets the blade leave the gate." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_alarm",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Chunyu Qiong", "text": "Cao Cao is near the stores! Wake the rear camp before the grain catches." },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Too late to wake quietly." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wuchao_depot_burned",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [14, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Wuchao is burning. Defeat every defender before Yuan Shao can recover the stores.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 12."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Light the granaries. Let Yuan Shao see dawn from an empty bowl." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Now the raid becomes an escape through panic. Keep formation." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_rear_guard",
|
||||||
|
"name": "Wuchao Rear Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [15, 4],
|
||||||
|
"base": { "hp": 54, "atk": 17, "def": 13 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "wuchao_rear_rider",
|
||||||
|
"name": "Wuchao Rear Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [15, 5],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_granary_collapse",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Smoke is rising above the camp. Yuan Shao's army will feel this before his messengers explain it." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then leave no guard behind to put the fire out." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 2100,
|
||||||
|
"items": ["bean", "wine", "war_drum"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Wuchao burns. Yuan Shao's army will wake hungry." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Hunger breaks pride more cleanly than argument." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Now Guandu turns from endurance to pursuit." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "press_yuan_rout",
|
||||||
|
"label": "Press the Rout",
|
||||||
|
"description": "Cao Cao prepares to chase Yuan Shao's shaken army before it reforms.",
|
||||||
|
"set_flags": { "pressed_yuan_rout": true, "secured_wuchao_stores": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "secure_wuchao_stores",
|
||||||
|
"label": "Secure Stores",
|
||||||
|
"description": "The army gathers usable supplies from Wuchao before pursuing.",
|
||||||
|
"set_flags": { "pressed_yuan_rout": false, "secured_wuchao_stores": true },
|
||||||
|
"gold": 300,
|
||||||
|
"items": ["bean", "wine"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
373
data/scenarios/014_cangting_pursuit.json
Normal file
373
data/scenarios/014_cangting_pursuit.json
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
{
|
||||||
|
"id": "014_cangting_pursuit",
|
||||||
|
"name": "Cangting Pursuit",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat Yuan Shao's rear guard after the retreat line rallies.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_yuan_rally" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 13, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Cangting Pursuit",
|
||||||
|
"location": "Cangting, 201 CE",
|
||||||
|
"lines": [
|
||||||
|
"With Wuchao destroyed, Yuan Shao's great army breaks into columns of wounded pride and hungry soldiers.",
|
||||||
|
"Cao Cao pursues the retreating rear guard before it can gather enough order to shield the northern crossings."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_yuan_rout": true },
|
||||||
|
"lines": [
|
||||||
|
"The army gave Yuan Shao no room to breathe after Wuchao, and the rear guard is already fraying."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_wuchao_stores": true },
|
||||||
|
"lines": [
|
||||||
|
"Captured stores from Wuchao keep the pursuit fed, letting Cao Cao press north without stretching the supply train."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_yuan_rout": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_wuchao_stores": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 6,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [3, 3], [3, 4], [3, 5]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 17,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGGGGG",
|
||||||
|
"GGFFGHHGGGFFGGGGG",
|
||||||
|
"GGRRRRRRRRRRRGFFG",
|
||||||
|
"GGGGGRGGGGRRGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRG",
|
||||||
|
"GGGGGRGGGGRRGGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGGG",
|
||||||
|
"GGGGGGFFGGHHHGGGG",
|
||||||
|
"GGFFGGGGGGHHHFFGG",
|
||||||
|
"GGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch14",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch14",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch14",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch14",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch14",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch14",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shao_rearguard",
|
||||||
|
"name": "Yuan Shao Rear Guard",
|
||||||
|
"class_id": "commander",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [14, 4],
|
||||||
|
"base": { "hp": 72, "mp": 16, "atk": 20, "def": 14, "int": 13, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_he_rearguard",
|
||||||
|
"name": "Zhang He Rearguard",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 62, "atk": 21, "def": 12, "agi": 16 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_rally_advisor",
|
||||||
|
"name": "Yuan Rally Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [13, 5],
|
||||||
|
"skills": ["blaze", "great_mend"],
|
||||||
|
"base": { "hp": 46, "mp": 30, "atk": 7, "def": 8, "int": 17, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_center_guard",
|
||||||
|
"name": "Cangting Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [10, 4],
|
||||||
|
"base": { "hp": 56, "atk": 18, "def": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_north_guard",
|
||||||
|
"name": "Cangting Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 50, "atk": 17, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_south_guard",
|
||||||
|
"name": "Cangting Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [11, 5],
|
||||||
|
"base": { "hp": 50, "atk": 17, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_archer_north",
|
||||||
|
"name": "Cangting Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [12, 2],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_archer_south",
|
||||||
|
"name": "Cangting Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [13, 6],
|
||||||
|
"base": { "hp": 40, "atk": 16, "def": 7, "agi": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Yuan Shao's retreating army tries to rally near Cangting." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Wuchao broke their hunger. Cangting must break their confidence." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Zhang He holds the rear with more discipline than the rest. If he wavers, the rout becomes a road." },
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "Then we strike the disciplined men first." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fast_pursuit_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "pressed_yuan_rout": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "Our pursuit caught them before their wagons even formed a wall." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Momentum is a weapon. Keep it sharp." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stored_supply_arrives",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "secured_wuchao_stores": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "The Wuchao stores are feeding our own pursuit now. Yuan Shao pays for both armies." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "A pleasant economy." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_yuan_rally",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Defeat Yuan Shao's rallied rear guard and every covering unit.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 13."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Hold the road. If the rear collapses, the whole army becomes fugitives." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "He sees the truth. That makes him dangerous, and useful." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_rout_rider_north",
|
||||||
|
"name": "Rout Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [15, 3],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_rout_rider_south",
|
||||||
|
"name": "Rout Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [15, 5],
|
||||||
|
"base": { "hp": 46, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "center_pursuit_trap",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [9, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Guard", "text": "Cao Cao is in the center road. Close the retreat line behind him!" },
|
||||||
|
{ "speaker": "Dian Wei", "text": "Try closing it from the ground." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_road_guard",
|
||||||
|
"name": "Road Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [8, 3],
|
||||||
|
"base": { "hp": 48, "atk": 17, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cangting_road_archer",
|
||||||
|
"name": "Road Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 12,
|
||||||
|
"pos": [8, 5],
|
||||||
|
"base": { "hp": 36, "atk": 15, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_6_zhang_he_wavers",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 6 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "Zhang He is guarding a lord who no longer listens to his guards." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then let him see which camp rewards judgment." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 2300,
|
||||||
|
"items": ["bean", "wine", "iron_armor"],
|
||||||
|
"join_officers": ["zhang_he"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Yuan Shao would rather lose an army than hear the men who tried to save it." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then serve where clear eyes are useful." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Cangting breaks the pursuit open. The Yuan heirs will now fight over what remains." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "advance_to_ye",
|
||||||
|
"label": "Advance to Ye",
|
||||||
|
"description": "Cao Cao prepares to press toward the Yuan heartland before the heirs can settle.",
|
||||||
|
"set_flags": { "advanced_to_ye": true, "secured_hebei_crossings": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "secure_hebei_crossings",
|
||||||
|
"label": "Secure Crossings",
|
||||||
|
"description": "The army consolidates river crossings and supply lanes before moving deeper north.",
|
||||||
|
"set_flags": { "advanced_to_ye": false, "secured_hebei_crossings": true },
|
||||||
|
"gold": 300,
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
379
data/scenarios/015_ye_campaign.json
Normal file
379
data/scenarios/015_ye_campaign.json
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
{
|
||||||
|
"id": "015_ye_campaign",
|
||||||
|
"name": "Ye Campaign",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat the Yuan heirs' outer defense after their split command reveals itself.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 14."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_3_yuan_heir_split" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 14, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Ye Campaign",
|
||||||
|
"location": "Ye Outskirts, 202 CE",
|
||||||
|
"lines": [
|
||||||
|
"Yuan Shao's heirs inherit banners, grudges, and a northern capital that cannot obey two masters.",
|
||||||
|
"Cao Cao advances toward Ye, while Zhang He explains where the Yuan defense is strong and where pride has hollowed it out."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "advanced_to_ye": true },
|
||||||
|
"lines": [
|
||||||
|
"The rapid march from Cangting catches Yuan Tan's scouts arguing over orders instead of watching the road."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_hebei_crossings": true },
|
||||||
|
"lines": [
|
||||||
|
"Secured crossings behind the army keep Cao Cao's supply line steady as the campaign enters Yuan territory."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "advanced_to_ye": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "secured_hebei_crossings": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 7,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 18,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGGGGGG",
|
||||||
|
"GGFFGHHGGGFFGGGFFG",
|
||||||
|
"GGRRRRRRRRRRRRGFFG",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGGGGGFFGGHHHGGGGG",
|
||||||
|
"GGFFGGGGGGHHHFFGGG",
|
||||||
|
"GGGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch15",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch15",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_he_ch15",
|
||||||
|
"officer_id": "zhang_he",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch15",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch15",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch15",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch15",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_tan_outer_guard",
|
||||||
|
"name": "Yuan Tan Guard",
|
||||||
|
"class_id": "commander",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [14, 4],
|
||||||
|
"base": { "hp": 72, "mp": 16, "atk": 20, "def": 14, "int": 12, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shang_inner_guard",
|
||||||
|
"name": "Yuan Shang Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [13, 5],
|
||||||
|
"base": { "hp": 64, "atk": 19, "def": 15, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_council_advisor",
|
||||||
|
"name": "Ye Council Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [15, 5],
|
||||||
|
"skills": ["blaze", "great_mend"],
|
||||||
|
"base": { "hp": 48, "mp": 34, "atk": 7, "def": 8, "int": 18, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_center_guard",
|
||||||
|
"name": "Ye Center Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 58, "atk": 18, "def": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_north_guard",
|
||||||
|
"name": "Ye Spear Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [12, 3],
|
||||||
|
"base": { "hp": 52, "atk": 17, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_south_guard",
|
||||||
|
"name": "Ye Spear Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [12, 5],
|
||||||
|
"base": { "hp": 52, "atk": 17, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_archer_north",
|
||||||
|
"name": "Ye Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [13, 2],
|
||||||
|
"base": { "hp": 42, "atk": 17, "def": 7, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_archer_south",
|
||||||
|
"name": "Ye Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [14, 6],
|
||||||
|
"base": { "hp": 42, "atk": 17, "def": 7, "agi": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "The Yuan heirs argue behind Ye's outer defense." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Ye's outer road looks strong, but Yuan Tan and Yuan Shang will not agree who commands it." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then their wall already has two gates." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Let them quarrel loudly enough for our blades to hear." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rapid_march_confusion",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2, "campaign_flags": { "advanced_to_ye": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Tan Guard", "text": "Cao Cao is already here? Yuan Shang promised another day!" },
|
||||||
|
{ "speaker": "Zhang He", "text": "They are blaming one another before the first order is given." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crossings_secure_supply",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "secured_hebei_crossings": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Yuan", "text": "The crossings behind us are secure. We can keep pressure on Ye without starving the rear." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "A campaign is won by the road as much as the sword." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_yuan_heir_split",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "Exploit the Yuan heirs' split command and defeat every outer defender.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 14."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Shang Guard", "text": "Hold for Yuan Shang! Do not yield command to Yuan Tan's men." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "There it is. Two drums, one army, no rhythm." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_tan_rider",
|
||||||
|
"name": "Yuan Tan Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [16, 3],
|
||||||
|
"base": { "hp": 48, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shang_rider",
|
||||||
|
"name": "Yuan Shang Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [16, 5],
|
||||||
|
"base": { "hp": 48, "atk": 17, "def": 9, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "outer_wall_trap",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [10, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Ye Guard", "text": "Cao Cao has pushed to the outer wall. Seal the road!" },
|
||||||
|
{ "speaker": "Zhang He", "text": "They seal late and from bad ground. Press through the center." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ye_road_blocker",
|
||||||
|
"name": "Ye Road Blocker",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [9, 3],
|
||||||
|
"base": { "hp": 50, "atk": 17, "def": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_road_archer",
|
||||||
|
"name": "Ye Road Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 13,
|
||||||
|
"pos": [9, 5],
|
||||||
|
"base": { "hp": 38, "atk": 16, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_7_ye_outer_breaks",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 7 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Their outer command breaks before the wall does." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "A city divided is already partly open." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 2500,
|
||||||
|
"items": ["bean", "wine", "war_axe"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Ye's outer defense is broken. The heirs will blame each other before they blame us." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Good. Their family quarrel is now a campaign map." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we decide whether to strike the city or widen the fracture." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "press_ye_siege",
|
||||||
|
"label": "Press Ye Siege",
|
||||||
|
"description": "Cao Cao prepares a direct siege before the Yuan heirs can reorganize.",
|
||||||
|
"set_flags": { "pressed_ye_siege": true, "split_yuan_heirs": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "split_yuan_heirs",
|
||||||
|
"label": "Split Heirs",
|
||||||
|
"description": "The army spreads rumors and pressure to deepen Yuan Tan and Yuan Shang's rivalry.",
|
||||||
|
"set_flags": { "pressed_ye_siege": false, "split_yuan_heirs": true },
|
||||||
|
"gold": 250,
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
347
data/scenarios/016_ye_siege.json
Normal file
347
data/scenarios/016_ye_siege.json
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
{
|
||||||
|
"id": "016_ye_siege",
|
||||||
|
"name": "Ye Siege",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Reach Ye's inner gate with Cao Cao, then defeat the palace defenders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 15."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "inner_gate_breached" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 15, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Ye Siege",
|
||||||
|
"location": "Ye, 202 CE",
|
||||||
|
"lines": [
|
||||||
|
"Ye's outer line has broken, but the Yuan heirs still command the inner city through suspicion and fear.",
|
||||||
|
"Cao Cao orders a direct push toward the inner gate while Zhang He marks the streets where the defenders will try to seal the advance."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_ye_siege": true },
|
||||||
|
"lines": [
|
||||||
|
"The rapid siege leaves Yuan Shang's inner guard scrambling to reinforce streets that were still being repaired."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "split_yuan_heirs": true },
|
||||||
|
"lines": [
|
||||||
|
"Rumors spread through Ye that Yuan Tan has been abandoned, and some defenders hesitate before answering Yuan Shang's orders."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pressed_ye_siege": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "split_yuan_heirs": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 7,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 18,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGGGGGG",
|
||||||
|
"GGFFGHHGGGFFGGGFFG",
|
||||||
|
"GGRRRRRRRRRRRRGFFG",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGGGGGFFGGHHHGGGGG",
|
||||||
|
"GGFFGGGGGGHHHFFGGG",
|
||||||
|
"GGGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch16",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch16",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_he_ch16",
|
||||||
|
"officer_id": "zhang_he",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch16",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch16",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch16",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch16",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_shang_inner_guard",
|
||||||
|
"name": "Yuan Shang Inner Guard",
|
||||||
|
"class_id": "commander",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [14, 4],
|
||||||
|
"base": { "hp": 76, "mp": 18, "atk": 21, "def": 15, "int": 13, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_gate_captain",
|
||||||
|
"name": "Ye Gate Captain",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [11, 4],
|
||||||
|
"base": { "hp": 62, "atk": 19, "def": 15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_inner_advisor",
|
||||||
|
"name": "Inner City Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [15, 5],
|
||||||
|
"skills": ["blaze", "great_mend"],
|
||||||
|
"base": { "hp": 50, "mp": 34, "atk": 7, "def": 8, "int": 18, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_north_wall_guard",
|
||||||
|
"name": "Ye Wall Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [12, 3],
|
||||||
|
"base": { "hp": 54, "atk": 18, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_south_wall_guard",
|
||||||
|
"name": "Ye Wall Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [12, 5],
|
||||||
|
"base": { "hp": 54, "atk": 18, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_wall_archer_north",
|
||||||
|
"name": "Ye Wall Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [13, 2],
|
||||||
|
"base": { "hp": 44, "atk": 18, "def": 8, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_wall_archer_south",
|
||||||
|
"name": "Ye Wall Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [14, 6],
|
||||||
|
"base": { "hp": 44, "atk": 18, "def": 8, "agi": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Cao Cao's army enters the streets outside Ye's inner gate." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "The inner gate is not Ye's strongest point. It is simply where the heirs trust each other least." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we press where mistrust must give orders." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "A siege ladder is useful. A frightened messenger is faster." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "direct_siege_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "pressed_ye_siege": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The rapid assault has them reinforcing with half-formed companies." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then do not give the other half time to arrive." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "heirs_split_inside",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2, "campaign_flags": { "split_yuan_heirs": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Ye Guard", "text": "Yuan Tan's men are refusing the inner gate order. They say Yuan Shang means to abandon them." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "Good. Every rumor is now doing the work of a ram." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_inner_gate_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Yuan Shang Inner Guard", "text": "Hold the gate. If Cao Cao takes the inner road, Ye will hear his drums before ours." },
|
||||||
|
{ "speaker": "Zhang He", "text": "He is defending the sound of command, not the city." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inner_gate_breached",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [10, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "The inner gate is breached. Defeat Yuan Shang's palace defenders.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 15."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "The gate is open. Drive through before the city remembers how to obey." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "The palace guard is moving. This is the last hard knot." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_guard",
|
||||||
|
"name": "Ye Palace Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [16, 4],
|
||||||
|
"base": { "hp": 58, "atk": 18, "def": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_rider",
|
||||||
|
"name": "Ye Palace Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 14,
|
||||||
|
"pos": [16, 5],
|
||||||
|
"base": { "hp": 50, "atk": 18, "def": 10, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_7_city_wavers",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 7 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Ye's defenders are waiting to see which heir survives the day." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then give them an easier answer." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 2800,
|
||||||
|
"items": ["bean", "wine", "imperial_seal"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Zhang He", "text": "The inner gate is broken. Ye cannot pretend the Yuan house is whole." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "One city remains, but the family that held it has become several smaller enemies." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then we choose whether to take the city by force or make the heirs surrender it piece by piece." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "force_ye_surrender",
|
||||||
|
"label": "Force Surrender",
|
||||||
|
"description": "Cao Cao prepares a final direct pressure on Ye's remaining defenders.",
|
||||||
|
"set_flags": { "forced_ye_surrender": true, "negotiated_yuan_defections": false },
|
||||||
|
"gold": 150,
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "negotiate_yuan_defections",
|
||||||
|
"label": "Negotiate Defections",
|
||||||
|
"description": "The army courts frightened Yuan officers and turns the city's divisions inward.",
|
||||||
|
"set_flags": { "forced_ye_surrender": false, "negotiated_yuan_defections": true },
|
||||||
|
"gold": 300,
|
||||||
|
"items": ["iron_armor"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
347
data/scenarios/017_ye_surrender.json
Normal file
347
data/scenarios/017_ye_surrender.json
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
{
|
||||||
|
"id": "017_ye_surrender",
|
||||||
|
"name": "Ye Surrender",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Break the last Yuan holdouts after the surrender signal reaches the palace road.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 14."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy", "after_event": "surrender_signal_sent" },
|
||||||
|
"defeat": [
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
|
||||||
|
{ "type": "turn_reached", "turn": 14, "team": "player" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Ye Surrender",
|
||||||
|
"location": "Ye Palace Road, 202 CE",
|
||||||
|
"lines": [
|
||||||
|
"Ye's inner gate has fallen. Only palace loyalists and frightened Yuan officers remain between Cao Cao and the city's surrender.",
|
||||||
|
"Zhang He warns that the last defenders will either break quickly or fight wildly to prove loyalty to absent heirs."
|
||||||
|
],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "forced_ye_surrender": true },
|
||||||
|
"lines": [
|
||||||
|
"Direct pressure on Ye leaves the palace guard short of time, but their cornered officers are ready for a final stand."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "negotiated_yuan_defections": true },
|
||||||
|
"lines": [
|
||||||
|
"Negotiations have drawn several Yuan captains away from the palace, leaving the loyalists angry and isolated."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": [
|
||||||
|
"bean",
|
||||||
|
"wine",
|
||||||
|
"iron_sword",
|
||||||
|
"training_spear",
|
||||||
|
"short_bow",
|
||||||
|
"hand_axe",
|
||||||
|
"war_axe",
|
||||||
|
"leather_armor",
|
||||||
|
"iron_armor"
|
||||||
|
],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "forced_ye_surrender": true },
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"campaign_flags": { "negotiated_yuan_defections": true },
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 7,
|
||||||
|
"required_officers": ["cao_cao", "guo_jia"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [1, 5], [2, 2], [2, 3], [2, 4], [2, 5], [2, 6], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6]]
|
||||||
|
},
|
||||||
|
"map": {
|
||||||
|
"width": 18,
|
||||||
|
"height": 11,
|
||||||
|
"terrain": [
|
||||||
|
"GGGGGHHGGGGGGGGGGG",
|
||||||
|
"GGFFGHHGGGFFGGGFFG",
|
||||||
|
"GGRRRRRRRRRRRRGFFG",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"RRRRRRRRRRRRRRRRRR",
|
||||||
|
"GGGGGRGGGGRRGGGGGG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGHHGFFFGGFFFGGHHG",
|
||||||
|
"GGGGGGFFGGHHHGGGGG",
|
||||||
|
"GGFFGGGGGGHHHFFGGG",
|
||||||
|
"GGGGGGGGGGGGGGGGGG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "cao_cao_ch17",
|
||||||
|
"officer_id": "cao_cao",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "guo_jia_ch17",
|
||||||
|
"officer_id": "guo_jia",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "zhang_he_ch17",
|
||||||
|
"officer_id": "zhang_he",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_dun_ch17",
|
||||||
|
"officer_id": "xiahou_dun",
|
||||||
|
"team": "player",
|
||||||
|
"pos": [1, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "dian_wei_ch17",
|
||||||
|
"officer_id": "dian_wei",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [2, 5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "xiahou_yuan_ch17",
|
||||||
|
"officer_id": "xiahou_yuan",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "cao_ren_ch17",
|
||||||
|
"officer_id": "cao_ren",
|
||||||
|
"team": "player",
|
||||||
|
"requires_joined": true,
|
||||||
|
"pos": [3, 4]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_last_loyalist",
|
||||||
|
"name": "Ye Last Loyalist",
|
||||||
|
"class_id": "commander",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 16,
|
||||||
|
"pos": [14, 4],
|
||||||
|
"base": { "hp": 78, "mp": 18, "atk": 22, "def": 16, "int": 12, "agi": 11 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_palace_guard",
|
||||||
|
"name": "Yuan Palace Guard",
|
||||||
|
"class_id": "guard_captain",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [12, 4],
|
||||||
|
"base": { "hp": 62, "atk": 19, "def": 15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "yuan_palace_advisor",
|
||||||
|
"name": "Palace Advisor",
|
||||||
|
"class_id": "military_advisor",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [15, 5],
|
||||||
|
"skills": ["blaze", "great_mend"],
|
||||||
|
"base": { "hp": 50, "mp": 34, "atk": 7, "def": 8, "int": 18, "agi": 10 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_spear_north",
|
||||||
|
"name": "Palace Spear Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [12, 3],
|
||||||
|
"base": { "hp": 54, "atk": 18, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_spear_south",
|
||||||
|
"name": "Palace Spear Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [12, 5],
|
||||||
|
"base": { "hp": 54, "atk": 18, "def": 12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_archer_north",
|
||||||
|
"name": "Palace Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [13, 2],
|
||||||
|
"base": { "hp": 44, "atk": 18, "def": 8, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_palace_archer_south",
|
||||||
|
"name": "Palace Archer",
|
||||||
|
"class_id": "marksman",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [14, 6],
|
||||||
|
"base": { "hp": 44, "atk": 18, "def": 8, "agi": 14 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"id": "opening",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_start" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Ye's last loyalists gather near the palace road." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opening_dialogue",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "battle_begin" },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Zhang He", "text": "The loyalists have no plan left, only the need to be seen dying for one." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then let the city see survival rewarded as clearly as resistance is ended." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "A surrender must look inevitable before it is signed." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "forced_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 2, "campaign_flags": { "forced_ye_surrender": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Xiahou Dun", "text": "The direct pressure has them packed against the palace road." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Good. A cornered guard can no longer pretend to command a city." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "defection_pressure",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 2, "campaign_flags": { "negotiated_yuan_defections": true } },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Palace Advisor", "text": "Two more captains have opened their gates to Cao Cao. The loyalists are fighting alone." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "A city can surrender one door at a time." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_3_last_stand",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "enemy", "turn": 3 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Ye Last Loyalist", "text": "If the Yuan name must leave Ye, let Cao Cao pay for every stone." },
|
||||||
|
{ "speaker": "Zhang He", "text": "That is no longer strategy. It is grief with a spear." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "surrender_signal_sent",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [10, 4] },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "set_objective",
|
||||||
|
"victory": "The surrender signal is sent. Defeat the loyalists who refuse it.",
|
||||||
|
"defeat": "Cao Cao is defeated or the battle reaches Turn 14."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Raise the surrender banner. Those who lay down arms will live under law." },
|
||||||
|
{ "speaker": "Ye Last Loyalist", "text": "Then we die outside that law." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spawn_deployments",
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"unit_id": "ye_last_rider",
|
||||||
|
"name": "Last Rider",
|
||||||
|
"class_id": "elite_cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [16, 4],
|
||||||
|
"base": { "hp": 56, "atk": 19, "def": 11, "agi": 14 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unit_id": "ye_last_archer",
|
||||||
|
"name": "Last Archer",
|
||||||
|
"class_id": "archer",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 15,
|
||||||
|
"pos": [16, 5],
|
||||||
|
"base": { "hp": 38, "atk": 16, "def": 6, "agi": 12 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "turn_7_city_surrenders",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "turn_start", "team": "player", "turn": 7 },
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Guo Jia", "text": "The palace road is quieting. Ye is deciding that tomorrow matters more than yesterday." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then end the last yesterday standing in our way." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 3000,
|
||||||
|
"items": ["bean", "wine", "iron_armor"]
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Zhang He", "text": "Ye has surrendered. The Yuan house still runs north, but its capital is no longer theirs." },
|
||||||
|
{ "speaker": "Guo Jia", "text": "A capital lost is a family argument made permanent." },
|
||||||
|
{ "speaker": "Cao Cao", "text": "Then Hebei must learn whether it belongs to heirs, rebels, or order." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "pursue_yuan_shang",
|
||||||
|
"label": "Pursue Yuan Shang",
|
||||||
|
"description": "Cao Cao turns immediately toward the fleeing Yuan heir.",
|
||||||
|
"set_flags": { "pursued_yuan_shang": true, "stabilized_ye": false },
|
||||||
|
"gold": 100,
|
||||||
|
"items": ["war_drum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stabilize_ye",
|
||||||
|
"label": "Stabilize Ye",
|
||||||
|
"description": "The army secures Ye's administration before marching farther north.",
|
||||||
|
"set_flags": { "pursued_yuan_shang": false, "stabilized_ye": true },
|
||||||
|
"gold": 350,
|
||||||
|
"items": ["imperial_seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
60
docs/ARCHITECTURE.md
Normal file
60
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
- Keep rules data-driven.
|
||||||
|
- Keep battle state independent from rendering.
|
||||||
|
- Build one complete vertical slice before expanding systems.
|
||||||
|
- Prefer simple Godot nodes and GDScript until a system proves it needs more structure.
|
||||||
|
|
||||||
|
## Current Layout
|
||||||
|
|
||||||
|
- `project.godot`: Godot 4 project configuration.
|
||||||
|
- `scenes/battle_scene.tscn`: Main playable battle scene.
|
||||||
|
- `scripts/core/data_catalog.gd`: JSON definition loader and deployment hydrator.
|
||||||
|
- `scripts/core/campaign_state.gd`: Thin campaign save state for gold, inventory, roster, completed scenarios, and campaign flags.
|
||||||
|
- `scripts/core/battle_state.gd`: Battle rules, unit state, turns, AI, and victory checks.
|
||||||
|
- `scripts/scenes/battle_scene.gd`: Rendering, input handling, and HUD wiring.
|
||||||
|
- `data/campaign/campaign.json`: Campaign order and scenario paths.
|
||||||
|
- `data/defs/*.json`: Officers, classes, terrain, items, and skills.
|
||||||
|
- `data/scenarios/*.json`: Scenario definitions.
|
||||||
|
|
||||||
|
## Data Files
|
||||||
|
|
||||||
|
- `data/defs/classes.json`: Class stats, movement types, skill access.
|
||||||
|
- `data/defs/officers.json`: Named officer base stats and growth.
|
||||||
|
- `data/defs/items.json`: Weapons, armor, accessories, and consumables.
|
||||||
|
- `data/defs/skills.json`: MP tactic definitions for damage, healing, target rules, and range.
|
||||||
|
- `data/defs/terrain.json`: Terrain colors, defense, avoid, and movement costs.
|
||||||
|
- `data/campaign/campaign.json`: Scenario sequence, titles, and resource paths.
|
||||||
|
- `data/scenarios/*.json`: Dialogue, deployments, rewards, and branching flags.
|
||||||
|
|
||||||
|
## Battle Loop
|
||||||
|
|
||||||
|
1. Load battle data.
|
||||||
|
2. Hydrate deployments through `DataCatalog`.
|
||||||
|
3. Dispatch battle-start scenario events.
|
||||||
|
4. Show scenario briefing, including the 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. Physical attacks roll hit chance from unit agility and target terrain avoid; misses give reduced EXP.
|
||||||
|
8. Player ends turn.
|
||||||
|
9. Enemy AI moves, attacks, and can cast damage or healing tactics through the same combat and skill resolvers.
|
||||||
|
10. Battle checks scenario-defined defeat conditions first, including commander loss and turn limits, then victory conditions such as enemy defeat or reaching a marked destination tile.
|
||||||
|
11. Turn-start and movement-triggered scenario events run as phases change or units enter marked cells, and may update objectives or deploy reinforcements.
|
||||||
|
12. On victory, `CampaignState` snapshots player progression and the battle inventory, applies rewards once, advances `current_scenario_id`, and writes `user://campaign_save.json`.
|
||||||
|
13. If the scenario defines post-battle dialogue, it plays once before the victory result panel opens.
|
||||||
|
14. The victory result panel summarizes rewards plus saved player level-ups and promotions.
|
||||||
|
15. If the scenario defines post-battle choices, the result panel waits for the player to select one, then saves the selected flags and optional choice rewards before enabling the next battle.
|
||||||
|
16. The result panel can load the next scenario, passing saved roster and inventory overlays back into `BattleState`.
|
||||||
|
17. Once all campaign scenarios are complete, the battle scene shows a campaign completion panel instead of loading another map.
|
||||||
|
|
||||||
|
## Save Boundary
|
||||||
|
|
||||||
|
`BattleState` owns tactical runtime fields. `CampaignState` saves only campaign-facing data: completed scenario ids, gold, inventory counts, joined officers, campaign flags, and a player roster snapshot. It does not save map positions, pre-battle Formation choices, action flags, or transient battle occupancy.
|
||||||
|
|
||||||
|
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. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks. Purchases are campaign transactions: they spend saved gold immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes fire on entry rather than from units already standing on a marker. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, and reinforcements from saved campaign choices. Consumable use can restore HP or MP, and in-battle item use plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Already completed scenarios can be replayed without granting duplicate rewards, choices, or replay inventory consumption; pre-battle Shop, Armory, Roster, and Formation are disabled on completed-scenario replays to avoid side effects.
|
||||||
|
|
||||||
|
The `New Campaign` button clears `user://campaign_save.json`, resets campaign state to the first scenario, and reloads the opening briefing.
|
||||||
378
docs/DATA_MODEL.md
Normal file
378
docs/DATA_MODEL.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# Data Model Plan
|
||||||
|
|
||||||
|
The prototype now uses a JSON catalog that hydrates compact scenario deployments into the same runtime unit dictionaries `BattleState` already understands. Legacy inline `units` remain supported for quick experiments.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/defs/officers.json
|
||||||
|
data/defs/classes.json
|
||||||
|
data/defs/terrain.json
|
||||||
|
data/defs/items.json
|
||||||
|
data/defs/skills.json
|
||||||
|
data/campaign/campaign.json
|
||||||
|
data/scenarios/001_yellow_turbans.json
|
||||||
|
data/scenarios/002_sishui_gate.json
|
||||||
|
data/scenarios/003_xingyang_ambush.json
|
||||||
|
data/scenarios/004_qingzhou_campaign.json
|
||||||
|
data/scenarios/005_puyang_raid.json
|
||||||
|
data/scenarios/006_dingtao_counterattack.json
|
||||||
|
data/scenarios/007_xian_emperor_escort.json
|
||||||
|
data/scenarios/008_wan_castle_escape.json
|
||||||
|
data/scenarios/009_xiapi_siege.json
|
||||||
|
data/scenarios/010_white_horse_relief.json
|
||||||
|
data/scenarios/011_yan_ford_pursuit.json
|
||||||
|
data/scenarios/012_guandu_showdown.json
|
||||||
|
data/scenarios/013_wuchao_raid.json
|
||||||
|
data/scenarios/014_cangting_pursuit.json
|
||||||
|
data/scenarios/015_ye_campaign.json
|
||||||
|
data/scenarios/016_ye_siege.json
|
||||||
|
data/scenarios/017_ye_surrender.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Campaign
|
||||||
|
|
||||||
|
The campaign file defines scenario order, resource paths, and officers who are available at the start of a new campaign.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cao_cao_campaign",
|
||||||
|
"title": "Cao Cao Campaign",
|
||||||
|
"start_scenario": "001_yellow_turbans",
|
||||||
|
"initial_joined_officers": ["cao_cao", "xiahou_dun"],
|
||||||
|
"scenarios": [
|
||||||
|
{ "id": "001_yellow_turbans", "title": "Yingchuan Skirmish", "path": "res://data/scenarios/001_yellow_turbans.json" },
|
||||||
|
{ "id": "002_sishui_gate", "title": "Sishui Gate Vanguard", "path": "res://data/scenarios/002_sishui_gate.json" },
|
||||||
|
{ "id": "003_xingyang_ambush", "title": "Xingyang Ambush", "path": "res://data/scenarios/003_xingyang_ambush.json" },
|
||||||
|
{ "id": "004_qingzhou_campaign", "title": "Qingzhou Campaign", "path": "res://data/scenarios/004_qingzhou_campaign.json" },
|
||||||
|
{ "id": "005_puyang_raid", "title": "Puyang Raid", "path": "res://data/scenarios/005_puyang_raid.json" },
|
||||||
|
{ "id": "006_dingtao_counterattack", "title": "Dingtao Counterattack", "path": "res://data/scenarios/006_dingtao_counterattack.json" },
|
||||||
|
{ "id": "007_xian_emperor_escort", "title": "Xian Emperor Escort", "path": "res://data/scenarios/007_xian_emperor_escort.json" },
|
||||||
|
{ "id": "008_wan_castle_escape", "title": "Wan Castle Escape", "path": "res://data/scenarios/008_wan_castle_escape.json" },
|
||||||
|
{ "id": "009_xiapi_siege", "title": "Xiapi Siege", "path": "res://data/scenarios/009_xiapi_siege.json" },
|
||||||
|
{ "id": "010_white_horse_relief", "title": "White Horse Relief", "path": "res://data/scenarios/010_white_horse_relief.json" },
|
||||||
|
{ "id": "011_yan_ford_pursuit", "title": "Yan Ford Pursuit", "path": "res://data/scenarios/011_yan_ford_pursuit.json" },
|
||||||
|
{ "id": "012_guandu_showdown", "title": "Guandu Showdown", "path": "res://data/scenarios/012_guandu_showdown.json" },
|
||||||
|
{ "id": "013_wuchao_raid", "title": "Wuchao Raid", "path": "res://data/scenarios/013_wuchao_raid.json" },
|
||||||
|
{ "id": "014_cangting_pursuit", "title": "Cangting Pursuit", "path": "res://data/scenarios/014_cangting_pursuit.json" },
|
||||||
|
{ "id": "015_ye_campaign", "title": "Ye Campaign", "path": "res://data/scenarios/015_ye_campaign.json" },
|
||||||
|
{ "id": "016_ye_siege", "title": "Ye Siege", "path": "res://data/scenarios/016_ye_siege.json" },
|
||||||
|
{ "id": "017_ye_surrender", "title": "Ye Surrender", "path": "res://data/scenarios/017_ye_surrender.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Officers
|
||||||
|
|
||||||
|
Officers hold identity, personal base stats, initial class, and starting equipment.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cao_cao": {
|
||||||
|
"name": "Cao Cao",
|
||||||
|
"faction": "wei",
|
||||||
|
"class_id": "hero",
|
||||||
|
"level": 1,
|
||||||
|
"exp": 0,
|
||||||
|
"base": { "hp": 38, "mp": 10, "atk": 13, "def": 8, "int": 12, "agi": 8 },
|
||||||
|
"growth_bonus": { "atk": 1, "int": 1 },
|
||||||
|
"equipment": { "weapon": "bronze_sword", "armor": "cloth_robe", "accessory": null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Classes
|
||||||
|
|
||||||
|
Classes provide movement, min/max attack range, skill access, growth grades, class bonuses, equipment permissions, and promotion routes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cavalry": {
|
||||||
|
"name": "Cavalry",
|
||||||
|
"tier": 1,
|
||||||
|
"move_type": "mounted",
|
||||||
|
"move": 5,
|
||||||
|
"attack_range": [1, 1],
|
||||||
|
"skills": [],
|
||||||
|
"growth": { "hp": "A", "mp": "D", "atk": "A", "def": "B", "int": "D", "agi": "B" },
|
||||||
|
"base_bonus": { "hp": 4, "atk": 2, "def": 0 },
|
||||||
|
"equipment_slots": {
|
||||||
|
"weapon": ["spear", "sword"],
|
||||||
|
"armor": ["light_armor", "heavy_armor"],
|
||||||
|
"accessory": ["accessory"]
|
||||||
|
},
|
||||||
|
"promotion": { "level": 15, "to": "elite_cavalry" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`attack_range` is `[min, max]` in Manhattan distance. A range of `[2, 2]` means the unit cannot attack adjacent targets.
|
||||||
|
|
||||||
|
`promotion.level` and `promotion.to` define an automatic class promotion route. When a unit reaches the threshold during battle, the runtime updates class name, movement, movement type, growth, class skills, attack range, and class base-bonus deltas. The promoted `class_id` and derived roster fields are captured in the campaign roster snapshot, and the next battle rehydrates class-derived fields from the saved `class_id`. Promotion targets must be higher-tier classes and must keep all source equipment slot permissions unless runtime auto-unequip logic is added later. The current core routes cover hero, cavalry, infantry, archer, warrior, and strategist first-tier classes.
|
||||||
|
|
||||||
|
`warrior` is the first player-facing axe class. It gives Dian Wei a heavy melee role without requiring unique weapon-instance logic yet.
|
||||||
|
|
||||||
|
`strategist` is the first MP-focused enemy-capable class. It uses the same `skills` list as player units, so scenario designers can add tactical pressure without adding new runtime unit fields.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Skills define MP tactics. A class, officer, or scenario deployment can add skill ids to the runtime unit. `range` is `[min, max]` in Manhattan distance and can use `0` as the minimum for self-targeting support skills. Enemy AI can use `damage` and `heal` skills when it has enough MP.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fire_tactic": {
|
||||||
|
"name": "Fire",
|
||||||
|
"kind": "damage",
|
||||||
|
"target": "enemy",
|
||||||
|
"mp_cost": 4,
|
||||||
|
"range": [1, 3],
|
||||||
|
"power": 8,
|
||||||
|
"stat": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The current single-target tactic set includes `spark`, `fire_tactic`, `blaze`, `mend`, and `great_mend`. Hero and strategist classes get low-cost Spark alongside Fire/Mend, 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
|
||||||
|
|
||||||
|
Terrain keeps compact map characters but moves costs and bonuses out of code. `defense` reduces physical and damage-tactic damage taken on that tile, while `avoid` reduces physical attack hit chance. Per-move-type costs can be added before pathfinding becomes more complex.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"G": {
|
||||||
|
"id": "plain",
|
||||||
|
"name": "Plain",
|
||||||
|
"color": "#6d9e4f",
|
||||||
|
"defense": 0,
|
||||||
|
"avoid": 0,
|
||||||
|
"move_cost": { "foot": 1, "mounted": 1, "archer": 1, "water": 99 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Physical hit chance is `90 + attacker AGI - target AGI - terrain avoid`, clamped to 25-100. Skill damage currently uses terrain defense but does not roll against terrain avoid.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bronze_sword": {
|
||||||
|
"name": "Bronze Sword",
|
||||||
|
"kind": "weapon",
|
||||||
|
"weapon_type": "sword",
|
||||||
|
"range": [1, 1],
|
||||||
|
"bonuses": { "atk": 3 },
|
||||||
|
"effects": [],
|
||||||
|
"price": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Weapon `range` also uses `[min, max]`. When class and equipment both provide range, the runtime unit keeps the lowest minimum and highest maximum. 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.
|
||||||
|
|
||||||
|
Consumables use `kind: "consumable"` with effect records. `heal_hp` restores HP and `heal_mp` restores MP to a valid target; `Bean` and `Wine` 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.
|
||||||
|
|
||||||
|
Scenario `shop.items` controls which positive-price items appear in the current pre-battle shop. `shop.conditional_items` can append extra stock when saved campaign flags match. Purchases spend campaign gold, increment the campaign inventory count, save immediately, and then refresh the battle inventory copy if a scenario briefing is already loaded. Later campaign data should add finite stock counts, unlock flags, and sell-back rules.
|
||||||
|
|
||||||
|
The pre-battle Armory operates on the currently loaded player deployments and the copied battle inventory, then saves the changed roster entries and inventory counts back into `CampaignState`. Unlike in-battle equipment swaps, Armory changes persist immediately and survive defeat or restart. Completed-scenario replays keep Shop and Armory disabled so replay setup cannot alter the campaign save.
|
||||||
|
|
||||||
|
Scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` control the first pre-battle Roster selection pass. Required officers and required scenario units always deploy, optional player deployments can move between sortie and reserve, and reserve units do not participate in map occupancy, targeting, or living-unit victory checks. If `roster` is omitted, all player deployments sortie as before.
|
||||||
|
|
||||||
|
Scenario-only protected units can use `team: "player"`, `controllable: false`, `persist_progression: false`, and an optional `ai_target_priority`. They remain valid targets for enemies, healing, and defeat conditions, but the player cannot select, move, attack, change equipment, or place them in Formation, and they are omitted from campaign roster progression snapshots. `ai_target_priority` is a non-negative scenario hint that makes enemy movement and damage-skill AI treat that unit as a more attractive target.
|
||||||
|
|
||||||
|
Scenario `formation.cells` controls the highlighted starting cells available during pre-battle Formation. These choices only change the current battle's unit positions; they are not saved to campaign state. If a scenario omits `formation`, player deployment positions become the default formation cells.
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
Scenarios should use deployments rather than full inline unit stat blocks. `DataCatalog` will resolve officer, class, and item references into runtime unit dictionaries. Scenarios may also define briefing text, visible objective text, battle conditions, and victory rewards.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "001_yellow_turbans",
|
||||||
|
"name": "Yingchuan Skirmish",
|
||||||
|
"objectives": {
|
||||||
|
"victory": "Defeat all Yellow Turban units.",
|
||||||
|
"defeat": "All allied units are defeated."
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"victory": { "type": "all_units_defeated", "team": "enemy" },
|
||||||
|
"defeat": { "type": "all_units_defeated", "team": "player" }
|
||||||
|
},
|
||||||
|
"briefing": {
|
||||||
|
"title": "Yingchuan Skirmish",
|
||||||
|
"location": "Yingchuan, 184 CE",
|
||||||
|
"lines": ["Yellow Turban rebels have gathered near Yingchuan."],
|
||||||
|
"conditional_lines": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "pursued_dong_zhuo": true },
|
||||||
|
"lines": ["The army arrives tired from a rapid pursuit."]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shop": {
|
||||||
|
"items": ["bean"],
|
||||||
|
"conditional_items": [
|
||||||
|
{
|
||||||
|
"campaign_flags": { "regrouped_after_sishui": true },
|
||||||
|
"items": ["leather_armor"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"roster": {
|
||||||
|
"max_units": 3,
|
||||||
|
"required_officers": ["cao_cao"],
|
||||||
|
"required_units": ["protected_envoy"]
|
||||||
|
},
|
||||||
|
"formation": {
|
||||||
|
"cells": [[1, 3], [1, 4], [2, 3], [2, 4]]
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{ "unit_id": "cao_cao", "officer_id": "cao_cao", "team": "player", "pos": [1, 3] },
|
||||||
|
{ "unit_id": "protected_envoy", "name": "Protected Envoy", "class_id": "infantry", "team": "player", "controllable": false, "persist_progression": false, "ai_target_priority": 6, "pos": [1, 4] },
|
||||||
|
{ "unit_id": "xiahou_yuan_ch2", "officer_id": "xiahou_yuan", "team": "player", "requires_joined": true, "pos": [2, 3] },
|
||||||
|
{
|
||||||
|
"unit_id": "yellow_turban_1",
|
||||||
|
"name": "Zhang Mancheng",
|
||||||
|
"class_id": "bandit",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 1,
|
||||||
|
"pos": [7, 2],
|
||||||
|
"base": { "hp": 34, "atk": 11, "def": 5 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"gold": 300,
|
||||||
|
"items": ["bean"],
|
||||||
|
"join_officers": ["xiahou_yuan"],
|
||||||
|
"leave_officers": []
|
||||||
|
},
|
||||||
|
"post_battle_dialogue": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "This victory is only the first fire." }
|
||||||
|
],
|
||||||
|
"post_battle_choices": [
|
||||||
|
{
|
||||||
|
"id": "pursue_dong_zhuo",
|
||||||
|
"label": "Pursue Dong Zhuo",
|
||||||
|
"description": "Cao Cao presses the retreating army.",
|
||||||
|
"set_flags": { "pursued_dong_zhuo": true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`objectives` are display text. `conditions` drive the actual victory and defeat checks. Defeat is evaluated before victory, matching Cao Cao Zhuan-style commander-loss rules. If `conditions` is omitted, battles fall back to defeating all enemies for victory and losing all player units for defeat.
|
||||||
|
|
||||||
|
Player deployments may set `requires_joined: true`; those units are only loaded if their `officer_id` is in campaign `joined_officers`. Scenario rewards can add officers through `rewards.join_officers` and remove them through `rewards.leave_officers`. Post-battle choices can also use `join_officers` and `leave_officers` for branch-specific membership changes. `data/campaign/campaign.json` seeds a new campaign with `initial_joined_officers`.
|
||||||
|
|
||||||
|
When a saved campaign is loaded, `CampaignState` reconciles completed-scenario officer membership in campaign order. It reapplies only `join_officers` and `leave_officers` from completed scenario rewards plus the saved choice id in `applied_post_battle_choices`. Old saves without that choice ledger are backfilled only when exactly one completed scenario choice has `set_flags` that match saved campaign flags. If zero or multiple choices match, choice membership is skipped for that scenario. Gold, items, roster progression, flags, and completed ids are not replayed by this reconciliation pass.
|
||||||
|
|
||||||
|
Supported condition forms:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "all_units_defeated", "team": "enemy" }
|
||||||
|
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }
|
||||||
|
{ "type": "any_unit_defeated", "unit_ids": ["cao_cao_ch2"] }
|
||||||
|
{ "type": "turn_reached", "turn": 9, "team": "player" }
|
||||||
|
{ "type": "turn_limit", "turn": 8 }
|
||||||
|
{ "type": "unit_reaches_tile", "team": "player", "officer_ids": ["cao_cao"], "pos": [13, 4] }
|
||||||
|
{ "type": "all", "conditions": [{ "type": "all_units_defeated", "team": "enemy" }] }
|
||||||
|
{ "type": "any", "conditions": [{ "type": "any_officer_defeated", "officer_ids": ["cao_cao"] }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Conditions may include `after_event` to stay inactive until a one-shot event has fired. This is useful for reinforcement battles where the player should not win before the reinforcement event occurs.
|
||||||
|
|
||||||
|
`turn_reached` is the precise condition form. `turn_limit` is a shorthand for "win by the end of this turn" and is evaluated as a player-phase defeat after the limit is exceeded.
|
||||||
|
|
||||||
|
`unit_reaches_tile` checks living deployed units against a zero-based `pos` coordinate. Use `officer_ids` for persistent named officers or `unit_ids` for scenario-specific units; the validator requires one of those filters so destination objectives do not accidentally trigger from any unit. Victory destination cells are drawn as objective markers on the battle map.
|
||||||
|
|
||||||
|
`post_battle_dialogue` plays once after victory and before the victory result panel opens. Lines may be plain strings or objects with `speaker` and non-empty `text`.
|
||||||
|
|
||||||
|
`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`, and movement-triggered `unit_reaches_tile` triggers. `battle_begin` runs after the briefing is closed. `unit_reaches_tile` events run when a moved unit enters the zero-based `pos` cell; use `team`, `unit_ids`, or `officer_ids` to limit who can trigger the event. Actions currently include `log`, `dialogue`, `set_objective`, `spawn_deployment`, and `spawn_deployments`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "turn_2_warning",
|
||||||
|
"once": true,
|
||||||
|
"when": {
|
||||||
|
"type": "turn_start",
|
||||||
|
"team": "enemy",
|
||||||
|
"turn": 2,
|
||||||
|
"campaign_flags": { "pursued_dong_zhuo": true }
|
||||||
|
},
|
||||||
|
"actions": [
|
||||||
|
{ "type": "log", "text": "Enemy reinforcements are approaching." },
|
||||||
|
{
|
||||||
|
"type": "dialogue",
|
||||||
|
"lines": [
|
||||||
|
{ "speaker": "Cao Cao", "text": "Hold formation." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "set_objective", "victory": "Defeat the vanguard and reinforcements." },
|
||||||
|
{
|
||||||
|
"type": "spawn_deployment",
|
||||||
|
"deployment": {
|
||||||
|
"unit_id": "enemy_reinforcement_1",
|
||||||
|
"name": "Enemy Rider",
|
||||||
|
"class_id": "cavalry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 2,
|
||||||
|
"pos": [11, 3],
|
||||||
|
"base": { "hp": 36, "atk": 12, "def": 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Movement-triggered ambushes use the same event action shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "midroad_ambush",
|
||||||
|
"once": true,
|
||||||
|
"when": { "type": "unit_reaches_tile", "team": "player", "pos": [7, 4] },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "dialogue", "lines": [{ "speaker": "Scout", "text": "Ambush!" }] },
|
||||||
|
{
|
||||||
|
"type": "spawn_deployment",
|
||||||
|
"deployment": {
|
||||||
|
"unit_id": "ambush_guard",
|
||||||
|
"name": "Ambush Guard",
|
||||||
|
"class_id": "infantry",
|
||||||
|
"team": "enemy",
|
||||||
|
"level": 7,
|
||||||
|
"pos": [6, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
## 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`, and `growth_bonus` so the battle resolver can handle counterattacks, MP tactics, 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, and player roster progression. Roster snapshots include level, EXP, stats, class id/name, class-derived movement/range/growth fields, skills, and equipment. Rewards, joined/left officers, and post-battle choices are guarded by completed scenario id so restarting a finished battle does not duplicate gold, items, membership changes, or branch decisions. On load, completed-scenario officer join/leave transitions are reconciled without replaying gold or item rewards, which lets new officer rewards added to older completed scenarios become available to existing saves.
|
||||||
|
|
||||||
|
After victory, `CampaignState` advances `current_scenario_id` to the next entry in `data/campaign/campaign.json`. The battle scene passes `CampaignState.get_roster_overrides()`, `CampaignState.get_inventory_snapshot()`, `CampaignState.get_flags_snapshot()`, and `CampaignState.get_joined_officers_snapshot()` into `BattleState.load_battle()`, so officers can keep level/EXP/stat progression, equipped gear, consumable/equipment stock counts, recruitment gates, and branch flags into the next scenario.
|
||||||
|
|
||||||
|
During a battle, item consumption and equipment swaps affect `BattleState.battle_inventory`. `CampaignState.apply_battle_result()` commits that inventory snapshot only for a first-time victory, then adds the victory rewards. Equipped gear is stored in the player roster snapshot together with the current derived stats; a later stat-model refactor should split base stats, growth, and equipment bonuses.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
Physical attacks grant normal attack/counter EXP only on hit after the attack/counter exchange resolves. A missed physical attack or counter grants a small miss EXP value, while defeat bonuses still require the attack to hit and defeat the target.
|
||||||
|
|
||||||
|
Before a battle starts, `BattleScene` can buy priced items through `CampaignState.try_buy_item()`. A successful purchase updates campaign gold and inventory in the save file immediately, then calls `BattleState.set_inventory_snapshot()` so the upcoming battle sees the new stock. Pre-battle Armory changes call `CampaignState.try_save_prebattle_loadout()`, which merges the currently deployed roster snapshot into the saved roster and saves the adjusted inventory in the same operation. Pre-battle Roster calls `BattleState.try_set_unit_deployed()` to toggle optional player units while enforcing required officers and sortie limits. Pre-battle Formation calls `BattleState.try_set_prebattle_formation()`, which can move a player unit to an open formation cell or swap two player units inside the formation area. Matching `briefing.conditional_lines` and `shop.conditional_items` are merged while the scenario loads, before the briefing panel opens. `post_battle_dialogue` is rendered by the battle scene after victory and before rewards/choices are shown in the result panel. Post-battle choices call `CampaignState.try_apply_post_battle_choice()`, which saves selected flags and rolls back flags, gold, and inventory if the save write fails.
|
||||||
|
|
||||||
|
When every campaign scenario id is present in `completed_scenarios`, the battle scene shows a campaign completion panel. Starting a new campaign clears the save file and resets `current_scenario_id` to `start_scenario`.
|
||||||
|
|
||||||
|
Run `tools/validate_data.ps1` after data edits to check campaign paths, map dimensions, terrain keys, class/officer/item/skill references, item effect names and amounts, equipment slot compatibility including accessory types, promotion routes and promotion equipment compatibility, condition/event names, condition unit references, destination condition 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.
|
||||||
56
docs/GODOT_4_6_MIGRATION.md
Normal file
56
docs/GODOT_4_6_MIGRATION.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Godot 4.6 Migration
|
||||||
|
|
||||||
|
This project targets Godot 4 and is being moved toward Godot 4.6 as the active editor baseline.
|
||||||
|
|
||||||
|
## Current Risk Profile
|
||||||
|
|
||||||
|
- The project is a simple Godot 4 `Node2D` application with data-driven JSON content.
|
||||||
|
- There are no addons, imported art assets, shaders, TileMaps, 3D physics scenes, or export presets yet.
|
||||||
|
- The current renderer is explicitly set to `gl_compatibility`, so new-project rendering defaults in Godot 4.6 should not silently change the existing project renderer.
|
||||||
|
- `project.godot` still advertises `config/features=PackedStringArray("4.2")`; let the Godot 4.6 editor update project files instead of editing generated scene/resource formats by hand.
|
||||||
|
|
||||||
|
## Before Opening In 4.6
|
||||||
|
|
||||||
|
1. Create a backup or initialize version control before the editor writes migration changes.
|
||||||
|
2. Run the data/readiness check:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readiness.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. If the Godot executable is not on `PATH`, set `GODOT_BIN` for command-line checks:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:GODOT_BIN = "C:\Path\To\Godot_v4.6-stable_win64.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Editor Migration Steps
|
||||||
|
|
||||||
|
1. Open the project folder in Godot 4.6.
|
||||||
|
2. Let the editor import the project and accept any project-file migration prompt.
|
||||||
|
3. Run `Project > Tools > Upgrade Project Files...`.
|
||||||
|
4. Save, close, and reopen the project once.
|
||||||
|
5. Run the readiness check again. If `GODOT_BIN` is set, include the optional headless editor import:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File tools\check_godot46_readiness.ps1 -RunGodotImport
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Smoke Test
|
||||||
|
|
||||||
|
- Main scene starts without script errors.
|
||||||
|
- Current campaign save loads or a new campaign starts cleanly.
|
||||||
|
- Briefing opens and can enter battle.
|
||||||
|
- Shop, Armory, Roster, and Formation open and close.
|
||||||
|
- A player unit can move, attack, wait, use a tactic, use an item, and equip compatible gear.
|
||||||
|
- Enemy turn advances and AI acts.
|
||||||
|
- Victory and defeat panels still appear.
|
||||||
|
- Post-battle dialogue and post-battle choices still block progression until a choice is saved.
|
||||||
|
- Next battle loads with saved roster, inventory, joined officers, and campaign flags.
|
||||||
|
- New Campaign clears the save and returns to the opening scenario.
|
||||||
|
|
||||||
|
## Watch Items
|
||||||
|
|
||||||
|
- If the editor updates `config/features`, review the diff before making content changes.
|
||||||
|
- If UI colors or canvas rendering look different, compare the battle board, overlays, and panels before changing art direction.
|
||||||
|
- If a save file behaves oddly after migration, test both a fresh campaign and an existing `user://campaign_save.json`.
|
||||||
92
docs/ROADMAP.md
Normal file
92
docs/ROADMAP.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Milestone 0: Project Foundation
|
||||||
|
|
||||||
|
- Godot 4 project opens and runs.
|
||||||
|
- Main battle scene loads a JSON battle.
|
||||||
|
- Rules and presentation are separated enough to keep iterating.
|
||||||
|
|
||||||
|
## Milestone 1: Playable Battle Prototype
|
||||||
|
|
||||||
|
- Scenario briefing before battle.
|
||||||
|
- Select player units.
|
||||||
|
- Show move and attack ranges.
|
||||||
|
- Show hover tile and unit information.
|
||||||
|
- Show basic damage forecast.
|
||||||
|
- Move, attack, wait, and end turn.
|
||||||
|
- Enemy units take AI turns with movement, attacks, and basic tactic use.
|
||||||
|
- Victory and defeat states are visible.
|
||||||
|
- Scenario-defined victory and defeat conditions exist.
|
||||||
|
- Destination victory conditions and objective map markers exist.
|
||||||
|
- Turn-limit conditions and HUD turn-limit display exist.
|
||||||
|
- Victory reward summary is visible.
|
||||||
|
- Next-battle flow exists across the current seventeen-scenario campaign.
|
||||||
|
- Campaign completion and new-campaign reset exist.
|
||||||
|
- Reward inventory can be brought into the next battle.
|
||||||
|
|
||||||
|
## Milestone 2: Cao Cao Zhuan-Style Combat Depth
|
||||||
|
|
||||||
|
- Unit classes and class growth tables. Basic version exists.
|
||||||
|
- Automatic class promotion routes. Basic level-threshold promotion exists for core first-tier battle classes.
|
||||||
|
- 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, a skill list menu with range/power hints, and enemy tactic AI exist.
|
||||||
|
- Consumable item use. Basic global inventory, Bean HP recovery, Wine MP recovery, item menu, preview, and target overlay exist.
|
||||||
|
- Counterattacks exist. Support effects are still planned.
|
||||||
|
- Battle EXP, level-ups, class promotion, and campaign roster persistence exist.
|
||||||
|
|
||||||
|
## Milestone 3: Scenario Layer
|
||||||
|
|
||||||
|
- Pre-battle briefing exists. Full dialogue scenes are still planned.
|
||||||
|
- Scenario events exist for battle start, battle begin, turn start, and movement-triggered map tiles.
|
||||||
|
- Mid-battle map/action events have a first action system with movement ambushes.
|
||||||
|
- Opening battle dialogue exists through event actions.
|
||||||
|
- Post-battle dialogue exists before the victory result panel.
|
||||||
|
- Reinforcements and objective changes exist in a basic form.
|
||||||
|
- Post-battle reward save, campaign choice flags, and first flag-driven briefing/shop/event branches exist in a basic form. Rich reward screens and wider branch support are still planned.
|
||||||
|
|
||||||
|
## Milestone 4: Campaign Progression
|
||||||
|
|
||||||
|
- Party roster persistence has a first save-file skeleton.
|
||||||
|
- Level, experience, and equipment persistence.
|
||||||
|
- Chapter selection and save/load.
|
||||||
|
- Linear seventeen-scenario campaign order exists.
|
||||||
|
- Save reset exists.
|
||||||
|
- Victory rewards, consumable counts, equipment stock, and equipped gear persist.
|
||||||
|
- A basic pre-battle shop with scenario-specific stock exists. Sell-back and richer item management are still planned.
|
||||||
|
- A basic pre-battle Armory exists for saved equipment changes before combat.
|
||||||
|
- A basic pre-battle Roster exists for required officers, required scenario units, optional officers, and sortie limits.
|
||||||
|
- A basic pre-battle Formation screen exists for scenario-defined starting cells.
|
||||||
|
- Officer join/leave rewards, choice membership changes, and joined-officer deployment gates exist in a basic form.
|
||||||
|
- Completed-save officer membership reconciliation exists for newly added join/leave rewards.
|
||||||
|
- Post-battle choice ids are saved in a campaign ledger for safer branch reconciliation.
|
||||||
|
- Pending post-battle choices survive reloads without replaying victory rewards.
|
||||||
|
- Fourth-scenario content exists with Qingzhou recruitment for Dian Wei.
|
||||||
|
- Fifth-scenario content exists with Dian Wei required in the Puyang raid and a new Lu Bu branch choice.
|
||||||
|
- Sixth-scenario content exists with Dingtao branch reactions to the Puyang choice.
|
||||||
|
- Seventh-scenario content exists with an emperor escort route, a protected envoy, a midroad ambush, and a destination victory condition.
|
||||||
|
- Eighth-scenario content exists with a Wan Castle escape route, protected Cao Ang, 007 branch reactions, and a destination victory condition.
|
||||||
|
- Ninth-scenario content exists with a Xiapi siege, flood-gate objective update, Lu Bu/Chen Gong pressure, and 008 branch reactions.
|
||||||
|
- Tenth-scenario content exists with White Horse relief, Guo Jia as a joined strategist, Yan Liang's vanguard, and 009 branch reactions.
|
||||||
|
- Eleventh-scenario content exists with Yan Ford pursuit, Wen Chou's countercharge, and 010 branch reactions.
|
||||||
|
- Twelfth-scenario content exists with Guandu's main assault, Yuan Shao's army, and 011 branch reactions.
|
||||||
|
- Thirteenth-scenario content exists with Wuchao depot raid, a burn-and-clear objective, and 012 branch reactions.
|
||||||
|
- Fourteenth-scenario content exists with Cangting pursuit, Zhang He recruitment, and 013 branch reactions.
|
||||||
|
- Fifteenth-scenario content exists with Ye outer defense, Zhang He as a deployable officer, and 014 branch reactions.
|
||||||
|
- Sixteenth-scenario content exists with Ye inner siege, Zhang He deployment, and 015 branch reactions.
|
||||||
|
- Seventeenth-scenario content exists with Ye surrender, last loyalist resistance, and 016 branch reactions.
|
||||||
|
|
||||||
|
## Milestone 5: Presentation
|
||||||
|
|
||||||
|
- Portrait pipeline.
|
||||||
|
- Unit sprites and animations.
|
||||||
|
- Battle effects.
|
||||||
|
- Music and sound.
|
||||||
|
- Chapter UI and visual novel scenes.
|
||||||
|
|
||||||
|
## Milestone 6: Content Expansion
|
||||||
|
|
||||||
|
- Recreate a long campaign structure chapter by chapter.
|
||||||
|
- Add historical officers, unique classes, and named equipment.
|
||||||
|
- Balance maps through repeated playtesting.
|
||||||
22
project.godot
Normal file
22
project.godot
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; Prefer editing through the Godot editor when project settings become more complex.
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="Heroes of Three Kingdoms"
|
||||||
|
run/main_scene="res://scenes/battle_scene.tscn"
|
||||||
|
config/features=PackedStringArray("4.2")
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1280
|
||||||
|
window/size/viewport_height=720
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
textures/canvas_textures/default_texture_filter=0
|
||||||
|
|
||||||
6
scenes/battle_scene.tscn
Normal file
6
scenes/battle_scene.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/scenes/battle_scene.gd" id="1_battle"]
|
||||||
|
|
||||||
|
[node name="BattleScene" type="Node2D"]
|
||||||
|
script = ExtResource("1_battle")
|
||||||
2388
scripts/core/battle_state.gd
Normal file
2388
scripts/core/battle_state.gd
Normal file
File diff suppressed because it is too large
Load Diff
592
scripts/core/campaign_state.gd
Normal file
592
scripts/core/campaign_state.gd
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name CampaignState
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://campaign_save.json"
|
||||||
|
const SAVE_VERSION := 3
|
||||||
|
|
||||||
|
var save_version := SAVE_VERSION
|
||||||
|
var campaign_id := ""
|
||||||
|
var campaign_title := ""
|
||||||
|
var start_scenario_id := ""
|
||||||
|
var scenario_order: Array[String] = []
|
||||||
|
var scenario_paths := {}
|
||||||
|
var scenario_titles := {}
|
||||||
|
var initial_joined_officers: Array[String] = []
|
||||||
|
var current_scenario_id := ""
|
||||||
|
var completed_scenarios: Array[String] = []
|
||||||
|
var gold := 0
|
||||||
|
var inventory := {}
|
||||||
|
var roster := {}
|
||||||
|
var flags := {}
|
||||||
|
var joined_officers: Array[String] = []
|
||||||
|
var applied_post_battle_choices := {}
|
||||||
|
var pending_post_battle_choice_scenario_id := ""
|
||||||
|
|
||||||
|
|
||||||
|
func load_campaign(path: String) -> bool:
|
||||||
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
push_error("Campaign file is not valid JSON: %s" % path)
|
||||||
|
return false
|
||||||
|
|
||||||
|
campaign_id = str(parsed.get("id", ""))
|
||||||
|
campaign_title = str(parsed.get("title", "Campaign"))
|
||||||
|
start_scenario_id = str(parsed.get("start_scenario", ""))
|
||||||
|
scenario_order.clear()
|
||||||
|
scenario_paths.clear()
|
||||||
|
scenario_titles.clear()
|
||||||
|
initial_joined_officers.clear()
|
||||||
|
|
||||||
|
for officer_id in parsed.get("initial_joined_officers", []):
|
||||||
|
var normalized := str(officer_id)
|
||||||
|
if normalized.is_empty() or initial_joined_officers.has(normalized):
|
||||||
|
continue
|
||||||
|
initial_joined_officers.append(normalized)
|
||||||
|
|
||||||
|
for scenario in parsed.get("scenarios", []):
|
||||||
|
if typeof(scenario) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var scenario_id := str(scenario.get("id", ""))
|
||||||
|
var scenario_path := str(scenario.get("path", ""))
|
||||||
|
if scenario_id.is_empty() or scenario_path.is_empty():
|
||||||
|
continue
|
||||||
|
scenario_order.append(scenario_id)
|
||||||
|
scenario_paths[scenario_id] = scenario_path
|
||||||
|
scenario_titles[scenario_id] = str(scenario.get("title", scenario_id))
|
||||||
|
|
||||||
|
if start_scenario_id.is_empty() and not scenario_order.is_empty():
|
||||||
|
start_scenario_id = scenario_order[0]
|
||||||
|
return not scenario_order.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func load_or_start(scenario_id: String) -> void:
|
||||||
|
var loaded_save := load_game()
|
||||||
|
if not loaded_save:
|
||||||
|
start_new(scenario_id)
|
||||||
|
else:
|
||||||
|
if current_scenario_id.is_empty():
|
||||||
|
current_scenario_id = scenario_id
|
||||||
|
var save_needs_update := false
|
||||||
|
if loaded_save:
|
||||||
|
save_needs_update = _clear_invalid_pending_post_battle_choice() or save_needs_update
|
||||||
|
_normalize_current_scenario()
|
||||||
|
if loaded_save:
|
||||||
|
save_needs_update = _backfill_applied_post_battle_choices() or save_needs_update
|
||||||
|
save_needs_update = _reconcile_completed_officer_transitions() or save_needs_update
|
||||||
|
if save_needs_update and not save_game():
|
||||||
|
push_error("Unable to save reconciled campaign officer state.")
|
||||||
|
|
||||||
|
|
||||||
|
func start_new(scenario_id: String) -> void:
|
||||||
|
current_scenario_id = scenario_id
|
||||||
|
completed_scenarios.clear()
|
||||||
|
gold = 0
|
||||||
|
inventory.clear()
|
||||||
|
roster.clear()
|
||||||
|
flags.clear()
|
||||||
|
joined_officers.clear()
|
||||||
|
applied_post_battle_choices.clear()
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
for officer_id in initial_joined_officers:
|
||||||
|
joined_officers.append(officer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func reset_save(scenario_id: String) -> bool:
|
||||||
|
var save_file := ProjectSettings.globalize_path(SAVE_PATH)
|
||||||
|
if FileAccess.file_exists(SAVE_PATH):
|
||||||
|
var error := DirAccess.remove_absolute(save_file)
|
||||||
|
if error != OK:
|
||||||
|
push_error("Unable to delete campaign save: %s" % save_file)
|
||||||
|
return false
|
||||||
|
start_new(scenario_id)
|
||||||
|
return save_game()
|
||||||
|
|
||||||
|
|
||||||
|
func load_game() -> bool:
|
||||||
|
if not FileAccess.file_exists(SAVE_PATH):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(SAVE_PATH))
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
|
||||||
|
save_version = int(parsed.get("save_version", 1))
|
||||||
|
current_scenario_id = str(parsed.get("current_scenario_id", ""))
|
||||||
|
completed_scenarios.clear()
|
||||||
|
for scenario_id in parsed.get("completed_scenarios", []):
|
||||||
|
completed_scenarios.append(str(scenario_id))
|
||||||
|
gold = int(parsed.get("gold", 0))
|
||||||
|
inventory = _copy_dictionary(parsed.get("inventory", {}))
|
||||||
|
roster = _copy_dictionary(parsed.get("roster", {}))
|
||||||
|
flags = _copy_dictionary(parsed.get("flags", {}))
|
||||||
|
applied_post_battle_choices = _copy_dictionary(parsed.get("applied_post_battle_choices", {}))
|
||||||
|
pending_post_battle_choice_scenario_id = str(parsed.get("pending_post_battle_choice_scenario_id", ""))
|
||||||
|
joined_officers.clear()
|
||||||
|
var saved_joined = parsed.get("joined_officers", initial_joined_officers)
|
||||||
|
for officer_id in saved_joined:
|
||||||
|
var normalized := str(officer_id)
|
||||||
|
if normalized.is_empty() or joined_officers.has(normalized):
|
||||||
|
continue
|
||||||
|
joined_officers.append(normalized)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func save_game() -> bool:
|
||||||
|
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
push_error("Unable to save campaign state: %s" % SAVE_PATH)
|
||||||
|
return false
|
||||||
|
file.store_string(JSON.stringify(to_dict()))
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func apply_battle_result(battle_state) -> Dictionary:
|
||||||
|
if battle_state.battle_status != "victory":
|
||||||
|
return {
|
||||||
|
"saved": false,
|
||||||
|
"rewards_applied": false,
|
||||||
|
"already_completed": false,
|
||||||
|
"gold": 0,
|
||||||
|
"items": [],
|
||||||
|
"joined_officers": [],
|
||||||
|
"left_officers": [],
|
||||||
|
"progression_events": []
|
||||||
|
}
|
||||||
|
|
||||||
|
var already_completed := completed_scenarios.has(battle_state.battle_id)
|
||||||
|
var rewards := battle_state.get_rewards()
|
||||||
|
var post_battle_choices := battle_state.get_post_battle_choices()
|
||||||
|
var previous_completed := completed_scenarios.duplicate()
|
||||||
|
var previous_gold := gold
|
||||||
|
var previous_inventory := inventory.duplicate(true)
|
||||||
|
var previous_roster := roster.duplicate(true)
|
||||||
|
var previous_current_scenario_id := current_scenario_id
|
||||||
|
var previous_joined_officers := joined_officers.duplicate()
|
||||||
|
var previous_pending_post_battle_choice_scenario_id := pending_post_battle_choice_scenario_id
|
||||||
|
var rewarded_items: Array = []
|
||||||
|
var joined_reward_officers: Array = []
|
||||||
|
var left_reward_officers: Array = []
|
||||||
|
var reward_gold := 0
|
||||||
|
var next_scenario_id := get_next_scenario_id(battle_state.battle_id)
|
||||||
|
|
||||||
|
if not already_completed:
|
||||||
|
roster = battle_state.get_player_roster_snapshot()
|
||||||
|
inventory = battle_state.get_inventory_snapshot()
|
||||||
|
|
||||||
|
if not already_completed:
|
||||||
|
completed_scenarios.append(battle_state.battle_id)
|
||||||
|
reward_gold = int(rewards.get("gold", 0))
|
||||||
|
gold += reward_gold
|
||||||
|
|
||||||
|
for item_id in rewards.get("items", []):
|
||||||
|
var key := str(item_id)
|
||||||
|
inventory[key] = int(inventory.get(key, 0)) + 1
|
||||||
|
rewarded_items.append(key)
|
||||||
|
joined_reward_officers = _apply_join_officers(rewards.get("join_officers", []))
|
||||||
|
left_reward_officers = _apply_leave_officers(rewards.get("leave_officers", []))
|
||||||
|
pending_post_battle_choice_scenario_id = battle_state.battle_id if not post_battle_choices.is_empty() else ""
|
||||||
|
|
||||||
|
if not next_scenario_id.is_empty():
|
||||||
|
current_scenario_id = next_scenario_id
|
||||||
|
else:
|
||||||
|
current_scenario_id = battle_state.battle_id
|
||||||
|
|
||||||
|
var saved := save_game()
|
||||||
|
if not saved:
|
||||||
|
completed_scenarios.clear()
|
||||||
|
for scenario_id in previous_completed:
|
||||||
|
completed_scenarios.append(str(scenario_id))
|
||||||
|
gold = previous_gold
|
||||||
|
inventory = previous_inventory
|
||||||
|
roster = previous_roster
|
||||||
|
current_scenario_id = previous_current_scenario_id
|
||||||
|
pending_post_battle_choice_scenario_id = previous_pending_post_battle_choice_scenario_id
|
||||||
|
joined_officers.clear()
|
||||||
|
for officer_id in previous_joined_officers:
|
||||||
|
joined_officers.append(str(officer_id))
|
||||||
|
reward_gold = 0
|
||||||
|
rewarded_items.clear()
|
||||||
|
joined_reward_officers.clear()
|
||||||
|
left_reward_officers.clear()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"saved": saved,
|
||||||
|
"rewards_applied": not already_completed and saved,
|
||||||
|
"already_completed": already_completed,
|
||||||
|
"gold": reward_gold,
|
||||||
|
"items": rewarded_items,
|
||||||
|
"joined_officers": joined_reward_officers,
|
||||||
|
"left_officers": left_reward_officers,
|
||||||
|
"progression_events": battle_state.get_progression_events() if saved and not already_completed else [],
|
||||||
|
"next_scenario_id": next_scenario_id,
|
||||||
|
"next_scenario_title": get_scenario_title(next_scenario_id),
|
||||||
|
"scenario_id": battle_state.battle_id,
|
||||||
|
"campaign_complete": next_scenario_id.is_empty(),
|
||||||
|
"post_battle_choices": post_battle_choices if saved and not already_completed else [],
|
||||||
|
"choice_applied": not saved or already_completed or post_battle_choices.is_empty(),
|
||||||
|
"choice_label": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func get_roster_overrides() -> Dictionary:
|
||||||
|
return roster.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_joined_officers_snapshot() -> Array:
|
||||||
|
return joined_officers.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_inventory_snapshot() -> Dictionary:
|
||||||
|
return inventory.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func try_buy_item(item_id: String, price: int) -> bool:
|
||||||
|
if item_id.is_empty() or price <= 0 or gold < price:
|
||||||
|
return false
|
||||||
|
var previous_count := int(inventory.get(item_id, 0))
|
||||||
|
gold -= price
|
||||||
|
inventory[item_id] = previous_count + 1
|
||||||
|
if save_game():
|
||||||
|
return true
|
||||||
|
gold += price
|
||||||
|
if previous_count > 0:
|
||||||
|
inventory[item_id] = previous_count
|
||||||
|
else:
|
||||||
|
inventory.erase(item_id)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func try_save_prebattle_loadout(roster_snapshot: Dictionary, inventory_snapshot: Dictionary) -> bool:
|
||||||
|
var previous_roster := roster.duplicate(true)
|
||||||
|
var previous_inventory := inventory.duplicate(true)
|
||||||
|
for roster_key in roster_snapshot.keys():
|
||||||
|
var entry = roster_snapshot[roster_key]
|
||||||
|
if typeof(entry) == TYPE_DICTIONARY:
|
||||||
|
roster[str(roster_key)] = entry.duplicate(true)
|
||||||
|
inventory = inventory_snapshot.duplicate(true)
|
||||||
|
if save_game():
|
||||||
|
return true
|
||||||
|
roster = previous_roster
|
||||||
|
inventory = previous_inventory
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func try_apply_post_battle_choice(choice: Dictionary, scenario_id: String = "") -> bool:
|
||||||
|
var previous_flags := flags.duplicate(true)
|
||||||
|
var previous_gold := gold
|
||||||
|
var previous_inventory := inventory.duplicate(true)
|
||||||
|
var previous_joined_officers := joined_officers.duplicate()
|
||||||
|
var previous_applied_post_battle_choices := applied_post_battle_choices.duplicate(true)
|
||||||
|
var previous_pending_post_battle_choice_scenario_id := pending_post_battle_choice_scenario_id
|
||||||
|
var previous_current_scenario_id := current_scenario_id
|
||||||
|
|
||||||
|
var set_flags: Dictionary = choice.get("set_flags", {})
|
||||||
|
for flag_name in set_flags.keys():
|
||||||
|
flags[str(flag_name)] = set_flags[flag_name]
|
||||||
|
|
||||||
|
var reward_gold := int(choice.get("gold", 0))
|
||||||
|
if reward_gold > 0:
|
||||||
|
gold += reward_gold
|
||||||
|
for item_id in choice.get("items", []):
|
||||||
|
var key := str(item_id)
|
||||||
|
if key.is_empty():
|
||||||
|
continue
|
||||||
|
inventory[key] = int(inventory.get(key, 0)) + 1
|
||||||
|
_apply_join_officers(choice.get("join_officers", []))
|
||||||
|
_apply_leave_officers(choice.get("leave_officers", []))
|
||||||
|
var choice_id := str(choice.get("id", ""))
|
||||||
|
if not scenario_id.is_empty() and not choice_id.is_empty():
|
||||||
|
applied_post_battle_choices[scenario_id] = choice_id
|
||||||
|
if pending_post_battle_choice_scenario_id == scenario_id:
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
var next_scenario_id := get_next_scenario_id(scenario_id)
|
||||||
|
current_scenario_id = next_scenario_id if not next_scenario_id.is_empty() else scenario_id
|
||||||
|
|
||||||
|
if save_game():
|
||||||
|
return true
|
||||||
|
flags = previous_flags
|
||||||
|
gold = previous_gold
|
||||||
|
inventory = previous_inventory
|
||||||
|
applied_post_battle_choices = previous_applied_post_battle_choices
|
||||||
|
pending_post_battle_choice_scenario_id = previous_pending_post_battle_choice_scenario_id
|
||||||
|
current_scenario_id = previous_current_scenario_id
|
||||||
|
joined_officers.clear()
|
||||||
|
for officer_id in previous_joined_officers:
|
||||||
|
joined_officers.append(str(officer_id))
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func has_pending_post_battle_choice() -> bool:
|
||||||
|
if pending_post_battle_choice_scenario_id.is_empty():
|
||||||
|
return false
|
||||||
|
if not scenario_paths.has(pending_post_battle_choice_scenario_id):
|
||||||
|
return false
|
||||||
|
if not completed_scenarios.has(pending_post_battle_choice_scenario_id):
|
||||||
|
return false
|
||||||
|
if applied_post_battle_choices.has(pending_post_battle_choice_scenario_id):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_pending_post_battle_choice_scenario_id() -> String:
|
||||||
|
return pending_post_battle_choice_scenario_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_pending_post_battle_choice_summary() -> Dictionary:
|
||||||
|
if pending_post_battle_choice_scenario_id.is_empty():
|
||||||
|
return {}
|
||||||
|
var scenario_data := _load_scenario_definition(pending_post_battle_choice_scenario_id)
|
||||||
|
if scenario_data.is_empty():
|
||||||
|
return {}
|
||||||
|
var choices = scenario_data.get("post_battle_choices", [])
|
||||||
|
if typeof(choices) != TYPE_ARRAY or choices.is_empty():
|
||||||
|
return {}
|
||||||
|
var next_scenario_id := get_next_scenario_id(pending_post_battle_choice_scenario_id)
|
||||||
|
return {
|
||||||
|
"saved": true,
|
||||||
|
"rewards_applied": false,
|
||||||
|
"already_completed": false,
|
||||||
|
"pending_choice": true,
|
||||||
|
"gold": 0,
|
||||||
|
"items": [],
|
||||||
|
"joined_officers": [],
|
||||||
|
"left_officers": [],
|
||||||
|
"progression_events": [],
|
||||||
|
"next_scenario_id": next_scenario_id,
|
||||||
|
"next_scenario_title": get_scenario_title(next_scenario_id),
|
||||||
|
"scenario_id": pending_post_battle_choice_scenario_id,
|
||||||
|
"campaign_complete": next_scenario_id.is_empty(),
|
||||||
|
"post_battle_choices": choices,
|
||||||
|
"choice_applied": false,
|
||||||
|
"choice_label": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_invalid_pending_post_battle_choice() -> bool:
|
||||||
|
if pending_post_battle_choice_scenario_id.is_empty():
|
||||||
|
return false
|
||||||
|
if not scenario_paths.has(pending_post_battle_choice_scenario_id):
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
return true
|
||||||
|
if not completed_scenarios.has(pending_post_battle_choice_scenario_id):
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
return true
|
||||||
|
if applied_post_battle_choices.has(pending_post_battle_choice_scenario_id):
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
return true
|
||||||
|
if get_pending_post_battle_choice_summary().is_empty():
|
||||||
|
pending_post_battle_choice_scenario_id = ""
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_join_officers(source) -> Array:
|
||||||
|
var result := []
|
||||||
|
if typeof(source) != TYPE_ARRAY:
|
||||||
|
return result
|
||||||
|
for officer_id in source:
|
||||||
|
var key := str(officer_id)
|
||||||
|
if key.is_empty() or joined_officers.has(key):
|
||||||
|
continue
|
||||||
|
joined_officers.append(key)
|
||||||
|
result.append(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_leave_officers(source) -> Array:
|
||||||
|
var result := []
|
||||||
|
if typeof(source) != TYPE_ARRAY:
|
||||||
|
return result
|
||||||
|
for officer_id in source:
|
||||||
|
var key := str(officer_id)
|
||||||
|
if key.is_empty() or not joined_officers.has(key):
|
||||||
|
continue
|
||||||
|
joined_officers.erase(key)
|
||||||
|
result.append(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _reconcile_completed_officer_transitions() -> bool:
|
||||||
|
var changed := false
|
||||||
|
for scenario_id in scenario_order:
|
||||||
|
if not completed_scenarios.has(scenario_id):
|
||||||
|
continue
|
||||||
|
var scenario_data := _load_scenario_definition(scenario_id)
|
||||||
|
if scenario_data.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var rewards = scenario_data.get("rewards", {})
|
||||||
|
if typeof(rewards) == TYPE_DICTIONARY:
|
||||||
|
changed = _apply_officer_transitions_from_block(rewards) or changed
|
||||||
|
|
||||||
|
var applied_choice := _get_reconciled_post_battle_choice(scenario_id, scenario_data)
|
||||||
|
if not applied_choice.is_empty():
|
||||||
|
changed = _apply_officer_transitions_from_block(applied_choice) or changed
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
func _backfill_applied_post_battle_choices() -> bool:
|
||||||
|
var changed := false
|
||||||
|
for scenario_id in scenario_order:
|
||||||
|
if not completed_scenarios.has(scenario_id) or applied_post_battle_choices.has(scenario_id):
|
||||||
|
continue
|
||||||
|
if scenario_id == pending_post_battle_choice_scenario_id:
|
||||||
|
continue
|
||||||
|
var scenario_data := _load_scenario_definition(scenario_id)
|
||||||
|
if scenario_data.is_empty():
|
||||||
|
continue
|
||||||
|
var choices = scenario_data.get("post_battle_choices", [])
|
||||||
|
if typeof(choices) != TYPE_ARRAY:
|
||||||
|
continue
|
||||||
|
var matching_choices := _matching_choices_from_flags(choices)
|
||||||
|
if matching_choices.size() == 1:
|
||||||
|
var choice_id := str(matching_choices[0].get("id", ""))
|
||||||
|
if not choice_id.is_empty():
|
||||||
|
applied_post_battle_choices[scenario_id] = choice_id
|
||||||
|
changed = true
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
func _get_reconciled_post_battle_choice(scenario_id: String, scenario_data: Dictionary) -> Dictionary:
|
||||||
|
if scenario_id == pending_post_battle_choice_scenario_id:
|
||||||
|
return {}
|
||||||
|
var choices = scenario_data.get("post_battle_choices", [])
|
||||||
|
if typeof(choices) != TYPE_ARRAY:
|
||||||
|
return {}
|
||||||
|
var applied_choice_id := str(applied_post_battle_choices.get(scenario_id, ""))
|
||||||
|
if not applied_choice_id.is_empty():
|
||||||
|
for choice in choices:
|
||||||
|
if typeof(choice) == TYPE_DICTIONARY and str(choice.get("id", "")) == applied_choice_id:
|
||||||
|
return choice
|
||||||
|
return {}
|
||||||
|
var matching_choices := _matching_choices_from_flags(choices)
|
||||||
|
if matching_choices.size() == 1:
|
||||||
|
return matching_choices[0]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _load_scenario_definition(scenario_id: String) -> Dictionary:
|
||||||
|
var path := get_scenario_path(scenario_id)
|
||||||
|
if path.is_empty():
|
||||||
|
return {}
|
||||||
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
push_error("Scenario file is not valid JSON: %s" % path)
|
||||||
|
return {}
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
func _matching_choices_from_flags(choices: Array) -> Array:
|
||||||
|
var result := []
|
||||||
|
for choice in choices:
|
||||||
|
if typeof(choice) != TYPE_DICTIONARY or not _choice_flags_match(choice):
|
||||||
|
continue
|
||||||
|
result.append(choice)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _choice_flags_match(choice: Dictionary) -> bool:
|
||||||
|
var set_flags = choice.get("set_flags", {})
|
||||||
|
if typeof(set_flags) != TYPE_DICTIONARY or set_flags.is_empty():
|
||||||
|
return false
|
||||||
|
for flag_name in set_flags.keys():
|
||||||
|
var key := str(flag_name)
|
||||||
|
if not flags.has(key) or flags[key] != set_flags[flag_name]:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_officer_transitions_from_block(block: Dictionary) -> bool:
|
||||||
|
var joined := _apply_join_officers(block.get("join_officers", []))
|
||||||
|
var left := _apply_leave_officers(block.get("leave_officers", []))
|
||||||
|
return not joined.is_empty() or not left.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func get_flags_snapshot() -> Dictionary:
|
||||||
|
return flags.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func is_scenario_completed(scenario_id: String) -> bool:
|
||||||
|
return completed_scenarios.has(scenario_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_start_scenario_id() -> String:
|
||||||
|
return start_scenario_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_scenario_path(scenario_id: String) -> String:
|
||||||
|
return str(scenario_paths.get(scenario_id, ""))
|
||||||
|
|
||||||
|
|
||||||
|
func get_current_scenario_path() -> String:
|
||||||
|
if current_scenario_id.is_empty():
|
||||||
|
current_scenario_id = get_start_scenario_id()
|
||||||
|
return get_scenario_path(current_scenario_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_scenario_title(scenario_id: String) -> String:
|
||||||
|
if scenario_id.is_empty():
|
||||||
|
return ""
|
||||||
|
return str(scenario_titles.get(scenario_id, scenario_id))
|
||||||
|
|
||||||
|
|
||||||
|
func get_next_scenario_id(scenario_id: String) -> String:
|
||||||
|
var index := scenario_order.find(scenario_id)
|
||||||
|
if index == -1 or index + 1 >= scenario_order.size():
|
||||||
|
return ""
|
||||||
|
return scenario_order[index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
func is_campaign_complete() -> bool:
|
||||||
|
if has_pending_post_battle_choice():
|
||||||
|
return false
|
||||||
|
if scenario_order.is_empty():
|
||||||
|
return false
|
||||||
|
for scenario_id in scenario_order:
|
||||||
|
if not completed_scenarios.has(scenario_id):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_progress_text() -> String:
|
||||||
|
var current_title := get_scenario_title(current_scenario_id)
|
||||||
|
if current_title.is_empty():
|
||||||
|
current_title = get_scenario_title(get_start_scenario_id())
|
||||||
|
var complete_text := "complete" if is_campaign_complete() else "in progress"
|
||||||
|
return "%s | %s | Gold %d | %d/%d complete" % [
|
||||||
|
campaign_title,
|
||||||
|
complete_text,
|
||||||
|
gold,
|
||||||
|
completed_scenarios.size(),
|
||||||
|
scenario_order.size()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _normalize_current_scenario() -> void:
|
||||||
|
if current_scenario_id.is_empty():
|
||||||
|
current_scenario_id = get_start_scenario_id()
|
||||||
|
if completed_scenarios.has(current_scenario_id):
|
||||||
|
var next_id := get_next_scenario_id(current_scenario_id)
|
||||||
|
if not next_id.is_empty():
|
||||||
|
current_scenario_id = next_id
|
||||||
|
|
||||||
|
|
||||||
|
func to_dict() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"save_version": SAVE_VERSION,
|
||||||
|
"current_scenario_id": current_scenario_id,
|
||||||
|
"completed_scenarios": completed_scenarios.duplicate(),
|
||||||
|
"gold": gold,
|
||||||
|
"inventory": inventory.duplicate(true),
|
||||||
|
"roster": roster.duplicate(true),
|
||||||
|
"flags": flags.duplicate(true),
|
||||||
|
"joined_officers": joined_officers.duplicate(),
|
||||||
|
"applied_post_battle_choices": applied_post_battle_choices.duplicate(true),
|
||||||
|
"pending_post_battle_choice_scenario_id": pending_post_battle_choice_scenario_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _copy_dictionary(value) -> Dictionary:
|
||||||
|
if typeof(value) == TYPE_DICTIONARY:
|
||||||
|
return value.duplicate(true)
|
||||||
|
return {}
|
||||||
234
scripts/core/data_catalog.gd
Normal file
234
scripts/core/data_catalog.gd
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name DataCatalog
|
||||||
|
|
||||||
|
const OFFICERS_PATH := "res://data/defs/officers.json"
|
||||||
|
const CLASSES_PATH := "res://data/defs/classes.json"
|
||||||
|
const TERRAIN_PATH := "res://data/defs/terrain.json"
|
||||||
|
const ITEMS_PATH := "res://data/defs/items.json"
|
||||||
|
const SKILLS_PATH := "res://data/defs/skills.json"
|
||||||
|
|
||||||
|
var officers: Dictionary = {}
|
||||||
|
var classes: Dictionary = {}
|
||||||
|
var terrain: Dictionary = {}
|
||||||
|
var items: Dictionary = {}
|
||||||
|
var skills: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func load_defaults() -> bool:
|
||||||
|
officers = _load_json_object(OFFICERS_PATH)
|
||||||
|
classes = _load_json_object(CLASSES_PATH)
|
||||||
|
terrain = _load_json_object(TERRAIN_PATH)
|
||||||
|
items = _load_json_object(ITEMS_PATH)
|
||||||
|
skills = _load_json_object(SKILLS_PATH)
|
||||||
|
return not officers.is_empty() and not classes.is_empty() and not terrain.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func hydrate_deployment(deployment: Dictionary) -> Dictionary:
|
||||||
|
var officer := _get_dict(officers, deployment.get("officer_id", ""))
|
||||||
|
var class_id := str(deployment.get("class_id", officer.get("class_id", "infantry")))
|
||||||
|
var class_def := _get_dict(classes, class_id)
|
||||||
|
var equipment := _merged_equipment(officer.get("equipment", {}), deployment.get("equipment", {}))
|
||||||
|
var range_pair := _resolve_attack_range(class_def, equipment)
|
||||||
|
|
||||||
|
var stats := {}
|
||||||
|
_overlay_stats(stats, officer.get("base", {}))
|
||||||
|
_overlay_stats(stats, deployment.get("base", {}))
|
||||||
|
_add_stats(stats, class_def.get("base_bonus", {}))
|
||||||
|
_add_equipment_bonuses(stats, equipment)
|
||||||
|
|
||||||
|
var unit := {}
|
||||||
|
unit["id"] = str(deployment.get("unit_id", deployment.get("id", deployment.get("officer_id", ""))))
|
||||||
|
unit["officer_id"] = str(deployment.get("officer_id", ""))
|
||||||
|
unit["name"] = str(deployment.get("name", officer.get("name", unit["id"])))
|
||||||
|
unit["class_id"] = class_id
|
||||||
|
unit["class"] = str(class_def.get("name", class_id.capitalize()))
|
||||||
|
unit["team"] = str(deployment.get("team", "enemy"))
|
||||||
|
unit["pos"] = deployment.get("pos", [0, 0])
|
||||||
|
unit["requires_joined"] = bool(deployment.get("requires_joined", false))
|
||||||
|
unit["controllable"] = bool(deployment.get("controllable", true))
|
||||||
|
unit["persist_progression"] = bool(deployment.get("persist_progression", true))
|
||||||
|
unit["ai_target_priority"] = int(deployment.get("ai_target_priority", 0))
|
||||||
|
unit["level"] = int(deployment.get("level", officer.get("level", 1)))
|
||||||
|
unit["exp"] = int(deployment.get("exp", officer.get("exp", 0)))
|
||||||
|
unit["max_hp"] = int(stats.get("hp", stats.get("max_hp", 1)))
|
||||||
|
unit["hp"] = int(deployment.get("hp", unit["max_hp"]))
|
||||||
|
unit["max_mp"] = int(stats.get("mp", 0))
|
||||||
|
unit["mp"] = int(deployment.get("mp", unit["max_mp"]))
|
||||||
|
unit["atk"] = int(stats.get("atk", 1))
|
||||||
|
unit["def"] = int(stats.get("def", 0))
|
||||||
|
unit["int"] = int(stats.get("int", 0))
|
||||||
|
unit["agi"] = int(stats.get("agi", 0))
|
||||||
|
unit["move"] = int(deployment.get("move", class_def.get("move", 3)))
|
||||||
|
unit["move_type"] = str(deployment.get("move_type", class_def.get("move_type", "foot")))
|
||||||
|
if deployment.has("range"):
|
||||||
|
range_pair = _range_pair_from_value(deployment["range"])
|
||||||
|
unit["min_range"] = int(deployment.get("min_range", range_pair["min"]))
|
||||||
|
unit["range"] = int(range_pair["max"])
|
||||||
|
unit["growth"] = class_def.get("growth", {}).duplicate(true)
|
||||||
|
unit["growth_bonus"] = officer.get("growth_bonus", {}).duplicate(true)
|
||||||
|
if deployment.has("growth_bonus") and typeof(deployment["growth_bonus"]) == TYPE_DICTIONARY:
|
||||||
|
for stat in deployment["growth_bonus"].keys():
|
||||||
|
unit["growth_bonus"][stat] = deployment["growth_bonus"][stat]
|
||||||
|
unit["equipment"] = equipment
|
||||||
|
unit["skills"] = _merged_skill_list(class_def.get("skills", []), officer.get("skills", []), deployment.get("skills", []))
|
||||||
|
return unit
|
||||||
|
|
||||||
|
|
||||||
|
func get_runtime_terrain_defs(default_defs: Dictionary) -> Dictionary:
|
||||||
|
var result := default_defs.duplicate(true)
|
||||||
|
for key in terrain.keys():
|
||||||
|
var source := _get_dict(terrain, key)
|
||||||
|
var fallback: Dictionary = result.get(key, {})
|
||||||
|
var runtime := fallback.duplicate(true)
|
||||||
|
runtime["id"] = str(source.get("id", runtime.get("id", key)))
|
||||||
|
runtime["name"] = str(source.get("name", runtime.get("name", key)))
|
||||||
|
runtime["defense"] = int(source.get("defense", runtime.get("defense", 0)))
|
||||||
|
runtime["avoid"] = int(source.get("avoid", runtime.get("avoid", 0)))
|
||||||
|
runtime["move_cost"] = source.get("move_cost", runtime.get("move_cost", 1))
|
||||||
|
runtime["color"] = _parse_color(source.get("color", runtime.get("color", Color.WHITE)), runtime.get("color", Color.WHITE))
|
||||||
|
result[key] = runtime
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func get_skill(skill_id: String) -> Dictionary:
|
||||||
|
return _get_dict(skills, skill_id).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_item(item_id: String) -> Dictionary:
|
||||||
|
return _get_dict(items, item_id).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_item_ids() -> Array:
|
||||||
|
var result := []
|
||||||
|
for item_id in items.keys():
|
||||||
|
result.append(str(item_id))
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func get_class_def(class_id: String) -> Dictionary:
|
||||||
|
return _get_dict(classes, class_id).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_json_object(path: String) -> Dictionary:
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
push_error("Missing data file: %s" % path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var text := FileAccess.get_file_as_string(path)
|
||||||
|
var parsed = JSON.parse_string(text)
|
||||||
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
|
push_error("Data file is not a JSON object: %s" % path)
|
||||||
|
return {}
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
func _get_dict(source: Dictionary, key) -> Dictionary:
|
||||||
|
if key == null:
|
||||||
|
return {}
|
||||||
|
var value = source.get(str(key), {})
|
||||||
|
if typeof(value) == TYPE_DICTIONARY:
|
||||||
|
return value
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _merged_equipment(base_equipment, override_equipment) -> Dictionary:
|
||||||
|
var result := {}
|
||||||
|
if typeof(base_equipment) == TYPE_DICTIONARY:
|
||||||
|
result = base_equipment.duplicate(true)
|
||||||
|
if typeof(override_equipment) == TYPE_DICTIONARY:
|
||||||
|
for slot in override_equipment.keys():
|
||||||
|
result[slot] = override_equipment[slot]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _overlay_stats(target: Dictionary, source) -> void:
|
||||||
|
if typeof(source) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
for key in source.keys():
|
||||||
|
target[key] = int(source[key])
|
||||||
|
|
||||||
|
|
||||||
|
func _add_stats(target: Dictionary, source) -> void:
|
||||||
|
if typeof(source) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
for key in source.keys():
|
||||||
|
target[key] = int(target.get(key, 0)) + int(source[key])
|
||||||
|
|
||||||
|
|
||||||
|
func _add_equipment_bonuses(target: Dictionary, equipment: Dictionary) -> void:
|
||||||
|
for slot in equipment.keys():
|
||||||
|
var item_id = equipment[slot]
|
||||||
|
if item_id == null:
|
||||||
|
continue
|
||||||
|
var item := _get_dict(items, item_id)
|
||||||
|
_add_stats(target, item.get("bonuses", {}))
|
||||||
|
|
||||||
|
|
||||||
|
func _merged_skill_list(class_skills, officer_skills, deployment_skills) -> Array:
|
||||||
|
var result := []
|
||||||
|
_append_unique_skills(result, class_skills)
|
||||||
|
_append_unique_skills(result, officer_skills)
|
||||||
|
_append_unique_skills(result, deployment_skills)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _append_unique_skills(target: Array, source) -> void:
|
||||||
|
if typeof(source) != TYPE_ARRAY:
|
||||||
|
return
|
||||||
|
for skill_id in source:
|
||||||
|
var normalized := str(skill_id)
|
||||||
|
if normalized.is_empty() or target.has(normalized):
|
||||||
|
continue
|
||||||
|
target.append(normalized)
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_attack_range(class_def: Dictionary, equipment: Dictionary) -> Dictionary:
|
||||||
|
var attack_range := _range_pair_from_value(class_def.get("attack_range", 1))
|
||||||
|
for slot in equipment.keys():
|
||||||
|
var item_id = equipment[slot]
|
||||||
|
if item_id == null:
|
||||||
|
continue
|
||||||
|
var item := _get_dict(items, item_id)
|
||||||
|
if item.has("range"):
|
||||||
|
var item_range := _range_pair_from_value(item["range"])
|
||||||
|
attack_range["min"] = min(int(attack_range["min"]), int(item_range["min"]))
|
||||||
|
attack_range["max"] = max(int(attack_range["max"]), int(item_range["max"]))
|
||||||
|
return attack_range
|
||||||
|
|
||||||
|
|
||||||
|
func _range_pair_from_value(value) -> Dictionary:
|
||||||
|
if typeof(value) == TYPE_ARRAY:
|
||||||
|
if value.size() >= 2:
|
||||||
|
return _normalized_range_pair(int(value[0]), int(value[1]))
|
||||||
|
if value.size() == 1:
|
||||||
|
return _normalized_range_pair(int(value[0]), int(value[0]))
|
||||||
|
return _normalized_range_pair(1, 1)
|
||||||
|
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
|
||||||
|
return _normalized_range_pair(1, 1)
|
||||||
|
var scalar := max(1, int(value))
|
||||||
|
return _normalized_range_pair(scalar, scalar)
|
||||||
|
|
||||||
|
|
||||||
|
func _normalized_range_pair(min_range: int, max_range: int) -> Dictionary:
|
||||||
|
var safe_min := max(1, min_range)
|
||||||
|
var safe_max := max(safe_min, max_range)
|
||||||
|
return {
|
||||||
|
"min": safe_min,
|
||||||
|
"max": safe_max
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_color(value, fallback: Color) -> Color:
|
||||||
|
if typeof(value) == TYPE_COLOR:
|
||||||
|
return value
|
||||||
|
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
|
||||||
|
var alpha := 1.0
|
||||||
|
if value.size() >= 4:
|
||||||
|
alpha = float(value[3])
|
||||||
|
return Color(float(value[0]), float(value[1]), float(value[2]), alpha)
|
||||||
|
|
||||||
|
var text := str(value)
|
||||||
|
if Color.html_is_valid(text):
|
||||||
|
return Color.html(text)
|
||||||
|
return fallback
|
||||||
2360
scripts/scenes/battle_scene.gd
Normal file
2360
scripts/scenes/battle_scene.gd
Normal file
File diff suppressed because it is too large
Load Diff
153
tools/check_godot46_readiness.ps1
Normal file
153
tools/check_godot46_readiness.ps1
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
param(
|
||||||
|
[switch]$RunGodotImport
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$Root = Split-Path -Parent $PSScriptRoot
|
||||||
|
$Failures = New-Object System.Collections.Generic.List[string]
|
||||||
|
$Warnings = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
function Add-Failure([string]$Message) {
|
||||||
|
$Failures.Add($Message) | Out-Null
|
||||||
|
Write-Host "[FAIL] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Warning([string]$Message) {
|
||||||
|
$Warnings.Add($Message) | Out-Null
|
||||||
|
Write-Host "[WARN] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Pass([string]$Message) {
|
||||||
|
Write-Host "[ OK ] $Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-Godot {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($env:GODOT_BIN) -and (Test-Path $env:GODOT_BIN)) {
|
||||||
|
return (Resolve-Path $env:GODOT_BIN).Path
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-Command godot -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $command) {
|
||||||
|
return $command.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
$commonCandidates = @(
|
||||||
|
(Join-Path $env:USERPROFILE "Downloads\Godot_v4.6-stable_win64.exe"),
|
||||||
|
(Join-Path $env:USERPROFILE "Desktop\Godot_v4.6-stable_win64.exe"),
|
||||||
|
(Join-Path $env:LOCALAPPDATA "Programs\Godot\Godot_v4.6-stable_win64.exe"),
|
||||||
|
"C:\Program Files\Godot\Godot_v4.6-stable_win64.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidate in $commonCandidates) {
|
||||||
|
if (Test-Path $candidate) {
|
||||||
|
return (Resolve-Path $candidate).Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $Root
|
||||||
|
try {
|
||||||
|
$validateScript = Join-Path $PSScriptRoot "validate_data.ps1"
|
||||||
|
if (Test-Path $validateScript) {
|
||||||
|
try {
|
||||||
|
& $validateScript | Out-Host
|
||||||
|
Add-Pass "data validation script passed"
|
||||||
|
} catch {
|
||||||
|
Add-Failure "data validation failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Add-Failure "missing tools\validate_data.ps1"
|
||||||
|
}
|
||||||
|
|
||||||
|
$jsonFiles = Get-ChildItem -Path (Join-Path $Root "data") -Filter "*.json" -Recurse
|
||||||
|
foreach ($file in $jsonFiles) {
|
||||||
|
try {
|
||||||
|
Get-Content $file.FullName -Raw | ConvertFrom-Json | Out-Null
|
||||||
|
} catch {
|
||||||
|
Add-Failure "invalid JSON: $($file.FullName) - $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($Failures.Count -eq 0) {
|
||||||
|
Add-Pass "all data JSON files parsed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectPath = Join-Path $Root "project.godot"
|
||||||
|
if (Test-Path $projectPath) {
|
||||||
|
$projectText = Get-Content $projectPath -Raw
|
||||||
|
if ($projectText -match 'config/features=PackedStringArray\("([^"]+)"\)') {
|
||||||
|
$featureVersion = $Matches[1]
|
||||||
|
if ($featureVersion -ne "4.6") {
|
||||||
|
Add-Warning "project feature tag is $featureVersion; expect Godot 4.6 editor migration to update it"
|
||||||
|
} else {
|
||||||
|
Add-Pass "project feature tag is 4.6"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Add-Warning "project feature tag was not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($projectText -match 'renderer/rendering_method="gl_compatibility"') {
|
||||||
|
Add-Pass "project keeps the compatibility renderer explicit"
|
||||||
|
} else {
|
||||||
|
Add-Warning "rendering method is not explicitly gl_compatibility"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Add-Failure "missing project.godot"
|
||||||
|
}
|
||||||
|
|
||||||
|
$riskPatterns = [ordered]@{
|
||||||
|
"Godot 3 yield syntax" = '\byield\s*\('
|
||||||
|
"Godot 3 File.new API" = '\bFile\.new\s*\('
|
||||||
|
"Godot 3 Directory.new API" = '\bDirectory\.new\s*\('
|
||||||
|
"Godot 3 Pool*Array type" = '\bPool[A-Za-z]+Array\b'
|
||||||
|
"Godot 3 KinematicBody node" = '\bKinematicBody(2D|3D)?\b'
|
||||||
|
"legacy TileMap set_cellv call" = '\bset_cellv\s*\('
|
||||||
|
}
|
||||||
|
|
||||||
|
$gdFiles = Get-ChildItem -Path (Join-Path $Root "scripts") -Filter "*.gd" -Recurse
|
||||||
|
foreach ($entry in $riskPatterns.GetEnumerator()) {
|
||||||
|
$matches = @()
|
||||||
|
foreach ($file in $gdFiles) {
|
||||||
|
$fileMatches = Select-String -Path $file.FullName -Pattern $entry.Value -AllMatches
|
||||||
|
if ($null -ne $fileMatches) {
|
||||||
|
$matches += $fileMatches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($matches.Count -gt 0) {
|
||||||
|
Add-Warning "$($entry.Key) found in $($matches.Count) location(s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Add-Pass "static Godot 3 API risk scan completed"
|
||||||
|
|
||||||
|
$godotBin = Find-Godot
|
||||||
|
if ([string]::IsNullOrWhiteSpace($godotBin)) {
|
||||||
|
Add-Warning "Godot executable not found. Set GODOT_BIN or add godot to PATH for engine checks."
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$version = & $godotBin --version
|
||||||
|
Add-Pass "Godot executable found: $godotBin ($version)"
|
||||||
|
if ($version -notmatch '^4\.6') {
|
||||||
|
Add-Warning "detected Godot version is not 4.6: $version"
|
||||||
|
}
|
||||||
|
if ($RunGodotImport) {
|
||||||
|
& $godotBin --headless --path $Root --quit
|
||||||
|
Add-Pass "Godot headless project open completed"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Add-Failure "Godot command failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Summary: $($Failures.Count) failure(s), $($Warnings.Count) warning(s)"
|
||||||
|
|
||||||
|
if ($Failures.Count -gt 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
1334
tools/validate_data.ps1
Normal file
1334
tools/validate_data.ps1
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user