294 lines
8.8 KiB
JavaScript
294 lines
8.8 KiB
JavaScript
import { Player } from '../entities/Player.js';
|
|
import { WaveManager } from '../systems/WaveManager.js';
|
|
import { XPSystem } from '../systems/XPSystem.js';
|
|
import { SkillTree } from '../systems/SkillTree.js';
|
|
import { HUD } from '../ui/HUD.js';
|
|
import { SkillTreeUI } from '../ui/SkillTreeUI.js';
|
|
|
|
export class GameScene extends Phaser.Scene {
|
|
constructor() {
|
|
super({ key: 'GameScene' });
|
|
}
|
|
|
|
preload() {
|
|
this.load.json('zones', './js/data/zones.json');
|
|
this.load.json('skillTree', './js/data/skillTree.json');
|
|
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
|
|
}
|
|
|
|
create() {
|
|
const W = this.scale.width;
|
|
const H = this.scale.height;
|
|
|
|
this._drawArena(W, H);
|
|
|
|
this.physics.world.setBounds(0, 0, W, H);
|
|
|
|
// Core systems — data already loaded by preload()
|
|
this.skillTree = new SkillTree();
|
|
this.skillTree.load(this.cache.json.get('skillTree'));
|
|
|
|
this.player = new Player(this, W / 2, H / 2);
|
|
|
|
this.xpSystem = new XPSystem(this);
|
|
|
|
this.waveManager = new WaveManager(this, this.player);
|
|
this.waveManager.load(this.cache.json.get('zones'));
|
|
|
|
this.hud = new HUD(this, this.player, this.xpSystem);
|
|
|
|
// Enemy projectile group for collision
|
|
this._enemyProjectiles = [];
|
|
|
|
// Event wiring
|
|
this.events.on('enemy-killed', ({ xp }) => this.xpSystem.addXP(xp));
|
|
this.events.on('enemy-projectile-spawned', proj => this._enemyProjectiles.push(proj));
|
|
this.events.on('level-up', level => this._showLevelUp(level));
|
|
this.events.on('game-over', () => this._onGameOver());
|
|
this.events.on('victory', () => this._onVictory());
|
|
|
|
this._levelUpPending = false;
|
|
this._frozen = false;
|
|
this._waitingForExit = false;
|
|
|
|
this.events.on('zone-waves-complete', () => this._startZoneExit());
|
|
|
|
this.waveManager.start();
|
|
}
|
|
|
|
_drawArena(W, H) {
|
|
// Dark background
|
|
this.add.rectangle(W / 2, H / 2, W, H, 0x111118);
|
|
|
|
// Grid lines for depth cue
|
|
const g = this.add.graphics();
|
|
g.lineStyle(1, 0x222233, 0.5);
|
|
for (let x = 0; x <= W; x += 80) { g.lineBetween(x, 0, x, H); }
|
|
for (let y = 0; y <= H; y += 80) { g.lineBetween(0, y, W, y); }
|
|
|
|
// Arena border
|
|
g.lineStyle(3, 0x334466, 1);
|
|
g.strokeRect(2, 2, W - 4, H - 4);
|
|
}
|
|
|
|
update(time, delta) {
|
|
if (!this.player || this._frozen) return;
|
|
|
|
this.player.update(delta);
|
|
this.waveManager.update(delta);
|
|
this.hud.update();
|
|
|
|
this._checkBulletHits();
|
|
this._checkEnemyProjectileHits();
|
|
this._pruneEnemyProjectiles();
|
|
|
|
if (this._waitingForExit) this._checkPlayerExit();
|
|
}
|
|
|
|
_checkBulletHits() {
|
|
const bullets = this.player.bullets.getChildren();
|
|
const enemies = this.waveManager.enemies;
|
|
|
|
for (const bullet of bullets) {
|
|
if (!bullet.active) continue;
|
|
for (const enemy of enemies) {
|
|
if (!enemy.active) continue;
|
|
const dist = Phaser.Math.Distance.Between(bullet.x, bullet.y, enemy.x, enemy.y);
|
|
if (dist < enemy.radius + 5) {
|
|
enemy.takeDamage(bullet.damage);
|
|
bullet.destroy();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_checkEnemyProjectileHits() {
|
|
this._enemyProjectiles = this._enemyProjectiles.filter(p => p?.active);
|
|
this._enemyProjectiles.forEach(proj => {
|
|
if (!proj.active) return;
|
|
const dist = Phaser.Math.Distance.Between(proj.x, proj.y, this.player.x, this.player.y);
|
|
if (dist < 16 + 6) {
|
|
this.player.takeDamage(proj.damage);
|
|
proj.destroy();
|
|
}
|
|
});
|
|
}
|
|
|
|
_pruneEnemyProjectiles() {
|
|
this._enemyProjectiles = this._enemyProjectiles.filter(p => p?.active);
|
|
}
|
|
|
|
// ── Zone exit (Smash TV style) ─────────────────────────────────────────────
|
|
|
|
_startZoneExit() {
|
|
this._waitingForExit = true;
|
|
// Let the player walk off any edge
|
|
this.player.sprite.body.setCollideWorldBounds(false);
|
|
this._showExitArrows();
|
|
}
|
|
|
|
_showExitArrows() {
|
|
const W = this.scale.width;
|
|
const H = this.scale.height;
|
|
|
|
const g = this.add.graphics();
|
|
g.setDepth(15);
|
|
this._exitArrowsGfx = g;
|
|
|
|
const ARROW_W = 30; // triangle base half-width
|
|
const ARROW_D = 28; // triangle depth (pointing direction)
|
|
const MARGIN = 18; // gap from screen edge to arrow tip
|
|
|
|
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
|
|
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
|
|
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
|
|
const drawRight = (tipX, cy) => g.fillTriangle(tipX - ARROW_D, cy - ARROW_W, tipX - ARROW_D, cy + ARROW_W, tipX, cy);
|
|
|
|
// Three arrows per edge
|
|
const xPositions = [W * 0.25, W * 0.5, W * 0.75];
|
|
const yPositions = [H * 0.25, H * 0.5, H * 0.75];
|
|
|
|
g.fillStyle(0xffffff, 1);
|
|
xPositions.forEach(cx => {
|
|
drawUp(cx, MARGIN);
|
|
drawDown(cx, H - MARGIN);
|
|
});
|
|
yPositions.forEach(cy => {
|
|
drawLeft(MARGIN, cy);
|
|
drawRight(W - MARGIN, cy);
|
|
});
|
|
|
|
// Blink the arrows
|
|
this._exitArrowsTween = this.tweens.add({
|
|
targets: g,
|
|
alpha: 0.1,
|
|
duration: 350,
|
|
yoyo: true,
|
|
repeat: -1,
|
|
ease: 'Sine.easeInOut',
|
|
});
|
|
|
|
// "Zone cleared" prompt
|
|
this._exitText = this.add.text(W / 2, 60, 'ZONE CLEARED — EXIT THROUGH ANY DOOR', {
|
|
fontSize: '20px',
|
|
fill: '#ffdd44',
|
|
fontStyle: 'bold',
|
|
stroke: '#000000',
|
|
strokeThickness: 4,
|
|
}).setOrigin(0.5).setDepth(15);
|
|
|
|
this.tweens.add({
|
|
targets: this._exitText,
|
|
alpha: 0,
|
|
duration: 400,
|
|
yoyo: true,
|
|
repeat: -1,
|
|
});
|
|
}
|
|
|
|
_hideExitArrows() {
|
|
this._exitArrowsTween?.stop();
|
|
this._exitArrowsGfx?.destroy();
|
|
this._exitText?.destroy();
|
|
this._exitArrowsGfx = null;
|
|
this._exitText = null;
|
|
}
|
|
|
|
_checkPlayerExit() {
|
|
const W = this.scale.width;
|
|
const H = this.scale.height;
|
|
const px = this.player.x;
|
|
const py = this.player.y;
|
|
|
|
if (px < -24) this._doZoneTransition('left');
|
|
else if (px > W + 24) this._doZoneTransition('right');
|
|
else if (py < -24) this._doZoneTransition('top');
|
|
else if (py > H + 24) this._doZoneTransition('bottom');
|
|
}
|
|
|
|
_doZoneTransition(direction) {
|
|
this._waitingForExit = false;
|
|
this._hideExitArrows();
|
|
|
|
const W = this.scale.width;
|
|
const H = this.scale.height;
|
|
|
|
// Respawn player on the opposite edge, same lateral position
|
|
const px = Phaser.Math.Clamp(this.player.x, 48, W - 48);
|
|
const py = Phaser.Math.Clamp(this.player.y, 48, H - 48);
|
|
const spawnX = direction === 'left' ? W - 48 : direction === 'right' ? 48 : px;
|
|
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
|
|
|
|
// White flash
|
|
const flash = this.add.rectangle(W / 2, H / 2, W, H, 0xffffff).setDepth(50);
|
|
this.tweens.add({
|
|
targets: flash,
|
|
alpha: 0,
|
|
duration: 300,
|
|
onComplete: () => flash.destroy(),
|
|
});
|
|
|
|
this.player.sprite.setPosition(spawnX, spawnY);
|
|
this.player.sprite.body.setCollideWorldBounds(true);
|
|
this.waveManager.startNextZone();
|
|
}
|
|
|
|
// ── Level up ───────────────────────────────────────────────────────────────
|
|
|
|
_showLevelUp(level) {
|
|
if (this._levelUpPending) return;
|
|
this._levelUpPending = true;
|
|
|
|
// Only show if there are skills available
|
|
const available = this.skillTree.getAvailable();
|
|
if (available.length === 0) {
|
|
this._levelUpPending = false;
|
|
return;
|
|
}
|
|
|
|
this._frozen = true;
|
|
this.physics.world.pause();
|
|
|
|
new SkillTreeUI(this, this.skillTree, this.player, () => {
|
|
this._levelUpPending = false;
|
|
this._frozen = false;
|
|
this.physics.world.resume();
|
|
});
|
|
}
|
|
|
|
_onGameOver() {
|
|
this.scene.launch('GameOverScene');
|
|
this.scene.pause();
|
|
}
|
|
|
|
_onVictory() {
|
|
const W = this.scale.width;
|
|
const H = this.scale.height;
|
|
|
|
this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.7).setDepth(30);
|
|
this.add.text(W / 2, H / 2 - 40, 'VICTORY!', {
|
|
fontSize: '72px', fill: '#ffdd00', fontStyle: 'bold'
|
|
}).setOrigin(0.5).setDepth(31);
|
|
|
|
const prompt = this.add.text(W / 2, H / 2 + 60, 'Press R to return to menu', {
|
|
fontSize: '22px', fill: '#ffffff'
|
|
}).setOrigin(0.5).setDepth(31);
|
|
this.tweens.add({ targets: prompt, alpha: 0, duration: 700, yoyo: true, repeat: -1 });
|
|
|
|
this.input.keyboard.once('keydown-R', () => {
|
|
this.scene.start('IntroScene');
|
|
});
|
|
}
|
|
|
|
shutdown() {
|
|
this.events.removeAllListeners();
|
|
this._hideExitArrows();
|
|
this._waitingForExit = false;
|
|
this.player?.destroy();
|
|
this.waveManager?.reset();
|
|
this.hud?.destroy();
|
|
this._enemyProjectiles = [];
|
|
}
|
|
}
|