feat(spireclimb): add 57 new cards and major combat mechanics
Add 25 Warrior, 24 Rogue, and 8 Neutral cards (frames 34–90), expanding the card pool from 34 to 91. Implement new card mechanics including: - Clash (0-cost if hand is all attacks), Innate cards in opening hand - Temp Strength (Flex), Next-Turn Energy (Flying Knee, Outmaneuver) - Barricade (block carries over), Noxious Fumes (turn-start poison) - Envenom (attacks apply poison), Thousand Cuts (per-card damage) - Heavy Blade strength multiplier, Bane bonus vs poisoned enemies - Second Wind, Fiend Fire, Reaper, Finisher, Flechettes, Grand Finale - Double Block, Recycle Discard, Shuffle Discard, Lose Enemy Strength - Add Card to Hand (Cloak and Dagger → Shiv) Add potion usage confirmation modal with backdrop and cancel. Visual improvements: enemy debuff labels, stat drain floater numbers, player debuff flash animation, card-fly animations for discard/draw. Update artwork docs and frame map for 91 cards (10×10 grid).
This commit is contained in:
parent
d2fd2481b8
commit
33ef300d1d
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 6.4 MiB |
Binary file not shown.
|
|
@ -33,7 +33,14 @@
|
|||
"daggerthrow": 18, "poisonedstab": 19, "deadlypoison": 20, "backflip": 21, "sneakyhit": 22,
|
||||
"footwork": 23, "bladedance": 24, "caltrops": 25, "bouncingblade": 26, "crippling": 27, "catalyst": 28,
|
||||
"bandage": 29, "flashofsteel": 30,
|
||||
"wound": 31, "dazed": 32, "burn": 33
|
||||
"wound": 31, "dazed": 32, "burn": 33,
|
||||
"anger": 34, "clash": 35, "flex": 36, "headbutt": 37, "heavyblade": 38, "twinstrike": 39, "thunderclap": 40, "wildstrike": 41,
|
||||
"bloodletting": 42, "carnage": 43, "disarm": 44, "entrench": 45, "hemokinesis": 46, "intimidate": 47, "pummel": 48, "secondwind": 49, "seeingred": 50, "shockwave": 51,
|
||||
"barricade": 52, "bludgeon": 53, "fiendfire": 54, "immolate": 55, "impervious": 56, "offering": 57, "reaper": 58,
|
||||
"acrobatics": 59, "bane": 60, "cloakdagger": 61, "daggerspray": 62, "deflect": 63, "flyingknee": 64, "outmaneuver": 65, "prepared": 66, "quickslash": 67, "slice": 68, "suckerpunch": 69,
|
||||
"backstab": 70, "dash": 71, "eviscerate": 72, "finisher": 73, "flechettes": 74, "legsweep": 75, "noxiousfumes": 76, "riddlewithholes": 77,
|
||||
"thousandcuts": 78, "adrenaline": 79, "diedidie": 80, "envenom": 81, "grandfinale": 82,
|
||||
"shiv": 83, "bite": 84, "blind": 85, "deepbreath": 86, "dramaticEntrance": 87, "finesse": 88, "goodinstincts": 89, "swiftstrike": 90
|
||||
},
|
||||
|
||||
"creatures": {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,54 @@ export const CARDS = {
|
|||
},
|
||||
|
||||
// —— Warrior common ——
|
||||
anger: {
|
||||
id: 'anger', name: 'Anger', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'enemy', effects: [{ op: 'damage', amount: 6 }, { op: 'addCardToDiscard', card: '__self__' }],
|
||||
text: 'Deal 6 damage. Add a copy of this card to your discard pile.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 8 }, { op: 'addCardToDiscard', card: '__self__' }], text: 'Deal 8 damage. Add a copy to your discard pile.' },
|
||||
},
|
||||
clash: {
|
||||
id: 'clash', name: 'Clash', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'enemy', clashOnly: true, effects: [{ op: 'damage', amount: 14 }],
|
||||
text: 'Can only be played if every card in your hand is an Attack. Deal 14 damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 18 }], text: 'Can only be played if every card in your hand is an Attack. Deal 18 damage.' },
|
||||
},
|
||||
flex: {
|
||||
id: 'flex', name: 'Flex', cls: 'warrior', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'tempStrength', amount: 2 }],
|
||||
text: 'Gain 2 Strength. At the end of your turn, lose 2 Strength.',
|
||||
upgrade: { effects: [{ op: 'tempStrength', amount: 4 }], text: 'Gain 4 Strength. At the end of your turn, lose 4 Strength.' },
|
||||
},
|
||||
headbutt: {
|
||||
id: 'headbutt', name: 'Headbutt', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 9 }, { op: 'recycleDiscard' }],
|
||||
text: 'Deal 9 damage. Put the top card of your discard pile on top of your draw pile.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 12 }, { op: 'recycleDiscard' }], text: 'Deal 12 damage. Put the top card of your discard pile on top of your draw pile.' },
|
||||
},
|
||||
heavyblade: {
|
||||
id: 'heavyblade', name: 'Heavy Blade', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 2, target: 'enemy', effects: [{ op: 'damage', amount: 14, strengthMul: 3 }],
|
||||
text: 'Deal 14 damage. Strength affects this card 3 times.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 14, strengthMul: 5 }], text: 'Deal 14 damage. Strength affects this card 5 times.' },
|
||||
},
|
||||
twinstrike: {
|
||||
id: 'twinstrike', name: 'Twin Strike', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 5, times: 2 }],
|
||||
text: 'Deal 5 damage twice.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 7, times: 2 }], text: 'Deal 7 damage twice.' },
|
||||
},
|
||||
thunderclap: {
|
||||
id: 'thunderclap', name: 'Thunder Clap', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'all', effects: [{ op: 'damageAll', amount: 4 }, { op: 'debuffAll', status: 'vulnerable', amount: 1 }],
|
||||
text: 'Deal 4 damage to ALL enemies. Apply 1 Vulnerable to ALL enemies.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 7 }, { op: 'debuffAll', status: 'vulnerable', amount: 1 }], text: 'Deal 7 damage to ALL enemies. Apply 1 Vulnerable to ALL enemies.' },
|
||||
},
|
||||
wildstrike: {
|
||||
id: 'wildstrike', name: 'Wild Strike', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 12 }, { op: 'addCardToDiscard', card: 'wound' }],
|
||||
text: 'Deal 12 damage. Add a Wound to your draw pile.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 17 }, { op: 'addCardToDiscard', card: 'wound' }], text: 'Deal 17 damage. Add a Wound to your draw pile.' },
|
||||
},
|
||||
ironwave: {
|
||||
id: 'ironwave', name: 'Iron Wave', cls: 'warrior', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'block', amount: 5 }, { op: 'damage', amount: 5 }],
|
||||
|
|
@ -154,20 +202,189 @@ export const CARDS = {
|
|||
text: 'Deal 5 damage to ALL enemies 3 times.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 8, times: 3 }], text: 'Deal 8 damage to ALL enemies 3 times.' },
|
||||
},
|
||||
bloodletting: {
|
||||
id: 'bloodletting', name: 'Bloodletting', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 0, target: 'self', effects: [{ op: 'loseHp', amount: 3 }, { op: 'energy', amount: 2 }],
|
||||
text: 'Lose 3 HP. Gain 2 Energy.',
|
||||
upgrade: { effects: [{ op: 'loseHp', amount: 3 }, { op: 'energy', amount: 3 }], text: 'Lose 3 HP. Gain 3 Energy.' },
|
||||
},
|
||||
carnage: {
|
||||
id: 'carnage', name: 'Carnage', cls: 'warrior', type: 'attack', rarity: 'uncommon',
|
||||
cost: 2, target: 'enemy', ethereal: true, effects: [{ op: 'damage', amount: 20 }],
|
||||
text: 'Ethereal. Deal 20 damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 28 }], text: 'Ethereal. Deal 28 damage.' },
|
||||
},
|
||||
disarm: {
|
||||
id: 'disarm', name: 'Disarm', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 1, target: 'enemy', exhaust: true, effects: [{ op: 'loseEnemyStrength', amount: 2 }],
|
||||
text: 'Enemy loses 2 Strength. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'loseEnemyStrength', amount: 3 }], text: 'Enemy loses 3 Strength. Exhaust.' },
|
||||
},
|
||||
entrench: {
|
||||
id: 'entrench', name: 'Entrench', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 2, target: 'self', effects: [{ op: 'doubleBlock' }],
|
||||
text: 'Double your current Block.',
|
||||
upgrade: { cost: 1, text: 'Double your current Block.' },
|
||||
},
|
||||
hemokinesis: {
|
||||
id: 'hemokinesis', name: 'Hemokinesis', cls: 'warrior', type: 'attack', rarity: 'uncommon',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'loseHp', amount: 2 }, { op: 'damage', amount: 15 }],
|
||||
text: 'Lose 2 HP. Deal 15 damage.',
|
||||
upgrade: { effects: [{ op: 'loseHp', amount: 2 }, { op: 'damage', amount: 20 }], text: 'Lose 2 HP. Deal 20 damage.' },
|
||||
},
|
||||
intimidate: {
|
||||
id: 'intimidate', name: 'Intimidate', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 0, target: 'all', exhaust: true, effects: [{ op: 'debuffAll', status: 'weak', amount: 1 }],
|
||||
text: 'Apply 1 Weak to ALL enemies. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'debuffAll', status: 'weak', amount: 2 }], text: 'Apply 2 Weak to ALL enemies. Exhaust.' },
|
||||
},
|
||||
pummel: {
|
||||
id: 'pummel', name: 'Pummel', cls: 'warrior', type: 'attack', rarity: 'uncommon',
|
||||
cost: 1, target: 'enemy', exhaust: true, effects: [{ op: 'damage', amount: 2, times: 4 }],
|
||||
text: 'Deal 2 damage 4 times. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 2, times: 5 }], text: 'Deal 2 damage 5 times. Exhaust.' },
|
||||
},
|
||||
secondwind: {
|
||||
id: 'secondwind', name: 'Second Wind', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 1, target: 'self', effects: [{ op: 'secondWind', amount: 5 }],
|
||||
text: 'Exhaust all non-Attack cards in your hand. Gain 5 Block for each card Exhausted.',
|
||||
upgrade: { effects: [{ op: 'secondWind', amount: 7 }], text: 'Exhaust all non-Attack cards in your hand. Gain 7 Block for each card Exhausted.' },
|
||||
},
|
||||
seeingred: {
|
||||
id: 'seeingred', name: 'Seeing Red', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 1, target: 'self', exhaust: true, effects: [{ op: 'energy', amount: 2 }],
|
||||
text: 'Gain 2 Energy. Exhaust.',
|
||||
upgrade: { cost: 0, text: 'Gain 2 Energy. Exhaust.' },
|
||||
},
|
||||
shockwave: {
|
||||
id: 'shockwave', name: 'Shockwave', cls: 'warrior', type: 'skill', rarity: 'uncommon',
|
||||
cost: 2, target: 'all', exhaust: true,
|
||||
effects: [{ op: 'debuffAll', status: 'vulnerable', amount: 3 }, { op: 'debuffAll', status: 'weak', amount: 3 }],
|
||||
text: 'Apply 3 Vulnerable and 3 Weak to ALL enemies. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'debuffAll', status: 'vulnerable', amount: 5 }, { op: 'debuffAll', status: 'weak', amount: 5 }], text: 'Apply 5 Vulnerable and 5 Weak to ALL enemies. Exhaust.' },
|
||||
},
|
||||
limitbreak: {
|
||||
id: 'limitbreak', name: 'Limit Break', cls: 'warrior', type: 'skill', rarity: 'rare',
|
||||
cost: 1, target: 'self', exhaust: true, effects: [{ op: 'doubleStrength' }],
|
||||
text: 'Double your Strength. Exhaust.',
|
||||
upgrade: { exhaust: false, text: 'Double your Strength.' },
|
||||
},
|
||||
barricade: {
|
||||
id: 'barricade', name: 'Barricade', cls: 'warrior', type: 'power', rarity: 'rare',
|
||||
cost: 3, target: 'self', effects: [{ op: 'barricade' }],
|
||||
text: 'Block is no longer removed at the start of your turn.',
|
||||
upgrade: { cost: 2, text: 'Block is no longer removed at the start of your turn.' },
|
||||
},
|
||||
bludgeon: {
|
||||
id: 'bludgeon', name: 'Bludgeon', cls: 'warrior', type: 'attack', rarity: 'rare',
|
||||
cost: 3, target: 'enemy', effects: [{ op: 'damage', amount: 32 }],
|
||||
text: 'Deal 32 damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 42 }], text: 'Deal 42 damage.' },
|
||||
},
|
||||
demonform: {
|
||||
id: 'demonform', name: 'Demon Form', cls: 'warrior', type: 'power', rarity: 'rare',
|
||||
cost: 3, target: 'self', effects: [{ op: 'buffSelf', status: 'ritual', amount: 2 }],
|
||||
text: 'At the start of each turn, gain 2 Strength.',
|
||||
upgrade: { effects: [{ op: 'buffSelf', status: 'ritual', amount: 3 }], text: 'At the start of each turn, gain 3 Strength.' },
|
||||
},
|
||||
fiendfire: {
|
||||
id: 'fiendfire', name: 'Fiend Fire', cls: 'warrior', type: 'attack', rarity: 'rare',
|
||||
cost: 2, target: 'enemy', exhaust: true, effects: [{ op: 'fiendFire', amount: 7 }],
|
||||
text: 'Exhaust your entire hand. Deal 7 damage for each card Exhausted.',
|
||||
upgrade: { effects: [{ op: 'fiendFire', amount: 10 }], text: 'Exhaust your entire hand. Deal 10 damage for each card Exhausted.' },
|
||||
},
|
||||
immolate: {
|
||||
id: 'immolate', name: 'Immolate', cls: 'warrior', type: 'attack', rarity: 'rare',
|
||||
cost: 2, target: 'all', effects: [{ op: 'damageAll', amount: 21 }, { op: 'addCardToDiscard', card: 'burn' }],
|
||||
text: 'Deal 21 damage to ALL enemies. Add a Burn to your discard pile.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 28 }, { op: 'addCardToDiscard', card: 'burn' }], text: 'Deal 28 damage to ALL enemies. Add a Burn to your discard pile.' },
|
||||
},
|
||||
impervious: {
|
||||
id: 'impervious', name: 'Impervious', cls: 'warrior', type: 'skill', rarity: 'rare',
|
||||
cost: 2, target: 'self', exhaust: true, effects: [{ op: 'block', amount: 30 }],
|
||||
text: 'Gain 30 Block. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 40 }], text: 'Gain 40 Block. Exhaust.' },
|
||||
},
|
||||
offering: {
|
||||
id: 'offering', name: 'Offering', cls: 'warrior', type: 'skill', rarity: 'rare',
|
||||
cost: 0, target: 'self', exhaust: true, effects: [{ op: 'loseHp', amount: 6 }, { op: 'energy', amount: 2 }, { op: 'draw', amount: 3 }],
|
||||
text: 'Lose 6 HP. Gain 2 Energy. Draw 3 cards. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'loseHp', amount: 6 }, { op: 'energy', amount: 2 }, { op: 'draw', amount: 5 }], text: 'Lose 6 HP. Gain 2 Energy. Draw 5 cards. Exhaust.' },
|
||||
},
|
||||
reaper: {
|
||||
id: 'reaper', name: 'Reaper', cls: 'warrior', type: 'attack', rarity: 'rare',
|
||||
cost: 2, target: 'all', exhaust: true, effects: [{ op: 'reaper', amount: 4 }],
|
||||
text: 'Deal 4 damage to ALL enemies. Heal HP equal to unblocked damage dealt. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'reaper', amount: 5 }], text: 'Deal 5 damage to ALL enemies. Heal HP equal to unblocked damage dealt. Exhaust.' },
|
||||
},
|
||||
|
||||
// —— Rogue common ——
|
||||
acrobatics: {
|
||||
id: 'acrobatics', name: 'Acrobatics', cls: 'rogue', type: 'skill', rarity: 'common',
|
||||
cost: 1, target: 'self', effects: [{ op: 'draw', amount: 3 }, { op: 'discard', amount: 1 }],
|
||||
text: 'Draw 3 cards. Discard 1 card.',
|
||||
upgrade: { effects: [{ op: 'draw', amount: 3 }], text: 'Draw 3 cards.' },
|
||||
},
|
||||
bane: {
|
||||
id: 'bane', name: 'Bane', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 7, bonusDamageIfPoisoned: 7 }],
|
||||
text: 'Deal 7 damage. If the enemy is Poisoned, deal 7 additional damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 10, bonusDamageIfPoisoned: 10 }], text: 'Deal 10 damage. If the enemy is Poisoned, deal 10 additional damage.' },
|
||||
},
|
||||
cloakdagger: {
|
||||
id: 'cloakdagger', name: 'Cloak and Dagger', cls: 'rogue', type: 'skill', rarity: 'common',
|
||||
cost: 1, target: 'self', effects: [{ op: 'block', amount: 6 }, { op: 'addCardToHand', card: 'shiv', amount: 1 }],
|
||||
text: 'Gain 6 Block. Add 1 Shiv to your hand.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 6 }, { op: 'addCardToHand', card: 'shiv', amount: 2 }], text: 'Gain 6 Block. Add 2 Shivs to your hand.' },
|
||||
},
|
||||
daggerspray: {
|
||||
id: 'daggerspray', name: 'Dagger Spray', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'all', effects: [{ op: 'damageAll', amount: 4, times: 2 }],
|
||||
text: 'Deal 4 damage to ALL enemies twice.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 6, times: 2 }], text: 'Deal 6 damage to ALL enemies twice.' },
|
||||
},
|
||||
deflect: {
|
||||
id: 'deflect', name: 'Deflect', cls: 'rogue', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'block', amount: 4 }],
|
||||
text: 'Gain 4 Block.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 7 }], text: 'Gain 7 Block.' },
|
||||
},
|
||||
flyingknee: {
|
||||
id: 'flyingknee', name: 'Flying Knee', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 8 }, { op: 'nextTurnEnergy', amount: 1 }],
|
||||
text: 'Deal 8 damage. Next turn, gain 1 Energy.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 11 }, { op: 'nextTurnEnergy', amount: 1 }], text: 'Deal 11 damage. Next turn, gain 1 Energy.' },
|
||||
},
|
||||
outmaneuver: {
|
||||
id: 'outmaneuver', name: 'Outmaneuver', cls: 'rogue', type: 'skill', rarity: 'common',
|
||||
cost: 1, target: 'self', exhaust: true, effects: [{ op: 'nextTurnEnergy', amount: 2 }],
|
||||
text: 'Next turn, gain 2 Energy. Exhaust.',
|
||||
upgrade: { cost: 0, text: 'Next turn, gain 2 Energy. Exhaust.' },
|
||||
},
|
||||
prepared: {
|
||||
id: 'prepared', name: 'Prepared', cls: 'rogue', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'draw', amount: 1 }, { op: 'discard', amount: 1 }],
|
||||
text: 'Draw 1 card. Discard 1 card.',
|
||||
upgrade: { effects: [{ op: 'draw', amount: 2 }, { op: 'discard', amount: 1 }], text: 'Draw 2 cards. Discard 1 card.' },
|
||||
},
|
||||
quickslash: {
|
||||
id: 'quickslash', name: 'Quick Slash', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 8 }, { op: 'draw', amount: 1 }],
|
||||
text: 'Deal 8 damage. Draw 1 card.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 12 }, { op: 'draw', amount: 1 }], text: 'Deal 12 damage. Draw 1 card.' },
|
||||
},
|
||||
slice: {
|
||||
id: 'slice', name: 'Slice', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'enemy', effects: [{ op: 'damage', amount: 6 }],
|
||||
text: 'Deal 6 damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 9 }], text: 'Deal 9 damage.' },
|
||||
},
|
||||
suckerpunch: {
|
||||
id: 'suckerpunch', name: 'Sucker Punch', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 7 }, { op: 'debuff', status: 'weak', amount: 1 }],
|
||||
text: 'Deal 7 damage. Apply 1 Weak.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 9 }, { op: 'debuff', status: 'weak', amount: 2 }], text: 'Deal 9 damage. Apply 2 Weak.' },
|
||||
},
|
||||
daggerthrow: {
|
||||
id: 'daggerthrow', name: 'Dagger Throw', cls: 'rogue', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 9 }, { op: 'draw', amount: 1 }, { op: 'discard', amount: 1 }],
|
||||
|
|
@ -223,6 +440,56 @@ export const CARDS = {
|
|||
text: 'Deal 3 damage to ALL enemies 3 times.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 4, times: 3 }], text: 'Deal 4 damage to ALL enemies 3 times.' },
|
||||
},
|
||||
// —— Rogue uncommon ——
|
||||
backstab: {
|
||||
id: 'backstab', name: 'Backstab', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 0, target: 'enemy', innate: true, exhaust: true, effects: [{ op: 'damage', amount: 11 }],
|
||||
text: 'Innate. Deal 11 damage. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 15 }], text: 'Innate. Deal 15 damage. Exhaust.' },
|
||||
},
|
||||
dash: {
|
||||
id: 'dash', name: 'Dash', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 2, target: 'enemy', effects: [{ op: 'block', amount: 10 }, { op: 'damage', amount: 10 }],
|
||||
text: 'Gain 10 Block. Deal 10 damage.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 13 }, { op: 'damage', amount: 13 }], text: 'Gain 13 Block. Deal 13 damage.' },
|
||||
},
|
||||
eviscerate: {
|
||||
id: 'eviscerate', name: 'Eviscerate', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 3, target: 'enemy', effects: [{ op: 'damage', amount: 7, times: 3 }],
|
||||
text: 'Deal 7 damage 3 times.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 9, times: 3 }], text: 'Deal 9 damage 3 times.' },
|
||||
},
|
||||
finisher: {
|
||||
id: 'finisher', name: 'Finisher', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'finisher', amount: 6 }],
|
||||
text: 'Deal 6 damage for each Attack played this turn.',
|
||||
upgrade: { effects: [{ op: 'finisher', amount: 8 }], text: 'Deal 8 damage for each Attack played this turn.' },
|
||||
},
|
||||
flechettes: {
|
||||
id: 'flechettes', name: 'Flechettes', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'flechettes', amount: 4 }],
|
||||
text: 'Deal 4 damage for each Skill card in your hand.',
|
||||
upgrade: { effects: [{ op: 'flechettes', amount: 6 }], text: 'Deal 6 damage for each Skill card in your hand.' },
|
||||
},
|
||||
legsweep: {
|
||||
id: 'legsweep', name: 'Leg Sweep', cls: 'rogue', type: 'skill', rarity: 'uncommon',
|
||||
cost: 2, target: 'enemy', effects: [{ op: 'debuff', status: 'weak', amount: 2 }, { op: 'block', amount: 11 }],
|
||||
text: 'Apply 2 Weak. Gain 11 Block.',
|
||||
upgrade: { effects: [{ op: 'debuff', status: 'weak', amount: 3 }, { op: 'block', amount: 14 }], text: 'Apply 3 Weak. Gain 14 Block.' },
|
||||
},
|
||||
noxiousfumes: {
|
||||
id: 'noxiousfumes', name: 'Noxious Fumes', cls: 'rogue', type: 'power', rarity: 'uncommon',
|
||||
cost: 1, target: 'self', effects: [{ op: 'noxiousFumes', amount: 2 }],
|
||||
text: 'At the start of each turn, apply 2 Poison to ALL enemies.',
|
||||
upgrade: { effects: [{ op: 'noxiousFumes', amount: 3 }], text: 'At the start of each turn, apply 3 Poison to ALL enemies.' },
|
||||
},
|
||||
riddlewithholes: {
|
||||
id: 'riddlewithholes', name: 'Riddle with Holes', cls: 'rogue', type: 'attack', rarity: 'uncommon',
|
||||
cost: 2, target: 'enemy', effects: [{ op: 'damage', amount: 3, times: 5 }],
|
||||
text: 'Deal 3 damage 5 times.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 4, times: 5 }], text: 'Deal 4 damage 5 times.' },
|
||||
},
|
||||
// —— Rogue rare ——
|
||||
crippling: {
|
||||
id: 'crippling', name: 'Crippling Cloud', cls: 'rogue', type: 'skill', rarity: 'rare',
|
||||
cost: 2, target: 'all', exhaust: true,
|
||||
|
|
@ -236,8 +503,86 @@ export const CARDS = {
|
|||
text: "Double an enemy's Poison. Exhaust.",
|
||||
upgrade: { effects: [{ op: 'multiplyPoison', factor: 3 }], text: "Triple an enemy's Poison. Exhaust." },
|
||||
},
|
||||
thousandcuts: {
|
||||
id: 'thousandcuts', name: 'A Thousand Cuts', cls: 'rogue', type: 'power', rarity: 'rare',
|
||||
cost: 2, target: 'self', effects: [{ op: 'thousandCuts', amount: 1 }],
|
||||
text: 'Whenever you play a card, deal 1 damage to ALL enemies.',
|
||||
upgrade: { effects: [{ op: 'thousandCuts', amount: 2 }], text: 'Whenever you play a card, deal 2 damage to ALL enemies.' },
|
||||
},
|
||||
adrenaline: {
|
||||
id: 'adrenaline', name: 'Adrenaline', cls: 'rogue', type: 'skill', rarity: 'rare',
|
||||
cost: 0, target: 'self', exhaust: true, effects: [{ op: 'energy', amount: 1 }, { op: 'draw', amount: 2 }],
|
||||
text: 'Gain 1 Energy. Draw 2 cards. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'energy', amount: 2 }, { op: 'draw', amount: 2 }], text: 'Gain 2 Energy. Draw 2 cards. Exhaust.' },
|
||||
},
|
||||
diedidie: {
|
||||
id: 'diedidie', name: 'Die Die Die', cls: 'rogue', type: 'attack', rarity: 'rare',
|
||||
cost: 1, target: 'all', exhaust: true, effects: [{ op: 'damageAll', amount: 13 }],
|
||||
text: 'Deal 13 damage to ALL enemies. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 17 }], text: 'Deal 17 damage to ALL enemies. Exhaust.' },
|
||||
},
|
||||
envenom: {
|
||||
id: 'envenom', name: 'Envenom', cls: 'rogue', type: 'power', rarity: 'rare',
|
||||
cost: 2, target: 'self', effects: [{ op: 'envenom', amount: 1 }],
|
||||
text: 'Whenever you deal unblocked Attack damage, apply 1 Poison.',
|
||||
upgrade: { effects: [{ op: 'envenom', amount: 2 }], text: 'Whenever you deal unblocked Attack damage, apply 2 Poison.' },
|
||||
},
|
||||
grandfinale: {
|
||||
id: 'grandfinale', name: 'Grand Finale', cls: 'rogue', type: 'attack', rarity: 'rare',
|
||||
cost: 0, target: 'all', exhaust: true, effects: [{ op: 'grandFinale', amount: 50 }],
|
||||
text: 'Can only be played when your draw pile is empty. Deal 50 damage to ALL enemies. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'grandFinale', amount: 60 }], text: 'Can only be played when your draw pile is empty. Deal 60 damage to ALL enemies. Exhaust.' },
|
||||
},
|
||||
|
||||
// —— Neutral / colorless ——
|
||||
shiv: {
|
||||
id: 'shiv', name: 'Shiv', cls: 'neutral', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'enemy', exhaust: true, effects: [{ op: 'damage', amount: 4 }],
|
||||
text: 'Deal 4 damage. Exhaust.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 6 }], text: 'Deal 6 damage. Exhaust.' },
|
||||
},
|
||||
bite: {
|
||||
id: 'bite', name: 'Bite', cls: 'neutral', type: 'attack', rarity: 'common',
|
||||
cost: 1, target: 'enemy', effects: [{ op: 'damage', amount: 7 }, { op: 'heal', amount: 2 }],
|
||||
text: 'Deal 7 damage. Heal 2 HP.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 8 }, { op: 'heal', amount: 3 }], text: 'Deal 8 damage. Heal 3 HP.' },
|
||||
},
|
||||
blind: {
|
||||
id: 'blind', name: 'Blind', cls: 'neutral', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'enemy', exhaust: true, effects: [{ op: 'debuff', status: 'weak', amount: 2 }],
|
||||
text: 'Apply 2 Weak. Exhaust.',
|
||||
upgrade: { exhaust: true, target: 'all', effects: [{ op: 'debuffAll', status: 'weak', amount: 2 }], text: 'Apply 2 Weak to ALL enemies. Exhaust.' },
|
||||
},
|
||||
deepbreath: {
|
||||
id: 'deepbreath', name: 'Deep Breath', cls: 'neutral', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'shuffleDiscardIntoDraw', draw: 1 }],
|
||||
text: 'Shuffle your discard pile into your draw pile. Draw 1 card.',
|
||||
upgrade: { effects: [{ op: 'shuffleDiscardIntoDraw', draw: 2 }], text: 'Shuffle your discard pile into your draw pile. Draw 2 cards.' },
|
||||
},
|
||||
dramaticEntrance: {
|
||||
id: 'dramaticEntrance', name: 'Dramatic Entrance', cls: 'neutral', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'all', innate: true, ethereal: true, effects: [{ op: 'damageAll', amount: 8 }],
|
||||
text: 'Innate. Ethereal. Deal 8 damage to ALL enemies.',
|
||||
upgrade: { effects: [{ op: 'damageAll', amount: 12 }], text: 'Innate. Ethereal. Deal 12 damage to ALL enemies.' },
|
||||
},
|
||||
finesse: {
|
||||
id: 'finesse', name: 'Finesse', cls: 'neutral', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'block', amount: 2 }, { op: 'draw', amount: 1 }],
|
||||
text: 'Gain 2 Block. Draw 1 card.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 4 }, { op: 'draw', amount: 1 }], text: 'Gain 4 Block. Draw 1 card.' },
|
||||
},
|
||||
goodinstincts: {
|
||||
id: 'goodinstincts', name: 'Good Instincts', cls: 'neutral', type: 'skill', rarity: 'common',
|
||||
cost: 0, target: 'self', effects: [{ op: 'block', amount: 6 }],
|
||||
text: 'Gain 6 Block.',
|
||||
upgrade: { effects: [{ op: 'block', amount: 9 }], text: 'Gain 9 Block.' },
|
||||
},
|
||||
swiftstrike: {
|
||||
id: 'swiftstrike', name: 'Swift Strike', cls: 'neutral', type: 'attack', rarity: 'common',
|
||||
cost: 0, target: 'enemy', effects: [{ op: 'damage', amount: 7 }],
|
||||
text: 'Deal 7 damage.',
|
||||
upgrade: { effects: [{ op: 'damage', amount: 10 }], text: 'Deal 10 damage.' },
|
||||
},
|
||||
bandage: {
|
||||
id: 'bandage', name: 'Bandage Up', cls: 'neutral', type: 'skill', rarity: 'uncommon',
|
||||
cost: 0, target: 'self', exhaust: true, effects: [{ op: 'heal', amount: 4 }],
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
this.view = 'classselect';
|
||||
this.pendingCard = null;
|
||||
this.pendingPotion = null;
|
||||
this._potionModal = null;
|
||||
this.animating = false;
|
||||
this.shop = null;
|
||||
this.activeEvent = null;
|
||||
|
|
@ -188,6 +189,7 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
|
||||
setView(v) {
|
||||
// Real view transitions clear any in-progress target selection.
|
||||
this.dismissPotionModal();
|
||||
this.view = v;
|
||||
this.pendingCard = null;
|
||||
this.pendingPotion = null;
|
||||
|
|
@ -471,9 +473,9 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
const c = this.add.circle(x, y, 26, 0x35204a).setStrokeStyle(2, 0xb05fd0).setInteractive({ useHandCursor: true });
|
||||
this.add2(c);
|
||||
this.add2(this.add.text(x, y, '⚗', { fontSize: '26px', color: '#d7a8ff' }).setOrigin(0.5));
|
||||
c.on('pointerover', () => this.showTip(x, y + 40, `${pot.name}: ${pot.desc}${inCombat ? ' (click to use)' : ''}`));
|
||||
c.on('pointerover', () => this.showTip(x, y + 40, `${pot.name}${inCombat ? ' (click to use)' : ''}`));
|
||||
c.on('pointerout', () => this.hideTip());
|
||||
if (inCombat) c.on('pointerdown', () => this.onUsePotion(idx, pid));
|
||||
if (inCombat) c.on('pointerdown', () => this.showPotionConfirm(idx, pid));
|
||||
}
|
||||
|
||||
onUsePotion(idx, pid) {
|
||||
|
|
@ -490,6 +492,73 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
this.afterAction();
|
||||
}
|
||||
|
||||
showPotionConfirm(idx, pid) {
|
||||
if (this.animating || this.combat.phase !== 'player') return;
|
||||
this.hideTip();
|
||||
this.dismissPotionModal();
|
||||
const pot = POTIONS[pid];
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
const W = 520, H = 290;
|
||||
const cont = this.add.container(0, 0).setDepth(115);
|
||||
this._potionModal = cont;
|
||||
const mv = (obj) => { cont.add(obj); return obj; };
|
||||
|
||||
// Dim backdrop — blocks input to game elements below
|
||||
mv(this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6).setInteractive()
|
||||
.on('pointerdown', () => this.dismissPotionModal()));
|
||||
|
||||
// Panel
|
||||
const bg = mv(this.add.graphics());
|
||||
bg.fillStyle(C.panel, 1); bg.fillRoundedRect(cx - W / 2, cy - H / 2, W, H, 18);
|
||||
bg.lineStyle(2, 0xb05fd0, 1); bg.strokeRoundedRect(cx - W / 2, cy - H / 2, W, H, 18);
|
||||
|
||||
// Icon + name
|
||||
mv(this.add.text(cx, cy - H / 2 + 44, '⚗', { fontSize: '44px' }).setOrigin(0.5));
|
||||
mv(this.add.text(cx, cy - H / 2 + 96, pot.name, {
|
||||
fontFamily: 'Righteous', fontSize: '30px', color: C.gold,
|
||||
}).setOrigin(0.5));
|
||||
|
||||
// Divider
|
||||
const div = mv(this.add.graphics());
|
||||
div.lineStyle(1, 0x3a2f4d, 0.9);
|
||||
div.lineBetween(cx - W / 2 + 40, cy - H / 2 + 124, cx + W / 2 - 40, cy - H / 2 + 124);
|
||||
|
||||
// Description
|
||||
mv(this.add.text(cx, cy - H / 2 + 138, pot.desc, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: C.ink,
|
||||
wordWrap: { width: W - 80 }, align: 'center',
|
||||
}).setOrigin(0.5, 0));
|
||||
|
||||
// — Use Potion button —
|
||||
const useX = cx - 110, useY = cy + H / 2 - 46;
|
||||
const useBg = mv(this.add.graphics());
|
||||
const drawUse = (h) => { useBg.clear(); useBg.fillStyle(h ? 0xffd66b : C.goldI, 1); useBg.fillRoundedRect(useX - 100, useY - 24, 200, 48, 9); };
|
||||
drawUse(false);
|
||||
mv(this.add.text(useX, useY, 'Use Potion', { fontFamily: '"Julius Sans One"', fontSize: '22px', color: '#17131d' }).setOrigin(0.5));
|
||||
const useHit = mv(this.add.rectangle(useX, useY, 200, 48, 0, 0).setInteractive({ useHandCursor: true }));
|
||||
useHit.on('pointerover', () => drawUse(true)).on('pointerout', () => drawUse(false))
|
||||
.on('pointerdown', () => { this.dismissPotionModal(); this.onUsePotion(idx, pid); });
|
||||
|
||||
// — Cancel button —
|
||||
const cxBtn = cx + 110, cyBtn = useY;
|
||||
const cancelBg = mv(this.add.graphics());
|
||||
const drawCancel = (h) => { cancelBg.clear(); cancelBg.lineStyle(2, h ? 0xb05fd0 : 0x3a2f4d, 1); cancelBg.strokeRoundedRect(cxBtn - 90, cyBtn - 24, 180, 48, 9); };
|
||||
drawCancel(false);
|
||||
const cancelTxt = mv(this.add.text(cxBtn, cyBtn, 'Cancel', { fontFamily: '"Julius Sans One"', fontSize: '22px', color: C.muted }).setOrigin(0.5));
|
||||
const cancelHit = mv(this.add.rectangle(cxBtn, cyBtn, 180, 48, 0, 0).setInteractive({ useHandCursor: true }));
|
||||
cancelHit.on('pointerover', () => { drawCancel(true); cancelTxt.setColor(C.ink); })
|
||||
.on('pointerout', () => { drawCancel(false); cancelTxt.setColor(C.muted); })
|
||||
.on('pointerdown', () => this.dismissPotionModal());
|
||||
|
||||
// Fade in
|
||||
cont.setAlpha(0);
|
||||
this.tweens.add({ targets: cont, alpha: 1, duration: 140 });
|
||||
}
|
||||
|
||||
dismissPotionModal() {
|
||||
if (this._potionModal) { this._potionModal.destroy(); this._potionModal = null; }
|
||||
}
|
||||
|
||||
renderEnemy(e, x, y) {
|
||||
if (!e.alive) {
|
||||
this.text(x, y, '☠', 60, '#5a4f6c', { ox: 0.5, oy: 0.5 });
|
||||
|
|
@ -1106,6 +1175,10 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
// Shake + colored neon outline + a type-specific signifier while the buff applies.
|
||||
animateEnemyBuff(e, sprite, home, done) {
|
||||
const cat = this.buffCategory(e);
|
||||
const effs = (e.intent && e.intent.move && e.intent.move.effects) || [];
|
||||
const debuffEffs = effs.filter(x => x.op === 'debuffPlayer' && x.status && STATUS[x.status]);
|
||||
const cardEffs = effs.filter(x => x.op === 'addCardToDiscard');
|
||||
|
||||
const statusBefore = this.captureStatuses();
|
||||
resolveEnemyMove(this.combat, e);
|
||||
this.flushStatusFx(statusBefore);
|
||||
|
|
@ -1114,7 +1187,99 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
this.neonOutline(home.x, home.y, cat.color, 860);
|
||||
this.spawnBuffSignifier(home.x, home.y, cat, sprite);
|
||||
this.shakeSprite(sprite, null, 7, 320);
|
||||
this.time.delayedCall(900, () => done());
|
||||
|
||||
debuffEffs.forEach((eff, i) => this.spawnEnemyDebuffLabel(home.x, home.y, eff.status, eff.amount, i));
|
||||
|
||||
// Animate the landing on the player.
|
||||
if (debuffEffs.length > 0 && this._playerPos) {
|
||||
// Stat drains (Lagavulin Siphon: −Strength, −Dexterity) → red damage-style floaters.
|
||||
debuffEffs.filter(x => x.amount < 0).forEach((eff, i) =>
|
||||
this.spawnStatDrainFx(eff.status, eff.amount, i));
|
||||
// Player visually reacts: hit frame + purple flash.
|
||||
this.setPlayerFrame(2);
|
||||
this.time.delayedCall(350, () => this.setPlayerFrame(0));
|
||||
this.spawnDebuffFlash(this._playerPos.x, this._playerPos.y);
|
||||
}
|
||||
|
||||
const cardDoneMs = this.spawnEnemyCardAdds(cardEffs, home.x, home.y);
|
||||
this.time.delayedCall(Math.max(900, cardDoneMs), () => done());
|
||||
}
|
||||
|
||||
// Red floater numbers for stat drains (e.g. "−1 Strength"), styled like HP damage numbers.
|
||||
spawnStatDrainFx(statusKey, delta, batchIdx = 0) {
|
||||
const sd = STATUS[statusKey]; if (!sd) return;
|
||||
const pp = this._playerPos || { x: 300, y: 635 };
|
||||
const cx = pp.x + (Math.random() * 40 - 20);
|
||||
const cy = pp.y - 80 - batchIdx * 46;
|
||||
const t = this.add.text(cx, cy, `${delta} ${sd.name}`, {
|
||||
fontFamily: 'Righteous', fontSize: '38px', color: '#ff6a6a',
|
||||
stroke: '#000000', strokeThickness: 5,
|
||||
}).setOrigin(0.5).setDepth(91);
|
||||
this.fxLayer.add(t);
|
||||
this.tweens.add({ targets: t, y: cy - 70, alpha: 0, duration: 850, ease: 'Cubic.easeOut', onComplete: () => t.destroy() });
|
||||
}
|
||||
|
||||
// Brief purple flash covering the player, signalling a debuff landed.
|
||||
spawnDebuffFlash(x, y) {
|
||||
const g = this.add.graphics().setDepth(86);
|
||||
g.fillStyle(0x9a3fcc, 0.38);
|
||||
g.fillRoundedRect(x - 110, y - 220, 220, 440, 20);
|
||||
this.fxLayer.add(g);
|
||||
this.tweens.add({ targets: g, alpha: 0, duration: 520, ease: 'Cubic.easeOut', onComplete: () => g.destroy() });
|
||||
}
|
||||
|
||||
// Float the debuff name in STATUS color above the casting enemy, then fade out.
|
||||
spawnEnemyDebuffLabel(cx, cy, statusKey, amount, idx) {
|
||||
const sd = STATUS[statusKey]; if (!sd) return;
|
||||
const isNegative = amount < 0;
|
||||
const label = isNegative ? `−${sd.name}` : sd.name;
|
||||
const colorStr = isNegative ? '#ff6a6a' : Phaser.Display.Color.IntegerToColor(sd.color).rgba;
|
||||
const startY = cy - 80 - idx * 52;
|
||||
const txt = this.add.text(cx, startY, label, {
|
||||
fontFamily: 'Righteous', fontSize: '40px', color: colorStr,
|
||||
stroke: '#000000', strokeThickness: 5,
|
||||
}).setOrigin(0.5).setDepth(93).setAlpha(0);
|
||||
this.fxLayer.add(txt);
|
||||
this.tweens.add({ targets: txt, alpha: 1, y: startY - 18, duration: 200, ease: 'Cubic.easeOut' });
|
||||
this.time.delayedCall(800, () => {
|
||||
this.tweens.add({ targets: txt, alpha: 0, y: startY - 70, duration: 420, ease: 'Cubic.easeIn', onComplete: () => txt.destroy() });
|
||||
});
|
||||
}
|
||||
|
||||
// Animate each added card floating above the enemy, then flying to its destination pile.
|
||||
// Returns the ms until the last card has landed (safe to call done() then).
|
||||
spawnEnemyCardAdds(cardEffs, ex, ey) {
|
||||
if (!cardEffs.length) return 0;
|
||||
const discardX = GAME_WIDTH - 120, discardY = 1000;
|
||||
const drawX = 120, drawY = 1000;
|
||||
let stagger = 0;
|
||||
let lastDoneMs = 0;
|
||||
for (const eff of cardEffs) {
|
||||
const amt = eff.amount || 1;
|
||||
const destX = eff.pile === 'draw' ? drawX : discardX;
|
||||
const destY = eff.pile === 'draw' ? drawY : discardY;
|
||||
for (let k = 0; k < amt; k++) {
|
||||
const delay = stagger;
|
||||
const inst = { id: eff.card, uid: `${eff.card}-enemy-anim-${k}`, upgraded: false };
|
||||
this.time.delayedCall(delay, () => {
|
||||
const card = this.makeCardSprite(ex, ey - 90, inst, 0.68, { parent: this.fxLayer });
|
||||
card.setDepth(88).setAlpha(0);
|
||||
this.tweens.add({ targets: card, alpha: 1, duration: 200 });
|
||||
this.time.delayedCall(750, () => {
|
||||
this.tweens.add({
|
||||
targets: card, x: destX, y: destY, scaleX: 0.22, scaleY: 0.22,
|
||||
duration: 600, ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
this.tweens.add({ targets: card, alpha: 0, duration: 750, onComplete: () => card.destroy() });
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
lastDoneMs = delay + 750 + 600; // up to landing (fade can overlap done)
|
||||
stagger += 220;
|
||||
}
|
||||
}
|
||||
return lastDoneMs;
|
||||
}
|
||||
|
||||
// Which kind of self-buff is the enemy doing? Drives color + signifier.
|
||||
|
|
@ -1290,18 +1455,25 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
// Compare before/after snapshots; spawn a text animation for each new or increased status.
|
||||
flushStatusFx(before) {
|
||||
const cb = this.combat;
|
||||
const check = (unit, unitKey, beforeSnap) => {
|
||||
const byUnit = {};
|
||||
const collect = (unit, unitKey, beforeSnap) => {
|
||||
for (const [key, val] of Object.entries(unit.statuses)) {
|
||||
const old = (beforeSnap || {})[key] || 0;
|
||||
if (val > old) this.spawnStatusFx(unitKey, key, unit);
|
||||
if (val > old) {
|
||||
if (!byUnit[unitKey]) byUnit[unitKey] = [];
|
||||
byUnit[unitKey].push({ key, unit });
|
||||
}
|
||||
}
|
||||
};
|
||||
check(cb.player, 'player', before.player);
|
||||
cb.enemies.forEach((e) => check(e, 'e' + e.slot, before['e' + e.slot]));
|
||||
collect(cb.player, 'player', before.player);
|
||||
cb.enemies.forEach((e) => collect(e, 'e' + e.slot, before['e' + e.slot]));
|
||||
for (const [unitKey, items] of Object.entries(byUnit)) {
|
||||
items.forEach(({ key, unit }, batchIdx) => this.spawnStatusFx(unitKey, key, unit, batchIdx));
|
||||
}
|
||||
}
|
||||
|
||||
// Animate a buff/debuff label appearing on a character, then shrinking to its status circle.
|
||||
spawnStatusFx(unitKey, statusKey, unit) {
|
||||
spawnStatusFx(unitKey, statusKey, unit, batchIdx = 0) {
|
||||
const sd = STATUS[statusKey]; if (!sd) return;
|
||||
const currentVal = unit.statuses[statusKey]; if (!currentVal) return;
|
||||
|
||||
|
|
@ -1317,6 +1489,9 @@ export default class SpireClimbGame extends Phaser.Scene {
|
|||
cx = ref.x; cy = ref.y - 60; baseX = ref.x - 90; baseY = ref.y + 142;
|
||||
}
|
||||
|
||||
// Stack multiple simultaneous labels upward so they don't overlap.
|
||||
cy -= batchIdx * 54;
|
||||
|
||||
// Destination: the slot this status occupies in the status row.
|
||||
const statusEntries = Object.keys(unit.statuses).filter((k) => unit.statuses[k] && STATUS[k]);
|
||||
const idx = Math.max(0, statusEntries.indexOf(statusKey));
|
||||
|
|
|
|||
|
|
@ -214,6 +214,14 @@ export function startCombat(run, enemyIds, seed) {
|
|||
run, // back-reference for persistence on combat end
|
||||
};
|
||||
applyCombatStartRelics(combat);
|
||||
// innate cards always start in the opening hand
|
||||
const innates = [];
|
||||
combat.draw = combat.draw.filter((inst) => {
|
||||
const c = resolvedCard(inst);
|
||||
if (c?.innate) { innates.push(inst); return false; }
|
||||
return true;
|
||||
});
|
||||
combat.hand.push(...innates);
|
||||
// choose initial intents
|
||||
combat.enemies.forEach((e) => chooseEnemyMove(combat, e));
|
||||
startPlayerTurn(combat, true);
|
||||
|
|
@ -243,10 +251,15 @@ function startPlayerTurn(combat, first = false) {
|
|||
const p = combat.player;
|
||||
combat.turn += 1;
|
||||
combat.phase = 'player';
|
||||
p.block = 0;
|
||||
if (!statusOf(p, 'barricade')) p.block = 0; // barricade: block carries over
|
||||
p.attacksThisTurn = 0;
|
||||
p.energy = p.maxEnergy + relicVal(combat, 'energy-core');
|
||||
// ritual / demon form, regen handled at relevant ends; strength-from-ritual is enemy-only here
|
||||
// flying knee / outmaneuver: bonus energy granted next turn
|
||||
const nte = statusOf(p, 'nextTurnEnergy');
|
||||
if (nte > 0) { p.energy += nte; delete p.statuses.nextTurnEnergy; }
|
||||
// noxious fumes: apply poison to all enemies at start of player turn
|
||||
const nf = statusOf(p, 'noxiousFumes');
|
||||
if (nf > 0) combat.enemies.filter((e) => e.alive).forEach((e) => addStatus(e, 'poison', nf));
|
||||
// turn-start poison on player
|
||||
tickPoison(combat, p, 'You');
|
||||
if (combat.phase === 'lost') return;
|
||||
|
|
@ -300,7 +313,8 @@ function dealRaw(combat, unit, amount, source) {
|
|||
// attack damage with modifiers (strength already added by caller for attacker)
|
||||
function dealAttack(combat, attacker, target, baseAmount, opts = {}) {
|
||||
let dmg = baseAmount;
|
||||
// attacker strength
|
||||
// attacker strength (strengthMul > 1 for heavy-blade-style cards)
|
||||
if (attacker) dmg += statusOf(attacker, 'strength') * ((opts.strengthMul || 1) - 1);
|
||||
if (attacker) dmg += statusOf(attacker, 'strength');
|
||||
// weak on attacker
|
||||
if (attacker && statusOf(attacker, 'weak') > 0) dmg = Math.floor(dmg * 0.75);
|
||||
|
|
@ -314,6 +328,11 @@ function dealAttack(combat, attacker, target, baseAmount, opts = {}) {
|
|||
if (target === combat.player && dealt > 0 && hasRelic(combat, 'bronze-scales') && attacker && attacker !== combat.player && attacker.alive) {
|
||||
dealRaw(combat, attacker, relicVal(combat, 'bronze-scales'), 'thorns');
|
||||
}
|
||||
// envenom: player attacks apply poison to enemies
|
||||
if (attacker === combat.player && target !== combat.player && target.alive) {
|
||||
const env = statusOf(combat.player, 'envenom');
|
||||
if (env > 0) addStatus(target, 'poison', env);
|
||||
}
|
||||
return dealt;
|
||||
}
|
||||
|
||||
|
|
@ -350,18 +369,20 @@ export function isCombatOver(combat) {
|
|||
|
||||
// Cards that can legally be played right now (cost affordable, playable).
|
||||
export function playableHand(combat) {
|
||||
return combat.hand.filter((inst) => {
|
||||
const c = resolvedCard(inst);
|
||||
if (c.unplayable) return false;
|
||||
return combat.player.energy >= Math.max(0, c.cost);
|
||||
});
|
||||
return combat.hand.filter((inst) => canPlay(combat, inst));
|
||||
}
|
||||
|
||||
export function canPlay(combat, inst) {
|
||||
const c = resolvedCard(inst);
|
||||
if (!c || c.unplayable) return false;
|
||||
if (combat.phase !== 'player') return false;
|
||||
return combat.player.energy >= Math.max(0, c.cost);
|
||||
if (combat.player.energy < Math.max(0, c.cost)) return false;
|
||||
// clash: only playable if every other card in hand is an attack
|
||||
if (c.clashOnly) {
|
||||
const others = combat.hand.filter((h) => h !== inst);
|
||||
if (!others.every((h) => { const hc = resolvedCard(h); return hc && hc.type === 'attack'; })) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Play a card from hand. targetSlot = enemy slot index (for enemy-target cards).
|
||||
|
|
@ -374,13 +395,18 @@ export function playCard(combat, inst, targetSlot = null) {
|
|||
combat.player.energy -= Math.max(0, c.cost);
|
||||
|
||||
const target = targetSlot != null ? combat.enemies[targetSlot] : pickDefaultTarget(combat);
|
||||
resolveEffects(combat, c.effects || [], { isCard: true, cardType: c.type, target });
|
||||
resolveEffects(combat, c.effects || [], { isCard: true, cardType: c.type, target, selfCardId: c.id });
|
||||
|
||||
// exhaust vs discard
|
||||
if (c.exhaust) combat.exhaust.push(inst);
|
||||
else combat.discard.push(inst);
|
||||
|
||||
// post-resolution death checks already handled inside deal*
|
||||
// thousand cuts: deal N damage to all enemies after every card played
|
||||
const cuts = statusOf(combat.player, 'thousandCuts');
|
||||
if (cuts > 0 && combat.phase === 'player') {
|
||||
combat.enemies.filter((e) => e.alive).forEach((e) => dealRaw(combat, e, cuts, 'thousand cuts'));
|
||||
}
|
||||
|
||||
if (combat.phase !== 'lost' && combat.enemies.every((e) => !e.alive) && combat.phase !== 'won') winCombat(combat);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -444,8 +470,10 @@ function resolveEffects(combat, effects, ctx) {
|
|||
const tgt = ctx.target && ctx.target.alive ? ctx.target : pickDefaultTarget(combat);
|
||||
for (let i = 0; i < times; i++) {
|
||||
if (!tgt || !tgt.alive) break;
|
||||
let amount = e.amount;
|
||||
if (e.bonusDamageIfPoisoned && tgt && statusOf(tgt, 'poison') > 0) amount += e.bonusDamageIfPoisoned;
|
||||
countAttack(combat, p, isPlayerSource);
|
||||
dealAttack(combat, attacker, tgt, e.amount, { source: 'card', penNibDouble: isPlayerSource && penNibTriggers(combat, p) });
|
||||
dealAttack(combat, attacker, tgt, amount, { source: 'card', penNibDouble: isPlayerSource && penNibTriggers(combat, p), strengthMul: e.strengthMul });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -479,7 +507,91 @@ function resolveEffects(combat, effects, ctx) {
|
|||
case 'healPct': p.hp = Math.min(p.maxHp, p.hp + Math.floor(p.maxHp * e.amount)); break;
|
||||
case 'loseHp': dealRaw(combat, p, e.amount, 'self'); break;
|
||||
case 'doubleStrength': p.statuses.strength = statusOf(p, 'strength') * 2; if (!p.statuses.strength) delete p.statuses.strength; break;
|
||||
case 'addCardToDiscard': for (let i = 0; i < (e.amount || 1); i++) combat.discard.push(cardInstance(e.card)); break;
|
||||
case 'addCardToDiscard': {
|
||||
const cardId = e.card === '__self__' ? ctx.selfCardId : e.card;
|
||||
if (cardId) for (let i = 0; i < (e.amount || 1); i++) combat.discard.push(cardInstance(cardId));
|
||||
break;
|
||||
}
|
||||
case 'addCardToHand': {
|
||||
for (let i = 0; i < (e.amount || 1); i++) combat.hand.push(cardInstance(e.card));
|
||||
break;
|
||||
}
|
||||
case 'doubleBlock': p.block = p.block * 2; break;
|
||||
case 'loseEnemyStrength':
|
||||
if (ctx.target && ctx.target.alive) addStatus(ctx.target, 'strength', -e.amount);
|
||||
break;
|
||||
case 'tempStrength':
|
||||
addStatus(p, 'strength', e.amount);
|
||||
addStatus(p, 'tempStrength', e.amount);
|
||||
break;
|
||||
case 'barricade': addStatus(p, 'barricade', 1); break;
|
||||
case 'noxiousFumes': addStatus(p, 'noxiousFumes', e.amount); break;
|
||||
case 'envenom': addStatus(p, 'envenom', e.amount); break;
|
||||
case 'thousandCuts': addStatus(p, 'thousandCuts', e.amount); break;
|
||||
case 'nextTurnEnergy': addStatus(p, 'nextTurnEnergy', e.amount); break;
|
||||
case 'recycleDiscard':
|
||||
if (combat.discard.length > 0) combat.draw.unshift(combat.discard.pop());
|
||||
break;
|
||||
case 'shuffleDiscardIntoDraw':
|
||||
combat.draw = combat.rng.shuffle([...combat.draw, ...combat.discard]);
|
||||
combat.discard = [];
|
||||
if (e.draw) drawCards(combat, e.draw);
|
||||
break;
|
||||
case 'secondWind': {
|
||||
const nonAttacks = combat.hand.filter((inst) => { const c = resolvedCard(inst); return c && c.type !== 'attack'; });
|
||||
const count = nonAttacks.length;
|
||||
for (const inst of nonAttacks) { const i = combat.hand.indexOf(inst); if (i >= 0) { combat.hand.splice(i, 1); combat.exhaust.push(inst); } }
|
||||
gainBlock(p, (e.amount || 5) * count, true);
|
||||
break;
|
||||
}
|
||||
case 'fiendFire': {
|
||||
const handCopy = [...combat.hand];
|
||||
combat.hand = [];
|
||||
for (const inst of handCopy) combat.exhaust.push(inst);
|
||||
const tgt = ctx.target && ctx.target.alive ? ctx.target : pickDefaultTarget(combat);
|
||||
for (let i = 0; i < handCopy.length; i++) {
|
||||
if (!tgt || !tgt.alive) break;
|
||||
countAttack(combat, p, true);
|
||||
dealAttack(combat, p, tgt, e.amount || 7, { source: 'card' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'reaper': {
|
||||
let healed = 0;
|
||||
combat.enemies.filter((en) => en.alive).forEach((en) => {
|
||||
countAttack(combat, p, true);
|
||||
healed += dealAttack(combat, p, en, e.amount, { source: 'card' });
|
||||
});
|
||||
if (healed > 0) p.hp = Math.min(p.maxHp, p.hp + healed);
|
||||
break;
|
||||
}
|
||||
case 'finisher': {
|
||||
const tgt = ctx.target && ctx.target.alive ? ctx.target : pickDefaultTarget(combat);
|
||||
if (tgt && tgt.alive && p.attacksThisTurn > 0) {
|
||||
countAttack(combat, p, true);
|
||||
dealAttack(combat, p, tgt, (e.amount || 6) * p.attacksThisTurn, { source: 'card' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'flechettes': {
|
||||
const skillCount = combat.hand.filter((inst) => { const hc = resolvedCard(inst); return hc && hc.type === 'skill'; }).length;
|
||||
const tgt = ctx.target && ctx.target.alive ? ctx.target : pickDefaultTarget(combat);
|
||||
for (let i = 0; i < skillCount; i++) {
|
||||
if (!tgt || !tgt.alive) break;
|
||||
countAttack(combat, p, true);
|
||||
dealAttack(combat, p, tgt, e.amount || 4, { source: 'card' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'grandFinale': {
|
||||
if (combat.draw.length === 0) {
|
||||
combat.enemies.filter((en) => en.alive).forEach((en) => {
|
||||
countAttack(combat, p, true);
|
||||
dealAttack(combat, p, en, e.amount || 50, { source: 'card' });
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
if (combat.phase === 'lost' || combat.phase === 'won') break;
|
||||
|
|
@ -510,9 +622,11 @@ function penNibTriggers(combat, p) {
|
|||
export function endTurn(combat) {
|
||||
if (combat.phase !== 'player') return;
|
||||
const p = combat.player;
|
||||
// end-of-turn: metallicize, regen, burn cards in hand
|
||||
// end-of-turn: metallicize, regen, tempStrength, burn cards in hand
|
||||
if (statusOf(p, 'metallicize') > 0) p.block += statusOf(p, 'metallicize');
|
||||
if (statusOf(p, 'regen') > 0) { p.hp = Math.min(p.maxHp, p.hp + statusOf(p, 'regen')); addStatus(p, 'regen', -1); }
|
||||
const ts = statusOf(p, 'tempStrength');
|
||||
if (ts > 0) { addStatus(p, 'strength', -ts); delete p.statuses.tempStrength; }
|
||||
for (const inst of combat.hand) { const c = resolvedCard(inst); if (c.endTurnSelfDamage) dealRaw(combat, p, c.endTurnSelfDamage, 'burn'); }
|
||||
if (combat.phase === 'lost') return;
|
||||
// ethereal cards exhaust at end of turn; the rest discard
|
||||
|
|
@ -559,6 +673,8 @@ export function beginEnemyPhase(combat) {
|
|||
const p = combat.player;
|
||||
if (statusOf(p, 'metallicize') > 0) p.block += statusOf(p, 'metallicize');
|
||||
if (statusOf(p, 'regen') > 0) { p.hp = Math.min(p.maxHp, p.hp + statusOf(p, 'regen')); addStatus(p, 'regen', -1); }
|
||||
const ts = statusOf(p, 'tempStrength');
|
||||
if (ts > 0) { addStatus(p, 'strength', -ts); delete p.statuses.tempStrength; }
|
||||
for (const inst of combat.hand) { const c = resolvedCard(inst); if (c.endTurnSelfDamage) dealRaw(combat, p, c.endTurnSelfDamage, 'burn'); }
|
||||
if (combat.phase === 'lost') return;
|
||||
for (const inst of combat.hand) { const c = resolvedCard(inst); if (c.ethereal) combat.exhaust.push(inst); else combat.discard.push(inst); }
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ on the battlefield cleanly — but an opaque square also works.
|
|||
|---|---|
|
||||
| **Path** | `public/assets/images/spireclimb-cards.png` |
|
||||
| **Frame size** | **250 × 160 px** (aspect ≈ 1.565 : 1, matches the card art window) |
|
||||
| **Sheet size** | your choice — e.g. **a 6 × 6 grid = 1500 × 960 px** holds all 34 frames |
|
||||
| **Sheet size** | **10 columns × 10 rows = 2500 × 1600 px** holds all 91 frames (9 spare) |
|
||||
| **Status** | ⛔ Procedural until you create it (then set `cardSheet.path` in the JSON) |
|
||||
| **JSON** | `cardSheet` (set `path`) + `cards` map |
|
||||
|
||||
|
|
@ -84,52 +84,129 @@ card). Art is scaled to **fit** that window (no cropping), so anything at the
|
|||
rectangular art is fine; design it to read at small size (the window is ~144×92
|
||||
on a resting hand card, ~266×170 during the play-to-center zoom).
|
||||
|
||||
You don't have to fill all 34 frames at once — any card without art (or any
|
||||
frame index missing from the `cards` map) simply stays procedural.
|
||||
You don't have to fill all 91 frames at once — any card without art (or any
|
||||
frame index missing from the `cards` map) simply stays procedural. Paint the
|
||||
most iconic/frequently-seen cards first (starters and commons).
|
||||
|
||||
### Frame map
|
||||
|
||||
Class legend: **N** = neutral, **W** = Warrior, **R** = Rogue, **S** = status.
|
||||
Rarity legend: **str** = starter, **com** = common, **unc** = uncommon, **rare** = rare.
|
||||
|
||||
| Frame | id | Name | Class | Type |
|
||||
|---:|---|---|---|---|
|
||||
| 0 | `strike` | Strike | N | attack |
|
||||
| 1 | `defend` | Defend | N | skill |
|
||||
| 2 | `bash` | Bash | W | attack |
|
||||
| 3 | `neutralize` | Neutralize | R | attack |
|
||||
| 4 | `survivor` | Survivor | R | skill |
|
||||
| 5 | `ironwave` | Iron Wave | W | attack |
|
||||
| 6 | `pommelstrike` | Pommel Strike | W | attack |
|
||||
| 7 | `cleave` | Cleave | W | attack |
|
||||
| 8 | `shrugitoff` | Shrug It Off | W | skill |
|
||||
| 9 | `clothesline` | Clothesline | W | attack |
|
||||
| 10 | `inflame` | Inflame | W | power |
|
||||
| 11 | `uppercut` | Uppercut | W | attack |
|
||||
| 12 | `ghostlyarmor` | Ghostly Armor | W | skill |
|
||||
| 13 | `bodyslam` | Body Slam | W | attack |
|
||||
| 14 | `metallicize` | Metallicize | W | power |
|
||||
| 15 | `whirlwind` | Whirlwind | W | attack |
|
||||
| 16 | `limitbreak` | Limit Break | W | skill |
|
||||
| 17 | `demonform` | Demon Form | W | power |
|
||||
| 18 | `daggerthrow` | Dagger Throw | R | attack |
|
||||
| 19 | `poisonedstab` | Poisoned Stab | R | attack |
|
||||
| 20 | `deadlypoison` | Deadly Poison | R | skill |
|
||||
| 21 | `backflip` | Backflip | R | skill |
|
||||
| 22 | `sneakyhit` | Sneaky Hit | R | attack |
|
||||
| 23 | `footwork` | Footwork | R | power |
|
||||
| 24 | `bladedance` | Blade Dance | R | attack |
|
||||
| 25 | `caltrops` | Caltrops | R | power |
|
||||
| 26 | `bouncingblade` | Bouncing Blade | R | attack |
|
||||
| 27 | `crippling` | Crippling Cloud | R | skill |
|
||||
| 28 | `catalyst` | Catalyst | R | skill |
|
||||
| 29 | `bandage` | Bandage Up | N | skill |
|
||||
| 30 | `flashofsteel` | Flash of Steel | N | attack |
|
||||
| 31 | `wound` | Wound | S | status (clutter) |
|
||||
| 32 | `dazed` | Dazed | S | status (clutter) |
|
||||
| 33 | `burn` | Burn | S | status (clutter) |
|
||||
#### Original cards (frames 0–33)
|
||||
|
||||
> The three status cards (31–33) are unplayable junk cards enemies shuffle into
|
||||
> your deck — give them a grimy/cursed look. Lowest priority.
|
||||
| Frame | id | Name | Class | Type | Rarity |
|
||||
|---:|---|---|---|---|---|
|
||||
| 0 | `strike` | Strike | N | attack | str |
|
||||
| 1 | `defend` | Defend | N | skill | str |
|
||||
| 2 | `bash` | Bash | W | attack | str |
|
||||
| 3 | `neutralize` | Neutralize | R | attack | str |
|
||||
| 4 | `survivor` | Survivor | R | skill | str |
|
||||
| 5 | `ironwave` | Iron Wave | W | attack | com |
|
||||
| 6 | `pommelstrike` | Pommel Strike | W | attack | com |
|
||||
| 7 | `cleave` | Cleave | W | attack | com |
|
||||
| 8 | `shrugitoff` | Shrug It Off | W | skill | com |
|
||||
| 9 | `clothesline` | Clothesline | W | attack | com |
|
||||
| 10 | `inflame` | Inflame | W | power | unc |
|
||||
| 11 | `uppercut` | Uppercut | W | attack | unc |
|
||||
| 12 | `ghostlyarmor` | Ghostly Armor | W | skill | unc |
|
||||
| 13 | `bodyslam` | Body Slam | W | attack | unc |
|
||||
| 14 | `metallicize` | Metallicize | W | power | unc |
|
||||
| 15 | `whirlwind` | Whirlwind | W | attack | unc |
|
||||
| 16 | `limitbreak` | Limit Break | W | skill | rare |
|
||||
| 17 | `demonform` | Demon Form | W | power | rare |
|
||||
| 18 | `daggerthrow` | Dagger Throw | R | attack | com |
|
||||
| 19 | `poisonedstab` | Poisoned Stab | R | attack | com |
|
||||
| 20 | `deadlypoison` | Deadly Poison | R | skill | com |
|
||||
| 21 | `backflip` | Backflip | R | skill | com |
|
||||
| 22 | `sneakyhit` | Sneaky Hit | R | attack | com |
|
||||
| 23 | `footwork` | Footwork | R | power | unc |
|
||||
| 24 | `bladedance` | Blade Dance | R | attack | unc |
|
||||
| 25 | `caltrops` | Caltrops | R | power | unc |
|
||||
| 26 | `bouncingblade` | Bouncing Blade | R | attack | unc |
|
||||
| 27 | `crippling` | Crippling Cloud | R | skill | rare |
|
||||
| 28 | `catalyst` | Catalyst | R | skill | rare |
|
||||
| 29 | `bandage` | Bandage Up | N | skill | unc |
|
||||
| 30 | `flashofsteel` | Flash of Steel | N | attack | com |
|
||||
| 31 | `wound` | Wound | S | status | — |
|
||||
| 32 | `dazed` | Dazed | S | status | — |
|
||||
| 33 | `burn` | Burn | S | status | — |
|
||||
|
||||
> Status cards (31–33) are unplayable junk enemies shuffle into your deck — give them a grimy/cursed look. Lowest priority.
|
||||
|
||||
#### New Warrior cards (frames 34–58)
|
||||
|
||||
| Frame | id | Name | Class | Type | Rarity | Notes |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| 34 | `anger` | Anger | W | attack | com | deal dmg, copy self to discard |
|
||||
| 35 | `clash` | Clash | W | attack | com | 0-cost big hit, only if all attacks in hand |
|
||||
| 36 | `flex` | Flex | W | skill | com | gain temp Strength |
|
||||
| 37 | `headbutt` | Headbutt | W | attack | com | deal dmg, recycle top discard |
|
||||
| 38 | `heavyblade` | Heavy Blade | W | attack | com | big hit, Strength ×3 |
|
||||
| 39 | `twinstrike` | Twin Strike | W | attack | com | 5 dmg × 2 |
|
||||
| 40 | `thunderclap` | Thunder Clap | W | attack | com | 4 to all + Vulnerable to all |
|
||||
| 41 | `wildstrike` | Wild Strike | W | attack | com | 12 dmg, add Wound to draw |
|
||||
| 42 | `bloodletting` | Bloodletting | W | skill | unc | lose 3 HP, gain 2 Energy |
|
||||
| 43 | `carnage` | Carnage | W | attack | unc | 20 dmg, ethereal |
|
||||
| 44 | `disarm` | Disarm | W | skill | unc | enemy loses 2 Strength, exhaust |
|
||||
| 45 | `entrench` | Entrench | W | skill | unc | double current Block |
|
||||
| 46 | `hemokinesis` | Hemokinesis | W | attack | unc | lose 2 HP, deal 15 dmg |
|
||||
| 47 | `intimidate` | Intimidate | W | skill | unc | 1 Weak to all, exhaust |
|
||||
| 48 | `pummel` | Pummel | W | attack | unc | 2 dmg × 4, exhaust |
|
||||
| 49 | `secondwind` | Second Wind | W | skill | unc | exhaust non-attacks, 5 block each |
|
||||
| 50 | `seeingred` | Seeing Red | W | skill | unc | gain 2 Energy, exhaust |
|
||||
| 51 | `shockwave` | Shockwave | W | skill | unc | 3 Vuln + 3 Weak to all, exhaust |
|
||||
| 52 | `barricade` | Barricade | W | power | rare | block no longer expires |
|
||||
| 53 | `bludgeon` | Bludgeon | W | attack | rare | 32 dmg |
|
||||
| 54 | `fiendfire` | Fiend Fire | W | attack | rare | exhaust hand, 7 dmg per card |
|
||||
| 55 | `immolate` | Immolate | W | attack | rare | 21 to all + Burn to discard |
|
||||
| 56 | `impervious` | Impervious | W | skill | rare | 30 block, exhaust |
|
||||
| 57 | `offering` | Offering | W | skill | rare | lose 6 HP, +2 energy, draw 3, exhaust |
|
||||
| 58 | `reaper` | Reaper | W | attack | rare | 4 to all, heal unblocked dmg, exhaust |
|
||||
|
||||
#### New Rogue cards (frames 59–82)
|
||||
|
||||
| Frame | id | Name | Class | Type | Rarity | Notes |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| 59 | `acrobatics` | Acrobatics | R | skill | com | draw 3, discard 1 |
|
||||
| 60 | `bane` | Bane | R | attack | com | 7 dmg, +7 if target is poisoned |
|
||||
| 61 | `cloakdagger` | Cloak and Dagger | R | skill | com | 6 block, add Shiv to hand |
|
||||
| 62 | `daggerspray` | Dagger Spray | R | attack | com | 4 to all × 2 |
|
||||
| 63 | `deflect` | Deflect | R | skill | com | 4 block |
|
||||
| 64 | `flyingknee` | Flying Knee | R | attack | com | 8 dmg, next turn +1 energy |
|
||||
| 65 | `outmaneuver` | Outmaneuver | R | skill | com | next turn +2 energy, exhaust |
|
||||
| 66 | `prepared` | Prepared | R | skill | com | draw 1, discard 1 |
|
||||
| 67 | `quickslash` | Quick Slash | R | attack | com | 8 dmg, draw 1 |
|
||||
| 68 | `slice` | Slice | R | attack | com | 6 dmg, free |
|
||||
| 69 | `suckerpunch` | Sucker Punch | R | attack | com | 7 dmg, 1 Weak |
|
||||
| 70 | `backstab` | Backstab | R | attack | unc | innate, 11 dmg, exhaust |
|
||||
| 71 | `dash` | Dash | R | attack | unc | 10 block + 10 dmg |
|
||||
| 72 | `eviscerate` | Eviscerate | R | attack | unc | 7 dmg × 3 |
|
||||
| 73 | `finisher` | Finisher | R | attack | unc | 6 dmg per attack played this turn |
|
||||
| 74 | `flechettes` | Flechettes | R | attack | unc | 4 dmg per skill in hand |
|
||||
| 75 | `legsweep` | Leg Sweep | R | skill | unc | 2 Weak + 11 block |
|
||||
| 76 | `noxiousfumes` | Noxious Fumes | R | power | unc | apply 2 poison to all each turn |
|
||||
| 77 | `riddlewithholes` | Riddle with Holes | R | attack | unc | 3 dmg × 5 |
|
||||
| 78 | `thousandcuts` | A Thousand Cuts | R | power | rare | deal 1 to all per card played |
|
||||
| 79 | `adrenaline` | Adrenaline | R | skill | rare | +1 energy, draw 2, exhaust |
|
||||
| 80 | `diedidie` | Die Die Die | R | attack | rare | 13 to all, exhaust |
|
||||
| 81 | `envenom` | Envenom | R | power | rare | attacks apply 1 Poison |
|
||||
| 82 | `grandfinale` | Grand Finale | R | attack | rare | 50 to all if draw empty, exhaust |
|
||||
|
||||
#### New Colorless / Neutral cards (frames 83–90)
|
||||
|
||||
| Frame | id | Name | Class | Type | Rarity | Notes |
|
||||
|---:|---|---|---|---|---|---|
|
||||
| 83 | `shiv` | Shiv | N | attack | com | 4 dmg, exhaust; also generated by Cloak and Dagger |
|
||||
| 84 | `bite` | Bite | N | attack | com | 7 dmg, heal 2 HP |
|
||||
| 85 | `blind` | Blind | N | skill | com | 2 Weak to target (upgraded: all), exhaust |
|
||||
| 86 | `deepbreath` | Deep Breath | N | skill | com | shuffle discard into draw, draw 1 |
|
||||
| 87 | `dramaticEntrance` | Dramatic Entrance | N | attack | com | innate, ethereal, 8 to all |
|
||||
| 88 | `finesse` | Finesse | N | skill | com | 2 block, draw 1 |
|
||||
| 89 | `goodinstincts` | Good Instincts | N | skill | com | 6 block |
|
||||
| 90 | `swiftstrike` | Swift Strike | N | attack | com | 7 dmg, free |
|
||||
|
||||
> Frames 91–99 are spare capacity in the 10×10 grid.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -168,9 +245,15 @@ gradient.
|
|||
## Quick checklist
|
||||
|
||||
- [ ] `spireclimb-creatures.png` — 1800×1200, 24 × (300×300). *(placeholder exists; replace to taste)*
|
||||
- [ ] `spireclimb-cards.png` — 250×160 frames, 34 cards; then set `cardSheet.path` in `spireclimb-artwork.json`.
|
||||
- [ ] `spireclimb-cards.png` — 2500×1600 (10×10 grid), 250×160 per frame, 91 frames (0–90); then set `cardSheet.path` in `spireclimb-artwork.json`.
|
||||
- Priority 1 (starters + highest-frequency): frames 0–9 (Strike, Defend, Bash, Neutralize, Survivor + 5 warrior commons)
|
||||
- Priority 2 (remaining commons): frames 10–22, 29–30, 59–69, 83–90
|
||||
- Priority 3 (uncommons): frames 23–28, 42–51, 70–77
|
||||
- Priority 4 (rares): frames 52–58, 78–82
|
||||
- Priority 5 (status clutter, lowest): frames 31–33
|
||||
- [ ] `game-icons.png` frame 74 — 44×44 menu icon.
|
||||
|
||||
After dropping in `spireclimb-cards.png`, set its `path` (and confirm
|
||||
`frameWidth`/`frameHeight`) in `public/data/spireclimb-artwork.json`. The
|
||||
creature sheet path is already set; the icon needs no config.
|
||||
After dropping in `spireclimb-cards.png`, the `cardSheet.path` is already set in
|
||||
`public/data/spireclimb-artwork.json` — the frame→id mapping is also already
|
||||
there for all 91 cards. The creature sheet path is already set; the icon needs
|
||||
no config.
|
||||
|
|
|
|||
Loading…
Reference in New Issue