refactor(ui): upgrade to 1920x1080 resolution with responsive scaling

- Update `game.js` configuration to 1920x1080 with `Phaser.Scale.FIT` for responsive window resizing.
- Scale all UI elements across scenes (MainMenu, Battle, DeckBuilder, Collection, Store, Fusion) to match the new resolution:
  - Increase font sizes by ~35-50% for better readability.
  - Enlarge buttons and interactive areas to accommodate the larger viewport.
  - Expand card display dimensions in battle and deck management screens.
- Adjust layout coordinates in `BattleScene` to center the 4-lane arena properly on the wider screen.
- Update `BootScene` loading text to center dynamically based on scale dimensions.
- Add project documentation (`CLAUDE.md`) outlining architecture, scene flow, and data models for future contributors.
This commit is contained in:
Brian Fertig 2026-03-12 23:52:11 -06:00
parent d865634a2f
commit 73302be3cc
11 changed files with 337 additions and 257 deletions

View File

@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(node:*)"
"Bash(node:*)",
"Bash(wc -l /home/brianfertig/git/tyrants-edge/src/**/*.js)"
]
}
}

73
CLAUDE.md Normal file
View File

@ -0,0 +1,73 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Running the Game
```bash
./start_web.sh # Starts Python HTTP server on port 8000
```
Open `http://localhost:8000` in a browser. No build step — all source is raw ES modules. Changes take effect on page refresh.
There are no tests or linting configured.
## Architecture
**Tyrants Edge** is a browser-based deck-building card game built with Phaser 3 (v3.60.0, loaded via CDN). Vanilla JavaScript only — no bundler, no npm.
### Scene Flow
`BootScene` → `MainMenuScene` → any of:
- `BattleScene` — 1v1 card battles vs AI
- `DeckBuilderScene` — customize decks (1 commander + 3–10 assault cards)
- `CampaignScene` → launches `BattleScene` with mission config
- `CollectionScene`, `StoreScene`, `FusionScene`
`BootScene` loads all JSON data (`data/*.json`) and card art images, then instantiates global managers stored in Phaser's game registry.
### Global Managers (accessed via `this.registry.get(...)`)
| Key | Class | Purpose |
|-----|-------|---------|
| `cardManager` | `CardManager` | Card definitions and instance creation |
| `packManager` | `PackManager` | Booster pack opening logic |
| `save` | `SaveManager` | localStorage persistence (versioned) |
| `missions` | raw JSON | Campaign mission data |
### Combat System (`src/combat/`)
- **`CombatEngine.js`** — Core battle loop. Turn phases: Draw → Deploy → Activation → Rupture → Jam → Commander Skills → Card Attacks → Cleanup. Uses event queue for animation sequencing.
- **`SkillProcessor.js`** — Resolves card skill effects (strike, swipe, pierce, rupture, berserk, siege, rally, heal, protect, jam, enfeeble, weaken, etc.) with triggers: `on_turn_start`, `on_attack`, `on_kill`.
- **`CombatAI.js`** — AI opponent decision-making.
Combat uses `RNG.js` (seeded random) for deterministic, replayable battles.
### Card Rendering
`CardObject.js` handles all visual card rendering (stats, art, skills). `BattleField.js` manages the 4-lane battle arena layout.
### Data Model
All game content is JSON in `data/`:
- **`cards.json`** — Card stats, faction, rarity, skills
- **`packs.json`** — Booster pack definitions
- **`missions.json`** — Campaign missions with enemy decks and rewards
- **`skills.json`** — Skill reference (skills are also inline in cards)
Card factions: `imperial`, `raider`, `bloodthirsty`, `xeno`, `righteous`
Card types: `commander`, `assault`
Rarities: `common`, `rare`, `epic`, `legendary`
### Save Format (localStorage)
```json
{
"version": 1,
"gold": 400,
"collection": { "cardId": count },
"decks": [],
"campaignProgress": { "completedMissions": [], "currentChapter": 1 },
"starterDeckReceived": false
}
```

View File

@ -9,10 +9,14 @@ import { FusionScene } from './scenes/FusionScene.js';
const config = {
type: Phaser.AUTO,
width: 1280,
height: 720,
width: 1920,
height: 1080,
backgroundColor: '#1a1a2e',
parent: document.body,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
scene: [
BootScene,
MainMenuScene,

View File

@ -18,10 +18,9 @@ export class BattleScene extends Phaser.Scene {
const save = this.registry.get('save');
// Layout constants
// Lanes: 4 lanes centred at x = 295, 505, 715, 925 (spacing 210)
// Commander column: x = 110
// Right UI panel: x = 1090–1280
// Opponent row: y = 240 Player row: y = 490 Midline: y = 362
// Lanes: 4 lanes centred at x = 430, 740, 1050, 1360 (spacing 310)
// Commander column: x = 150
// Opponent row: y = 305 Player row: y = 715 Midline: y = 510
// Particle dot texture (white circle, 8×8) used for attack effects
if (!this.textures.exists('particle_dot')) {
@ -36,25 +35,25 @@ export class BattleScene extends Phaser.Scene {
this.add.rectangle(width / 2, height / 2, width, height, 0x1a1a2e);
// Midfield divider
this.add.rectangle(width / 2, 362, width, 2, 0x334466);
this.add.rectangle(width / 2, 510, width, 2, 0x334466);
// Vertical lane separators (5 lines bounding 4 lanes)
for (let i = 0; i < 5; i++) {
const x = 190 + i * 210;
this.add.rectangle(x, 362, 1, 600, 0x222244);
const x = 280 + i * 310;
this.add.rectangle(x, 510, 1, 780, 0x222244);
}
// Commander column right edge
this.add.rectangle(190, height / 2, 1, height, 0x222244);
this.add.rectangle(280, height / 2, 1, height, 0x222244);
// Side labels
this.add.text(14, 72, 'OPPONENT', { fontSize: '13px', color: '#ff8888' });
this.add.text(14, 368, 'PLAYER', { fontSize: '13px', color: '#88aaff' });
this.add.text(14, 95, 'OPPONENT', { fontSize: '17px', color: '#ff8888' });
this.add.text(14, 520, 'PLAYER', { fontSize: '17px', color: '#88aaff' });
this.battlefield = new BattleField(this, {
playerY: 490, opponentY: 240,
commanderPlayerX: 110, commanderOpponentX: 110,
laneStartX: 295, laneSpacing: 210
playerY: 715, opponentY: 305,
commanderPlayerX: 150, commanderOpponentX: 150,
laneStartX: 430, laneSpacing: 310
});
// Setup combat
@ -93,46 +92,46 @@ export class BattleScene extends Phaser.Scene {
this._buildCommanderDisplay();
// ── Top bar ──────────────────────────────────────────────────────────────
this.turnText = this.add.text(width / 2, 18, 'Turn 0', {
this.turnText = this.add.text(960, 30, 'Turn 0', {
fontSize: '20px', color: '#ffffff'
}).setOrigin(0.5);
const battleLabel = this.missionData ? this.missionData.name : 'Skirmish Battle';
this.add.text(width / 2, 44, battleLabel, {
this.add.text(960, 58, battleLabel, {
fontSize: '14px', color: '#888888'
}).setOrigin(0.5);
// ── Bottom bar ────────────────────────────────────────────────────────────
this.statusText = this.add.text(width / 2, 706, 'Press SPACE or click Next Turn to start', {
fontSize: '13px', color: '#aaaaaa'
// ── Combat log — bottom strip ────────────────────────────────────────────
this.statusText = this.add.text(width / 2, 1058, 'Press SPACE or click Next Turn to start', {
fontSize: '17px', color: '#aaaaaa'
}).setOrigin(0.5);
this.logLines = [];
for (let i = 0; i < 5; i++) {
this.logLines.push(this.add.text(14, 632 + i * 14, '', {
fontSize: '11px', color: '#777777'
for (let i = 0; i < 6; i++) {
this.logLines.push(this.add.text(14, 915 + i * 20, '', {
fontSize: '15px', color: '#777777'
}));
}
// ── Right panel buttons ───────────────────────────────────────────────────
// ── Top bar buttons ─────────────────────────────────────────────────────
this.autoPlay = false;
this.autoTimer = null;
this.waitingForPick = false;
this.isAnimating = false;
const autoBtn = this.add.rectangle(1185, 80, 170, 38, 0x224422)
const autoBtn = this.add.rectangle(1680, 35, 180, 44, 0x224422)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x44aa44);
this.autoBtnText = this.add.text(1185, 80, 'Auto: OFF', {
fontSize: '15px', color: '#ffffff'
this.autoBtnText = this.add.text(1680, 35, 'Auto: OFF', {
fontSize: '18px', color: '#ffffff'
}).setOrigin(0.5);
autoBtn.on('pointerdown', () => this._toggleAuto());
const nextBtn = this.add.rectangle(1185, 130, 170, 38, 0x1a3a5c)
const nextBtn = this.add.rectangle(1840, 35, 180, 44, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x4488ff);
this.add.text(1185, 130, 'Next Turn', {
fontSize: '15px', color: '#ffffff'
this.add.text(1840, 35, 'Next Turn', {
fontSize: '18px', color: '#ffffff'
}).setOrigin(0.5);
nextBtn.on('pointerdown', () => this._beginTurn());
@ -148,16 +147,16 @@ export class BattleScene extends Phaser.Scene {
this.commanderObjects = new Map(); // instanceId → CardObject (persists across _renderState)
const specs = [
{ data: state.opponent.commander, cx: 110, cy: 240, label: 'ENEMY CMD', labelColor: '#ff8888' },
{ data: state.player.commander, cx: 110, cy: 490, label: 'COMMANDER', labelColor: '#ffd700' }
{ data: state.opponent.commander, cx: 150, cy: 305, label: 'ENEMY CMD', labelColor: '#ff8888' },
{ data: state.player.commander, cx: 150, cy: 715, label: 'COMMANDER', labelColor: '#ffd700' }
];
for (const s of specs) {
const w = 160, h = 180;
const w = 240, h = 336;
// Label above the card (separate text so it never scales/shakes with the card)
this.add.text(s.cx, s.cy - h / 2 - 14, s.label,
{ fontSize: '11px', color: s.labelColor }).setOrigin(0.5);
{ fontSize: '15px', color: s.labelColor }).setOrigin(0.5);
// Use CardObject for visual consistency with lane cards
const cardObj = new CardObject(this, s.cx, s.cy, s.data, { width: w, height: h });
@ -166,8 +165,8 @@ export class BattleScene extends Phaser.Scene {
this.commanderObjects.set(s.data.instanceId, cardObj);
}
this.oDeckText = this.add.text(14, 92, '', { fontSize: '12px', color: '#aaaaaa' });
this.pDeckText = this.add.text(14, 385, '', { fontSize: '12px', color: '#aaaaaa' });
this.oDeckText = this.add.text(14, 115, '', { fontSize: '15px', color: '#aaaaaa' });
this.pDeckText = this.add.text(14, 540, '', { fontSize: '15px', color: '#aaaaaa' });
}
_beginTurn() {
@ -381,7 +380,7 @@ export class BattleScene extends Phaser.Scene {
// ── Attacker: particle burst from card edges ──────────────────────────────
if (attackerObj) {
const cardW = 170, cardH = 190;
const cardW = 260, cardH = 364;
const emitter = this.add.particles(attackerObj.x, attackerObj.y, 'particle_dot', {
speed: { min: 90, max: 300 },
scale: { start: 1.8, end: 0 },
@ -419,7 +418,7 @@ export class BattleScene extends Phaser.Scene {
const origX = defenderObj.x;
this.tweens.add({
targets: defenderObj,
x: { from: origX - 18, to: origX + 18 },
x: { from: origX - 26, to: origX + 26 },
duration: 60,
yoyo: true,
repeat: 6,
@ -429,17 +428,17 @@ export class BattleScene extends Phaser.Scene {
}
// Floating damage number
const dmgAnchorX = defenderObj?.x ?? (attackerObj?.x ?? 640);
const dmgAnchorY = defenderObj?.y ?? (attackerObj?.y ?? 360);
const dmgAnchorX = defenderObj?.x ?? (attackerObj?.x ?? 960);
const dmgAnchorY = defenderObj?.y ?? (attackerObj?.y ?? 510);
const dmgText = this.add.text(
dmgAnchorX, dmgAnchorY - 70,
dmgAnchorX, dmgAnchorY - 100,
`-${event.damage}`,
{ fontSize: '46px', color: '#ff3333', stroke: '#000000', strokeThickness: 5 }
{ fontSize: '56px', color: '#ff3333', stroke: '#000000', strokeThickness: 5 }
).setOrigin(0.5).setDepth(30);
this.tweens.add({
targets: dmgText,
y: dmgAnchorY - 170,
y: dmgAnchorY - 250,
alpha: 0,
duration: 1300,
ease: 'Power2',
@ -462,21 +461,21 @@ export class BattleScene extends Phaser.Scene {
this.pickerObjects.push(overlay);
// Panel background
const cardW = 170, cardH = 220;
const gap = 30;
const panelPadX = 50, panelPadY = 60;
const cardW = 260, cardH = 364;
const gap = 40;
const panelPadX = 60, panelPadY = 70;
const totalW = hand.length * cardW + (hand.length - 1) * gap;
const panelW = Math.max(totalW + panelPadX * 2, 400);
const panelW = Math.max(totalW + panelPadX * 2, 500);
const panelH = cardH + panelPadY * 2;
const panelY = height / 2;
const panelY = 540;
const panel = this.add.rectangle(width / 2, panelY, panelW, panelH, 0x0d1b2a, 0.97)
.setStrokeStyle(2, 0x4488ff)
.setDepth(10);
this.pickerObjects.push(panel);
const title = this.add.text(width / 2, panelY - panelH / 2 + 22, 'Choose a card to deploy', {
fontSize: '20px', color: '#d4af37'
const title = this.add.text(width / 2, panelY - panelH / 2 + 26, 'Choose a card to deploy', {
fontSize: '22px', color: '#d4af37'
}).setOrigin(0.5).setDepth(11);
this.pickerObjects.push(title);
@ -513,12 +512,12 @@ export class BattleScene extends Phaser.Scene {
});
// Pass button
const passBtn = this.add.rectangle(width / 2, panelY + panelH / 2 - 24, 180, 34, 0x333333)
const passBtn = this.add.rectangle(width / 2, panelY + panelH / 2 - 28, 220, 44, 0x333333)
.setStrokeStyle(1, 0x888888)
.setInteractive({ useHandCursor: true })
.setDepth(11);
const passTxt = this.add.text(width / 2, panelY + panelH / 2 - 24, 'Pass (deploy nothing)', {
fontSize: '13px', color: '#aaaaaa'
const passTxt = this.add.text(width / 2, panelY + panelH / 2 - 28, 'Pass (deploy nothing)', {
fontSize: '17px', color: '#aaaaaa'
}).setOrigin(0.5).setDepth(12);
passBtn.on('pointerdown', () => {
this._destroyCardPicker();
@ -542,19 +541,19 @@ export class BattleScene extends Phaser.Scene {
state.player.lanes.forEach((card, i) => {
const pos = this.battlefield.getPlayerLanePos(i);
const co = new CardObject(this, pos.x, pos.y, card, { width: 170, height: 190 });
const co = new CardObject(this, pos.x, pos.y, card, { width: 260, height: 364 });
this.cardObjects.set(card.instanceId, co);
});
state.opponent.lanes.forEach((card, i) => {
const pos = this.battlefield.getOpponentLanePos(i);
const co = new CardObject(this, pos.x, pos.y, card, { width: 170, height: 190 });
const co = new CardObject(this, pos.x, pos.y, card, { width: 260, height: 364 });
this.cardObjects.set(card.instanceId, co);
});
}
_updateLog() {
const recent = this.engine.log.slice(-5);
const recent = this.engine.log.slice(-6);
this.logLines.forEach((line, i) => {
line.setText(recent[i] || '');
});
@ -591,13 +590,13 @@ export class BattleScene extends Phaser.Scene {
// Overlay
this.add.rectangle(width / 2, height / 2, width, height, won ? 0x003300 : 0x330000, 0.75);
this.add.text(width / 2, height / 2 - 100, won ? 'VICTORY!' : 'DEFEAT', {
fontSize: '56px', color: won ? '#44ff44' : '#ff4444',
this.add.text(width / 2, height / 2 - 120, won ? 'VICTORY!' : 'DEFEAT', {
fontSize: '72px', color: won ? '#44ff44' : '#ff4444',
stroke: '#000000', strokeThickness: 6
}).setOrigin(0.5);
this.add.text(width / 2, height / 2 - 40, `Battle lasted ${this.engine.turn} turns`, {
fontSize: '18px', color: '#aaaaaa'
fontSize: '24px', color: '#aaaaaa'
}).setOrigin(0.5);
if (won && this.missionData) {
@ -609,8 +608,8 @@ export class BattleScene extends Phaser.Scene {
}
this.registry.set('save', save);
this.add.text(width / 2, height / 2 + 10, `+${rewards.gold} Gold`, {
fontSize: '28px', color: '#ffd700'
this.add.text(width / 2, height / 2 + 20, `+${rewards.gold} Gold`, {
fontSize: '36px', color: '#ffd700'
}).setOrigin(0.5);
if (rewards.cards && rewards.cards.length > 0) {
@ -619,30 +618,30 @@ export class BattleScene extends Phaser.Scene {
const c = cardManager.getCard(id);
return c ? c.name : id;
}).join(', ');
this.add.text(width / 2, height / 2 + 50, `New card(s): ${cardNames}`, {
fontSize: '16px', color: '#aaffaa'
this.add.text(width / 2, height / 2 + 70, `New card(s): ${cardNames}`, {
fontSize: '22px', color: '#aaffaa'
}).setOrigin(0.5);
}
} else if (!won) {
this.add.text(width / 2, height / 2 + 10, 'Better luck next time!', {
fontSize: '20px', color: '#aaaaaa'
this.add.text(width / 2, height / 2 + 20, 'Better luck next time!', {
fontSize: '24px', color: '#aaaaaa'
}).setOrigin(0.5);
}
const backBtn = this.add.rectangle(width / 2, height / 2 + 130, 200, 50, 0x1a3a5c)
const backBtn = this.add.rectangle(width / 2, height / 2 + 160, 260, 60, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff);
this.add.text(width / 2, height / 2 + 130, 'Continue', {
fontSize: '22px', color: '#ffffff'
this.add.text(width / 2, height / 2 + 160, 'Continue', {
fontSize: '28px', color: '#ffffff'
}).setOrigin(0.5);
backBtn.on('pointerdown', () => this.scene.start(this.missionData ? 'CampaignScene' : 'MainMenuScene'));
}
_makeBackButton() {
const bg = this.add.rectangle(1185, 185, 170, 38, 0x333333)
const bg = this.add.rectangle(80, 35, 180, 44, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(1185, 185, '← Back', { fontSize: '15px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 35, '← Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => {
if (this.autoTimer) this.autoTimer.remove();
this.scene.start('MainMenuScene');

View File

@ -24,7 +24,7 @@ export class BootScene extends Phaser.Scene {
});
// Loading text
const loadText = this.add.text(640, 360, 'Loading...', {
const loadText = this.add.text(this.scale.width / 2, this.scale.height / 2, 'Loading...', {
fontSize: '28px', color: '#ffffff'
}).setOrigin(0.5);

View File

@ -7,8 +7,8 @@ export class CampaignScene extends Phaser.Scene {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
this.add.text(width / 2, 40, 'Campaign — Chapter 1', {
fontSize: '32px', color: '#d4af37'
this.add.text(width / 2, 45, 'Campaign — Chapter 1', {
fontSize: '42px', color: '#d4af37'
}).setOrigin(0.5);
const completed = save.campaignProgress.completedMissions;
@ -16,13 +16,13 @@ export class CampaignScene extends Phaser.Scene {
missions.forEach((mission, i) => {
const isUnlocked = !mission.unlockCondition || completed.includes(mission.unlockCondition);
const isDone = completed.includes(mission.id);
const x = 200 + (i % 4) * 250;
const y = 300 + Math.floor(i / 4) * 200;
const x = 300 + (i % 4) * 375;
const y = 380 + Math.floor(i / 4) * 260;
const color = isDone ? 0x228822 : isUnlocked ? 0x1a3a5c : 0x333333;
const borderColor = isDone ? 0x44ff44 : isUnlocked ? 0x4488ff : 0x666666;
const node = this.add.rectangle(x, y, 200, 140, color)
const node = this.add.rectangle(x, y, 300, 220, color)
.setStrokeStyle(2, borderColor);
if (isUnlocked) {
@ -32,34 +32,34 @@ export class CampaignScene extends Phaser.Scene {
node.on('pointerdown', () => this._startMission(mission));
}
this.add.text(x, y - 45, mission.name, {
fontSize: '14px', color: isUnlocked ? '#ffffff' : '#666666',
wordWrap: { width: 180 }, align: 'center'
this.add.text(x, y - 70, mission.name, {
fontSize: '20px', color: isUnlocked ? '#ffffff' : '#666666',
wordWrap: { width: 270 }, align: 'center'
}).setOrigin(0.5);
this.add.text(x, y - 22, `Chapter ${mission.chapter}`, {
fontSize: '12px', color: '#888888'
this.add.text(x, y - 35, `Chapter ${mission.chapter}`, {
fontSize: '16px', color: '#888888'
}).setOrigin(0.5);
this.add.text(x, y, isDone ? 'Complete' : isUnlocked ? 'Click to battle' : 'Locked', {
fontSize: '12px', color: isDone ? '#44ff44' : isUnlocked ? '#aaffaa' : '#666666'
fontSize: '16px', color: isDone ? '#44ff44' : isUnlocked ? '#aaffaa' : '#666666'
}).setOrigin(0.5);
// Rewards
this.add.text(x, y + 22, `+${mission.rewards.gold} gold`, {
fontSize: '11px', color: '#ffd700'
this.add.text(x, y + 30, `+${mission.rewards.gold} gold`, {
fontSize: '14px', color: '#ffd700'
}).setOrigin(0.5);
if (mission.rewards.cards && mission.rewards.cards.length > 0) {
this.add.text(x, y + 40, `Card reward available`, {
fontSize: '10px', color: '#aaffaa'
this.add.text(x, y + 55, `Card reward available`, {
fontSize: '14px', color: '#aaffaa'
}).setOrigin(0.5);
}
});
// Mission lore panel
this.loreText = this.add.text(width / 2, height - 60, 'Hover over a mission to learn more.', {
fontSize: '13px', color: '#888888', align: 'center', wordWrap: { width: width - 100 }
this.loreText = this.add.text(width / 2, height - 70, 'Hover over a mission to learn more.', {
fontSize: '18px', color: '#888888', align: 'center', wordWrap: { width: width - 100 }
}).setOrigin(0.5);
// Back button
@ -69,8 +69,8 @@ export class CampaignScene extends Phaser.Scene {
_startMission(mission) {
const save = this.registry.get('save');
if (!save.decks || save.decks.length === 0) {
this.add.text(640, 660, 'No deck! Create a deck in Deck Builder first.', {
fontSize: '16px', color: '#ff4444'
this.add.text(960, 940, 'No deck! Create a deck in Deck Builder first.', {
fontSize: '22px', color: '#ff4444'
}).setOrigin(0.5);
return;
}
@ -78,10 +78,10 @@ export class CampaignScene extends Phaser.Scene {
}
_makeBackButton() {
const bg = this.add.rectangle(80, 40, 120, 40, 0x333333)
const bg = this.add.rectangle(80, 45, 160, 50, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 40, 'Back', { fontSize: '16px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 45, 'Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
}
}

View File

@ -7,15 +7,15 @@ export class CollectionScene extends Phaser.Scene {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
this.add.text(width / 2, 35, 'Card Collection', {
fontSize: '28px', color: '#d4af37'
this.add.text(width / 2, 40, 'Card Collection', {
fontSize: '38px', color: '#d4af37'
}).setOrigin(0.5);
// Stats
const totalCards = cardManager.getAllCards().length;
const ownedCards = Object.keys(save.collection).length;
this.add.text(width - 20, 20, `Owned: ${ownedCards}/${totalCards}`, {
fontSize: '14px', color: '#aaaaaa'
fontSize: '20px', color: '#aaaaaa'
}).setOrigin(1, 0);
this.filter = { faction: 'all', rarity: 'all' };
@ -29,26 +29,26 @@ export class CollectionScene extends Phaser.Scene {
const factions = ['all', 'imperial', 'raider'];
const rarities = ['all', 'common', 'rare', 'epic', 'legendary'];
this.add.text(20, 68, 'Faction:', { fontSize: '12px', color: '#888888' });
let x = 100;
this.add.text(20, 90, 'Faction:', { fontSize: '16px', color: '#888888' });
let x = 130;
factions.forEach(f => {
const btn = this.add.rectangle(x + 40, 75, 80, 26, 0x1a3a5c)
const btn = this.add.rectangle(x + 55, 97, 110, 34, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x4488ff);
this.add.text(x + 40, 75, f, { fontSize: '11px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(x + 55, 97, f, { fontSize: '15px', color: '#ffffff' }).setOrigin(0.5);
btn.on('pointerdown', () => { this.filter.faction = f; this._renderCards(); });
x += 90;
x += 120;
});
this.add.text(20, 98, 'Rarity:', { fontSize: '12px', color: '#888888' });
x = 100;
this.add.text(20, 128, 'Rarity:', { fontSize: '16px', color: '#888888' });
x = 130;
rarities.forEach(r => {
const btn = this.add.rectangle(x + 40, 105, 80, 26, 0x1a3a5c)
const btn = this.add.rectangle(x + 55, 135, 110, 34, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x4488ff);
this.add.text(x + 40, 105, r, { fontSize: '11px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(x + 55, 135, r, { fontSize: '15px', color: '#ffffff' }).setOrigin(0.5);
btn.on('pointerdown', () => { this.filter.rarity = r; this._renderCards(); });
x += 90;
x += 120;
});
}
@ -64,19 +64,19 @@ export class CollectionScene extends Phaser.Scene {
.filter(c => this.filter.rarity === 'all' || c.rarity === this.filter.rarity);
const cols = 8;
const cardW = 130;
const cardH = 90;
const startX = 75;
const startY = 148;
const padX = 10;
const padY = 10;
const cardW = 195;
const cardH = 135;
const startX = 110;
const startY = 190;
const padX = 15;
const padY = 15;
allCards.forEach((card, i) => {
const col = i % cols;
const row = Math.floor(i / cols);
const x = startX + col * (cardW + padX);
const y = startY + row * (cardH + padY);
if (y > 670) return; // clip overflow
if (y > 970) return; // clip overflow
const owned = save.collection[card.id] || 0;
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
@ -87,53 +87,53 @@ export class CollectionScene extends Phaser.Scene {
.setStrokeStyle(2, owned > 0 ? (rarityColors[card.rarity] || 0x888888) : 0x333333);
this.cardGroup.push(bg);
const nameT = this.add.text(x, y - 30, card.name, {
fontSize: '9px', color: owned > 0 ? '#ffffff' : '#555555',
wordWrap: { width: cardW - 4 }, align: 'center'
const nameT = this.add.text(x, y - 45, card.name, {
fontSize: '14px', color: owned > 0 ? '#ffffff' : '#555555',
wordWrap: { width: cardW - 6 }, align: 'center'
}).setOrigin(0.5);
this.cardGroup.push(nameT);
const typeT = this.add.text(x, y - 18, `[${card.type}]`, {
fontSize: '8px', color: owned > 0 ? '#aaaaaa' : '#444444'
const typeT = this.add.text(x, y - 28, `[${card.type}]`, {
fontSize: '12px', color: owned > 0 ? '#aaaaaa' : '#444444'
}).setOrigin(0.5);
this.cardGroup.push(typeT);
const statsT = this.add.text(x, y - 4, `ATK:${card.attack} HP:${card.health}${card.armor > 0 ? ' ARM:' + card.armor : ''}`, {
fontSize: '9px', color: owned > 0 ? '#aaaaff' : '#444455'
const statsT = this.add.text(x, y - 8, `ATK:${card.attack} HP:${card.health}${card.armor > 0 ? ' ARM:' + card.armor : ''}`, {
fontSize: '14px', color: owned > 0 ? '#aaaaff' : '#444455'
}).setOrigin(0.5);
this.cardGroup.push(statsT);
const skillsT = this.add.text(x, y + 10, card.skills.map(s => s.name).join(', '), {
fontSize: '8px', color: owned > 0 ? '#ffcc44' : '#443300',
wordWrap: { width: cardW - 4 }, align: 'center'
const skillsT = this.add.text(x, y + 12, card.skills.map(s => s.name).join(', '), {
fontSize: '12px', color: owned > 0 ? '#ffcc44' : '#443300',
wordWrap: { width: cardW - 6 }, align: 'center'
}).setOrigin(0.5);
this.cardGroup.push(skillsT);
const ownedT = this.add.text(x, y + 28, `x${owned}`, {
fontSize: '11px', color: owned > 0 ? '#44ff44' : '#333333'
const ownedT = this.add.text(x, y + 38, `x${owned}`, {
fontSize: '16px', color: owned > 0 ? '#44ff44' : '#333333'
}).setOrigin(0.5);
this.cardGroup.push(ownedT);
const rarityT = this.add.text(x + cardW / 2 - 4, y - cardH / 2 + 6, card.rarity[0].toUpperCase(), {
fontSize: '9px',
fontSize: '13px',
color: '#' + (rarityColors[card.rarity] || 0x888888).toString(16).padStart(6, '0')
}).setOrigin(1, 0);
this.cardGroup.push(rarityT);
});
if (allCards.length === 0) {
const t = this.add.text(640, 400, 'No cards match the current filter.', {
fontSize: '16px', color: '#666666'
const t = this.add.text(960, 540, 'No cards match the current filter.', {
fontSize: '22px', color: '#666666'
}).setOrigin(0.5);
this.cardGroup.push(t);
}
}
_makeBackButton() {
const bg = this.add.rectangle(80, 700, 120, 35, 0x333333)
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 700, 'Back', { fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
}
}

View File

@ -7,7 +7,7 @@ export class DeckBuilderScene extends Phaser.Scene {
create() {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
this.add.text(width / 2, 35, 'Deck Builder', { fontSize: '28px', color: '#d4af37' }).setOrigin(0.5);
this.add.text(width / 2, 40, 'Deck Builder', { fontSize: '38px', color: '#d4af37' }).setOrigin(0.5);
const save = this.registry.get('save');
@ -30,25 +30,25 @@ export class DeckBuilderScene extends Phaser.Scene {
if (this.deckPanelGroup) this.deckPanelGroup.forEach(o => o.destroy());
this.deckPanelGroup = [];
const x0 = 870;
const x0 = 1300;
const cardManager = this.registry.get('cardManager');
const title = this.add.text(x0 + 175, 70, 'Current Deck', { fontSize: '18px', color: '#d4af37' }).setOrigin(0.5);
const title = this.add.text(x0 + 250, 90, 'Current Deck', { fontSize: '24px', color: '#d4af37' }).setOrigin(0.5);
this.deckPanelGroup.push(title);
// Commander slot
const cmdCard = this.workingDeck.commander ? cardManager.getCard(this.workingDeck.commander) : null;
const cmdBg = this.add.rectangle(x0 + 175, 110, 330, 36, cmdCard ? 0x1a3a5c : 0x2a1a1c).setStrokeStyle(2, 0xffd700);
const cmdText = this.add.text(x0 + 175, 110, cmdCard ? `Commander: ${cmdCard.name}` : 'No Commander Selected', {
fontSize: '13px', color: cmdCard ? '#ffd700' : '#888888'
const cmdBg = this.add.rectangle(x0 + 250, 140, 500, 50, cmdCard ? 0x1a3a5c : 0x2a1a1c).setStrokeStyle(2, 0xffd700);
const cmdText = this.add.text(x0 + 250, 140, cmdCard ? `Commander: ${cmdCard.name}` : 'No Commander Selected', {
fontSize: '18px', color: cmdCard ? '#ffd700' : '#888888'
}).setOrigin(0.5);
this.deckPanelGroup.push(cmdBg, cmdText);
// Remove commander button
if (cmdCard) {
const rmCmdBtn = this.add.rectangle(x0 + 325, 110, 28, 26, 0x662222)
const rmCmdBtn = this.add.rectangle(x0 + 480, 140, 38, 32, 0x662222)
.setInteractive({ useHandCursor: true }).setStrokeStyle(1, 0xff4444);
const rmCmdTxt = this.add.text(x0 + 325, 110, 'X', { fontSize: '13px', color: '#ff4444' }).setOrigin(0.5);
const rmCmdTxt = this.add.text(x0 + 480, 140, 'X', { fontSize: '16px', color: '#ff4444' }).setOrigin(0.5);
rmCmdBtn.on('pointerdown', () => {
this.workingDeck.commander = null;
this._renderDeckPanel();
@ -60,50 +60,50 @@ export class DeckBuilderScene extends Phaser.Scene {
const counts = {};
this.workingDeck.cards.forEach(id => { counts[id] = (counts[id] || 0) + 1; });
let y = 148;
let y = 190;
Object.entries(counts).forEach(([cardId, count]) => {
const card = cardManager.getCard(cardId);
if (!card) return;
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
const bg = this.add.rectangle(x0 + 175, y, 330, 28, 0x1a3a5c).setStrokeStyle(1, rarityColors[card.rarity] || 0x336699);
const txt = this.add.text(x0 + 20, y, `${card.name} x${count}`, { fontSize: '12px', color: '#ffffff' });
const statsT = this.add.text(x0 + 240, y, `ATK:${card.attack}`, { fontSize: '10px', color: '#aaaaff' }).setOrigin(0.5);
const bg = this.add.rectangle(x0 + 250, y, 500, 40, 0x1a3a5c).setStrokeStyle(1, rarityColors[card.rarity] || 0x336699);
const txt = this.add.text(x0 + 20, y, `${card.name} x${count}`, { fontSize: '16px', color: '#ffffff' });
const statsT = this.add.text(x0 + 360, y, `ATK:${card.attack}`, { fontSize: '14px', color: '#aaaaff' }).setOrigin(0.5);
// Remove one button
const rmBtn = this.add.rectangle(x0 + 325, y, 28, 22, 0x662222)
const rmBtn = this.add.rectangle(x0 + 480, y, 38, 32, 0x662222)
.setInteractive({ useHandCursor: true }).setStrokeStyle(1, 0xff4444);
const rmTxt = this.add.text(x0 + 325, y, '-', { fontSize: '16px', color: '#ff4444' }).setOrigin(0.5);
const rmTxt = this.add.text(x0 + 480, y, '-', { fontSize: '20px', color: '#ff4444' }).setOrigin(0.5);
rmBtn.on('pointerdown', () => {
const idx = this.workingDeck.cards.lastIndexOf(cardId);
if (idx !== -1) this.workingDeck.cards.splice(idx, 1);
this._renderDeckPanel();
});
this.deckPanelGroup.push(bg, txt, statsT, rmBtn, rmTxt);
y += 30;
y += 44;
});
// Filler slots
const totalSlots = 10;
const filledSlots = this.workingDeck.cards.length;
for (let s = filledSlots; s < Math.min(totalSlots, filledSlots + 2); s++) {
const slotBg = this.add.rectangle(x0 + 175, y, 330, 28, 0x111122).setStrokeStyle(1, 0x333355);
const slotTxt = this.add.text(x0 + 175, y, '— empty slot —', { fontSize: '11px', color: '#333355' }).setOrigin(0.5);
const slotBg = this.add.rectangle(x0 + 250, y, 500, 40, 0x111122).setStrokeStyle(1, 0x333355);
const slotTxt = this.add.text(x0 + 250, y, '— empty slot —', { fontSize: '14px', color: '#333355' }).setOrigin(0.5);
this.deckPanelGroup.push(slotBg, slotTxt);
y += 30;
y += 44;
}
// Total count
const countColor = this.workingDeck.cards.length > 10 ? '#ff4444' : this.workingDeck.cards.length >= 3 ? '#aaaaff' : '#888888';
const total = this.add.text(x0 + 175, y + 14, `Cards: ${this.workingDeck.cards.length} / 10`, {
fontSize: '14px', color: countColor
const total = this.add.text(x0 + 250, y + 18, `Cards: ${this.workingDeck.cards.length} / 10`, {
fontSize: '18px', color: countColor
}).setOrigin(0.5);
this.deckPanelGroup.push(total);
// Validation indicator
const validation = DeckManager.validate(this.workingDeck, this.registry.get('cardManager'));
const validText = this.add.text(x0 + 175, y + 38, validation.valid ? 'Valid Deck' : validation.errors[0], {
fontSize: '12px', color: validation.valid ? '#44ff44' : '#ff6644'
const validText = this.add.text(x0 + 250, y + 46, validation.valid ? 'Valid Deck' : validation.errors[0], {
fontSize: '16px', color: validation.valid ? '#44ff44' : '#ff6644'
}).setOrigin(0.5);
this.deckPanelGroup.push(validText);
}
@ -116,24 +116,24 @@ export class DeckBuilderScene extends Phaser.Scene {
const cardManager = this.registry.get('cardManager');
// Header
const hdr = this.add.text(420, 72, 'Your Cards — click to add to deck', { fontSize: '13px', color: '#888888' }).setOrigin(0.5);
const hdr = this.add.text(500, 100, 'Your Cards — click to add to deck', { fontSize: '18px', color: '#888888' }).setOrigin(0.5);
this.cardListGroup.push(hdr);
const ownedIds = Object.keys(save.collection).filter(id => save.collection[id] > 0);
const cols = 6;
const cardW = 130;
const cardH = 90;
const startX = 80;
const startY = 145;
const cardW = 195;
const cardH = 135;
const startX = 120;
const startY = 180;
ownedIds.forEach((cardId, i) => {
const card = cardManager.getCard(cardId);
if (!card) return;
const col = i % cols;
const row = Math.floor(i / cols);
const x = startX + col * (cardW + 8);
const y = startY + row * (cardH + 8);
if (y > 635) return; // clip
const x = startX + col * (cardW + 12);
const y = startY + row * (cardH + 12);
if (y > 920) return; // clip
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
const factionColors = { imperial: 0x2244aa, raider: 0xaa2222 };
@ -167,39 +167,39 @@ export class DeckBuilderScene extends Phaser.Scene {
});
}
const nameT = this.add.text(x, y - 30, card.name, {
fontSize: '9px', color: canAdd ? '#ffffff' : '#555555',
wordWrap: { width: cardW - 6 }, align: 'center'
const nameT = this.add.text(x, y - 45, card.name, {
fontSize: '14px', color: canAdd ? '#ffffff' : '#555555',
wordWrap: { width: cardW - 8 }, align: 'center'
}).setOrigin(0.5);
const typeLabel = isCommander ? '[CMD]' : `[${card.rarity}]`;
const typeT = this.add.text(x, y - 18, typeLabel, {
fontSize: '8px', color: canAdd ? '#aaaaaa' : '#444444'
const typeT = this.add.text(x, y - 28, typeLabel, {
fontSize: '12px', color: canAdd ? '#aaaaaa' : '#444444'
}).setOrigin(0.5);
const statsT = this.add.text(x, y - 4, `ATK:${card.attack} HP:${card.health} ARM:${card.armor}`, {
fontSize: '9px', color: canAdd ? '#aaaaff' : '#444455'
const statsT = this.add.text(x, y - 8, `ATK:${card.attack} HP:${card.health} ARM:${card.armor}`, {
fontSize: '14px', color: canAdd ? '#aaaaff' : '#444455'
}).setOrigin(0.5);
const inDeckCount = isCommander
? (this.workingDeck.commander === cardId ? 1 : 0)
: this.workingDeck.cards.filter(id => id === cardId).length;
const countT = this.add.text(x, y + 10, `Owned:${save.collection[cardId]} In deck:${inDeckCount}`, {
fontSize: '8px', color: canAdd ? '#44ff44' : '#444444'
const countT = this.add.text(x, y + 12, `Owned:${save.collection[cardId]} In deck:${inDeckCount}`, {
fontSize: '12px', color: canAdd ? '#44ff44' : '#444444'
}).setOrigin(0.5);
const skillT = this.add.text(x, y + 24, card.skills.map(s => s.name).join(', '), {
fontSize: '8px', color: canAdd ? '#ffcc44' : '#443300',
wordWrap: { width: cardW - 6 }, align: 'center'
const skillT = this.add.text(x, y + 34, card.skills.map(s => s.name).join(', '), {
fontSize: '12px', color: canAdd ? '#ffcc44' : '#443300',
wordWrap: { width: cardW - 8 }, align: 'center'
}).setOrigin(0.5);
this.cardListGroup.push(bg, nameT, typeT, statsT, countT, skillT);
});
if (ownedIds.length === 0) {
const t = this.add.text(420, 300, 'No cards in collection.\nVisit the Store to buy packs!', {
fontSize: '16px', color: '#666666', align: 'center'
const t = this.add.text(500, 450, 'No cards in collection.\nVisit the Store to buy packs!', {
fontSize: '22px', color: '#666666', align: 'center'
}).setOrigin(0.5);
this.cardListGroup.push(t);
}
@ -207,10 +207,10 @@ export class DeckBuilderScene extends Phaser.Scene {
_makeButtons() {
// Save deck button
const saveBtn = this.add.rectangle(1050, 640, 150, 40, 0x224422)
const saveBtn = this.add.rectangle(1550, 930, 200, 50, 0x224422)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x44aa44);
this.add.text(1050, 640, 'Save Deck', { fontSize: '16px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(1550, 930, 'Save Deck', { fontSize: '20px', color: '#ffffff' }).setOrigin(0.5);
saveBtn.on('pointerdown', () => {
const save = this.registry.get('save');
const validation = DeckManager.validate(this.workingDeck, this.registry.get('cardManager'));
@ -224,10 +224,10 @@ export class DeckBuilderScene extends Phaser.Scene {
});
// Clear deck button
const clearBtn = this.add.rectangle(1050, 690, 150, 35, 0x442222)
const clearBtn = this.add.rectangle(1550, 990, 200, 45, 0x442222)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0xaa4444);
this.add.text(1050, 690, 'Clear Deck', { fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(1550, 990, 'Clear Deck', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
clearBtn.on('pointerdown', () => {
this.workingDeck.commander = null;
this.workingDeck.cards = [];
@ -238,15 +238,15 @@ export class DeckBuilderScene extends Phaser.Scene {
_showMsg(msg, color) {
const { width } = this.scale;
const t = this.add.text(width / 2, 580, msg, { fontSize: '14px', color }).setOrigin(0.5);
const t = this.add.text(width / 2, 860, msg, { fontSize: '18px', color }).setOrigin(0.5);
this.time.delayedCall(2000, () => t.destroy());
}
_makeBackButton() {
const bg = this.add.rectangle(80, 700, 120, 35, 0x333333)
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 700, 'Back', { fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
}
}

View File

@ -12,21 +12,21 @@ export class FusionScene extends Phaser.Scene {
create() {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
this.add.text(width / 2, 35, 'Fusion Lab', { fontSize: '28px', color: '#d4af37' }).setOrigin(0.5);
this.add.text(width / 2, 68, 'Combine 3 copies of a card to upgrade it to the next rarity tier', {
fontSize: '13px', color: '#888888'
this.add.text(width / 2, 40, 'Fusion Lab', { fontSize: '38px', color: '#d4af37' }).setOrigin(0.5);
this.add.text(width / 2, 80, 'Combine 3 copies of a card to upgrade it to the next rarity tier', {
fontSize: '18px', color: '#888888'
}).setOrigin(0.5);
// Recipe reference
const recipeX = width - 200;
this.add.text(recipeX, 120, 'Fusion Recipes', { fontSize: '14px', color: '#d4af37' }).setOrigin(0.5);
const recipeX = width - 300;
this.add.text(recipeX, 155, 'Fusion Recipes', { fontSize: '20px', color: '#d4af37' }).setOrigin(0.5);
const recipeLines = [
'3x Common -> 1x Rare',
'3x Rare -> 1x Epic',
'3x Epic -> 1x Legendary'
];
recipeLines.forEach((line, i) => {
this.add.text(recipeX, 145 + i * 20, line, { fontSize: '12px', color: '#aaaaaa' }).setOrigin(0.5);
this.add.text(recipeX, 195 + i * 28, line, { fontSize: '16px', color: '#aaaaaa' }).setOrigin(0.5);
});
this.listGroup = [];
@ -49,13 +49,13 @@ export class FusionScene extends Phaser.Scene {
});
if (fusibleCards.length === 0) {
const t = this.add.text(width / 2, 360, 'No cards available for fusion.\nCollect 3 or more copies of the same card to fuse them.', {
fontSize: '16px', color: '#888888', align: 'center'
const t = this.add.text(width / 2, 480, 'No cards available for fusion.\nCollect 3 or more copies of the same card to fuse them.', {
fontSize: '22px', color: '#888888', align: 'center'
}).setOrigin(0.5);
this.listGroup.push(t);
// Show progress toward next fusion
this.add.text(width / 2, 460, 'Your collection progress:', { fontSize: '14px', color: '#666666' }).setOrigin(0.5);
this.add.text(width / 2, 610, 'Your collection progress:', { fontSize: '18px', color: '#666666' }).setOrigin(0.5);
const nearFusion = Object.entries(save.collection)
.filter(([id, count]) => {
const card = cardManager.getCard(id);
@ -68,9 +68,9 @@ export class FusionScene extends Phaser.Scene {
const card = cardManager.getCard(id);
if (!card) return;
const recipe = FUSION_RECIPES[card.rarity];
const t2 = this.add.text(width / 2, 490 + i * 24,
const t2 = this.add.text(width / 2, 650 + i * 30,
`${card.name} (${card.rarity}): ${count}/${recipe.count}`,
{ fontSize: '13px', color: '#555555' }
{ fontSize: '18px', color: '#555555' }
).setOrigin(0.5);
this.listGroup.push(t2);
});
@ -81,37 +81,37 @@ export class FusionScene extends Phaser.Scene {
const card = cardManager.getCard(cardId);
const recipe = FUSION_RECIPES[card.rarity];
const cols = 4;
const x = 180 + (i % cols) * 260;
const y = 210 + Math.floor(i / cols) * 200;
const x = 270 + (i % cols) * 380;
const y = 280 + Math.floor(i / cols) * 265;
if (y > 640) return;
if (y > 940) return;
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
const factionColors = { imperial: 0x2244aa, raider: 0xaa2222 };
const bg = this.add.rectangle(x, y, 230, 150, factionColors[card.faction] || 0x1a3a5c)
const bg = this.add.rectangle(x, y, 340, 230, factionColors[card.faction] || 0x1a3a5c)
.setStrokeStyle(2, rarityColors[card.rarity] || 0x888888);
const nameT = this.add.text(x, y - 55, card.name, {
fontSize: '14px', color: '#ffffff', wordWrap: { width: 220 }, align: 'center'
const nameT = this.add.text(x, y - 85, card.name, {
fontSize: '20px', color: '#ffffff', wordWrap: { width: 320 }, align: 'center'
}).setOrigin(0.5);
const rarityT = this.add.text(x, y - 36, card.rarity.toUpperCase(), {
fontSize: '11px', color: '#' + (rarityColors[card.rarity] || 0x888888).toString(16).padStart(6, '0')
const rarityT = this.add.text(x, y - 56, card.rarity.toUpperCase(), {
fontSize: '16px', color: '#' + (rarityColors[card.rarity] || 0x888888).toString(16).padStart(6, '0')
}).setOrigin(0.5);
const statsT = this.add.text(x, y - 20, `ATK:${card.attack} HP:${card.health} ARM:${card.armor}`, {
fontSize: '11px', color: '#aaaaff'
const statsT = this.add.text(x, y - 32, `ATK:${card.attack} HP:${card.health} ARM:${card.armor}`, {
fontSize: '16px', color: '#aaaaff'
}).setOrigin(0.5);
const countT = this.add.text(x, y - 4, `You have: ${count} (need ${recipe.count})`, {
fontSize: '11px', color: '#44ff44'
const countT = this.add.text(x, y - 8, `You have: ${count} (need ${recipe.count})`, {
fontSize: '16px', color: '#44ff44'
}).setOrigin(0.5);
const resultT = this.add.text(x, y + 14, `Result: ${recipe.result_rarity.toUpperCase()}`, {
fontSize: '13px', color: '#ffd700'
const resultT = this.add.text(x, y + 18, `Result: ${recipe.result_rarity.toUpperCase()}`, {
fontSize: '18px', color: '#ffd700'
}).setOrigin(0.5);
const fuseBtn = this.add.rectangle(x, y + 48, 160, 36, 0x553300)
const fuseBtn = this.add.rectangle(x, y + 68, 220, 48, 0x553300)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0xffaa00);
const fuseTxt = this.add.text(x, y + 48, 'FUSE (3x)', { fontSize: '14px', color: '#ffd700' }).setOrigin(0.5);
const fuseTxt = this.add.text(x, y + 68, 'FUSE (3x)', { fontSize: '18px', color: '#ffd700' }).setOrigin(0.5);
fuseBtn.on('pointerover', () => fuseBtn.setFillStyle(0x775500));
fuseBtn.on('pointerout', () => fuseBtn.setFillStyle(0x553300));
@ -161,7 +161,7 @@ export class FusionScene extends Phaser.Scene {
_showMsg(msg, color) {
const { width } = this.scale;
if (this._msgText) this._msgText.destroy();
this._msgText = this.add.text(width / 2, 660, msg, { fontSize: '18px', color }).setOrigin(0.5);
this._msgText = this.add.text(width / 2, 960, msg, { fontSize: '24px', color }).setOrigin(0.5);
this.time.delayedCall(2500, () => {
if (this._msgText) this._msgText.destroy();
this._msgText = null;
@ -169,10 +169,10 @@ export class FusionScene extends Phaser.Scene {
}
_makeBackButton() {
const bg = this.add.rectangle(80, 700, 120, 35, 0x333333)
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 700, 'Back', { fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
}
}

View File

@ -12,25 +12,25 @@ export class MainMenuScene extends Phaser.Scene {
this.add.rectangle(width / 2, height / 2, width, 2, 0x1a3a5c).setAlpha(0.5);
// Title
this.add.text(width / 2, 100, "TYRANT'S EDGE", {
fontSize: '48px',
this.add.text(width / 2, 120, "TYRANT'S EDGE", {
fontSize: '72px',
color: '#d4af37',
stroke: '#000000',
strokeThickness: 4
strokeThickness: 6
}).setOrigin(0.5);
this.add.text(width / 2, 155, 'A Collectible Card Game', {
fontSize: '18px', color: '#888888'
this.add.text(width / 2, 190, 'A Collectible Card Game', {
fontSize: '24px', color: '#888888'
}).setOrigin(0.5);
// Gold display
this.goldText = this.add.text(width - 20, 20, `Gold: ${save.gold}`, {
fontSize: '18px', color: '#ffd700'
this.goldText = this.add.text(width - 30, 30, `Gold: ${save.gold}`, {
fontSize: '24px', color: '#ffd700'
}).setOrigin(1, 0);
// Version
this.add.text(20, height - 20, 'v1.0', {
fontSize: '12px', color: '#444444'
fontSize: '14px', color: '#444444'
}).setOrigin(0, 1);
const buttons = [
@ -42,20 +42,23 @@ export class MainMenuScene extends Phaser.Scene {
{ label: 'Fusion Lab', scene: 'FusionScene' }
];
const startY = 310;
const spacing = 90;
buttons.forEach((btn, i) => {
this._makeButton(width / 2, 250 + i * 70, btn.label, () => {
this._makeButton(width / 2, startY + i * spacing, btn.label, () => {
this.scene.start(btn.scene, btn.data || {});
});
});
}
_makeButton(x, y, label, callback) {
const bg = this.add.rectangle(x, y, 280, 50, 0x1a3a5c)
const bg = this.add.rectangle(x, y, 400, 65, 0x1a3a5c)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x4488ff);
const text = this.add.text(x, y, label, {
fontSize: '20px', color: '#ffffff'
fontSize: '28px', color: '#ffffff'
}).setOrigin(0.5);
bg.on('pointerover', () => bg.setFillStyle(0x2a5a8c));

View File

@ -9,19 +9,19 @@ export class StoreScene extends Phaser.Scene {
const packManager = this.registry.get('packManager');
this.add.rectangle(width / 2, height / 2, width, height, 0x0d1b2a);
this.add.text(width / 2, 40, 'Card Store', { fontSize: '32px', color: '#d4af37' }).setOrigin(0.5);
this.add.text(width / 2, 75, 'Buy packs to expand your collection', {
fontSize: '14px', color: '#888888'
this.add.text(width / 2, 45, 'Card Store', { fontSize: '42px', color: '#d4af37' }).setOrigin(0.5);
this.add.text(width / 2, 90, 'Buy packs to expand your collection', {
fontSize: '20px', color: '#888888'
}).setOrigin(0.5);
this.goldText = this.add.text(width - 20, 20, `Gold: ${save.gold}`, {
fontSize: '18px', color: '#ffd700'
fontSize: '24px', color: '#ffd700'
}).setOrigin(1, 0);
const packs = packManager.getAllPacks();
packs.forEach((pack, i) => {
const x = width / 2 + (i - packs.length / 2 + 0.5) * 300;
this._renderPack(pack, x, 280);
const x = width / 2 + (i - packs.length / 2 + 0.5) * 440;
this._renderPack(pack, x, 350);
});
this.revealArea = [];
@ -30,29 +30,29 @@ export class StoreScene extends Phaser.Scene {
_renderPack(pack, x, y) {
// Pack visual
const packBg = this.add.rectangle(x, y, 260, 220, 0x1a3a5c)
const packBg = this.add.rectangle(x, y, 380, 340, 0x1a3a5c)
.setStrokeStyle(3, 0xffd700);
// Pack art (procedural)
this.add.rectangle(x, y - 40, 120, 100, 0x0d2240).setStrokeStyle(1, 0x4488ff);
this.add.text(x, y - 40, 'PACK', { fontSize: '28px', color: '#4488ff' }).setOrigin(0.5);
this.add.rectangle(x, y - 60, 180, 150, 0x0d2240).setStrokeStyle(1, 0x4488ff);
this.add.text(x, y - 60, 'PACK', { fontSize: '38px', color: '#4488ff' }).setOrigin(0.5);
this.add.text(x, y + 30, pack.name, { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(x, y + 52, `${pack.cardCount} cards per pack`, { fontSize: '13px', color: '#aaaaaa' }).setOrigin(0.5);
this.add.text(x, y + 70, `Cost: ${pack.cost} Gold`, { fontSize: '15px', color: '#ffd700' }).setOrigin(0.5);
this.add.text(x, y + 45, pack.name, { fontSize: '24px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(x, y + 75, `${pack.cardCount} cards per pack`, { fontSize: '18px', color: '#aaaaaa' }).setOrigin(0.5);
this.add.text(x, y + 100, `Cost: ${pack.cost} Gold`, { fontSize: '20px', color: '#ffd700' }).setOrigin(0.5);
// Drop rates display
const rateLines = Object.entries(pack.dropRates).map(([r, v]) =>
`${r.charAt(0).toUpperCase() + r.slice(1)}: ${Math.floor(v * 100)}%`
).join(' ');
this.add.text(x, y + 90, rateLines, {
fontSize: '10px', color: '#888888', wordWrap: { width: 240 }, align: 'center'
this.add.text(x, y + 128, rateLines, {
fontSize: '14px', color: '#888888', wordWrap: { width: 360 }, align: 'center'
}).setOrigin(0.5);
const buyBtn = this.add.rectangle(x, y + 120, 180, 45, 0x224422)
const buyBtn = this.add.rectangle(x, y + 170, 240, 55, 0x224422)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(2, 0x44aa44);
const buyTxt = this.add.text(x, y + 120, 'Buy Pack', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
const buyTxt = this.add.text(x, y + 170, 'Buy Pack', { fontSize: '22px', color: '#ffffff' }).setOrigin(0.5);
buyBtn.on('pointerover', () => buyBtn.setFillStyle(0x336633));
buyBtn.on('pointerout', () => buyBtn.setFillStyle(0x224422));
@ -83,40 +83,40 @@ export class StoreScene extends Phaser.Scene {
this.revealArea = [];
// Show revealed cards with animation
const totalW = cards.length * 200 + (cards.length - 1) * 20;
const startX = width / 2 - totalW / 2 + 90;
const totalW = cards.length * 300 + (cards.length - 1) * 20;
const startX = width / 2 - totalW / 2 + 140;
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
const factionColors = { imperial: 0x2244aa, raider: 0xaa2222 };
// Label
const label = this.add.text(width / 2, 510, 'You received:', {
fontSize: '16px', color: '#ffffff'
const label = this.add.text(width / 2, 700, 'You received:', {
fontSize: '22px', color: '#ffffff'
}).setOrigin(0.5).setAlpha(0);
this.revealArea.push(label);
this.tweens.add({ targets: label, alpha: 1, duration: 300 });
cards.forEach((card, i) => {
const x = startX + i * 220;
const y = 610;
const x = startX + i * 320;
const y = 860;
const bg = this.add.rectangle(x, y, 190, 140, factionColors[card.faction] || 0x1a3a5c)
const bg = this.add.rectangle(x, y, 280, 210, factionColors[card.faction] || 0x1a3a5c)
.setStrokeStyle(3, rarityColors[card.rarity] || 0x888888)
.setAlpha(0);
const nameT = this.add.text(x, y - 45, card.name, {
fontSize: '14px', color: '#ffffff', wordWrap: { width: 180 }, align: 'center'
const nameT = this.add.text(x, y - 70, card.name, {
fontSize: '20px', color: '#ffffff', wordWrap: { width: 260 }, align: 'center'
}).setOrigin(0.5).setAlpha(0);
const rarityT = this.add.text(x, y - 18, card.rarity.toUpperCase(), {
fontSize: '12px',
const rarityT = this.add.text(x, y - 30, card.rarity.toUpperCase(), {
fontSize: '16px',
color: '#' + (rarityColors[card.rarity] || 0x888888).toString(16).padStart(6, '0')
}).setOrigin(0.5).setAlpha(0);
const statsT = this.add.text(x, y + 4, `ATK:${card.attack} HP:${card.health} ARM:${card.armor}`, {
fontSize: '12px', color: '#aaaaff'
fontSize: '16px', color: '#aaaaff'
}).setOrigin(0.5).setAlpha(0);
const skillT = this.add.text(x, y + 24, card.skills.map(s => s.name).join(', '), {
fontSize: '10px', color: '#ffcc44'
const skillT = this.add.text(x, y + 34, card.skills.map(s => s.name).join(', '), {
fontSize: '14px', color: '#ffcc44'
}).setOrigin(0.5).setAlpha(0);
const newT = this.add.text(x, y + 45, (save.collection[card.id] || 0) === 1 ? 'NEW!' : `x${save.collection[card.id]}`, {
fontSize: '12px', color: (save.collection[card.id] || 0) === 1 ? '#44ff44' : '#aaaaaa'
const newT = this.add.text(x, y + 65, (save.collection[card.id] || 0) === 1 ? 'NEW!' : `x${save.collection[card.id]}`, {
fontSize: '16px', color: (save.collection[card.id] || 0) === 1 ? '#44ff44' : '#aaaaaa'
}).setOrigin(0.5).setAlpha(0);
this.revealArea.push(bg, nameT, rarityT, statsT, skillT, newT);
@ -131,15 +131,15 @@ export class StoreScene extends Phaser.Scene {
_showMsg(msg, color) {
const { width } = this.scale;
const t = this.add.text(width / 2, 470, msg, { fontSize: '18px', color }).setOrigin(0.5);
const t = this.add.text(width / 2, 660, msg, { fontSize: '24px', color }).setOrigin(0.5);
this.time.delayedCall(2000, () => t.destroy());
}
_makeBackButton() {
const bg = this.add.rectangle(80, 700, 120, 35, 0x333333)
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 700, 'Back', { fontSize: '14px', color: '#ffffff' }).setOrigin(0.5);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff' }).setOrigin(0.5);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
}
}