tyrants-edge/src/objects/CardObject.js

323 lines
11 KiB
JavaScript

const RARITY_COLORS = {
common: 0x888888,
rare: 0x4488ff,
epic: 0xaa44ff,
legendary: 0xffaa00
};
const FACTION_COLORS = {
imperial: 0x2244aa,
raider: 0xaa2222,
bloodthirsty: 0x882244,
xeno: 0x22aa44,
righteous: 0xaaaa22
};
const BANNER_COLOR = 0x0c0c1e;
export class CardObject extends Phaser.GameObjects.Container {
constructor(scene, x, y, cardData, options = {}) {
super(scene, x, y);
this.cardData = cardData;
this.options = options;
this._build();
scene.add.existing(this);
}
_build() {
const w = this.options.width || 80;
const h = this.options.height || 110;
const scale = w / 80;
const fs = n => `${Math.round(n * scale)}px`;
const rarityColor = RARITY_COLORS[this.cardData.rarity] || 0x888888;
const factionColor = FACTION_COLORS[this.cardData.faction] || 0x444444;
// Section heights
const bannerH = Math.round(h * 0.12); // ~22px for h=190
const imageH = Math.round(w * 9 / 16); // 16:9 placeholder
// Vertical positions (relative to container centre = 0)
const topBannerCY = -h / 2 + bannerH / 2;
const imageCY = -h / 2 + bannerH + imageH / 2;
const contentTop = -h / 2 + bannerH + imageH;
const bottomBannerCY = h / 2 - bannerH / 2;
const contentH = (bottomBannerCY - bannerH / 2) - contentTop;
// ── Card outline ──────────────────────────────────────────────────────────
const outline = this.scene.add.rectangle(0, 0, w, h, 0x0a0a1a)
.setStrokeStyle(Math.max(1, Math.round(2 * scale)), rarityColor);
this.add(outline);
// ── Top banner ────────────────────────────────────────────────────────────
const topBanner = this.scene.add.rectangle(0, topBannerCY, w, bannerH, BANNER_COLOR);
this.add(topBanner);
// ATK value — left side of top banner
this.atkText = this.scene.add.text(
-w / 2 + Math.round(5 * scale), topBannerCY,
`${this.cardData.currentAttack}`,
{ fontSize: fs(9), color: '#ff8877', fontStyle: 'bold' }
).setOrigin(0, 0.5);
this.add(this.atkText);
// Small "ATK" label under the number (tiny, just for clarity)
const atkLabel = this.scene.add.text(
-w / 2 + Math.round(5 * scale), topBannerCY + Math.round(bannerH * 0.30),
'ATK',
{ fontSize: fs(5.5), color: '#886666' }
).setOrigin(0, 0.5);
this.add(atkLabel);
// ARM value — right side of top banner
this.armText = this.scene.add.text(
w / 2 - Math.round(5 * scale), topBannerCY,
`${this.cardData.currentArmor}`,
{ fontSize: fs(9), color: '#88aaff', fontStyle: 'bold' }
).setOrigin(1, 0.5);
this.add(this.armText);
// Small "ARM" label
const armLabel = this.scene.add.text(
w / 2 - Math.round(5 * scale), topBannerCY + Math.round(bannerH * 0.30),
'ARM',
{ fontSize: fs(5.5), color: '#667799' }
).setOrigin(1, 0.5);
this.add(armLabel);
// ── 16:9 image area ───────────────────────────────────────────────────────
// Faction-coloured backing (shows when no art is loaded)
const imgBacking = this.scene.add.rectangle(0, imageCY, w, imageH, factionColor);
this.add(imgBacking);
// Card art image (if the texture was loaded)
if (this.cardData.artKey && this.scene.textures.exists(this.cardData.artKey)) {
const img = this.scene.add.image(0, imageCY, this.cardData.artKey)
.setDisplaySize(w, imageH);
this.add(img);
}
// Delay dimmer overlay (covers image when card has delay remaining)
if (this.cardData.currentDelay > 0) {
this.delayOverlay = this.scene.add.rectangle(0, imageCY, w, imageH, 0x000000, 0.60);
this.add(this.delayOverlay);
}
// Flash overlay — transparent red rect; .bg is targeted by _animateAttack
this.bg = this.scene.add.rectangle(0, imageCY, w, imageH, 0xff2200, 0);
this.add(this.bg);
// ── Content area: name + skills ───────────────────────────────────────────
const hasSkills = this.cardData.skills && this.cardData.skills.length > 0;
const nameY = hasSkills
? contentTop + contentH * 0.32
: contentTop + contentH * 0.50;
this.nameText = this.scene.add.text(0, nameY, this.cardData.name, {
fontSize: fs(8), color: '#ffffff',
wordWrap: { width: w - Math.round(8 * scale) }, align: 'center'
}).setOrigin(0.5, 0.5);
this.add(this.nameText);
if (hasSkills) {
const skillStr = this.cardData.skills
.map(s => s.value != null ? `${s.name} ${s.value}` : s.name)
.join(' · ');
const skillY = contentTop + contentH * 0.72;
this.skillText = this.scene.add.text(0, skillY, skillStr, {
fontSize: fs(7), color: '#ffcc44',
wordWrap: { width: w - Math.round(8 * scale) }, align: 'center'
}).setOrigin(0.5, 0.5);
this.add(this.skillText);
}
// ── Bottom banner ─────────────────────────────────────────────────────────
const bottomBanner = this.scene.add.rectangle(0, bottomBannerCY, w, bannerH, BANNER_COLOR);
this.add(bottomBanner);
// HP value — left side of bottom banner
this.hpText = this.scene.add.text(
-w / 2 + Math.round(5 * scale), bottomBannerCY,
`${Math.max(0, this.cardData.currentHP)}`,
{ fontSize: fs(9), color: '#44ee88', fontStyle: 'bold' }
).setOrigin(0, 0.5);
this.add(this.hpText);
// Small "HP" label
const hpLabel = this.scene.add.text(
-w / 2 + Math.round(5 * scale), bottomBannerCY + Math.round(bannerH * 0.30),
'HP',
{ fontSize: fs(5.5), color: '#337755' }
).setOrigin(0, 0.5);
this.add(hpLabel);
// DLY value — right side of bottom banner
this.dlyText = this.scene.add.text(
w / 2 - Math.round(5 * scale), bottomBannerCY,
`${this.cardData.currentDelay}`,
{ fontSize: fs(9), color: '#aaaaaa', fontStyle: 'bold' }
).setOrigin(1, 0.5);
this.add(this.dlyText);
// Small "DLY" label
const dlyLabel = this.scene.add.text(
w / 2 - Math.round(5 * scale), bottomBannerCY + Math.round(bannerH * 0.30),
'DLY',
{ fontSize: fs(5.5), color: '#777777' }
).setOrigin(1, 0.5);
this.add(dlyLabel);
// ── Rarity gem (top-right of image area) ──────────────────────────────────
const gemR = Math.max(3, Math.round(3.5 * scale));
const gem = this.scene.add.circle(
w / 2 - gemR - Math.round(3 * scale),
-h / 2 + bannerH + gemR + Math.round(3 * scale),
gemR, rarityColor
);
this.add(gem);
}
refresh() {
if (this.atkText) this.atkText.setText(`${this.cardData.currentAttack}`);
if (this.armText) this.armText.setText(`${this.cardData.currentArmor}`);
if (this.hpText) this.hpText.setText(`${Math.max(0, this.cardData.currentHP)}`);
if (this.dlyText) this.dlyText.setText(`${this.cardData.currentDelay}`);
if (this.delayOverlay) {
this.delayOverlay.setVisible(this.cardData.currentDelay > 0);
}
}
animateBerserkGain(gain, onComplete) {
if (!this.atkText || !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 topBannerCY = -h / 2 + bannerH / 2;
// Scale ATK text up to 2x
this.scene.tweens.add({
targets: this.atkText,
scaleX: 2, scaleY: 2,
duration: 200,
ease: 'Back.Out',
onComplete: () => {
if (!this.scene) { if (onComplete) onComplete(); return; }
// +N label added to container so it moves with the card
const bonusText = this.scene.add.text(
this.atkText.x + this.atkText.width * 2 + Math.round(4 * s),
topBannerCY,
`+${gain}`,
{
fontSize: `${Math.round(11 * s)}px`,
color: '#ffaa00',
fontStyle: 'bold',
stroke: '#000000',
strokeThickness: Math.max(1, Math.round(2 * s))
}
).setOrigin(0, 0.5).setDepth(50);
this.add(bonusText);
// Hold briefly, then update value and scale back
this.scene.time.delayedCall(500, () => {
if (!this.scene) { if (onComplete) onComplete(); return; }
this.atkText.setText(`${this.cardData.currentAttack}`);
this.scene.tweens.add({
targets: this.atkText,
scaleX: 1, scaleY: 1,
duration: 200,
ease: 'Power2'
});
this.scene.tweens.add({
targets: bonusText,
alpha: 0,
y: topBannerCY - Math.round(14 * s),
duration: 300,
ease: 'Power2',
onComplete: () => {
if (bonusText.scene) bonusText.destroy();
if (onComplete) onComplete();
}
});
});
}
});
}
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) {
const factionColor = FACTION_COLORS[this.cardData.faction] || 0x444444;
this.scene.tweens.add({
targets: this.bg,
fillColor: { from: color, to: factionColor },
duration: 300,
ease: 'Linear'
});
}
destroy() {
super.destroy();
}
}