import { SaveManager } from '../managers/SaveManager.js'; import { TutorialManager } from '../managers/TutorialManager.js'; // ── Layout Constants ──────────────────────────────────────────────────────────── const LEFT_PANEL_W = 420; const DIVIDER_X = 420; const MISSION_BTN_X = 210; const MISSION_BTN_W = 360; const MISSION_BTN_H = 90; const MISSION_BTN_GAP = 10; const MISSION_LIST_TOP = 130; const TERM_PAD = 30; const TERM_X = LEFT_PANEL_W + TERM_PAD; const TERM_Y = 120; const TERM_W = 1920 - LEFT_PANEL_W - TERM_PAD * 2; const TERM_H = 940; const VIDEO_W = 640; const VIDEO_H = 360; // ── Amber CRT Palette ─────────────────────────────────────────────────────────── const AMBER = '#ffaa33'; const AMBER_DIM = '#cc8822'; const AMBER_BRIGHT = '#ff8800'; const AMBER_HEX = 0xffaa33; const AMBER_DARK_HEX = 0x332200; const AMBER_BG_HEX = 0x1a1100; const TERM_BG_HEX = 0x0a0800; const FACTION_HEX = { raider: 0xcc4444, bloodthirsty: 0xcc4488, xeno: 0x44cc66, righteous: 0xcccc44, imperial: 0x4466cc }; const FACTION_COLORS = { raider: '#cc4444', bloodthirsty: '#cc4488', xeno: '#44cc66', righteous: '#cccc44', imperial: '#4466cc' }; export class CampaignScene extends Phaser.Scene { constructor() { super('CampaignScene'); } init(data) { this.campaignId = data.campaignId || 'campaign_raider'; } preload() { this.load.video('corridors', 'assets/video/corridors.mp4'); } create() { const allMissions = this.registry.get('missions'); const campaigns = this.registry.get('campaigns'); const save = this.registry.get('save'); const { width, height } = this.scale; const campaign = campaigns.find(c => c.id === this.campaignId); this._campaign = campaign; this._missions = allMissions.filter(m => m.campaignId === this.campaignId); this._completed = save.campaignProgress.completedMissions; this._save = save; this._typingEvents = []; this._terminalTextObjects = []; this._terminalLines = []; this._currentTermLine = 0; this._selectedMission = null; this._selectedIndex = -1; this._missionButtons = []; this._briefingElements = []; // Check if campaign is fully complete const allDone = this._missions.length > 0 && this._missions.every(m => this._completed.includes(m.id)); if (allDone && campaign) { this._checkCampaignRewards(campaign, save, campaigns); } // ── Video background ────────────────────────────────────────────────────── const bg = this.add.video(width / 2, height / 2, 'corridors'); bg.setMute(true); bg.on('created', () => { if (!bg.scene) return; bg.setDisplaySize(width, height); const t = this.registry.get('corridors_time'); if (t > 0) bg.setCurrentTime(t); }); bg.play(true); this.events.on('shutdown', () => { if (bg.scene) this.registry.set('corridors_time', bg.getCurrentTime()); this._clearTerminal(); }); // Dark overlay this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.55); // Scanlines const scanlines = this.add.graphics(); for (let sy = 0; sy < height; sy += 3) { scanlines.fillStyle(0x000000, 0.22); scanlines.fillRect(0, sy, width, 1); } // ── Header ──────────────────────────────────────────────────────────────── this.add.text(width / 2, 45, campaign ? campaign.name : 'Campaign', { fontSize: '42px', color: '#d4af37', fontFamily: 'RaiderCrusader' }).setOrigin(0.5); if (campaign) { const fColor = FACTION_COLORS[campaign.faction] || '#888888'; this.add.text(width / 2, 90, `Level ${campaign.level} — ${campaign.faction.toUpperCase()}`, { fontSize: '18px', color: fColor, fontFamily: 'Audiowide' }).setOrigin(0.5); } // ── Faction-colored divider ─────────────────────────────────────────────── const divColor = FACTION_HEX[campaign?.faction] || 0x444444; const divider = this.add.graphics(); divider.lineStyle(2, divColor, 0.6); divider.lineBetween(DIVIDER_X, 115, DIVIDER_X, height - 10); // ── Build panels ────────────────────────────────────────────────────────── this._buildMissionList(); this._buildBriefingTerminal(); this._makeBackButton(); this._showTutorial(); } // ════════════════════════════════════════════════════════════════════════════ // ── LEFT PANEL: Mission List ─────────────────────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _buildMissionList() { const missions = this._missions; const completed = this._completed; missions.forEach((mission, i) => { const isUnlocked = !mission.unlockCondition || completed.includes(mission.unlockCondition); const isDone = completed.includes(mission.id); const x = MISSION_BTN_X; const y = MISSION_LIST_TOP + i * (MISSION_BTN_H + MISSION_BTN_GAP); // Button background const bgColor = isDone ? 0x1a3322 : isUnlocked ? 0x1a2a3c : 0x222222; const node = this.add.rectangle(x, y, MISSION_BTN_W, MISSION_BTN_H, bgColor) .setStrokeStyle(1, isDone ? 0x336633 : isUnlocked ? 0x334466 : 0x333333); // Faction accent stripe (left edge) const factionColor = FACTION_HEX[this._campaign?.faction] || 0x444444; const stripe = this.add.rectangle( x - MISSION_BTN_W / 2 + 2, y, 4, MISSION_BTN_H, isDone ? factionColor : isUnlocked ? 0x4488ff : 0x333333 ); // Mission number (small, dim) const numText = this.add.text(x - MISSION_BTN_W / 2 + 16, y - 18, `MISSION ${i + 1}`, { fontSize: '12px', color: '#666666', fontFamily: 'Audiowide' }); // Mission name const nameColor = isDone ? '#aaffaa' : isUnlocked ? '#ffffff' : '#555555'; const nameText = this.add.text(x - MISSION_BTN_W / 2 + 16, y + 2, mission.name, { fontSize: '16px', color: nameColor, fontFamily: 'Audiowide', wordWrap: { width: MISSION_BTN_W - 60 } }); // Status marker (right side) const marker = isDone ? '[\u2713]' : !isUnlocked ? '[\ud83d\udd12]' : ''; const markerColor = isDone ? '#44ff44' : '#555555'; if (marker) { this.add.text(x + MISSION_BTN_W / 2 - 16, y, marker, { fontSize: '16px', color: markerColor, fontFamily: 'Audiowide' }).setOrigin(1, 0.5); } // Interactivity if (isUnlocked) { node.setInteractive({ useHandCursor: true }); node.on('pointerover', () => { if (this._selectedIndex !== i) { node.setFillStyle(isDone ? 0x2a4433 : 0x2a4466); } }); node.on('pointerout', () => { if (this._selectedIndex !== i) { node.setFillStyle(bgColor); } }); node.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); this._selectMission(mission, i); }); } this._missionButtons.push({ node, bgColor, isDone, isUnlocked, index: i }); }); } // ════════════════════════════════════════════════════════════════════════════ // ── RIGHT PANEL: Tactical Operations Terminal ────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _buildBriefingTerminal() { this._termContainer = this.add.container(0, 0); const tx = TERM_X; const ty = TERM_Y; const tw = TERM_W; const th = TERM_H; this._termBoundsX = tx; this._termBoundsY = ty; this._termBoundsW = tw; this._termBoundsH = th; // ── Outer border ────────────────────────────────────────────────────────── const termBg = this.add.rectangle(tx + tw / 2, ty + th / 2, tw, th, TERM_BG_HEX, 0.95) .setStrokeStyle(2, AMBER_DARK_HEX); this._termContainer.add(termBg); // Inner border (shadow effect) const innerBorder = this.add.rectangle(tx + tw / 2, ty + th / 2, tw - 8, th - 8) .setStrokeStyle(1, AMBER_BG_HEX) .setFillStyle(0x000000, 0); this._termContainer.add(innerBorder); // ── Terminal header bar ─────────────────────────────────────────────────── const headerBar = this.add.rectangle(tx + tw / 2, ty + 18, tw - 12, 28, AMBER_DARK_HEX, 0.3); this._termContainer.add(headerBar); const headerText = this.add.text(tx + 16, ty + 10, '\u2588 TACTICAL OPERATIONS TERMINAL v2.7', { fontSize: '14px', color: AMBER_DIM, fontFamily: 'monospace' }); this._termContainer.add(headerText); // Status indicator (right side of header) this._statusIndicator = this.add.text(tx + tw - 16, ty + 10, '[\u25CF ONLINE]', { fontSize: '14px', color: '#44aa33', fontFamily: 'monospace' }).setOrigin(1, 0); this._termContainer.add(this._statusIndicator); // ── Tactical grid overlay ───────────────────────────────────────────────── const grid = this.add.graphics(); grid.lineStyle(1, 0x331100, 0.06); for (let gx = tx; gx < tx + tw; gx += 40) { grid.lineBetween(gx, ty, gx, ty + th); } for (let gy = ty; gy < ty + th; gy += 40) { grid.lineBetween(tx, gy, tx + tw, gy); } this._termContainer.add(grid); // ── Scanlines ───────────────────────────────────────────────────────────── const scanlines = this.add.graphics(); scanlines.fillStyle(0x000000, 0.1); for (let y = ty; y < ty + th; y += 3) { scanlines.fillRect(tx, y, tw, 1); } this._termContainer.add(scanlines); // ── Vignette ────────────────────────────────────────────────────────────── this._buildVignette(tx, ty, tw, th); // ── Flicker effect ──────────────────────────────────────────────────────── this.time.addEvent({ delay: 100, loop: true, callback: () => { if (this._termContainer) { this._termContainer.alpha = 0.96 + Math.random() * 0.04; } } }); // ── Status indicator blink ──────────────────────────────────────────────── this.time.addEvent({ delay: 800, loop: true, callback: () => { if (this._statusIndicator) { this._statusIndicator.setVisible(!this._statusIndicator.visible); } } }); // ── Text content area ───────────────────────────────────────────────────── this._termTextX = tx + 20; this._termTextY = ty + 44; this._termMaxW = tw - 40; this._termLineH = 22; // ── Cursor ──────────────────────────────────────────────────────────────── this._cursor = this.add.text(this._termTextX, this._termTextY, '\u2588', { fontSize: '18px', color: AMBER, fontFamily: 'monospace' }); this._termContainer.add(this._cursor); this._cursor.setVisible(false); this._cursorTimer = this.time.addEvent({ delay: 400, loop: true, callback: () => { if (this._cursor) this._cursor.setVisible(!this._cursor.visible); } }); // ── Boot sequence ───────────────────────────────────────────────────────── const campaignName = this._campaign ? this._campaign.name.toUpperCase() : 'UNKNOWN'; this._typeLines([ `> TACTICAL OPERATIONS TERMINAL v2.7`, `> INITIALIZING SECURE UPLINK...`, `> CAMPAIGN: ${campaignName}`, `> FACTION THREAT LEVEL: ${this._campaign?.faction?.toUpperCase() || 'UNKNOWN'}`, '', `> ${this._missions.length} MISSIONS LOADED.`, `> ${this._completed.filter(id => this._missions.some(m => m.id === id)).length} / ${this._missions.length} MISSIONS COMPLETE.`, '', '> STATUS: AWAITING MISSION SELECTION...', '> SELECT A MISSION TO VIEW BRIEFING.' ], 0); } _buildVignette(tx, ty, tw, th) { const vig = this.add.graphics(); // Top for (let i = 0; i < 30; i++) { vig.fillStyle(0x000000, 0.3 * (1 - i / 30)); vig.fillRect(tx, ty + i, tw, 1); } // Bottom for (let i = 0; i < 30; i++) { vig.fillStyle(0x000000, 0.3 * (1 - i / 30)); vig.fillRect(tx, ty + th - i, tw, 1); } // Left for (let i = 0; i < 20; i++) { vig.fillStyle(0x000000, 0.25 * (1 - i / 20)); vig.fillRect(tx + i, ty, 1, th); } // Right for (let i = 0; i < 20; i++) { vig.fillStyle(0x000000, 0.25 * (1 - i / 20)); vig.fillRect(tx + tw - i, ty, 1, th); } this._termContainer.add(vig); } // ════════════════════════════════════════════════════════════════════════════ // ── Mission Selection ────────────────────────────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _selectMission(mission, index) { // Update button highlights for (const btn of this._missionButtons) { if (btn.index === index) { btn.node.setStrokeStyle(2, AMBER_HEX); btn.node.setFillStyle(btn.isDone ? 0x2a4433 : 0x2a3a4c); } else { btn.node.setStrokeStyle(1, btn.isDone ? 0x336633 : btn.isUnlocked ? 0x334466 : 0x333333); btn.node.setFillStyle(btn.bgColor); } } this._selectedMission = mission; this._selectedIndex = index; // Destroy previous briefing elements for (const el of this._briefingElements) { if (el && el.scene) el.destroy(); } this._briefingElements = []; this._renderMissionBriefing(mission, index); } // ════════════════════════════════════════════════════════════════════════════ // ── Mission Briefing Renderer ────────────────────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _renderMissionBriefing(mission, index) { this._clearTerminal(); const tx = this._termBoundsX; const ty = this._termBoundsY; const tw = this._termBoundsW; const th = this._termBoundsH; // ── Mission Title ───────────────────────────────────────────────────────── const titleStr = `[ MISSION ${index + 1}: ${mission.name.toUpperCase()} ]`; const title = this.add.text(tx + tw / 2, ty + 52, titleStr, { fontSize: '24px', color: AMBER_BRIGHT, fontFamily: 'monospace', fontStyle: 'bold' }).setOrigin(0.5); this._termContainer.add(title); this._briefingElements.push(title); // ── Video Placeholder ───────────────────────────────────────────────────── const vidX = tx + tw / 2; const vidY = ty + 100 + VIDEO_H / 2; // Dark background const vidBg = this.add.rectangle(vidX, vidY, VIDEO_W, VIDEO_H, 0x0a0a0a) .setStrokeStyle(2, FACTION_HEX[this._campaign?.faction] || 0x444444); this._termContainer.add(vidBg); this._briefingElements.push(vidBg); // Corner bracket decorations (L-shaped amber lines) const bracketG = this.add.graphics(); const arm = 24; const bPad = 4; const left = vidX - VIDEO_W / 2 - bPad; const right = vidX + VIDEO_W / 2 + bPad; const top = vidY - VIDEO_H / 2 - bPad; const bottom = vidY + VIDEO_H / 2 + bPad; bracketG.lineStyle(2, AMBER_HEX, 0.7); // Top-left bracketG.lineBetween(left, top, left + arm, top); bracketG.lineBetween(left, top, left, top + arm); // Top-right bracketG.lineBetween(right, top, right - arm, top); bracketG.lineBetween(right, top, right, top + arm); // Bottom-left bracketG.lineBetween(left, bottom, left + arm, bottom); bracketG.lineBetween(left, bottom, left, bottom - arm); // Bottom-right bracketG.lineBetween(right, bottom, right - arm, bottom); bracketG.lineBetween(right, bottom, right, bottom - arm); this._termContainer.add(bracketG); this._briefingElements.push(bracketG); // Placeholder text const offlineText = this.add.text(vidX, vidY - 15, 'SURVEILLANCE FEED OFFLINE', { fontSize: '18px', color: AMBER_DIM, fontFamily: 'monospace' }).setOrigin(0.5); this._termContainer.add(offlineText); this._briefingElements.push(offlineText); const fileText = this.add.text(vidX, vidY + 15, `Expected: ${mission.id}.mp4`, { fontSize: '13px', color: '#665522', fontFamily: 'monospace' }).setOrigin(0.5); this._termContainer.add(fileText); this._briefingElements.push(fileText); // Attempt to load actual video const vidKey = `vid_mission_${mission.id}`; try { if (this.cache.video.has(mission.id)) { // If video was previously loaded, show it offlineText.setVisible(false); fileText.setVisible(false); const vid = this.add.video(vidX, vidY, mission.id); vid.setMute(true); vid.on('created', () => { if (vid.scene) vid.setDisplaySize(VIDEO_W, VIDEO_H); }); vid.play(true); this._termContainer.add(vid); this._briefingElements.push(vid); } } catch (e) { /* Video not available — placeholder stays */ } // ── DEPLOY Button ───────────────────────────────────────────────────────── const deployX = tx + tw / 2; const deployY = ty + th - 50; const deployBg = this.add.rectangle(deployX, deployY, 280, 55, 0x1a1100) .setStrokeStyle(2, AMBER_HEX) .setInteractive({ useHandCursor: true }); this._briefingElements.push(deployBg); const deployText = this.add.text(deployX, deployY, '>> DEPLOY <<', { fontSize: '22px', color: AMBER, fontFamily: 'monospace', fontStyle: 'bold' }).setOrigin(0.5); this._briefingElements.push(deployText); // Deploy button glow pulse this.tweens.add({ targets: deployBg, alpha: { from: 1, to: 0.7 }, duration: 1200, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); deployBg.on('pointerover', () => { deployBg.setFillStyle(0x332200); deployBg.setStrokeStyle(3, 0xffcc44); }); deployBg.on('pointerout', () => { deployBg.setFillStyle(0x1a1100); deployBg.setStrokeStyle(2, AMBER_HEX); }); deployBg.on('pointerdown', () => { this._startMission(this._selectedMission, this._campaign); }); // ── Typed Briefing Text ─────────────────────────────────────────────────── // Position typing below the video this._termTextY = vidY + VIDEO_H / 2 + 24; this._currentTermLine = 0; // Resolve card reward names const cardManager = this.registry.get('cardManager'); const rewardCards = (mission.rewards.cards || []).map(id => { const card = cardManager?.getCard?.(id); return card ? card.name : id; }); const lines = [ `> \u2500\u2500\u2500 SITUATION BRIEFING \u2500\u2500\u2500`, `> ${mission.description}`, '', `> \u2500\u2500\u2500 INTELLIGENCE REPORT \u2500\u2500\u2500`, `> ${mission.lore}`, '', `> \u2500\u2500\u2500 REWARDS MANIFEST \u2500\u2500\u2500`, `> GOLD: +${mission.rewards.gold} credits` ]; for (const cardName of rewardCards) { lines.push(`> CARD: ${cardName}`); } lines.push(''); lines.push(`> \u2500\u2500\u2500 END TRANSMISSION \u2500\u2500\u2500`); this._typeLines(lines, 0); } // ════════════════════════════════════════════════════════════════════════════ // ── Terminal Typing System (ported from CollectionScene, amber palette) ──── // ════════════════════════════════════════════════════════════════════════════ _clearTerminal() { for (const evt of this._typingEvents) { if (evt && evt.remove) evt.remove(false); } this._typingEvents = []; this._terminalLines = []; for (const t of this._terminalTextObjects) { if (t && t.scene) t.destroy(); } this._terminalTextObjects = []; if (this._cursor) { this._cursor.setPosition(this._termTextX, this._termTextY); this._cursor.setVisible(false); } this._currentTermLine = 0; } _typeLines(lines, lineIdx) { if (lineIdx >= lines.length) return; const line = lines[lineIdx]; if (line === '') { this._currentTermLine = (this._currentTermLine || 0) + 1; const evt = this.time.delayedCall(20, () => this._typeLines(lines, lineIdx + 1)); this._typingEvents.push(evt); return; } this._currentTermLine = this._currentTermLine || 0; const y = this._termTextY + this._currentTermLine * this._termLineH; // Check overflow const maxY = this._termBoundsY + this._termBoundsH - 80; // Leave room for deploy button if (y + this._termLineH > maxY) { this._scrollTerminal(); this._typeLines(lines, lineIdx); return; } const textObj = this.add.text(this._termTextX, y, '', { fontSize: '16px', color: AMBER, fontFamily: 'monospace', wordWrap: { width: this._termMaxW } }); this._termContainer.add(textObj); this._terminalTextObjects.push(textObj); let charIdx = 0; const typeChar = () => { charIdx = Math.min(charIdx + 5, line.length); textObj.setText(line.substring(0, charIdx)); if (charIdx % 15 === 0) this._playKeyclick(); this._updateCursorPos(textObj); if (charIdx >= line.length) { const wrappedCount = Math.max(1, Math.round(textObj.height / this._termLineH)); this._currentTermLine += wrappedCount; const evt = this.time.delayedCall(24, () => this._typeLines(lines, lineIdx + 1)); this._typingEvents.push(evt); return; } const evt = this.time.delayedCall(4, typeChar); this._typingEvents.push(evt); }; if (this._cursor) this._cursor.setVisible(true); typeChar(); } _scrollTerminal() { if (this._terminalTextObjects.length > 0) { const oldest = this._terminalTextObjects.shift(); if (oldest && oldest.scene) oldest.destroy(); } for (const t of this._terminalTextObjects) { t.y -= this._termLineH; } this._currentTermLine--; } _updateCursorPos(textObj) { if (!this._cursor || !textObj) return; this._cursor.setPosition(textObj.x + textObj.width + 2, textObj.y); this._cursor.setVisible(true); } _playKeyclick() { try { const ctx = this.sound.context; if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 800 + Math.random() * 200; gain.gain.value = 0.04; osc.start(ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.03); osc.stop(ctx.currentTime + 0.03); } catch (e) { /* WebAudio not available */ } } // ════════════════════════════════════════════════════════════════════════════ // ── Tutorial ─────────────────────────────────────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _showTutorial() { const save = this._save; if (TutorialManager.isStageComplete(save, TutorialManager.STAGES.CAMPAIGN_SELECT)) return; if (this.campaignId !== 'campaign_raider') return; const missions = this._missions; const { width, height } = this.scale; const DEPTH = 1000; const mission = missions[0]; if (!mission) return; // Dark overlay const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75) .setDepth(DEPTH) .setInteractive(); // Mission 1 button position (matches _buildMissionList layout) const btnX = MISSION_BTN_X; const btnY = MISSION_LIST_TOP; // Duplicate Mission 1 button const boxBg = this.add.rectangle(btnX, btnY, MISSION_BTN_W, MISSION_BTN_H, 0x1a2a3c) .setDepth(DEPTH + 1) .setInteractive({ useHandCursor: true }) .setStrokeStyle(2, 0x4488ff); const numText = this.add.text(btnX - MISSION_BTN_W / 2 + 16, btnY - 18, 'MISSION 1', { fontSize: '12px', color: '#666666', fontFamily: 'Audiowide' }).setDepth(DEPTH + 1); const nameText = this.add.text(btnX - MISSION_BTN_W / 2 + 16, btnY + 2, mission.name, { fontSize: '16px', color: '#ffffff', fontFamily: 'Audiowide', wordWrap: { width: MISSION_BTN_W - 60 } }).setDepth(DEPTH + 1); boxBg.on('pointerover', () => boxBg.setFillStyle(0x2a4466)); boxBg.on('pointerout', () => boxBg.setFillStyle(0x1a2a3c)); // Bobbing arrow const arrow = this.add.text(btnX, btnY - 70, '\u25BC', { fontSize: '48px', color: '#ffd700' }).setOrigin(0.5).setDepth(DEPTH + 1); this.tweens.add({ targets: arrow, y: btnY - 55, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); // Instructional text (to the right) const infoText = this.add.text(btnX + MISSION_BTN_W / 2 + 30, btnY, [ 'Select a mission to view its briefing!', 'Then click DEPLOY to begin battle.' ].join('\n'), { fontSize: '20px', color: '#ffffff', fontFamily: 'Audiowide', align: 'left', lineSpacing: 6 }).setOrigin(0, 0.5).setDepth(DEPTH + 1); const tutorialElements = [overlay, boxBg, numText, nameText, arrow, infoText]; boxBg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); TutorialManager.completeStage(save, TutorialManager.STAGES.CAMPAIGN_SELECT); tutorialElements.forEach(el => el.destroy()); this._selectMission(mission, 0); }); } // ════════════════════════════════════════════════════════════════════════════ // ── Existing Methods (preserved) ─────────────────────────────────────────── // ════════════════════════════════════════════════════════════════════════════ _startMission(mission, campaign) { const save = this.registry.get('save'); if (!save.decks || save.decks.length === 0) { // Show error in terminal area this._clearTerminal(); this._typeLines([ '> \u2500\u2500\u2500 ERROR \u2500\u2500\u2500', '> NO DECK DETECTED.', '> CREATE A DECK IN DECK BUILDER FIRST.', '> DEPLOY ABORTED.' ], 0); return; } this.sound.play('sfx_menu_select', { volume: 0.7 }); this.scene.start('BattleScene', { mission, deck: save.decks[0], playerLevel: save.level || 1, enemyLevel: save.level || 1, campaignId: this.campaignId }); } _checkCampaignRewards(campaign, save, allCampaigns) { const rewardKey = `campaignReward_${campaign.id}`; if (campaign.starterReward?.cards?.length > 0 && !save[rewardKey]) { for (const cardId of campaign.starterReward.cards) { save.collection[cardId] = (save.collection[cardId] || 0) + 1; } save[rewardKey] = true; if (campaign.level >= save.level && save.level < 5) { save.level = Math.min(5, campaign.level + 1); } const nextCampaign = allCampaigns.find(c => c.unlockCondition === campaign.id); if (nextCampaign && !save.unlockedCampaigns.includes(nextCampaign.id)) { save.unlockedCampaigns.push(nextCampaign.id); } SaveManager.save(save); } } _makeBackButton() { const bg = this.add.rectangle(80, 45, 160, 50, 0x333333) .setInteractive({ useHandCursor: true }) .setStrokeStyle(1, 0x888888); this.add.text(80, 45, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5); bg.on('pointerdown', () => { this.sound.play('sfx_menu_select', { volume: 0.7 }); this.scene.start('CampaignSelectScene'); }); } }