Campaign 3

This commit is contained in:
Brian Fertig 2026-03-17 14:22:15 -06:00
parent eb62ba972d
commit 0f1db0884c
34 changed files with 852 additions and 30 deletions

View File

@ -7,7 +7,10 @@
"Bash(ls:*)", "Bash(ls:*)",
"Bash(cd:*)", "Bash(cd:*)",
"Bash(python3:*)", "Bash(python3:*)",
"Bash(head:*)" "Bash(head:*)",
"Bash(jq:*)",
"Bash(identify /home/brianfertig/git/tyrants-edge/assets/images/cards/imp_cmd_1.png)",
"Bash(identify /home/brianfertig/git/tyrants-edge/assets/images/ui/imperial_01.png)"
] ]
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

View File

@ -38,9 +38,13 @@
"level": 3, "level": 3,
"requiredLevel": 3, "requiredLevel": 3,
"unlockCondition": "campaign_bloodthirsty", "unlockCondition": "campaign_bloodthirsty",
"missions": [], "missions": ["mission_014", "mission_015", "mission_016", "mission_017", "mission_018", "mission_019", "mission_020", "mission_021"],
"starterReward": { "starterReward": {
"cards": [] "cards": [
"xeno_drone_1", "xeno_drone_1", "xeno_drone_1",
"xeno_spitter_1", "xeno_spitter_1", "xeno_spitter_1",
"xeno_beetle_1", "xeno_beetle_1", "xeno_beetle_1"
]
} }
}, },
{ {

View File

@ -2841,5 +2841,330 @@
], ],
"flavorText": "The tide rises. The tide claims all.", "flavorText": "The tide rises. The tide claims all.",
"artKey": "bt_archon_1" "artKey": "bt_archon_1"
},
{
"id": "xeno_cmd_1",
"name": "Hatchery Keeper",
"faction": "xeno",
"type": "commander",
"rarity": "legendary",
"levels": [
{ "attack": 4, "health": 55, "armor": 2, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }] },
{ "attack": 6, "health": 83, "armor": 3, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }] },
{ "attack": 9, "health": 124, "armor": 4, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }] },
{ "attack": 14, "health": 186, "armor": 6, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }] },
{ "attack": 20, "health": 279, "armor": 9, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }] }
],
"flavorText": "Every egg she lays is another mouth to feed on your flesh.",
"artKey": "xeno_cmd_1"
},
{
"id": "xeno_cmd_2",
"name": "War Drone Alpha",
"faction": "xeno",
"type": "commander",
"rarity": "legendary",
"levels": [
{ "attack": 6, "health": 50, "armor": 2, "delay": 0, "skills": [{ "name": "rally", "value": 1, "trigger": "preBattle", "all": true }] },
{ "attack": 9, "health": 75, "armor": 3, "delay": 0, "skills": [{ "name": "rally", "value": 2, "trigger": "preBattle", "all": true }] },
{ "attack": 14, "health": 113, "armor": 5, "delay": 0, "skills": [{ "name": "rally", "value": 3, "trigger": "preBattle", "all": true }] },
{ "attack": 20, "health": 169, "armor": 7, "delay": 0, "skills": [{ "name": "rally", "value": 5, "trigger": "preBattle", "all": true }] },
{ "attack": 30, "health": 253, "armor": 10, "delay": 0, "skills": [{ "name": "rally", "value": 7, "trigger": "preBattle", "all": true }] }
],
"flavorText": "The Alpha does not lead. It commands through shared fury.",
"artKey": "xeno_cmd_2"
},
{
"id": "xeno_cmd_3",
"name": "The Hive Mind",
"faction": "xeno",
"type": "commander",
"rarity": "legendary",
"levels": [
{ "attack": 5, "health": 60, "armor": 3, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }, { "name": "enfeeble", "value": 1, "trigger": "preBattle", "all": true }] },
{ "attack": 8, "health": 90, "armor": 5, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }, { "name": "enfeeble", "value": 2, "trigger": "preBattle", "all": true }] },
{ "attack": 11, "health": 135, "armor": 7, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }, { "name": "enfeeble", "value": 3, "trigger": "preBattle", "all": true }] },
{ "attack": 17, "health": 203, "armor": 10, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }, { "name": "enfeeble", "value": 5, "trigger": "preBattle", "all": true }] },
{ "attack": 25, "health": 304, "armor": 15, "delay": 0, "skills": [{ "name": "spawn", "value": 1, "trigger": "on_turn_start" }, { "name": "enfeeble", "value": 7, "trigger": "preBattle", "all": true }] }
],
"flavorText": "One mind. A billion claws. It has already seen what you will do next.",
"artKey": "xeno_cmd_3"
},
{
"id": "xeno_drone_token",
"name": "Xeno Drone",
"faction": "xeno",
"type": "token",
"rarity": "common",
"levels": [
{ "attack": 2, "health": 5, "armor": 0, "delay": 0, "skills": [] },
{ "attack": 3, "health": 8, "armor": 0, "delay": 0, "skills": [] },
{ "attack": 4, "health": 12, "armor": 0, "delay": 0, "skills": [] },
{ "attack": 6, "health": 18, "armor": 0, "delay": 0, "skills": [] },
{ "attack": 9, "health": 27, "armor": 0, "delay": 0, "skills": [] }
],
"flavorText": "Born to die. Born to swarm.",
"artKey": "xeno_drone_token"
},
{
"id": "xeno_drone_1",
"name": "Chitin Drone",
"faction": "xeno",
"type": "assault",
"rarity": "common",
"levels": [
{ "attack": 3, "health": 14, "armor": 1, "delay": 1, "skills": [{ "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 5, "health": 21, "armor": 2, "delay": 1, "skills": [{ "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 7, "health": 32, "armor": 2, "delay": 1, "skills": [{ "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 10, "health": 48, "armor": 3, "delay": 1, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }] },
{ "attack": 15, "health": 71, "armor": 5, "delay": 1, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }] }
],
"flavorText": "Each wound seals with a harder shell.",
"artKey": "xeno_drone_1"
},
{
"id": "xeno_spitter_1",
"name": "Spitter Larva",
"faction": "xeno",
"type": "assault",
"rarity": "common",
"levels": [
{ "attack": 2, "health": 12, "armor": 0, "delay": 1, "skills": [{ "name": "venom", "value": 2, "trigger": "on_attack" }] },
{ "attack": 3, "health": 18, "armor": 0, "delay": 1, "skills": [{ "name": "venom", "value": 3, "trigger": "on_attack" }] },
{ "attack": 5, "health": 27, "armor": 1, "delay": 1, "skills": [{ "name": "venom", "value": 4, "trigger": "on_attack" }] },
{ "attack": 7, "health": 41, "armor": 1, "delay": 1, "skills": [{ "name": "venom", "value": 6, "trigger": "on_attack" }] },
{ "attack": 10, "health": 61, "armor": 2, "delay": 1, "skills": [{ "name": "venom", "value": 8, "trigger": "on_attack" }] }
],
"flavorText": "The burning starts slow. It never stops.",
"artKey": "xeno_spitter_1"
},
{
"id": "xeno_beetle_1",
"name": "Tunnel Beetle",
"faction": "xeno",
"type": "assault",
"rarity": "common",
"levels": [
{ "attack": 4, "health": 11, "armor": 0, "delay": 1, "skills": [{ "name": "burrow", "value": 2, "trigger": "passive" }] },
{ "attack": 6, "health": 17, "armor": 0, "delay": 1, "skills": [{ "name": "burrow", "value": 2, "trigger": "passive" }] },
{ "attack": 9, "health": 25, "armor": 1, "delay": 1, "skills": [{ "name": "burrow", "value": 2, "trigger": "passive" }] },
{ "attack": 14, "health": 38, "armor": 1, "delay": 1, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }] },
{ "attack": 20, "health": 56, "armor": 2, "delay": 1, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }] }
],
"flavorText": "You can't target what you can't see.",
"artKey": "xeno_beetle_1"
},
{
"id": "xeno_worker_1",
"name": "Hive Worker",
"faction": "xeno",
"type": "assault",
"rarity": "common",
"levels": [
{ "attack": 3, "health": 13, "armor": 2, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 5, "health": 20, "armor": 3, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 7, "health": 29, "armor": 4, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 10, "health": 44, "armor": 6, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 15, "health": 66, "armor": 9, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }] }
],
"flavorText": "Shed the old. Grow something meaner.",
"artKey": "xeno_worker_1"
},
{
"id": "xeno_guard_1",
"name": "Swarm Guard",
"faction": "xeno",
"type": "assault",
"rarity": "rare",
"levels": [
{ "attack": 3, "health": 18, "armor": 3, "delay": 1, "skills": [{ "name": "counter", "value": 2, "trigger": "on_defend" }, { "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 5, "health": 27, "armor": 5, "delay": 1, "skills": [{ "name": "counter", "value": 3, "trigger": "on_defend" }, { "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 7, "health": 41, "armor": 7, "delay": 1, "skills": [{ "name": "counter", "value": 5, "trigger": "on_defend" }, { "name": "carapace", "value": 1, "trigger": "on_defend" }] },
{ "attack": 10, "health": 61, "armor": 10, "delay": 1, "skills": [{ "name": "counter", "value": 7, "trigger": "on_defend" }, { "name": "carapace", "value": 2, "trigger": "on_defend" }] },
{ "attack": 15, "health": 91, "armor": 15, "delay": 1, "skills": [{ "name": "counter", "value": 10, "trigger": "on_defend" }, { "name": "carapace", "value": 2, "trigger": "on_defend" }] }
],
"flavorText": "Strike the hive, and the hive strikes back harder.",
"artKey": "xeno_guard_1"
},
{
"id": "xeno_mantis_1",
"name": "Acid Mantis",
"faction": "xeno",
"type": "assault",
"rarity": "rare",
"levels": [
{ "attack": 5, "health": 13, "armor": 1, "delay": 1, "skills": [{ "name": "venom", "value": 2, "trigger": "on_attack" }, { "name": "strike", "value": 1, "trigger": "preAttack" }] },
{ "attack": 8, "health": 20, "armor": 2, "delay": 1, "skills": [{ "name": "venom", "value": 3, "trigger": "on_attack" }, { "name": "strike", "value": 2, "trigger": "preAttack" }] },
{ "attack": 11, "health": 29, "armor": 2, "delay": 1, "skills": [{ "name": "venom", "value": 4, "trigger": "on_attack" }, { "name": "strike", "value": 2, "trigger": "preAttack" }] },
{ "attack": 17, "health": 44, "armor": 3, "delay": 1, "skills": [{ "name": "venom", "value": 6, "trigger": "on_attack" }, { "name": "strike", "value": 3, "trigger": "preAttack" }] },
{ "attack": 25, "health": 66, "armor": 5, "delay": 1, "skills": [{ "name": "venom", "value": 8, "trigger": "on_attack" }, { "name": "strike", "value": 5, "trigger": "preAttack" }] }
],
"flavorText": "Its spit dissolves armor. Its claws dissolve everything else.",
"artKey": "xeno_mantis_1"
},
{
"id": "xeno_horror_1",
"name": "Burrowing Horror",
"faction": "xeno",
"type": "assault",
"rarity": "rare",
"levels": [
{ "attack": 6, "health": 14, "armor": 2, "delay": 2, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "swarm", "value": 1, "trigger": "preBattle" }] },
{ "attack": 9, "health": 21, "armor": 3, "delay": 2, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "swarm", "value": 2, "trigger": "preBattle" }] },
{ "attack": 14, "health": 32, "armor": 5, "delay": 2, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "swarm", "value": 3, "trigger": "preBattle" }] },
{ "attack": 20, "health": 47, "armor": 7, "delay": 2, "skills": [{ "name": "burrow", "value": 4, "trigger": "passive" }, { "name": "swarm", "value": 5, "trigger": "preBattle" }] },
{ "attack": 30, "health": 71, "armor": 10, "delay": 2, "skills": [{ "name": "burrow", "value": 4, "trigger": "passive" }, { "name": "swarm", "value": 7, "trigger": "preBattle" }] }
],
"flavorText": "The ground shook three times. Then nothing moved above it.",
"artKey": "xeno_horror_1"
},
{
"id": "xeno_msentinel_1",
"name": "Molt Sentinel",
"faction": "xeno",
"type": "assault",
"rarity": "rare",
"levels": [
{ "attack": 2, "health": 20, "armor": 3, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "protect", "value": 2, "trigger": "preBattle" }] },
{ "attack": 3, "health": 30, "armor": 5, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "protect", "value": 3, "trigger": "preBattle" }] },
{ "attack": 5, "health": 45, "armor": 7, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "protect", "value": 5, "trigger": "preBattle" }] },
{ "attack": 7, "health": 68, "armor": 10, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "protect", "value": 7, "trigger": "preBattle" }] },
{ "attack": 10, "health": 101, "armor": 15, "delay": 1, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "protect", "value": 10, "trigger": "preBattle" }] }
],
"flavorText": "It sheds its skin to shield the swarm.",
"artKey": "xeno_msentinel_1"
},
{
"id": "xeno_parasite_1",
"name": "Brood Parasite",
"faction": "xeno",
"type": "assault",
"rarity": "rare",
"levels": [
{ "attack": 4, "health": 12, "armor": 1, "delay": 2, "skills": [{ "name": "enfeeble", "value": 2, "trigger": "preBattle" }, { "name": "venom", "value": 1, "trigger": "on_attack" }] },
{ "attack": 6, "health": 18, "armor": 2, "delay": 2, "skills": [{ "name": "enfeeble", "value": 3, "trigger": "preBattle" }, { "name": "venom", "value": 2, "trigger": "on_attack" }] },
{ "attack": 9, "health": 27, "armor": 3, "delay": 2, "skills": [{ "name": "enfeeble", "value": 5, "trigger": "preBattle" }, { "name": "venom", "value": 2, "trigger": "on_attack" }] },
{ "attack": 14, "health": 41, "armor": 5, "delay": 2, "skills": [{ "name": "enfeeble", "value": 7, "trigger": "preBattle" }, { "name": "venom", "value": 3, "trigger": "on_attack" }] },
{ "attack": 20, "health": 61, "armor": 7, "delay": 2, "skills": [{ "name": "enfeeble", "value": 10, "trigger": "preBattle" }, { "name": "venom", "value": 5, "trigger": "on_attack" }] }
],
"flavorText": "It crawls inside. You stop fighting. Then you start fighting for them.",
"artKey": "xeno_parasite_1"
},
{
"id": "xeno_hiveguard_1",
"name": "Hive Guardian",
"faction": "xeno",
"type": "assault",
"rarity": "epic",
"levels": [
{ "attack": 6, "health": 18, "armor": 3, "delay": 2, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "armored", "value": 2, "trigger": "passive" }] },
{ "attack": 9, "health": 27, "armor": 5, "delay": 2, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "armored", "value": 3, "trigger": "passive" }] },
{ "attack": 14, "health": 41, "armor": 7, "delay": 2, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "armored", "value": 5, "trigger": "passive" }] },
{ "attack": 20, "health": 61, "armor": 10, "delay": 2, "skills": [{ "name": "carapace", "value": 3, "trigger": "on_defend" }, { "name": "armored", "value": 7, "trigger": "passive" }] },
{ "attack": 30, "health": 91, "armor": 15, "delay": 2, "skills": [{ "name": "carapace", "value": 3, "trigger": "on_defend" }, { "name": "armored", "value": 10, "trigger": "passive" }] }
],
"flavorText": "Blades shatter. Shells harden. The hive remembers every wound.",
"artKey": "xeno_hiveguard_1"
},
{
"id": "xeno_wyrm_1",
"name": "Venom Wyrm",
"faction": "xeno",
"type": "assault",
"rarity": "epic",
"levels": [
{ "attack": 6, "health": 16, "armor": 2, "delay": 2, "skills": [{ "name": "venom", "value": 3, "trigger": "on_attack", "all": true }] },
{ "attack": 9, "health": 24, "armor": 3, "delay": 2, "skills": [{ "name": "venom", "value": 4, "trigger": "on_attack", "all": true }] },
{ "attack": 14, "health": 36, "armor": 5, "delay": 2, "skills": [{ "name": "venom", "value": 6, "trigger": "on_attack", "all": true }] },
{ "attack": 20, "health": 54, "armor": 7, "delay": 2, "skills": [{ "name": "venom", "value": 8, "trigger": "on_attack", "all": true }] },
{ "attack": 30, "health": 81, "armor": 10, "delay": 2, "skills": [{ "name": "venom", "value": 12, "trigger": "on_attack", "all": true }] }
],
"flavorText": "A single drop paralyzes. A stream dissolves.",
"artKey": "xeno_wyrm_1"
},
{
"id": "xeno_tyrant_1",
"name": "Swarm Tyrant",
"faction": "xeno",
"type": "assault",
"rarity": "epic",
"levels": [
{ "attack": 7, "health": 16, "armor": 2, "delay": 2, "skills": [{ "name": "swarm", "value": 2, "trigger": "preBattle" }, { "name": "rally", "value": 2, "trigger": "preBattle" }] },
{ "attack": 11, "health": 24, "armor": 3, "delay": 2, "skills": [{ "name": "swarm", "value": 3, "trigger": "preBattle" }, { "name": "rally", "value": 3, "trigger": "preBattle" }] },
{ "attack": 16, "health": 36, "armor": 5, "delay": 2, "skills": [{ "name": "swarm", "value": 5, "trigger": "preBattle" }, { "name": "rally", "value": 5, "trigger": "preBattle" }] },
{ "attack": 24, "health": 54, "armor": 7, "delay": 2, "skills": [{ "name": "swarm", "value": 7, "trigger": "preBattle" }, { "name": "rally", "value": 7, "trigger": "preBattle" }] },
{ "attack": 35, "health": 81, "armor": 10, "delay": 2, "skills": [{ "name": "swarm", "value": 10, "trigger": "preBattle" }, { "name": "rally", "value": 10, "trigger": "preBattle" }] }
],
"flavorText": "Where one xeno stands, a thousand follow.",
"artKey": "xeno_tyrant_1"
},
{
"id": "xeno_queen_1",
"name": "Burrow Queen",
"faction": "xeno",
"type": "assault",
"rarity": "epic",
"levels": [
{ "attack": 6, "health": 20, "armor": 3, "delay": 3, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "hive_link", "value": 2, "trigger": "on_death" }] },
{ "attack": 9, "health": 30, "armor": 5, "delay": 3, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "hive_link", "value": 3, "trigger": "on_death" }] },
{ "attack": 14, "health": 45, "armor": 7, "delay": 3, "skills": [{ "name": "burrow", "value": 3, "trigger": "passive" }, { "name": "hive_link", "value": 5, "trigger": "on_death" }] },
{ "attack": 20, "health": 68, "armor": 10, "delay": 3, "skills": [{ "name": "burrow", "value": 4, "trigger": "passive" }, { "name": "hive_link", "value": 7, "trigger": "on_death" }] },
{ "attack": 30, "health": 101, "armor": 15, "delay": 3, "skills": [{ "name": "burrow", "value": 4, "trigger": "passive" }, { "name": "hive_link", "value": 10, "trigger": "on_death" }] }
],
"flavorText": "Kill the queen and the swarm grows angrier.",
"artKey": "xeno_queen_1"
},
{
"id": "xeno_colossus_1",
"name": "Chitin Colossus",
"faction": "xeno",
"type": "assault",
"rarity": "epic",
"levels": [
{ "attack": 7, "health": 18, "armor": 3, "delay": 3, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "swipe", "value": 2, "trigger": "preAttack" }] },
{ "attack": 11, "health": 27, "armor": 5, "delay": 3, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "swipe", "value": 3, "trigger": "preAttack" }] },
{ "attack": 16, "health": 41, "armor": 7, "delay": 3, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "swipe", "value": 5, "trigger": "preAttack" }] },
{ "attack": 24, "health": 61, "armor": 10, "delay": 3, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "swipe", "value": 7, "trigger": "preAttack" }] },
{ "attack": 35, "health": 91, "armor": 15, "delay": 3, "skills": [{ "name": "molt", "trigger": "on_turn_start" }, { "name": "swipe", "value": 10, "trigger": "preAttack" }] }
],
"flavorText": "It sheds its skin, and the ground splits beneath its weight.",
"artKey": "xeno_colossus_1"
},
{
"id": "xeno_apex_1",
"name": "Apex Predator",
"faction": "xeno",
"type": "assault",
"rarity": "legendary",
"levels": [
{ "attack": 8, "health": 22, "armor": 3, "delay": 3, "skills": [{ "name": "venom", "value": 3, "trigger": "on_attack" }, { "name": "flurry", "value": 1, "trigger": "passive" }, { "name": "hive_link", "value": 3, "trigger": "on_death" }] },
{ "attack": 12, "health": 33, "armor": 5, "delay": 3, "skills": [{ "name": "venom", "value": 4, "trigger": "on_attack" }, { "name": "flurry", "value": 1, "trigger": "passive" }, { "name": "hive_link", "value": 5, "trigger": "on_death" }] },
{ "attack": 18, "health": 50, "armor": 7, "delay": 3, "skills": [{ "name": "venom", "value": 6, "trigger": "on_attack" }, { "name": "flurry", "value": 1, "trigger": "passive" }, { "name": "hive_link", "value": 7, "trigger": "on_death" }] },
{ "attack": 27, "health": 74, "armor": 10, "delay": 3, "skills": [{ "name": "venom", "value": 8, "trigger": "on_attack" }, { "name": "flurry", "value": 1, "trigger": "passive" }, { "name": "hive_link", "value": 10, "trigger": "on_death" }] },
{ "attack": 41, "health": 111, "armor": 15, "delay": 3, "skills": [{ "name": "venom", "value": 12, "trigger": "on_attack" }, { "name": "flurry", "value": 1, "trigger": "passive" }, { "name": "hive_link", "value": 15, "trigger": "on_death" }] }
],
"flavorText": "It was here long before us. It will be here long after.",
"artKey": "xeno_apex_1"
},
{
"id": "xeno_leviathan_1",
"name": "Hive Leviathan",
"faction": "xeno",
"type": "assault",
"rarity": "legendary",
"levels": [
{ "attack": 8, "health": 20, "armor": 4, "delay": 3, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "swarm", "value": 2, "trigger": "preBattle" }, { "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 12, "health": 30, "armor": 6, "delay": 3, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "swarm", "value": 3, "trigger": "preBattle" }, { "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 18, "health": 45, "armor": 9, "delay": 3, "skills": [{ "name": "carapace", "value": 2, "trigger": "on_defend" }, { "name": "swarm", "value": 5, "trigger": "preBattle" }, { "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 27, "health": 68, "armor": 14, "delay": 3, "skills": [{ "name": "carapace", "value": 3, "trigger": "on_defend" }, { "name": "swarm", "value": 7, "trigger": "preBattle" }, { "name": "molt", "trigger": "on_turn_start" }] },
{ "attack": 41, "health": 101, "armor": 20, "delay": 3, "skills": [{ "name": "carapace", "value": 3, "trigger": "on_defend" }, { "name": "swarm", "value": 10, "trigger": "preBattle" }, { "name": "molt", "trigger": "on_turn_start" }] }
],
"flavorText": "It does not sleep. It does not forget. It only grows.",
"artKey": "xeno_leviathan_1"
} }
] ]

View File

@ -361,5 +361,126 @@
] ]
}, },
"lore": "The tide rises. Every drop of blood spilled feeds the Countess. She is ancient, she is patient, and she has had enough of slumber." "lore": "The tide rises. Every drop of blood spilled feeds the Countess. She is ancient, she is patient, and she has had enough of slumber."
},
{
"id": "mission_014",
"name": "Emergence",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "Strange tunnels have appeared near the eastern settlements. Something crawls within.",
"unlockCondition": null,
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_drone_1", "xeno_drone_1", "xeno_drone_1", "xeno_spitter_1", "xeno_spitter_1"]
},
"rewards": { "gold": 150, "cards": ["xeno_drone_1"] },
"lore": "The first chitin fragments were dismissed as natural formations. Then the formations moved."
},
{
"id": "mission_015",
"name": "Hive Scouts",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "Advance scouts test the perimeter. The hive is probing our defenses.",
"unlockCondition": "mission_014",
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_drone_1", "xeno_drone_1", "xeno_spitter_1", "xeno_beetle_1", "xeno_beetle_1", "xeno_worker_1"]
},
"rewards": { "gold": 200, "cards": ["xeno_spitter_1"] },
"lore": "They move in patterns too precise for instinct. Something is directing them."
},
{
"id": "mission_016",
"name": "Tunnel War",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "They attack from below. Our formations mean nothing underground.",
"unlockCondition": "mission_015",
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_drone_1", "xeno_beetle_1", "xeno_worker_1", "xeno_guard_1", "xeno_mantis_1", "xeno_horror_1"]
},
"rewards": { "gold": 250, "cards": ["xeno_beetle_1"] },
"lore": "The tunnels go deeper than any mine. Deeper than any root. Something vast breathes in the dark."
},
{
"id": "mission_017",
"name": "The Swarm Rises",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "War Drone Alpha takes the field. The swarm has found its general.",
"unlockCondition": "mission_016",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_drone_1", "xeno_spitter_1", "xeno_guard_1", "xeno_mantis_1", "xeno_horror_1", "xeno_msentinel_1", "xeno_parasite_1"]
},
"rewards": { "gold": 300, "cards": ["xeno_worker_1"] },
"lore": "The Alpha moved differently from the others. With purpose. With malice."
},
{
"id": "mission_018",
"name": "Acid Rain",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "The venom wyrms have arrived. The very air burns.",
"unlockCondition": "mission_017",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_spitter_1", "xeno_mantis_1", "xeno_guard_1", "xeno_horror_1", "xeno_parasite_1", "xeno_wyrm_1", "xeno_hiveguard_1"]
},
"rewards": { "gold": 350, "cards": ["xeno_guard_1"] },
"lore": "Our shields corroded in minutes. Our armor lasted seconds. Only retreat lasted."
},
{
"id": "mission_019",
"name": "Queen's Nest",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "The Burrow Queens breed beneath our feet. Destroy the nests before they multiply.",
"unlockCondition": "mission_018",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_beetle_1", "xeno_guard_1", "xeno_msentinel_1", "xeno_hiveguard_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1"]
},
"rewards": { "gold": 400, "cards": ["xeno_mantis_1"] },
"lore": "Every queen killed made the swarm fight harder. We began to understand the hive link."
},
{
"id": "mission_020",
"name": "Apex Hunt",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "The Apex Predator has emerged. Our best warriors have gone to meet it. Few will return.",
"unlockCondition": "mission_019",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_mantis_1", "xeno_horror_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1", "xeno_apex_1"]
},
"rewards": { "gold": 450, "cards": ["xeno_horror_1"] },
"lore": "It moved faster than anything that size should. The venom hit before we even saw it."
},
{
"id": "mission_021",
"name": "The Hive Mind",
"chapter": 3,
"campaignId": "campaign_xeno",
"enemyLevel": 1,
"description": "One mind controls them all. Sever it, or humanity falls.",
"unlockCondition": "mission_020",
"opponent": {
"commander": "xeno_cmd_3",
"cards": ["xeno_guard_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1", "xeno_apex_1", "xeno_leviathan_1"]
},
"rewards": { "gold": 500, "cards": ["xeno_hiveguard_1"] },
"lore": "It spoke through the swarm. Not words. Something older. The feeling of being consumed."
} }
] ]

View File

@ -164,5 +164,49 @@
"description": "When below 50% HP, gain +value ATK (recalculated each attack).", "description": "When below 50% HP, gain +value ATK (recalculated each attack).",
"trigger": "passive", "trigger": "passive",
"category": "offense" "category": "offense"
},
{
"name": "spawn",
"description": "Summon a Xeno Drone into an empty lane.",
"trigger": "on_turn_start",
"category": "special"
},
{
"name": "swarm",
"description": "Gain +value ATK for each allied xeno card on the field.",
"trigger": "preBattle",
"category": "offense"
},
{
"name": "carapace",
"description": "When damaged, permanently gain +value armor.",
"trigger": "on_defend",
"category": "defense"
},
{
"name": "venom",
"description": "Apply value venom stacks to target. Each turn, deal damage equal to stacks then reduce by 1. Only targets cards without existing venom.",
"allDescription": "Apply value venom stacks to ALL enemy cards without existing venom.",
"trigger": "on_attack",
"category": "offense",
"supportsAll": true
},
{
"name": "burrow",
"description": "Cannot be targeted by enemy skills for value turns. Can still attack normally.",
"trigger": "passive",
"category": "defense"
},
{
"name": "molt",
"description": "Convert all current armor into HP healing, then armor resets to 0.",
"trigger": "on_turn_start",
"category": "defense"
},
{
"name": "hive_link",
"description": "On death, all allied xeno cards gain +value ATK permanently.",
"trigger": "on_death",
"category": "offense"
} }
] ]

View File

@ -65,6 +65,7 @@ export class CombatEngine {
const tick = cards => { const tick = cards => {
for (const c of cards) { for (const c of cards) {
if (c.currentDelay > 0) c.currentDelay--; if (c.currentDelay > 0) c.currentDelay--;
if (c.burrowTurns > 0) c.burrowTurns--;
} }
}; };
tick(this.playerLanes); tick(this.playerLanes);
@ -85,6 +86,22 @@ export class CombatEngine {
applyRupture(this.opponentLanes); applyRupture(this.opponentLanes);
} }
// Apply venom damage to all cards
_venomPhase() {
const applyVenom = cards => {
for (const c of cards) {
if (c.venomStacks > 0) {
c.currentHP -= c.venomStacks;
this._log(`${c.name} takes ${c.venomStacks} venom damage`);
this.events.push({ type: 'venomTick', card: c, damage: c.venomStacks });
c.venomStacks--;
}
}
};
applyVenom(this.playerLanes);
applyVenom(this.opponentLanes);
}
// Tick jam timers // Tick jam timers
_jamPhase() { _jamPhase() {
const tickJam = cards => { const tickJam = cards => {
@ -98,28 +115,44 @@ export class CombatEngine {
// Fire on_turn_start skills for commanders // Fire on_turn_start skills for commanders
_commanderSkillPhase() { _commanderSkillPhase() {
if (this.playerCommander.currentHP > 0) { const processCommander = (commander, lanes, enemyCommander, enemyLanes) => {
for (const s of this.playerCommander.skills) { if (commander.currentHP <= 0) return;
if (s.trigger === 'on_turn_start') { for (const s of commander.skills) {
this.skillProcessor.process(s, this.playerCommander, null, if (s.trigger !== 'on_turn_start') continue;
[this.playerCommander, ...this.playerLanes], const ctx = { enemyCommander, cardManager: this.cardManager, laneCards: lanes };
[this.opponentCommander, ...this.opponentLanes], this.skillProcessor.process(s, commander, null,
{ enemyCommander: this.opponentCommander, cardManager: this.cardManager } [commander, ...lanes], [enemyCommander, ...enemyLanes], ctx);
); if (ctx.spawnedDrone && lanes.length < MAX_LANES) {
lanes.push(ctx.spawnedDrone);
const side = (commander === this.playerCommander) ? 'player' : 'opponent';
this._log(`${commander.name} spawns a ${ctx.spawnedDrone.name}`);
this.events.push({ type: 'spawn', side, card: ctx.spawnedDrone });
} }
} }
} };
if (this.opponentCommander.currentHP > 0) { processCommander(this.playerCommander, this.playerLanes, this.opponentCommander, this.opponentLanes);
for (const s of this.opponentCommander.skills) { processCommander(this.opponentCommander, this.opponentLanes, this.playerCommander, this.playerLanes);
if (s.trigger === 'on_turn_start') { }
this.skillProcessor.process(s, this.opponentCommander, null,
[this.opponentCommander, ...this.opponentLanes], // Fire on_turn_start skills for lane cards (e.g. molt)
[this.playerCommander, ...this.playerLanes], _cardOnTurnStartPhase() {
{ enemyCommander: this.playerCommander, cardManager: this.cardManager } const processCards = (cards, commander, enemyCommander, enemyLanes) => {
); for (const card of cards) {
if (card.currentHP <= 0 || card.currentDelay > 0) continue;
for (const s of card.skills) {
if (s.trigger !== 'on_turn_start') continue;
const ctx = { enemyCommander, cardManager: this.cardManager, laneCards: cards };
this.skillProcessor.process(s, card, null,
[commander, ...cards], [enemyCommander, ...enemyLanes], ctx);
if (s.name === 'molt' && ctx.moltHeal > 0) {
this._log(`${card.name} molts: heals ${ctx.moltHeal}, loses ${ctx.moltArmorLost} armor`);
this.events.push({ type: 'molt', card, heal: ctx.moltHeal, armorLost: ctx.moltArmorLost });
}
} }
} }
} };
processCards(this.playerLanes, this.playerCommander, this.opponentCommander, this.opponentLanes);
processCards(this.opponentLanes, this.opponentCommander, this.playerCommander, this.playerLanes);
} }
// Cards attack across lanes // Cards attack across lanes
@ -196,6 +229,12 @@ export class CombatEngine {
if (onAtkCtx.weakenAllTargets) { if (onAtkCtx.weakenAllTargets) {
this.events.push({ type: 'weakenAll', attacker, targets: onAtkCtx.weakenAllTargets, side }); this.events.push({ type: 'weakenAll', attacker, targets: onAtkCtx.weakenAllTargets, side });
} }
if (onAtkCtx.venomTarget) {
this.events.push({ type: 'venomApply', attacker, target: onAtkCtx.venomTarget, stacks: onAtkCtx.venomStacks, side });
}
if (onAtkCtx.venomAllTargets) {
this.events.push({ type: 'venomApplyAll', attacker, targets: onAtkCtx.venomAllTargets, side });
}
} }
} }
@ -208,7 +247,7 @@ export class CombatEngine {
} }
} }
// counter skill on defender — only fires if defender survived the main attack // counter and carapace skills on defender — only fire if defender survived the main attack
if (currentTarget.currentHP > 0) { if (currentTarget.currentHP > 0) {
for (const s of currentTarget.skills) { for (const s of currentTarget.skills) {
if (s.name === 'counter') { if (s.name === 'counter') {
@ -223,16 +262,30 @@ export class CombatEngine {
if (attacker.currentHP <= 0) { if (attacker.currentHP <= 0) {
this._log(`${attacker.name} is destroyed by counter`); this._log(`${attacker.name} is destroyed by counter`);
this.events.push({ type: 'death', card: attacker, side }); this.events.push({ type: 'death', card: attacker, side });
this._processOnDeath(attacker, alliedLanes, side);
} }
} }
} }
if (s.name === 'carapace' && dmg > 0) {
const carapaceCtx = { trigger: 'on_defend' };
this.skillProcessor.process(s, currentTarget, attacker,
enemyLanes.filter(c => c.currentHP > 0),
alliedLanes.filter(c => c.currentHP > 0),
carapaceCtx);
if (carapaceCtx.carapaceGain > 0) {
this._log(`${currentTarget.name} carapace: +${carapaceCtx.carapaceGain} ARM`);
this.events.push({ type: 'carapace', card: currentTarget, gain: carapaceCtx.carapaceGain });
}
}
} }
} }
// Check if defender died // Check if defender died
if (currentTarget.currentHP <= 0) { if (currentTarget.currentHP <= 0) {
this._log(`${currentTarget.name} is destroyed`); this._log(`${currentTarget.name} is destroyed`);
this.events.push({ type: 'death', card: currentTarget, side: side === 'player' ? 'opponent' : 'player' }); const defenderSide = side === 'player' ? 'opponent' : 'player';
this.events.push({ type: 'death', card: currentTarget, side: defenderSide });
this._processOnDeath(currentTarget, enemyLanes, defenderSide);
} }
} else { } else {
// Attack commander directly (pierce has already reduced currentArmor if applicable) // Attack commander directly (pierce has already reduced currentArmor if applicable)
@ -253,6 +306,20 @@ export class CombatEngine {
} }
} }
// Process on_death skills for a card that just died
_processOnDeath(deadCard, alliedLanes, side) {
for (const s of deadCard.skills) {
if (s.trigger !== 'on_death') continue;
const allies = alliedLanes.filter(a => a.currentHP > 0);
const ctx = {};
this.skillProcessor.process(s, deadCard, null, allies, [], ctx);
if (s.name === 'hive_link' && ctx.hiveLinkTargets?.length > 0) {
this._log(`${deadCard.name} hive_link: +${s.value} ATK to ${ctx.hiveLinkTargets.length} allies`);
this.events.push({ type: 'hive_link', source: deadCard, targets: ctx.hiveLinkTargets, gain: s.value, side });
}
}
}
// Remove dead cards, compact lanes // Remove dead cards, compact lanes
_deathCheck() { _deathCheck() {
this.playerLanes = this.playerLanes.filter(c => c.currentHP > 0); this.playerLanes = this.playerLanes.filter(c => c.currentHP > 0);
@ -315,7 +382,9 @@ export class CombatEngine {
for (const c of this.playerHand) { if (c.currentDelay > 0) c.currentDelay--; } for (const c of this.playerHand) { if (c.currentDelay > 0) c.currentDelay--; }
for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; } for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; }
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase();
this._rupturePhase(); this._rupturePhase();
this._venomPhase();
this._jamPhase(); this._jamPhase();
this._attackPhase(); this._attackPhase();
this._deathCheck(); this._deathCheck();
@ -356,7 +425,10 @@ export class CombatEngine {
// Tick AI hand cards that were not deployed this turn // Tick AI hand cards that were not deployed this turn
for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; } for (const c of this.ai.hand) { if (c.currentDelay > 0) c.currentDelay--; }
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase();
this._rupturePhase(); this._rupturePhase();
this._venomPhase();
this._deathCheck(); // remove cards killed by rupture/venom before combat
this._jamPhase(); this._jamPhase();
this._preBattlePhase(); this._preBattlePhase();
// NOTE: No _deathCheck() here — dead cards stay in lanes so the scene can // NOTE: No _deathCheck() here — dead cards stay in lanes so the scene can
@ -401,6 +473,10 @@ export class CombatEngine {
buffs.push({ skill: 'legion', source: card, target: card, amount: ctx.legionGain }); buffs.push({ skill: 'legion', source: card, target: card, amount: ctx.legionGain });
this._log(`${card.name} legion: +${ctx.legionGain} ATK`); this._log(`${card.name} legion: +${ctx.legionGain} ATK`);
} }
if (s.name === 'swarm' && ctx.swarmCard) {
buffs.push({ skill: 'swarm', source: card, target: card, amount: ctx.swarmGain });
this._log(`${card.name} swarm: +${ctx.swarmGain} ATK`);
}
if (s.name === 'bloodrage' && ctx.bloodrageGain) { if (s.name === 'bloodrage' && ctx.bloodrageGain) {
buffs.push({ skill: 'bloodrage', source: card, target: card, amount: ctx.bloodrageGain }); buffs.push({ skill: 'bloodrage', source: card, target: card, amount: ctx.bloodrageGain });
this._log(`${card.name} bloodrage: +${ctx.bloodrageGain} ATK`); this._log(`${card.name} bloodrage: +${ctx.bloodrageGain} ATK`);
@ -723,7 +799,9 @@ export class CombatEngine {
this._deployPhase(); this._deployPhase();
this._activationPhase(); this._activationPhase();
this._commanderSkillPhase(); this._commanderSkillPhase();
this._cardOnTurnStartPhase();
this._rupturePhase(); this._rupturePhase();
this._venomPhase();
this._jamPhase(); this._jamPhase();
this._attackPhase(); this._attackPhase();
this._deathCheck(); this._deathCheck();

View File

@ -7,6 +7,7 @@ export class SkillProcessor {
} }
strike(skill, attacker, defender, allies, enemies, context) { strike(skill, attacker, defender, allies, enemies, context) {
if (!skill.all && defender?.burrowTurns > 0) return;
if (skill.all) { if (skill.all) {
const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0) : (context?.enemyCommander ? [context.enemyCommander] : []); const targets = enemies.length > 0 ? enemies.filter(e => e.currentHP > 0) : (context?.enemyCommander ? [context.enemyCommander] : []);
if (targets.length === 0) return; if (targets.length === 0) return;
@ -73,6 +74,7 @@ export class SkillProcessor {
} }
rupture(skill, attacker, defender, allies, enemies, context) { rupture(skill, attacker, defender, allies, enemies, context) {
if (!skill.all && defender?.burrowTurns > 0) return;
if (skill.all) { if (skill.all) {
const targets = enemies.filter(e => e.currentHP > 0); const targets = enemies.filter(e => e.currentHP > 0);
if (targets.length === 0) return; if (targets.length === 0) return;
@ -189,7 +191,7 @@ export class SkillProcessor {
// Primary: directly opposite card // Primary: directly opposite card
const primary = enemyLaneCards[idx]; const primary = enemyLaneCards[idx];
if (primary?.currentHP > 0) { if (primary?.currentHP > 0 && !(primary.burrowTurns > 0)) {
const reductions = this._applyJamReductions(primary, primaryAmount); const reductions = this._applyJamReductions(primary, primaryAmount);
if (reductions.length > 0) { if (reductions.length > 0) {
primary._tempBuffs = primary._tempBuffs || []; primary._tempBuffs = primary._tempBuffs || [];
@ -252,6 +254,7 @@ export class SkillProcessor {
if (idx < 0 || idx >= enemyLaneCards.length) return; if (idx < 0 || idx >= enemyLaneCards.length) return;
const target = enemyLaneCards[idx]; const target = enemyLaneCards[idx];
if (!target || target.currentHP <= 0) return; if (!target || target.currentHP <= 0) return;
if (target.burrowTurns > 0) return;
const amount = Math.min(skill.value, target.currentAttack); const amount = Math.min(skill.value, target.currentAttack);
target.currentAttack = Math.max(0, target.currentAttack - amount); target.currentAttack = Math.max(0, target.currentAttack - amount);
// Store negative amount so _removeAndCollectDebuffs reverses it by adding back // Store negative amount so _removeAndCollectDebuffs reverses it by adding back
@ -264,6 +267,7 @@ export class SkillProcessor {
} }
weaken(skill, attacker, defender, allies, enemies, context) { weaken(skill, attacker, defender, allies, enemies, context) {
if (!skill.all && defender?.burrowTurns > 0) return;
if (skill.all) { if (skill.all) {
const targets = enemies.filter(e => e.currentHP > 0); const targets = enemies.filter(e => e.currentHP > 0);
if (targets.length === 0) return; if (targets.length === 0) return;
@ -319,9 +323,11 @@ export class SkillProcessor {
if (context) context.mortarAllTargets = allTargets; if (context) context.mortarAllTargets = allTargets;
return; return;
} }
const validEnemies = enemies.filter(e => e.currentHP > 0 && !(e.burrowTurns > 0));
if (validEnemies.length === 0) return;
const rng = context?.rng; const rng = context?.rng;
const idx = rng ? rng.nextInt(0, enemies.length - 1) : Math.floor(Math.random() * enemies.length); const idx = rng ? rng.nextInt(0, validEnemies.length - 1) : Math.floor(Math.random() * validEnemies.length);
const target = enemies[idx]; const target = validEnemies[idx];
const dmg = Math.max(0, skill.value - Math.max(0, target.currentArmor)); const dmg = Math.max(0, skill.value - Math.max(0, target.currentArmor));
target.currentHP -= dmg; target.currentHP -= dmg;
if (context) { if (context) {
@ -432,4 +438,77 @@ export class SkillProcessor {
if (context) { context.bloodrageCard = attacker; context.bloodrageGain = skill.value; } if (context) { context.bloodrageCard = attacker; context.bloodrageGain = skill.value; }
} }
} }
// ── Xeno skills ─────────────────────────────────────────────────────────────
spawn(skill, attacker, defender, allies, enemies, context) {
const laneCards = context?.laneCards || [];
if (laneCards.length >= 4) return; // no empty lane
const drone = context.cardManager.createInstance('xeno_drone_token', attacker.level || 1);
drone.currentDelay = 0;
context.spawnedDrone = drone;
}
swarm(skill, attacker, defender, allies, enemies, context) {
const xenoAllies = allies.filter(a =>
a.faction === 'xeno' && a !== attacker && a.currentHP > 0 && a.type !== 'commander'
);
const gain = skill.value * xenoAllies.length;
if (gain <= 0) return;
attacker.currentAttack += gain;
attacker._tempBuffs = attacker._tempBuffs || [];
attacker._tempBuffs.push({ stat: 'currentAttack', amount: gain, source: 'swarm' });
if (context) { context.swarmCard = attacker; context.swarmGain = gain; }
}
carapace(skill, attacker, defender, allies, enemies, context) {
// attacker is the defending card (the one with carapace)
attacker.currentArmor += skill.value;
attacker.armor += skill.value; // permanent base increase
if (context) context.carapaceGain = skill.value;
}
venom(skill, attacker, defender, allies, enemies, context) {
if (skill.all) {
const targets = enemies.filter(e => e.currentHP > 0 && !(e.venomStacks > 0));
if (targets.length === 0) return;
const allTargets = [];
for (const t of targets) {
if (t.burrowTurns > 0) continue; // burrowed cards immune to targeted venom
t.venomStacks = skill.value;
allTargets.push({ target: t, stacks: skill.value });
}
if (context && allTargets.length > 0) context.venomAllTargets = allTargets;
return;
}
if (!defender || defender.currentHP <= 0) return;
if (defender.venomStacks > 0) return;
if (defender.burrowTurns > 0) return;
defender.venomStacks = skill.value;
if (context) { context.venomTarget = defender; context.venomStacks = skill.value; }
}
molt(skill, attacker, defender, allies, enemies, context) {
const armorToConvert = attacker.currentArmor;
if (armorToConvert <= 0) return;
const healAmount = Math.min(armorToConvert, attacker.health - attacker.currentHP);
attacker.currentHP += healAmount;
attacker.currentArmor = 0;
if (context) { context.moltHeal = healAmount; context.moltArmorLost = armorToConvert; }
}
hive_link(skill, attacker, defender, allies, enemies, context) {
// attacker is the dying card
const xenoAllies = allies.filter(a =>
a.faction === 'xeno' && a.currentHP > 0 && a !== attacker
);
if (xenoAllies.length === 0) return;
const targets = [];
for (const ally of xenoAllies) {
ally.currentAttack += skill.value;
ally.attack += skill.value; // permanent
targets.push({ target: ally, gain: skill.value });
}
if (context) context.hiveLinkTargets = targets;
}
} }

View File

@ -48,6 +48,8 @@ export class CardManager {
currentArmor: stats.armor, currentArmor: stats.armor,
currentDelay: stats.delay, currentDelay: stats.delay,
ruptureStacks: 0, ruptureStacks: 0,
venomStacks: 0,
burrowTurns: (() => { const bs = stats.skills.find(s => s.name === 'burrow'); return bs ? bs.value : 0; })(),
jamTurns: 0, jamTurns: 0,
berserkBonus: 0, berserkBonus: 0,
instanceId: Math.random().toString(36).slice(2) instanceId: Math.random().toString(36).slice(2)

View File

@ -33,6 +33,7 @@ export class PackManager {
const pool = this.cardManager.getAllCards().filter(c => const pool = this.cardManager.getAllCards().filter(c =>
c.rarity === rarity && c.rarity === rarity &&
c.type !== 'commander' && c.type !== 'commander' &&
c.type !== 'token' &&
slot.cardPool.includes(c.faction) slot.cardPool.includes(c.faction)
); );
if (pool.length === 0) continue; if (pool.length === 0) continue;

View File

@ -106,6 +106,27 @@ export class CardObject extends Phaser.GameObjects.Container {
this.add(this.delayOverlay); this.add(this.delayOverlay);
} }
// Burrow overlay (brown tint when burrowed)
this.burrowOverlay = this.scene.add.rectangle(0, imageCY, w, imageH, 0x553311, 0.45);
this.burrowOverlay.setVisible(this.cardData.burrowTurns > 0);
this.add(this.burrowOverlay);
// Venom indicator (top-right of image area)
this.venomBadge = this.scene.add.text(
w / 2 - Math.round(4 * scale),
-h / 2 + bannerH + Math.round(3 * scale),
'',
{
fontSize: fs(7), color: '#44ff44', fontStyle: 'bold',
backgroundColor: '#000000aa',
padding: { x: Math.round(2 * scale), y: Math.round(1 * scale) },
fontFamily: 'Audiowide'
}
).setOrigin(1, 0);
this.venomBadge.setVisible(false);
this.add(this.venomBadge);
this._updateVenomBadge();
// Flash overlay — transparent red rect; .bg is targeted by _animateAttack // Flash overlay — transparent red rect; .bg is targeted by _animateAttack
this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0); this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0);
this.add(this.bg); this.add(this.bg);
@ -191,6 +212,17 @@ export class CardObject extends Phaser.GameObjects.Container {
} }
} }
_updateVenomBadge() {
if (!this.venomBadge) return;
const stacks = this.cardData.venomStacks || 0;
if (stacks > 0) {
this.venomBadge.setText(`☠${stacks}`);
this.venomBadge.setVisible(true);
} else {
this.venomBadge.setVisible(false);
}
}
refresh() { refresh() {
const w = this.options.width || 80; const w = this.options.width || 80;
const scale = w / 80; const scale = w / 80;
@ -210,6 +242,10 @@ export class CardObject extends Phaser.GameObjects.Container {
if (this.delayOverlay) { if (this.delayOverlay) {
this.delayOverlay.setVisible(this.cardData.currentDelay > 0); this.delayOverlay.setVisible(this.cardData.currentDelay > 0);
} }
if (this.burrowOverlay) {
this.burrowOverlay.setVisible(this.cardData.burrowTurns > 0);
}
this._updateVenomBadge();
if (this.skillText && this.cardData.skills?.length) { if (this.skillText && this.cardData.skills?.length) {
const skillStr = this.cardData.skills const skillStr = this.cardData.skills
.map(s => { .map(s => {

View File

@ -8,7 +8,8 @@ import { SaveManager } from '../managers/SaveManager.js';
const BATTLE_BACKGROUNDS = { const BATTLE_BACKGROUNDS = {
imperial: ['imperial_01', 'imperial_02', 'imperial_03'], imperial: ['imperial_01', 'imperial_02', 'imperial_03'],
raider: ['raider_01', 'raider_02', 'raider_03'], raider: ['raider_01', 'raider_02', 'raider_03'],
bloodthirsty: ['bloodthirsty_01', 'bloodthirsty_02', 'bloodthirsty_03'] bloodthirsty: ['bloodthirsty_01', 'bloodthirsty_02', 'bloodthirsty_03'],
xeno: ['xeno_01', 'xeno_02', 'xeno_03']
}; };
// ── Battle music playlist ───────────────────────────────────────────────────── // ── Battle music playlist ─────────────────────────────────────────────────────
@ -338,8 +339,15 @@ export class BattleScene extends Phaser.Scene {
? this.engine.getState().opponent.lanes.find(c => !oldOpponentIds.has(c.instanceId)) ? this.engine.getState().opponent.lanes.find(c => !oldOpponentIds.has(c.instanceId))
: null; : null;
const startAttacks = () => this._processPreBattle(preBattleEvents, () => { // Collect new-phase events (spawn, molt, venomTick) emitted between deploy and preBattle
this.time.delayedCall(200, () => this._processNextAttackStep()); const phaseEvents = commitEvents.filter(e =>
['spawn', 'molt', 'venomTick', 'carapace', 'hive_link'].includes(e.type)
);
const startAttacks = () => this._processPhaseEvents(phaseEvents, () => {
this._processPreBattle(preBattleEvents, () => {
this.time.delayedCall(200, () => this._processNextAttackStep());
});
}); });
const animateOpponentThenAttack = () => { const animateOpponentThenAttack = () => {
@ -401,7 +409,7 @@ export class BattleScene extends Phaser.Scene {
} else if (currentRound) { } else if (currentRound) {
if (e.type === 'berserk') currentRound.berserkEvent = e; if (e.type === 'berserk') currentRound.berserkEvent = e;
else if (e.type === 'counter') currentRound.counterEvent = e; else if (e.type === 'counter') currentRound.counterEvent = e;
else if (e.type === 'ruptureAll' || e.type === 'healAll' || e.type === 'weakenAll') else if (['ruptureAll', 'healAll', 'weakenAll', 'venomApply', 'venomApplyAll', 'carapace', 'hive_link'].includes(e.type))
currentRound.onAttackAllEvents.push(e); currentRound.onAttackAllEvents.push(e);
} }
} }
@ -476,6 +484,8 @@ export class BattleScene extends Phaser.Scene {
if (event.type === 'ruptureAll') this._animateRuptureAll(event, cb); if (event.type === 'ruptureAll') this._animateRuptureAll(event, cb);
else if (event.type === 'healAll') this._animateHealAll(event, cb); else if (event.type === 'healAll') this._animateHealAll(event, cb);
else if (event.type === 'weakenAll') this._animateWeakenAll(event, cb); else if (event.type === 'weakenAll') this._animateWeakenAll(event, cb);
else if (event.type === 'venomApply') this._animateVenomApply(event, cb);
else if (event.type === 'venomApplyAll') this._animateVenomApplyAll(event, cb);
else cb(); else cb();
}; };
next(0); next(0);
@ -537,6 +547,123 @@ export class BattleScene extends Phaser.Scene {
} }
} }
// ── Xeno skill animations ──────────────────────────────────────────────────
_processPhaseEvents(events, onComplete) {
if (!events || events.length === 0) { onComplete(); return; }
const next = (idx) => {
if (idx >= events.length) { onComplete(); return; }
const event = events[idx];
const cb = () => next(idx + 1);
if (event.type === 'spawn') this._animateSpawn(event, cb);
else if (event.type === 'molt') this._animateMolt(event, cb);
else if (event.type === 'venomTick') this._animateVenomTick(event, cb);
else if (event.type === 'carapace') this._animateCarapace(event, cb);
else if (event.type === 'hive_link') this._animateHiveLink(event, cb);
else cb();
};
next(0);
}
_animateSpawn(event, onComplete) {
// Re-render state so the new drone card object exists
this._renderState();
const cardObj = this.cardObjects.get(event.card.instanceId);
if (!cardObj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.side === 'player' ? 'Your' : 'Enemy'} commander spawns a drone!`);
cardObj.setAlpha(0).setScale(0.5);
this.tweens.add({
targets: cardObj,
alpha: 1, scaleX: 1, scaleY: 1,
duration: 400,
ease: 'Back.Out',
onComplete: () => { this.time.delayedCall(200, onComplete); }
});
}
_animateMolt(event, onComplete) {
const obj = this.cardObjects.get(event.card.instanceId);
if (!obj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.card.name} molts: converts ${event.armorLost} armor to ${event.heal} HP`);
obj.flash(0x22aa44);
this.time.delayedCall(400, () => {
if (obj.scene) obj.refresh();
onComplete();
});
}
_animateVenomTick(event, onComplete) {
const obj = this.cardObjects.get(event.card.instanceId) || this.commanderObjects?.get(event.card.instanceId);
if (!obj?.scene) { onComplete(); return; }
obj.flash(0x44ff44);
this.time.delayedCall(300, () => {
if (obj.scene) obj.refresh();
onComplete();
});
}
_animateCarapace(event, onComplete) {
const obj = this.cardObjects.get(event.card.instanceId);
if (!obj?.scene) { onComplete(); return; }
obj.animateArmorGain(event.gain, onComplete);
}
_animateHiveLink(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; }
this.statusText.setText(`${event.source.name} dies — hive link grants +${event.gain} ATK!`);
let remaining = event.targets.length;
const done = () => {
if (--remaining <= 0) onComplete();
};
for (const t of event.targets) {
const obj = this.cardObjects.get(t.target.instanceId);
if (!obj?.scene) { done(); continue; }
obj.flash(0xff4400);
this.time.delayedCall(400, () => {
if (obj.scene) obj.refresh();
done();
});
}
}
_animateVenomApply(event, onComplete) {
const obj = this.cardObjects.get(event.target.instanceId) || this.commanderObjects?.get(event.target.instanceId);
if (!obj?.scene) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} applies ${event.stacks} venom to ${event.target.name}!`);
obj.flash(0x44ff44);
this.time.delayedCall(400, () => {
if (obj.scene) obj.refresh();
onComplete();
});
}
_animateVenomApplyAll(event, onComplete) {
if (!event.targets?.length) { onComplete(); return; }
this.statusText.setText(`${event.attacker.name} venoms all enemies!`);
let remaining = event.targets.length;
const done = () => {
if (--remaining <= 0) onComplete();
};
for (const t of event.targets) {
const obj = this.cardObjects.get(t.target.instanceId) || this.commanderObjects?.get(t.target.instanceId);
if (!obj?.scene) { done(); continue; }
obj.flash(0x44ff44);
this.time.delayedCall(400, () => {
if (obj.scene) obj.refresh();
done();
});
}
}
_animateSwarmBuff(buff, onComplete) {
// Reuse the legion buff animation pattern
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
const targetObj = _lookup(buff.target.instanceId);
if (!targetObj?.scene) { onComplete(); return; }
this.statusText.setText(`${buff.target.name} swarm: +${buff.amount} ATK`);
targetObj.animateBerserkGain(buff.amount, onComplete);
}
// Explosion sprite + fade-out for a defeated lane card. // Explosion sprite + fade-out for a defeated lane card.
_playCardDeath(obj, cardData, onComplete = null) { _playCardDeath(obj, cardData, onComplete = null) {
this.cardObjects.delete(cardData.instanceId); this.cardObjects.delete(cardData.instanceId);
@ -2042,6 +2169,8 @@ export class BattleScene extends Phaser.Scene {
this._animateRallyBuff(group, () => next(idx + 1)); this._animateRallyBuff(group, () => next(idx + 1));
} else if (group.skill === 'legion') { } else if (group.skill === 'legion') {
this._animateLegionBuff(group, () => next(idx + 1)); this._animateLegionBuff(group, () => next(idx + 1));
} else if (group.skill === 'swarm') {
this._animateSwarmBuff(group, () => next(idx + 1));
} else if (group.skill === 'bloodrage') { } else if (group.skill === 'bloodrage') {
this._animateBloodrageBuff(group, () => next(idx + 1)); this._animateBloodrageBuff(group, () => next(idx + 1));
} else { } else {