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(); } }