feat(combat): implement alternating initiative with enhanced attack animations
- Added `playerGoesFirst` state to `CombatEngine` to alternate turn order each round. - Updated `beginTurn` and `beginCommit` to handle AI deployment based on the current initiative (opponent deploys first if they have initiative, otherwise after the player). - Modified `_buildPendingAttacks` to respect the alternating attack order. - Added `animateHPLoss` to `CardObject.js` for a visual HP reduction animation similar to the Berserk gain effect. - Enhanced `_animateAttack` in `BattleScene.js`: - Integrated sprite animations for attacks (gun turret) and explosions using new spritesheets. - Synchronized HP loss and Berserk gain animations with the attack sprite completion. - Added an "Attacks First" indicator icon next to the commander of the side with initiative. - Updated `BootScene.js` to load the new `attacksFirst` UI image and `attacks` spritesheet.
This commit is contained in:
parent
73ce826100
commit
c7f4dc9bbb
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
|
|
@ -28,6 +28,7 @@ export class CombatEngine {
|
||||||
this.log = [];
|
this.log = [];
|
||||||
this.winner = null; // 'player' | 'opponent' | null
|
this.winner = null; // 'player' | 'opponent' | null
|
||||||
this.events = []; // events for animation: [{type, data}]
|
this.events = []; // events for animation: [{type, data}]
|
||||||
|
this.playerGoesFirst = true; // alternates each turn
|
||||||
}
|
}
|
||||||
|
|
||||||
_log(msg) {
|
_log(msg) {
|
||||||
|
|
@ -260,22 +261,24 @@ export class CombatEngine {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: draw cards, AI deploys, return player's hand for selection
|
// Phase 1: draw cards, AI deploys (only when opponent goes first), return player's hand
|
||||||
beginTurn() {
|
beginTurn() {
|
||||||
if (this.winner) return { hand: [], canDeploy: false };
|
if (this.winner) return { hand: [], canDeploy: false, playerGoesFirst: this.playerGoesFirst };
|
||||||
this.events = [];
|
this.events = [];
|
||||||
this.turn++;
|
this.turn++;
|
||||||
this._log(`--- Turn ${this.turn} ---`);
|
this._log(`--- Turn ${this.turn} ---`);
|
||||||
this._drawPhase();
|
this._drawPhase();
|
||||||
// AI deploys its card immediately
|
// AI deploys immediately only on opponent-first turns
|
||||||
|
if (!this.playerGoesFirst) {
|
||||||
const aiCard = this.ai.getNextCard();
|
const aiCard = this.ai.getNextCard();
|
||||||
if (aiCard && this.opponentLanes.length < MAX_LANES) {
|
if (aiCard && this.opponentLanes.length < MAX_LANES) {
|
||||||
this.opponentLanes.push(aiCard);
|
this.opponentLanes.push(aiCard);
|
||||||
this._log(`Opponent deploys ${aiCard.name}`);
|
this._log(`Opponent deploys ${aiCard.name}`);
|
||||||
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
|
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const canDeploy = this.playerHand.length > 0 && this.playerLanes.length < MAX_LANES;
|
const canDeploy = this.playerHand.length > 0 && this.playerLanes.length < MAX_LANES;
|
||||||
return { hand: [...this.playerHand], canDeploy };
|
return { hand: [...this.playerHand], canDeploy, playerGoesFirst: this.playerGoesFirst };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: deploy chosen card (or null to pass), then resolve the turn
|
// Phase 2: deploy chosen card (or null to pass), then resolve the turn
|
||||||
|
|
@ -314,40 +317,48 @@ export class CombatEngine {
|
||||||
this.events.push({ type: 'deploy', side: 'player', card: chosenCard });
|
this.events.push({ type: 'deploy', side: 'player', card: chosenCard });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// On player-first turns, AI deploys after the player
|
||||||
|
if (this.playerGoesFirst) {
|
||||||
|
const aiCard = this.ai.getNextCard();
|
||||||
|
if (aiCard && this.opponentLanes.length < MAX_LANES) {
|
||||||
|
this.opponentLanes.push(aiCard);
|
||||||
|
this._log(`Opponent deploys ${aiCard.name}`);
|
||||||
|
this.events.push({ type: 'deploy', side: 'opponent', card: aiCard });
|
||||||
|
}
|
||||||
|
}
|
||||||
this._activationPhase();
|
this._activationPhase();
|
||||||
this._commanderSkillPhase();
|
this._commanderSkillPhase();
|
||||||
this._rupturePhase();
|
this._rupturePhase();
|
||||||
this._jamPhase();
|
this._jamPhase();
|
||||||
this._buildPendingAttacks();
|
this._buildPendingAttacks();
|
||||||
|
this.playerGoesFirst = !this.playerGoesFirst; // flip initiative for next turn
|
||||||
return [...this.events];
|
return [...this.events];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the ordered list of attacks for this turn (called by beginCommit).
|
// Build the ordered list of attacks for this turn (called by beginCommit).
|
||||||
|
// Attack order respects the current initiative (playerGoesFirst, before it is flipped).
|
||||||
_buildPendingAttacks() {
|
_buildPendingAttacks() {
|
||||||
this._pendingAttacks = [];
|
this._pendingAttacks = [];
|
||||||
for (let i = 0; i < this.playerLanes.length; i++) {
|
const addAttacks = (attackerLanes, enemyLanes, enemyCommander, alliedLanes, side) => {
|
||||||
const attacker = this.playerLanes[i];
|
for (let i = 0; i < attackerLanes.length; i++) {
|
||||||
|
const attacker = attackerLanes[i];
|
||||||
if (attacker.currentHP <= 0 || attacker.currentDelay > 0 || attacker.jamTurns > 0) continue;
|
if (attacker.currentHP <= 0 || attacker.currentDelay > 0 || attacker.jamTurns > 0) continue;
|
||||||
this._pendingAttacks.push({
|
this._pendingAttacks.push({
|
||||||
attacker,
|
attacker,
|
||||||
target: this.opponentLanes[i] || null,
|
target: enemyLanes[i] || null,
|
||||||
alliedLanes: this.playerLanes,
|
alliedLanes,
|
||||||
enemyLanes: this.opponentLanes,
|
enemyLanes,
|
||||||
enemyCommander: this.opponentCommander,
|
enemyCommander,
|
||||||
side: 'player'
|
side
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.opponentLanes.length; i++) {
|
};
|
||||||
const attacker = this.opponentLanes[i];
|
if (this.playerGoesFirst) {
|
||||||
if (attacker.currentHP <= 0 || attacker.currentDelay > 0 || attacker.jamTurns > 0) continue;
|
addAttacks(this.playerLanes, this.opponentLanes, this.opponentCommander, this.playerLanes, 'player');
|
||||||
this._pendingAttacks.push({
|
addAttacks(this.opponentLanes, this.playerLanes, this.playerCommander, this.opponentLanes, 'opponent');
|
||||||
attacker,
|
} else {
|
||||||
target: this.playerLanes[i] || null,
|
addAttacks(this.opponentLanes, this.playerLanes, this.playerCommander, this.opponentLanes, 'opponent');
|
||||||
alliedLanes: this.opponentLanes,
|
addAttacks(this.playerLanes, this.opponentLanes, this.opponentCommander, this.playerLanes, 'player');
|
||||||
enemyLanes: this.playerLanes,
|
|
||||||
enemyCommander: this.playerCommander,
|
|
||||||
side: 'opponent'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,65 @@ export class CardObject extends Phaser.GameObjects.Container {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animateHPLoss(damage, onComplete) {
|
||||||
|
if (!this.hpText || !this.scene) { if (onComplete) onComplete(); return; }
|
||||||
|
|
||||||
|
const w = this.options.width || 80;
|
||||||
|
const s = w / 80;
|
||||||
|
const h = this.options.height || 110;
|
||||||
|
const bannerH = Math.round(h * 0.12);
|
||||||
|
const bottomBannerCY = h / 2 - bannerH / 2;
|
||||||
|
|
||||||
|
// Rewind to pre-damage value so animation transitions old → new
|
||||||
|
this.hpText.setText(`${Math.max(0, this.cardData.currentHP + damage)}`);
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.hpText,
|
||||||
|
scaleX: 2, scaleY: 2,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Back.Out',
|
||||||
|
onComplete: () => {
|
||||||
|
if (!this.scene) { if (onComplete) onComplete(); return; }
|
||||||
|
|
||||||
|
const lossText = this.scene.add.text(
|
||||||
|
this.hpText.x + this.hpText.width * 2 + Math.round(4 * s),
|
||||||
|
bottomBannerCY,
|
||||||
|
`-${damage}`,
|
||||||
|
{
|
||||||
|
fontSize: `${Math.round(11 * s)}px`,
|
||||||
|
color: '#ff4444',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: Math.max(1, Math.round(2 * s))
|
||||||
|
}
|
||||||
|
).setOrigin(0, 0.5).setDepth(50);
|
||||||
|
this.add(lossText);
|
||||||
|
|
||||||
|
this.scene.time.delayedCall(500, () => {
|
||||||
|
if (!this.scene) { if (onComplete) onComplete(); return; }
|
||||||
|
this.hpText.setText(`${Math.max(0, this.cardData.currentHP)}`);
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: this.hpText,
|
||||||
|
scaleX: 1, scaleY: 1,
|
||||||
|
duration: 200,
|
||||||
|
ease: 'Power2'
|
||||||
|
});
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: lossText,
|
||||||
|
alpha: 0,
|
||||||
|
y: bottomBannerCY - Math.round(14 * s),
|
||||||
|
duration: 300,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
if (lossText.scene) lossText.destroy();
|
||||||
|
if (onComplete) onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
flash(color = 0xff4444) {
|
flash(color = 0xff4444) {
|
||||||
const factionColor = FACTION_COLORS[this.cardData.faction] || 0x444444;
|
const factionColor = FACTION_COLORS[this.cardData.faction] || 0x444444;
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,24 @@ export class BattleScene extends Phaser.Scene {
|
||||||
g.destroy();
|
g.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sprite animations (global — skip if already registered from a previous battle)
|
||||||
|
if (!this.anims.exists('attack_anim')) {
|
||||||
|
this.anims.create({
|
||||||
|
key: 'attack_anim',
|
||||||
|
frames: this.anims.generateFrameNumbers('attacks', { start: 0, end: 4 }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.anims.exists('explosion_anim')) {
|
||||||
|
this.anims.create({
|
||||||
|
key: 'explosion_anim',
|
||||||
|
frames: this.anims.generateFrameNumbers('attacks', { start: 5, end: 8 }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
this.add.rectangle(width / 2, height / 2, width, height, 0x1a1a2e);
|
this.add.rectangle(width / 2, height / 2, width, height, 0x1a1a2e);
|
||||||
|
|
||||||
|
|
@ -118,6 +136,8 @@ export class BattleScene extends Phaser.Scene {
|
||||||
this.autoTimer = null;
|
this.autoTimer = null;
|
||||||
this.waitingForPick = false;
|
this.waitingForPick = false;
|
||||||
this.isAnimating = false;
|
this.isAnimating = false;
|
||||||
|
this.playerGoesFirst = true;
|
||||||
|
this.initiativeIndicator = null;
|
||||||
|
|
||||||
const autoBtn = this.add.rectangle(1680, 35, 180, 44, 0x224422)
|
const autoBtn = this.add.rectangle(1680, 35, 180, 44, 0x224422)
|
||||||
.setInteractive({ useHandCursor: true })
|
.setInteractive({ useHandCursor: true })
|
||||||
|
|
@ -173,18 +193,16 @@ export class BattleScene extends Phaser.Scene {
|
||||||
if (this.waitingForPick || this.isAnimating) return;
|
if (this.waitingForPick || this.isAnimating) return;
|
||||||
if (this.engine.winner) { this._showResult(); return; }
|
if (this.engine.winner) { this._showResult(); return; }
|
||||||
|
|
||||||
// Snapshot which opponent cards are already on the field before this turn
|
// Snapshot opponent lanes before this turn (only matters on opponent-first turns)
|
||||||
const oldOpponentIds = new Set(
|
const oldOpponentIds = new Set(
|
||||||
this.engine.getState().opponent.lanes.map(c => c.instanceId)
|
this.engine.getState().opponent.lanes.map(c => c.instanceId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { hand, canDeploy } = this.engine.beginTurn();
|
const { hand, canDeploy, playerGoesFirst } = this.engine.beginTurn();
|
||||||
|
this.playerGoesFirst = playerGoesFirst;
|
||||||
this.turnText.setText(`Turn ${this.engine.turn}`);
|
this.turnText.setText(`Turn ${this.engine.turn}`);
|
||||||
this._renderState();
|
this._renderState();
|
||||||
|
this._updateInitiativeIndicator(playerGoesFirst);
|
||||||
// Find the newly deployed opponent card (if any)
|
|
||||||
const newOpponentCard = this.engine.getState().opponent.lanes
|
|
||||||
.find(c => !oldOpponentIds.has(c.instanceId));
|
|
||||||
|
|
||||||
const proceedAfterDeploy = () => {
|
const proceedAfterDeploy = () => {
|
||||||
if (canDeploy && !this.autoPlay) {
|
if (canDeploy && !this.autoPlay) {
|
||||||
|
|
@ -196,6 +214,12 @@ export class BattleScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// On opponent-first turns, show enemy deploy animation before showing picker.
|
||||||
|
// On player-first turns, opponent hasn't deployed yet — go straight to picker.
|
||||||
|
const newOpponentCard = !playerGoesFirst
|
||||||
|
? this.engine.getState().opponent.lanes.find(c => !oldOpponentIds.has(c.instanceId))
|
||||||
|
: null;
|
||||||
|
|
||||||
if (newOpponentCard) {
|
if (newOpponentCard) {
|
||||||
this.isAnimating = true;
|
this.isAnimating = true;
|
||||||
const cardObj = this.cardObjects.get(newOpponentCard.instanceId);
|
const cardObj = this.cardObjects.get(newOpponentCard.instanceId);
|
||||||
|
|
@ -214,29 +238,48 @@ export class BattleScene extends Phaser.Scene {
|
||||||
this.isAnimating = true;
|
this.isAnimating = true;
|
||||||
this.statusText.setText('Deploying...');
|
this.statusText.setText('Deploying...');
|
||||||
|
|
||||||
// Snapshot player lanes before deploy so we can detect the new card
|
// Snapshot both sides before deploy so we can detect new cards
|
||||||
const oldPlayerIds = new Set(
|
const oldPlayerIds = new Set(
|
||||||
this.engine.getState().player.lanes.map(c => c.instanceId)
|
this.engine.getState().player.lanes.map(c => c.instanceId)
|
||||||
);
|
);
|
||||||
|
const oldOpponentIds = new Set(
|
||||||
|
this.engine.getState().opponent.lanes.map(c => c.instanceId)
|
||||||
|
);
|
||||||
|
|
||||||
// Deploy + pre-attack phases (NO damage applied yet)
|
// Deploy + pre-attack phases (NO damage applied yet)
|
||||||
|
// On player-first turns, beginCommit also deploys the AI card
|
||||||
this.engine.beginCommit(chosenCard);
|
this.engine.beginCommit(chosenCard);
|
||||||
|
|
||||||
// Render the field with the newly deployed card(s) but pre-combat HP
|
// Render the field with all newly deployed cards but pre-combat HP
|
||||||
this._renderState();
|
this._renderState();
|
||||||
|
|
||||||
const newPlayerCard = chosenCard
|
const newPlayerCard = chosenCard
|
||||||
? this.engine.getState().player.lanes.find(c => !oldPlayerIds.has(c.instanceId))
|
? this.engine.getState().player.lanes.find(c => !oldPlayerIds.has(c.instanceId))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// On player-first turns the AI deployed inside beginCommit — animate it after player
|
||||||
|
const newOpponentCard = this.playerGoesFirst
|
||||||
|
? this.engine.getState().opponent.lanes.find(c => !oldOpponentIds.has(c.instanceId))
|
||||||
|
: null;
|
||||||
|
|
||||||
const startAttacks = () => this.time.delayedCall(200, () => this._processNextAttackStep());
|
const startAttacks = () => this.time.delayedCall(200, () => this._processNextAttackStep());
|
||||||
|
|
||||||
|
const animateOpponentThenAttack = () => {
|
||||||
|
if (newOpponentCard) {
|
||||||
|
const cardObj = this.cardObjects.get(newOpponentCard.instanceId);
|
||||||
|
this.statusText.setText(`Enemy deploys: ${newOpponentCard.name}`);
|
||||||
|
this._animateDeploy(cardObj, startAttacks);
|
||||||
|
} else {
|
||||||
|
startAttacks();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (newPlayerCard) {
|
if (newPlayerCard) {
|
||||||
const cardObj = this.cardObjects.get(newPlayerCard.instanceId);
|
const cardObj = this.cardObjects.get(newPlayerCard.instanceId);
|
||||||
this.statusText.setText(`You deploy: ${newPlayerCard.name}`);
|
this.statusText.setText(`You deploy: ${newPlayerCard.name}`);
|
||||||
this._animateDeploy(cardObj, startAttacks);
|
this._animateDeploy(cardObj, animateOpponentThenAttack);
|
||||||
} else {
|
} else {
|
||||||
startAttacks();
|
animateOpponentThenAttack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,27 +322,10 @@ export class BattleScene extends Phaser.Scene {
|
||||||
const berserkEvent = events.find(e => e.type === 'berserk');
|
const berserkEvent = events.find(e => e.type === 'berserk');
|
||||||
|
|
||||||
this._animateAttack(attackEvent, () => {
|
this._animateAttack(attackEvent, () => {
|
||||||
// Refresh / kill defender
|
|
||||||
this._refreshCardAfterAttack(attackEvent.defender);
|
this._refreshCardAfterAttack(attackEvent.defender);
|
||||||
|
|
||||||
const proceed = () => {
|
|
||||||
this._refreshCardAfterAttack(attackEvent.attacker);
|
this._refreshCardAfterAttack(attackEvent.attacker);
|
||||||
this.time.delayedCall(220, () => this._processNextAttackStep());
|
this.time.delayedCall(220, () => this._processNextAttackStep());
|
||||||
};
|
}, berserkEvent);
|
||||||
|
|
||||||
if (berserkEvent) {
|
|
||||||
const cardObj = this.cardObjects.get(berserkEvent.card.instanceId)
|
|
||||||
?? this.commanderObjects?.get(berserkEvent.card.instanceId);
|
|
||||||
if (cardObj) {
|
|
||||||
// Rewind ATK text to pre-gain value so animation transitions old → new
|
|
||||||
cardObj.atkText.setText(`${berserkEvent.card.currentAttack - berserkEvent.gain}`);
|
|
||||||
cardObj.animateBerserkGain(berserkEvent.gain, proceed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proceed();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update a card's HP bar and stats at the moment of impact, then handle death.
|
// Update a card's HP bar and stats at the moment of impact, then handle death.
|
||||||
|
|
@ -311,6 +337,13 @@ export class BattleScene extends Phaser.Scene {
|
||||||
obj.refresh(); // updates HP bar + stats text to current values
|
obj.refresh(); // updates HP bar + stats text to current values
|
||||||
// Commanders never get destroyed — the win-check handles game-over
|
// Commanders never get destroyed — the win-check handles game-over
|
||||||
if (cardData.currentHP <= 0 && !obj.isCommander) {
|
if (cardData.currentHP <= 0 && !obj.isCommander) {
|
||||||
|
// Explosion sprite centered on the card
|
||||||
|
const explosionSprite = this.add.sprite(obj.x, obj.y, 'attacks')
|
||||||
|
.setDisplaySize(630, 630)
|
||||||
|
.setDepth(23);
|
||||||
|
explosionSprite.play('explosion_anim');
|
||||||
|
explosionSprite.once('animationcomplete', () => { if (explosionSprite.scene) explosionSprite.destroy(); });
|
||||||
|
|
||||||
this.cardObjects.delete(cardData.instanceId);
|
this.cardObjects.delete(cardData.instanceId);
|
||||||
this.tweens.killTweensOf(obj);
|
this.tweens.killTweensOf(obj);
|
||||||
obj.setScale(1).setDepth(0);
|
obj.setScale(1).setDepth(0);
|
||||||
|
|
@ -318,7 +351,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
targets: obj,
|
targets: obj,
|
||||||
scaleX: 1.25, scaleY: 1.25,
|
scaleX: 1.25, scaleY: 1.25,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
duration: 480,
|
duration: 400,
|
||||||
ease: 'Power2',
|
ease: 'Power2',
|
||||||
onComplete: () => { if (obj.scene) obj.destroy(); }
|
onComplete: () => { if (obj.scene) obj.destroy(); }
|
||||||
});
|
});
|
||||||
|
|
@ -350,7 +383,7 @@ export class BattleScene extends Phaser.Scene {
|
||||||
// Animate a single attack.
|
// Animate a single attack.
|
||||||
// Cards slide to center (attacker left, defender right), scale up, hold while
|
// Cards slide to center (attacker left, defender right), scale up, hold while
|
||||||
// particle burst + shake + damage number play, then scale down and slide back.
|
// particle burst + shake + damage number play, then scale down and slide back.
|
||||||
_animateAttack(event, onComplete) {
|
_animateAttack(event, onComplete, berserkEvent = null) {
|
||||||
const SCALE = 1.78;
|
const SCALE = 1.78;
|
||||||
const SCALE_UP_MS = 280;
|
const SCALE_UP_MS = 280;
|
||||||
const HOLD_MS = 3100;
|
const HOLD_MS = 3100;
|
||||||
|
|
@ -443,11 +476,24 @@ export class BattleScene extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Attacker: particle burst — fires once card has arrived at center ──────
|
// ── Attacker: attack sprite + particle burst — fires once card arrives at center ──
|
||||||
if (attackerObj) {
|
if (attackerObj) {
|
||||||
const enlargedH = 364 * SCALE;
|
const enlargedH = 364 * SCALE;
|
||||||
this.time.delayedCall(SCALE_UP_MS, () => {
|
this.time.delayedCall(SCALE_UP_MS, () => {
|
||||||
if (!attackerObj.scene) return;
|
if (!attackerObj.scene) return;
|
||||||
|
|
||||||
|
// Gun turret animation centered on the attacking card
|
||||||
|
const attackSprite = this.add.sprite(
|
||||||
|
attackerDestX,
|
||||||
|
centerY,
|
||||||
|
'attacks'
|
||||||
|
).setDisplaySize(480, 480).setDepth(22);
|
||||||
|
attackSprite.play('attack_anim');
|
||||||
|
attackSprite.once('animationcomplete', () => {
|
||||||
|
if (attackSprite.scene) attackSprite.destroy();
|
||||||
|
this._onAttackReconcile(event, berserkEvent);
|
||||||
|
});
|
||||||
|
|
||||||
const emitter = this.add.particles(attackerDestX, centerY, 'particle_dot', {
|
const emitter = this.add.particles(attackerDestX, centerY, 'particle_dot', {
|
||||||
speed: { min: 90, max: 300 },
|
speed: { min: 90, max: 300 },
|
||||||
scale: { start: 1.8, end: 0 },
|
scale: { start: 1.8, end: 0 },
|
||||||
|
|
@ -516,6 +562,24 @@ export class BattleScene extends Phaser.Scene {
|
||||||
this.time.delayedCall(ATTACK_MS, onComplete);
|
this.time.delayedCall(ATTACK_MS, onComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fires when the attack animation ends — attach all post-attack consequences here.
|
||||||
|
_onAttackReconcile(event, berserkEvent) {
|
||||||
|
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
|
||||||
|
const defenderObj = _lookup(event.defender?.instanceId);
|
||||||
|
const attackerObj = _lookup(event.attacker?.instanceId);
|
||||||
|
|
||||||
|
// HP loss animation on defender
|
||||||
|
if (defenderObj?.scene && event.damage > 0) {
|
||||||
|
defenderObj.animateHPLoss(event.damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Berserk ATK gain animation on attacker
|
||||||
|
if (berserkEvent && attackerObj?.scene) {
|
||||||
|
attackerObj.atkText.setText(`${berserkEvent.card.currentAttack - berserkEvent.gain}`);
|
||||||
|
attackerObj.animateBerserkGain(berserkEvent.gain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_showCardPicker(hand) {
|
_showCardPicker(hand) {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
|
@ -704,6 +768,20 @@ export class BattleScene extends Phaser.Scene {
|
||||||
backBtn.on('pointerdown', () => this.scene.start(this.missionData ? 'CampaignScene' : 'MainMenuScene'));
|
backBtn.on('pointerdown', () => this.scene.start(this.missionData ? 'CampaignScene' : 'MainMenuScene'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateInitiativeIndicator(playerGoesFirst) {
|
||||||
|
if (this.initiativeIndicator) {
|
||||||
|
this.initiativeIndicator.destroy();
|
||||||
|
this.initiativeIndicator = null;
|
||||||
|
}
|
||||||
|
if (!this.textures.exists('attacksFirst')) return;
|
||||||
|
// Position the indicator to the left of the commander card (commander at x=150, width=240)
|
||||||
|
const indicatorX = 22;
|
||||||
|
const indicatorY = playerGoesFirst ? 715 : 305;
|
||||||
|
this.initiativeIndicator = this.add.image(indicatorX, indicatorY, 'attacksFirst')
|
||||||
|
.setDisplaySize(44, 44)
|
||||||
|
.setDepth(5);
|
||||||
|
}
|
||||||
|
|
||||||
_makeBackButton() {
|
_makeBackButton() {
|
||||||
const bg = this.add.rectangle(80, 35, 180, 44, 0x333333)
|
const bg = this.add.rectangle(80, 35, 180, 44, 0x333333)
|
||||||
.setInteractive({ useHandCursor: true })
|
.setInteractive({ useHandCursor: true })
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ export class BootScene extends Phaser.Scene {
|
||||||
preload() {
|
preload() {
|
||||||
// Load all JSON data files
|
// Load all JSON data files
|
||||||
this.load.image('vs', 'assets/images/ui/vs.png');
|
this.load.image('vs', 'assets/images/ui/vs.png');
|
||||||
|
this.load.image('attacksFirst', 'assets/images/ui/attacksFirst.png');
|
||||||
|
this.load.spritesheet('attacks', 'assets/images/spritesheets/attacks.png', { frameWidth: 460, frameHeight: 460 });
|
||||||
this.load.json('cards', 'data/cards.json');
|
this.load.json('cards', 'data/cards.json');
|
||||||
this.load.json('packs', 'data/packs.json');
|
this.load.json('packs', 'data/packs.json');
|
||||||
this.load.json('missions', 'data/missions.json');
|
this.load.json('missions', 'data/missions.json');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue