feat: Implement Siege mechanic and add Fullscreen toggle

- **Combat Logic**: Changed `siege` skill trigger from `on_attack` to `preBattle`.
- **Engine Updates**: Modified `_collectPreBattleBuffs` (renamed to `_collectPreBattleFires`) in `CombatEngine.js` to capture and log siege damage events separately from buffs.
- **Skill Processor**: Updated `SkillProcessor.js` to record `siegeTarget` and `siegeDamage` in the context for event logging.
- **Data Updates**: Adjusted descriptions and triggers in `cards.json` and `skills.json`.
- **UI**: Added a "Toggle Fullscreen" button to the Main Menu with state-aware labeling.
This commit is contained in:
Brian Fertig 2026-03-13 17:08:30 -06:00
parent 315b3ddccd
commit 031264bbb9
7 changed files with 61 additions and 26 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -348,7 +348,7 @@
{ {
"name": "siege", "name": "siege",
"value": 2, "value": 2,
"trigger": "on_attack" "trigger": "preBattle"
} }
], ],
"flavorText": "Leave nothing standing. Take everything else.", "flavorText": "Leave nothing standing. Take everything else.",
@ -413,7 +413,7 @@
{ {
"name": "siege", "name": "siege",
"value": 5, "value": 5,
"trigger": "on_attack" "trigger": "preBattle"
}, },
{ {
"name": "strike", "name": "strike",

View File

@ -31,8 +31,8 @@
}, },
{ {
"name": "siege", "name": "siege",
"description": "Deal value damage directly to the enemy commander, ignoring lane cards.", "description": "Before battle: launch siege missiles directly at the enemy commander for value damage (armor applies).",
"trigger": "on_attack", "trigger": "preBattle",
"category": "offense" "category": "offense"
}, },
{ {

View File

@ -337,24 +337,29 @@ export class CombatEngine {
return [...this.events]; return [...this.events];
} }
// Process preBattle skills for a set of cards and return buff descriptors. // Process preBattle skills for a set of cards and return buff + siege fire descriptors.
_collectPreBattleBuffs(cards, allies, enemies) { _collectPreBattleFires(cards, allies, enemies, enemyCommander) {
const buffs = []; const buffs = [];
const siegeFires = [];
const liveAllies = allies.filter(c => c.currentHP > 0); const liveAllies = allies.filter(c => c.currentHP > 0);
const liveEnemies = enemies.filter(c => c.currentHP > 0); const liveEnemies = enemies.filter(c => c.currentHP > 0);
for (const card of cards) { for (const card of cards) {
if (card.currentHP <= 0) continue; if (card.currentHP <= 0) continue;
for (const s of card.skills) { for (const s of card.skills) {
if (s.trigger === 'preBattle') { if (s.trigger !== 'preBattle') continue;
const ctx = { rng: this.rng }; const ctx = { rng: this.rng, enemyCommander };
this.skillProcessor.process(s, card, null, liveAllies, liveEnemies, ctx); this.skillProcessor.process(s, card, null, liveAllies, liveEnemies, ctx);
if (ctx.rallyTarget) { if (s.name === 'rally' && ctx.rallyTarget) {
buffs.push({ skill: 'rally', source: card, target: ctx.rallyTarget, amount: s.value }); buffs.push({ skill: 'rally', source: card, target: ctx.rallyTarget, amount: s.value });
} }
if (s.name === 'siege' && ctx.siegeTarget) {
const hpBefore = ctx.siegeTarget.currentHP + ctx.siegeDamage;
siegeFires.push({ skill: 'siege', source: card, target: ctx.siegeTarget, damage: ctx.siegeDamage, hpBefore });
this._log(`${card.name} siege hits ${ctx.siegeTarget.name} for ${ctx.siegeDamage}`);
} }
} }
} }
return buffs; return { buffs, siegeFires };
} }
// Emit the 8-step pre-battle sequence and process preBattle skills. // Emit the 8-step pre-battle sequence and process preBattle skills.
@ -369,24 +374,24 @@ export class CombatEngine {
const otherAllies = [otherCmd, ...otherLanes]; const otherAllies = [otherCmd, ...otherLanes];
// Steps 12: commander defensive buffs (placeholder — no defensive skills yet) // Steps 12: commander defensive buffs (placeholder — no defensive skills yet)
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: firstSide, card: firstCmd, buffs: [], siegeFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'commander', side: otherSide, card: otherCmd, buffs: [], siegeFires: [] });
// Steps 34: commander offensive buffs // Steps 34: commander offensive buffs
const firstCmdBuffs = this._collectPreBattleBuffs([firstCmd], firstAllies, otherAllies); const firstCmdFires = this._collectPreBattleFires([firstCmd], firstAllies, otherAllies, otherCmd);
const otherCmdBuffs = this._collectPreBattleBuffs([otherCmd], otherAllies, firstAllies); const otherCmdFires = this._collectPreBattleFires([otherCmd], otherAllies, firstAllies, firstCmd);
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: firstSide, card: firstCmd, buffs: firstCmdBuffs }); this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: firstSide, card: firstCmd, buffs: firstCmdFires.buffs, siegeFires: firstCmdFires.siegeFires });
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, buffs: otherCmdBuffs }); this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, buffs: otherCmdFires.buffs, siegeFires: otherCmdFires.siegeFires });
// Steps 56: lane defensive buffs (placeholder) // Steps 56: lane defensive buffs (placeholder)
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [] });
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [] }); this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: [], siegeFires: [] });
// Steps 78: lane offensive buffs // Steps 78: lane offensive buffs + siege fires
const firstLaneBuffs = this._collectPreBattleBuffs(firstLanes, firstAllies, otherAllies); const firstLaneFires = this._collectPreBattleFires(firstLanes, firstAllies, otherAllies, otherCmd);
const otherLaneBuffs = this._collectPreBattleBuffs(otherLanes, otherAllies, firstAllies); const otherLaneFires = this._collectPreBattleFires(otherLanes, otherAllies, firstAllies, firstCmd);
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: firstLaneBuffs }); this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: firstLaneFires.buffs, siegeFires: firstLaneFires.siegeFires });
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: otherLaneBuffs }); this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: otherLaneFires.buffs, siegeFires: otherLaneFires.siegeFires });
} }
// Build the ordered list of attacks for this turn (called by beginCommit). // Build the ordered list of attacks for this turn (called by beginCommit).

View File

@ -46,6 +46,8 @@ export class SkillProcessor {
if (context.enemyCommander) { if (context.enemyCommander) {
const dmg = Math.max(0, skill.value - Math.max(0, context.enemyCommander.currentArmor)); const dmg = Math.max(0, skill.value - Math.max(0, context.enemyCommander.currentArmor));
context.enemyCommander.currentHP -= dmg; context.enemyCommander.currentHP -= dmg;
context.siegeTarget = context.enemyCommander;
context.siegeDamage = dmg;
} }
} }

View File

@ -50,6 +50,10 @@ export class MainMenuScene extends Phaser.Scene {
this.scene.start(btn.scene, btn.data || {}); this.scene.start(btn.scene, btn.data || {});
}); });
}); });
// Fullscreen toggle — utility button, visually distinct
const fsY = startY + buttons.length * spacing + 20;
this._makeFullscreenButton(width / 2, fsY);
} }
_makeButton(x, y, label, callback) { _makeButton(x, y, label, callback) {
@ -67,4 +71,28 @@ export class MainMenuScene extends Phaser.Scene {
return { bg, text }; return { bg, text };
} }
_makeFullscreenButton(x, y) {
const getLabel = () => this.scale.isFullscreen ? '⛶ Exit Fullscreen' : '⛶ Toggle Fullscreen';
const bg = this.add.rectangle(x, y, 320, 50, 0x1a2a1a)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x448844);
const text = this.add.text(x, y, getLabel(), {
fontSize: '20px', color: '#88cc88'
}).setOrigin(0.5);
bg.on('pointerover', () => bg.setFillStyle(0x2a3f2a));
bg.on('pointerout', () => bg.setFillStyle(0x1a2a1a));
bg.on('pointerdown', () => {
if (this.scale.isFullscreen) {
this.scale.stopFullscreen();
} else {
this.scale.startFullscreen();
}
// Update label after a short delay to let the state change
this.time.delayedCall(100, () => text.setText(getLabel()));
});
}
} }