785 lines
32 KiB
JavaScript
785 lines
32 KiB
JavaScript
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');
|
|
});
|
|
}
|
|
}
|