tyrants-edge/src/objects/CardTooltip.js

211 lines
6.8 KiB
JavaScript

const CATEGORY_COLORS = {
offense: '#ff8877',
support: '#88ff88',
control: '#88ccff',
defense: '#aaaaff',
special: '#ffcc44'
};
const TRIGGER_LABELS = {
preAttack: 'Pre-Attack',
on_attack: 'On Attack',
on_defend: 'On Defend',
preBattle: 'Pre-Battle',
passive: 'Passive',
on_turn_start: 'Turn Start',
on_kill: 'On Kill'
};
export class CardTooltip extends Phaser.GameObjects.Container {
/**
* @param {Phaser.Scene} scene
* @param {object} cardData - full card data (same object CardObject receives)
*/
constructor(scene, cardData) {
super(scene, 0, 0);
this.cardData = cardData;
this.setDepth(100);
this.setAlpha(0);
this._build();
scene.add.existing(this);
}
_build() {
const W = 420;
const PAD = 22;
const skillDefs = this.scene.cache.json.get('skills') || [];
const skillMap = {};
for (const sd of skillDefs) skillMap[sd.name] = sd;
// ── Collect text lines and their styles to measure total height ──────────
const lines = []; // { text, style, marginTop }
// Card name
lines.push({
text: this.cardData.name,
style: { fontSize: '28px', color: '#ffffff', fontStyle: 'bold', wordWrap: { width: W - PAD * 2 }, align: 'center', fontFamily: 'Audiowide' },
marginTop: 0, align: 'center'
});
// Type / Faction / Rarity
const meta = `${this._cap(this.cardData.type)} · ${this._cap(this.cardData.faction)} · ${this._cap(this.cardData.rarity)}`;
lines.push({
text: meta,
style: { fontSize: '16px', color: '#999999', wordWrap: { width: W - PAD * 2 }, align: 'center', fontFamily: 'Audiowide' },
marginTop: 6, align: 'center'
});
// Divider
lines.push({ divider: true, marginTop: 14 });
// Stats line
const stats = `ATK ${this.cardData.attack} HP ${this.cardData.health} ARM ${this.cardData.armor} DLY ${this.cardData.delay}`;
lines.push({
text: stats,
style: { fontSize: '20px', color: '#cccccc', fontStyle: 'bold', wordWrap: { width: W - PAD * 2 }, align: 'center', fontFamily: 'Audiowide' },
marginTop: 12, align: 'center'
});
// Divider before skills
if (this.cardData.skills?.length) {
lines.push({ divider: true, marginTop: 14 });
for (const skill of this.cardData.skills) {
const def = skillMap[skill.name];
const catColor = CATEGORY_COLORS[def?.category] || '#ffcc44';
const triggerLabel = TRIGGER_LABELS[skill.trigger] || skill.trigger;
// Skill header: "strike all 3 (Pre-Attack)"
const allMod = skill.all ? ' all' : '';
const valStr = skill.value != null ? ` ${skill.value}` : '';
lines.push({
text: `${skill.name}${allMod}${valStr}`,
style: { fontSize: '21px', color: catColor, fontStyle: 'bold', wordWrap: { width: W - PAD * 2 }, fontFamily: 'Audiowide' },
marginTop: 14, align: 'left',
suffix: { text: ` ${triggerLabel}`, style: { fontSize: '15px', color: '#888888', fontFamily: 'Audiowide' } }
});
// Skill description — use allDescription when "all" modifier is present
const rawDesc = (skill.all && def?.allDescription) ? def.allDescription : def?.description;
if (rawDesc) {
// Replace "value" with the actual number for clarity
const desc = skill.value != null
? rawDesc.replace(/\bvalue\b/g, `${skill.value}`)
: rawDesc;
lines.push({
text: desc,
style: { fontSize: '17px', color: '#bbbbbb', wordWrap: { width: W - PAD * 2 }, fontFamily: 'Audiowide' },
marginTop: 4, align: 'left'
});
}
}
}
// Flavor text
if (this.cardData.flavorText) {
lines.push({ divider: true, marginTop: 14 });
lines.push({
text: `"${this.cardData.flavorText}"`,
style: { fontSize: '16px', color: '#777777', fontStyle: 'italic', wordWrap: { width: W - PAD * 2 }, align: 'center', fontFamily: 'Audiowide' },
marginTop: 10, align: 'center'
});
}
// ── Measure total height by creating temporary text objects ──────────────
let cursorY = PAD;
const elements = []; // { type, y, ... }
for (const line of lines) {
cursorY += line.marginTop || 0;
if (line.divider) {
elements.push({ type: 'divider', y: cursorY });
cursorY += 1;
continue;
}
const tmp = this.scene.add.text(0, 0, line.text, line.style);
const h = tmp.height;
elements.push({ type: 'text', y: cursorY, height: h, line });
if (line.suffix) {
// suffix is rendered inline; doesn't add extra height
}
cursorY += h;
tmp.destroy();
}
cursorY += PAD;
const H = cursorY;
// ── Background panel ─────────────────────────────────────────────────────
const bg = this.scene.add.rectangle(W / 2, H / 2, W, H, 0x0c0c1e, 0.95)
.setStrokeStyle(2, 0x444466);
this.add(bg);
// ── Render text elements ─────────────────────────────────────────────────
for (const el of elements) {
if (el.type === 'divider') {
const div = this.scene.add.rectangle(W / 2, el.y, W - PAD * 2, 1, 0x333355);
this.add(div);
continue;
}
const { line, y } = el;
const x = line.align === 'center' ? W / 2 : PAD;
const origin = line.align === 'center' ? 0.5 : 0;
const txt = this.scene.add.text(x, y, line.text, line.style).setOrigin(origin, 0);
this.add(txt);
if (line.suffix) {
const sfx = this.scene.add.text(
txt.x + txt.width + 4, y + 5,
line.suffix.text, line.suffix.style
).setOrigin(0, 0);
this.add(sfx);
}
}
this._tooltipW = W;
this._tooltipH = H;
}
_cap(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
/** Position the tooltip near the pointer, keeping it within the game bounds. */
positionAt(pointerX, pointerY) {
const { width, height } = this.scene.scale;
const OFFSET_X = 24;
const OFFSET_Y = 0;
let tx = pointerX + OFFSET_X;
let ty = pointerY + OFFSET_Y;
// Keep within right/left bounds
if (tx + this._tooltipW > width - 8) {
tx = pointerX - this._tooltipW - OFFSET_X;
}
if (tx < 8) tx = 8;
// Keep within bottom/top bounds
if (ty + this._tooltipH > height - 8) {
ty = height - 8 - this._tooltipH;
}
if (ty < 8) ty = 8;
this.setPosition(tx, ty);
}
show() {
this.setAlpha(1);
this.setVisible(true);
}
hide() {
this.setAlpha(0);
this.setVisible(false);
}
destroy() {
super.destroy();
}
}