Add main menu, ghost enemy, and sword slash trail
This commit is contained in:
+150
-55
@@ -1,15 +1,20 @@
|
||||
import * as THREE from 'three';
|
||||
import { Player } from './Player';
|
||||
import { Enemy, initSpiderModel } from './Enemy';
|
||||
import { Enemy } from './Enemy';
|
||||
import { Spider, initSpiderModel } from './Spider';
|
||||
import { Ghost } from './Ghost';
|
||||
import { Sword } from './Sword';
|
||||
import { Staff } from './Staff';
|
||||
import { Fireball } from './Fireball';
|
||||
import { Cross, initCrossModel } from './Cross';
|
||||
import { Arena } from './Arena';
|
||||
import { SlashEffect } from './SwordTrail';
|
||||
import { initSounds, playSound, resumeContext } from './SoundManager';
|
||||
|
||||
const SPAWN_TIME = 10;
|
||||
|
||||
type GameState = 'menu' | 'playing' | 'gameover';
|
||||
|
||||
export class Game {
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
@@ -20,6 +25,9 @@ export class Game {
|
||||
enemies: Enemy[] = [];
|
||||
fireballs: Fireball[] = [];
|
||||
crosses: Cross[] = [];
|
||||
slashEffects: SlashEffect[] = [];
|
||||
|
||||
state: GameState = 'menu';
|
||||
|
||||
keys: Set<string> = new Set();
|
||||
mouseButtons: Set<number> = new Set();
|
||||
@@ -31,7 +39,6 @@ export class Game {
|
||||
spawnTimer = 0;
|
||||
spawns = 0;
|
||||
killed = 0;
|
||||
gameOver = false;
|
||||
|
||||
// Debug hitzone visualization
|
||||
private showHitzones = false;
|
||||
@@ -51,6 +58,9 @@ export class Game {
|
||||
private uiWaveBarFill!: HTMLElement;
|
||||
private uiWaveBarLabel!: HTMLElement;
|
||||
private uiGameOver!: HTMLElement;
|
||||
private uiGameOverPanel!: HTMLElement;
|
||||
private uiGameOverStats!: HTMLElement;
|
||||
private uiMainMenu!: HTMLElement;
|
||||
private mouseOnCanvas = false;
|
||||
|
||||
constructor() {
|
||||
@@ -115,6 +125,13 @@ export class Game {
|
||||
this.uiWaveBarFill = document.getElementById('wave-bar-fill')!;
|
||||
this.uiWaveBarLabel = document.getElementById('wave-bar-label')!;
|
||||
this.uiGameOver = document.getElementById('game-over')!;
|
||||
this.uiGameOverPanel = document.getElementById('game-over-panel')!;
|
||||
this.uiGameOverStats = document.getElementById('game-over-stats')!;
|
||||
this.uiMainMenu = document.getElementById('main-menu')!;
|
||||
|
||||
document.getElementById('btn-start')!.addEventListener('click', () => this.startGame());
|
||||
document.getElementById('btn-restart')!.addEventListener('click', () => this.restart());
|
||||
document.getElementById('btn-menu')!.addEventListener('click', () => this.showMenu());
|
||||
|
||||
const canvas = this.renderer.domElement;
|
||||
canvas.addEventListener('mouseenter', () => this.mouseOnCanvas = true);
|
||||
@@ -125,17 +142,21 @@ export class Game {
|
||||
private setupInput() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
this.keys.add(e.code);
|
||||
if (e.code === 'Space') {
|
||||
if (e.code === 'Space' && this.state === 'playing') {
|
||||
this.player.jump();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.code === 'KeyH') {
|
||||
this.showHitzones = !this.showHitzones;
|
||||
}
|
||||
if (e.code === 'Enter' || e.code === 'Space') {
|
||||
if (this.state === 'menu') this.startGame();
|
||||
else if (this.state === 'gameover') this.restart();
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
this.keys.delete(e.code);
|
||||
if (e.code === 'KeyE' && !this.gameOver) {
|
||||
if (e.code === 'KeyE' && this.state === 'playing') {
|
||||
this.player.leftHandWeapon.skill2(this.groundPoint, this.player);
|
||||
}
|
||||
});
|
||||
@@ -157,7 +178,7 @@ export class Game {
|
||||
}
|
||||
|
||||
private handleMouseRelease(button: number) {
|
||||
if (this.gameOver) return;
|
||||
if (this.state !== 'playing') return;
|
||||
if (button === 0) {
|
||||
this.player.leftHandWeapon.skill1(this.groundPoint, this.player);
|
||||
} else if (button === 1) {
|
||||
@@ -286,11 +307,104 @@ export class Game {
|
||||
}
|
||||
}
|
||||
|
||||
startGame() {
|
||||
if (this.state === 'playing') return;
|
||||
this.resetWorld();
|
||||
this.state = 'playing';
|
||||
this.uiMainMenu.style.opacity = '0';
|
||||
this.uiMainMenu.style.pointerEvents = 'none';
|
||||
this.uiGameOver.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.3s ease';
|
||||
this.uiGameOverPanel.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.startGame();
|
||||
}
|
||||
|
||||
showMenu() {
|
||||
this.resetWorld();
|
||||
this.state = 'menu';
|
||||
this.uiGameOver.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.3s ease';
|
||||
this.uiGameOverPanel.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'none';
|
||||
this.uiMainMenu.style.opacity = '1';
|
||||
this.uiMainMenu.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
private gameOver() {
|
||||
this.state = 'gameover';
|
||||
this.player.die();
|
||||
this.uiGameOver.style.opacity = '1';
|
||||
this.uiGameOverStats.textContent = `Wave ${this.spawns} · Kills: ${this.killed}`;
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.8s ease-in 1.5s';
|
||||
this.uiGameOverPanel.style.opacity = '1';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'auto';
|
||||
playSound('gameover', 0.8);
|
||||
}
|
||||
|
||||
private resetWorld() {
|
||||
for (const enemy of this.enemies) {
|
||||
enemy.dispose();
|
||||
this.scene.remove(enemy.body);
|
||||
}
|
||||
this.enemies = [];
|
||||
|
||||
for (const fb of this.fireballs) this.scene.remove(fb.body);
|
||||
this.fireballs = [];
|
||||
|
||||
for (const cross of this.crosses) this.scene.remove(cross.body);
|
||||
this.crosses = [];
|
||||
|
||||
for (const slash of this.slashEffects) slash.dispose();
|
||||
this.slashEffects = [];
|
||||
|
||||
for (const [enemy, ring] of this.enemyHitRings) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.enemyHitRings.delete(enemy);
|
||||
}
|
||||
for (const [fb, ring] of this.fireballHitRings) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.fireballHitRings.delete(fb);
|
||||
}
|
||||
for (const [cross, ring] of this.crossHitRings) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
(ring.material as THREE.Material).dispose();
|
||||
this.crossHitRings.delete(cross);
|
||||
}
|
||||
|
||||
this.spawnTimer = 0;
|
||||
this.spawns = 0;
|
||||
this.killed = 0;
|
||||
|
||||
this.player.reset();
|
||||
this.player.rightHandWeapon.cooldown1 = 0;
|
||||
this.player.rightHandWeapon.cooldown2 = 0;
|
||||
this.player.rightHandWeapon.cooldown3 = 0;
|
||||
this.player.leftHandWeapon.cooldown1 = 0;
|
||||
this.player.leftHandWeapon.cooldown2 = 0;
|
||||
this.player.leftHandWeapon.cooldown3 = 0;
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
private spawnEnemy() {
|
||||
const ghostChance = this.spawns >= 3
|
||||
? Math.min(0.15 + (this.spawns - 3) * 0.08, 0.6)
|
||||
: 0;
|
||||
const isGhost = Math.random() < ghostChance;
|
||||
const enemy = isGhost ? new Ghost() : new Spider();
|
||||
|
||||
const x = (Math.random() - 0.5) * 80;
|
||||
const z = (Math.random() - 0.5) * 80;
|
||||
const enemy = new Enemy(this.player.body.position);
|
||||
enemy.body.position.set(x, 0.1, z);
|
||||
enemy.body.position.set(x, isGhost ? 1.0 : 0.1, z);
|
||||
this.scene.add(enemy.body);
|
||||
this.enemies.push(enemy);
|
||||
}
|
||||
@@ -319,7 +433,7 @@ export class Game {
|
||||
}
|
||||
|
||||
private updatePlayerMovement(dt: number) {
|
||||
if (this.gameOver) {
|
||||
if (this.state !== 'playing') {
|
||||
this.player.body.position.y = 0.5;
|
||||
return;
|
||||
}
|
||||
@@ -379,45 +493,18 @@ export class Game {
|
||||
);
|
||||
}
|
||||
|
||||
private clampToArena(pos: THREE.Vector3) {
|
||||
clampToArena(pos: THREE.Vector3) {
|
||||
pos.x = Math.max(-47, Math.min(47, pos.x));
|
||||
pos.z = Math.max(-47, Math.min(47, pos.z));
|
||||
}
|
||||
|
||||
private updateEnemies(dt: number) {
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) {
|
||||
if (enemy.dead || this.state !== 'playing') {
|
||||
enemy.updateAnimation(dt);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toPlayer = new THREE.Vector3()
|
||||
.subVectors(this.player.body.position, enemy.body.position);
|
||||
toPlayer.y = 0;
|
||||
|
||||
const dist = toPlayer.length();
|
||||
if (dist > 0.1) {
|
||||
const dir = toPlayer.normalize();
|
||||
enemy.body.lookAt(
|
||||
enemy.body.position.x + dir.x,
|
||||
enemy.body.position.y,
|
||||
enemy.body.position.z + dir.z
|
||||
);
|
||||
|
||||
enemy.playAnim('walk');
|
||||
|
||||
let moveDir = dir.multiplyScalar(6);
|
||||
if (enemy.knockback > 0) {
|
||||
moveDir = dir.multiplyScalar(-6);
|
||||
enemy.knockback -= dt;
|
||||
}
|
||||
enemy.body.position.x += moveDir.x * dt;
|
||||
enemy.body.position.z += moveDir.z * dt;
|
||||
this.clampToArena(enemy.body.position);
|
||||
} else {
|
||||
enemy.playAnim('stand');
|
||||
}
|
||||
|
||||
enemy.update(dt, this);
|
||||
enemy.updateAnimation(dt);
|
||||
}
|
||||
}
|
||||
@@ -489,11 +576,8 @@ export class Game {
|
||||
this.uiWaveBarFill.style.width = `${wavePct * 100}%`;
|
||||
this.uiWaveBarLabel.textContent = `Next Wave: ${Math.ceil(SPAWN_TIME - this.spawnTimer)}s`;
|
||||
|
||||
if (this.player.health <= 0 && !this.gameOver) {
|
||||
this.gameOver = true;
|
||||
this.player.die();
|
||||
this.uiGameOver.style.opacity = '1';
|
||||
playSound('gameover', 0.8);
|
||||
if (this.player.health <= 0 && this.state === 'playing') {
|
||||
this.gameOver();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,12 +592,19 @@ export class Game {
|
||||
this.updateFireballs(dt);
|
||||
this.updateHitzones();
|
||||
|
||||
// Update slash effects
|
||||
for (let i = this.slashEffects.length - 1; i >= 0; i--) {
|
||||
if (!this.slashEffects[i].update(dt)) {
|
||||
this.slashEffects.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update animations
|
||||
this.player.updateAnimations(dt);
|
||||
this.player.rightHandWeapon.updateAnimation(dt);
|
||||
|
||||
// Spawn enemies in waves
|
||||
if (!this.gameOver) {
|
||||
if (this.state === 'playing') {
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer >= SPAWN_TIME) {
|
||||
this.spawns++;
|
||||
@@ -524,12 +615,14 @@ export class Game {
|
||||
}
|
||||
}
|
||||
|
||||
// Check close encounters (spider touching player)
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.health -= 25 * dt;
|
||||
// Check close encounters (enemy touching player)
|
||||
if (this.state === 'playing') {
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.health -= 25 * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,11 +630,13 @@ export class Game {
|
||||
for (let i = this.crosses.length - 1; i >= 0; i--) {
|
||||
const cross = this.crosses[i];
|
||||
cross.update(dt);
|
||||
const dist = cross.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.heal(25);
|
||||
this.scene.remove(cross.body);
|
||||
this.crosses.splice(i, 1);
|
||||
if (this.state === 'playing') {
|
||||
const dist = cross.body.position.distanceTo(this.player.body.position);
|
||||
if (dist < 2) {
|
||||
this.player.heal(25);
|
||||
this.scene.remove(cross.body);
|
||||
this.crosses.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user