Add skill system, level-ups, and new enemy types
This commit is contained in:
+355
-27
@@ -3,17 +3,27 @@ import { Player } from './Player';
|
||||
import { Enemy } from './Enemy';
|
||||
import { Spider, initSpiderModel } from './Spider';
|
||||
import { Ghost } from './Ghost';
|
||||
import { Frog } from './Frog';
|
||||
import { Bat } from './Bat';
|
||||
import { BeeSwarm } from './BeeSwarm';
|
||||
import { Slime } from './Slime';
|
||||
import { Brute } from './Brute';
|
||||
import { Boss } from './Boss';
|
||||
import { Crab } from './Crab';
|
||||
import { Turret } from './Turret';
|
||||
import { EnemyProjectile } from './EnemyProjectile';
|
||||
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 { SlashEffect, isInSlashArc } from './SwordTrail';
|
||||
import { SkillSystem, getSkill, type SkillOffer } from './Skills';
|
||||
import { initSounds, playSound, resumeContext } from './SoundManager';
|
||||
|
||||
const SPAWN_TIME = 10;
|
||||
|
||||
type GameState = 'menu' | 'playing' | 'gameover';
|
||||
type GameState = 'menu' | 'playing' | 'gameover' | 'levelup';
|
||||
|
||||
export class Game {
|
||||
scene: THREE.Scene;
|
||||
@@ -26,8 +36,15 @@ export class Game {
|
||||
fireballs: Fireball[] = [];
|
||||
crosses: Cross[] = [];
|
||||
slashEffects: SlashEffect[] = [];
|
||||
projectiles: EnemyProjectile[] = [];
|
||||
|
||||
state: GameState = 'menu';
|
||||
skillSystem: SkillSystem = new SkillSystem(0);
|
||||
private seed: number | null = null;
|
||||
private offers: SkillOffer[] = [];
|
||||
private selectedOffer = -1;
|
||||
private pendingPicks = 0;
|
||||
private gameOverAt = 0;
|
||||
|
||||
keys: Set<string> = new Set();
|
||||
mouseButtons: Set<number> = new Set();
|
||||
@@ -53,14 +70,30 @@ export class Game {
|
||||
private uiWaveLabel!: HTMLElement;
|
||||
private uiKillsLabel!: HTMLElement;
|
||||
private uiFireballCD!: HTMLElement;
|
||||
private uiFireballContainer!: HTMLElement;
|
||||
private uiSwordCD!: HTMLElement;
|
||||
private uiTeleportCD!: HTMLElement;
|
||||
private uiTeleportContainer!: HTMLElement;
|
||||
private uiLightningCD!: HTMLElement;
|
||||
private uiLightningContainer!: HTMLElement;
|
||||
private uiWaveBarFill!: HTMLElement;
|
||||
private uiWaveBarLabel!: HTMLElement;
|
||||
private uiGameOver!: HTMLElement;
|
||||
private uiGameOverPanel!: HTMLElement;
|
||||
private uiGameOverStats!: HTMLElement;
|
||||
private uiMainMenu!: HTMLElement;
|
||||
private uiSeedInput!: HTMLInputElement;
|
||||
private uiLevelLabel!: HTMLElement;
|
||||
private uiSeedLabel!: HTMLElement;
|
||||
private uiXpBarFill!: HTMLElement;
|
||||
private uiXpBarLabel!: HTMLElement;
|
||||
private uiShieldBar!: HTMLElement;
|
||||
private uiShieldBarBg!: HTMLElement;
|
||||
private uiShieldText!: HTMLElement;
|
||||
private uiLevelUp!: HTMLElement;
|
||||
private uiConfirmBtn!: HTMLButtonElement;
|
||||
private uiOfferTitles: HTMLElement[] = [];
|
||||
private uiOfferDescs: HTMLElement[] = [];
|
||||
private mouseOnCanvas = false;
|
||||
|
||||
constructor() {
|
||||
@@ -120,23 +153,58 @@ export class Game {
|
||||
this.uiWaveLabel = document.getElementById('wave-label')!;
|
||||
this.uiKillsLabel = document.getElementById('kills-label')!;
|
||||
this.uiFireballCD = document.querySelector('#fireball-cooldown .cooldown-fill')!;
|
||||
this.uiFireballContainer = document.getElementById('fireball-cooldown')!;
|
||||
this.uiSwordCD = document.querySelector('#sword-cooldown .cooldown-fill')!;
|
||||
this.uiTeleportCD = document.querySelector('#teleport-cooldown .cooldown-fill')!;
|
||||
this.uiTeleportContainer = document.getElementById('teleport-cooldown')!;
|
||||
this.uiLightningCD = document.querySelector('#lightning-cooldown .cooldown-fill')!;
|
||||
this.uiLightningContainer = document.getElementById('lightning-cooldown')!;
|
||||
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')!;
|
||||
this.uiSeedInput = document.getElementById('seed-input') as HTMLInputElement;
|
||||
this.uiLevelLabel = document.getElementById('level-label')!;
|
||||
this.uiSeedLabel = document.getElementById('seed-label')!;
|
||||
this.uiXpBarFill = document.getElementById('xp-bar-fill')!;
|
||||
this.uiXpBarLabel = document.getElementById('xp-bar-label')!;
|
||||
this.uiShieldBar = document.getElementById('shield-bar')!;
|
||||
this.uiShieldBarBg = document.getElementById('shield-bar-bg')!;
|
||||
this.uiShieldText = document.getElementById('shield-text')!;
|
||||
this.uiLevelUp = document.getElementById('level-up')!;
|
||||
this.uiConfirmBtn = document.getElementById('btn-confirm') as HTMLButtonElement;
|
||||
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const card = document.getElementById(`offer-${i}`)!;
|
||||
card.addEventListener('click', () => this.selectOffer(i));
|
||||
this.uiOfferTitles.push(document.getElementById(`offer-title-${i}`)!);
|
||||
this.uiOfferDescs.push(document.getElementById(`offer-desc-${i}`)!);
|
||||
}
|
||||
|
||||
document.getElementById('btn-start')!.addEventListener('click', () => this.startGame());
|
||||
document.getElementById('btn-restart')!.addEventListener('click', () => this.restart());
|
||||
document.getElementById('btn-menu')!.addEventListener('click', () => this.showMenu());
|
||||
|
||||
// Diagnostics: detect a full page reload that wiped a running game
|
||||
if (sessionStorage.getItem('wackelpeter-run') === '1') {
|
||||
console.warn('[Wackelpeter] Seite wurde während eines Laufs neu geladen!');
|
||||
sessionStorage.removeItem('wackelpeter-run');
|
||||
}
|
||||
window.addEventListener('beforeunload', () => {
|
||||
console.log('[Wackelpeter] beforeunload (Seite wird neu geladen/geschlossen)');
|
||||
});
|
||||
|
||||
const canvas = this.renderer.domElement;
|
||||
canvas.addEventListener('mouseenter', () => this.mouseOnCanvas = true);
|
||||
canvas.addEventListener('mouseleave', () => this.mouseOnCanvas = false);
|
||||
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
||||
window.addEventListener('contextmenu', e => e.preventDefault());
|
||||
}
|
||||
|
||||
private logState(next: GameState, reason: string) {
|
||||
console.log(`[Wackelpeter] state: ${this.state} → ${next} (${reason})`);
|
||||
}
|
||||
|
||||
private setupInput() {
|
||||
@@ -149,9 +217,25 @@ export class Game {
|
||||
if (e.code === 'KeyH') {
|
||||
this.showHitzones = !this.showHitzones;
|
||||
}
|
||||
if (e.code === 'KeyQ' && this.state === 'playing') {
|
||||
this.player.skillLightning.skill1(this.groundPoint, this.player);
|
||||
}
|
||||
if (this.state === 'levelup') {
|
||||
if (e.code === 'Digit1' || e.code === 'Numpad1') this.selectOffer(0);
|
||||
else if (e.code === 'Digit2' || e.code === 'Numpad2') this.selectOffer(1);
|
||||
else if (e.code === 'Digit3' || e.code === 'Numpad3') this.selectOffer(2);
|
||||
else if (e.code === 'Enter') this.confirmOffer();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Enter' || e.code === 'Space') {
|
||||
if (this.state === 'menu') this.startGame();
|
||||
else if (this.state === 'gameover') this.restart();
|
||||
else if (
|
||||
this.state === 'gameover' &&
|
||||
performance.now() - this.gameOverAt > 1500
|
||||
) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
@@ -180,11 +264,9 @@ export class Game {
|
||||
private handleMouseRelease(button: number) {
|
||||
if (this.state !== 'playing') return;
|
||||
if (button === 0) {
|
||||
this.player.leftHandWeapon.skill1(this.groundPoint, this.player);
|
||||
} else if (button === 1) {
|
||||
this.player.switchHands();
|
||||
} else if (button === 2) {
|
||||
this.player.rightHandWeapon.skill1(this.groundPoint, this.player);
|
||||
} else if (button === 2) {
|
||||
this.player.leftHandWeapon.skill1(this.groundPoint, this.player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,15 +390,31 @@ export class Game {
|
||||
}
|
||||
|
||||
startGame() {
|
||||
if (this.state === 'playing') return;
|
||||
if (this.state === 'playing' || this.state === 'levelup') return;
|
||||
if (this.seed === null) {
|
||||
const input = this.uiSeedInput.value.trim();
|
||||
if (input !== '' && !Number.isNaN(Number(input))) {
|
||||
this.seed = Number(input) >>> 0;
|
||||
} else {
|
||||
this.seed = Math.floor(Math.random() * 1000000);
|
||||
}
|
||||
}
|
||||
this.resetWorld();
|
||||
this.logState('playing', 'startGame');
|
||||
this.state = 'playing';
|
||||
sessionStorage.setItem('wackelpeter-run', '1');
|
||||
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';
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
private blurFocus() {
|
||||
const el = document.activeElement;
|
||||
if (el instanceof HTMLElement) el.blur();
|
||||
}
|
||||
|
||||
restart() {
|
||||
@@ -325,20 +423,27 @@ export class Game {
|
||||
|
||||
showMenu() {
|
||||
this.resetWorld();
|
||||
this.logState('menu', 'showMenu');
|
||||
this.state = 'menu';
|
||||
this.seed = null;
|
||||
sessionStorage.removeItem('wackelpeter-run');
|
||||
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';
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
private gameOver() {
|
||||
this.logState('gameover', 'gameOver');
|
||||
this.state = 'gameover';
|
||||
this.gameOverAt = performance.now();
|
||||
this.player.die();
|
||||
this.blurFocus();
|
||||
this.uiGameOver.style.opacity = '1';
|
||||
this.uiGameOverStats.textContent = `Wave ${this.spawns} · Kills: ${this.killed}`;
|
||||
this.uiGameOverStats.textContent = `Wave ${this.spawns} · Kills: ${this.killed} · Level ${this.skillSystem.level}`;
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.8s ease-in 1.5s';
|
||||
this.uiGameOverPanel.style.opacity = '1';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'auto';
|
||||
@@ -361,6 +466,9 @@ export class Game {
|
||||
for (const slash of this.slashEffects) slash.dispose();
|
||||
this.slashEffects = [];
|
||||
|
||||
for (const p of this.projectiles) p.dispose();
|
||||
this.projectiles = [];
|
||||
|
||||
for (const [enemy, ring] of this.enemyHitRings) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
@@ -383,32 +491,185 @@ export class Game {
|
||||
this.spawnTimer = 0;
|
||||
this.spawns = 0;
|
||||
this.killed = 0;
|
||||
this.pendingPicks = 0;
|
||||
this.hideLevelUp();
|
||||
this.skillSystem = new SkillSystem(this.seed ?? 0);
|
||||
|
||||
this.player.reset();
|
||||
this.player.applySkillSystem(this.skillSystem);
|
||||
this.syncSkillVisibility();
|
||||
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.player.skillLightning.cooldown1 = 0;
|
||||
this.player.skillLightning.cooldown2 = 0;
|
||||
this.player.skillLightning.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();
|
||||
onDamageDealt(damage: number) {
|
||||
if (this.state === 'playing' && this.player.stats.lifesteal > 0) {
|
||||
this.player.heal(damage * this.player.stats.lifesteal);
|
||||
}
|
||||
}
|
||||
|
||||
private openLevelUp() {
|
||||
this.offers = this.skillSystem.rollOffers();
|
||||
if (this.offers.length === 0) {
|
||||
// Everything maxed out: no picks left, just continue playing
|
||||
this.pendingPicks = 0;
|
||||
this.hideLevelUp();
|
||||
this.logState('playing', 'openLevelUp (keine Angebote)');
|
||||
this.state = 'playing';
|
||||
return;
|
||||
}
|
||||
this.logState('levelup', 'openLevelUp');
|
||||
this.state = 'levelup';
|
||||
this.selectedOffer = -1;
|
||||
this.blurFocus();
|
||||
playSound('spawn', 0.6);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const title = this.uiOfferTitles[i];
|
||||
const desc = this.uiOfferDescs[i];
|
||||
const card = document.getElementById(`offer-${i}`)!;
|
||||
if (i < this.offers.length) {
|
||||
const offer = this.offers[i];
|
||||
const skill = getSkill(offer.id);
|
||||
const isNew = offer.nextLevel === 1;
|
||||
title.textContent = `${skill.name}${isNew ? ' (NEU)' : ` · Stufe ${offer.nextLevel}`}`;
|
||||
desc.textContent = skill.describe(offer.nextLevel);
|
||||
card.style.display = '';
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
}
|
||||
this.uiConfirmBtn.disabled = true;
|
||||
this.uiLevelUp.style.opacity = '1';
|
||||
this.uiLevelUp.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
private hideLevelUp() {
|
||||
this.uiLevelUp.style.opacity = '0';
|
||||
this.uiLevelUp.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
private selectOffer(index: number) {
|
||||
if (this.state !== 'levelup' || index >= this.offers.length) return;
|
||||
this.selectedOffer = index;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const card = document.getElementById(`offer-${i}`)!;
|
||||
card.classList.toggle('selected', i === index);
|
||||
}
|
||||
this.uiConfirmBtn.disabled = false;
|
||||
}
|
||||
|
||||
private confirmOffer() {
|
||||
if (this.state !== 'levelup' || this.selectedOffer < 0) return;
|
||||
const offer = this.offers[this.selectedOffer];
|
||||
this.selectedOffer = -1;
|
||||
this.skillSystem.apply(offer.id);
|
||||
this.player.applySkillSystem(this.skillSystem);
|
||||
this.syncSkillVisibility();
|
||||
this.blurFocus();
|
||||
|
||||
if (this.pendingPicks > 0) {
|
||||
this.pendingPicks--;
|
||||
this.openLevelUp();
|
||||
} else if (this.skillSystem.hasPendingLevelUp()) {
|
||||
this.openLevelUp();
|
||||
} else {
|
||||
this.hideLevelUp();
|
||||
this.logState('playing', 'confirmOffer');
|
||||
this.state = 'playing';
|
||||
}
|
||||
}
|
||||
|
||||
private syncSkillVisibility() {
|
||||
const s = this.skillSystem;
|
||||
this.player.leftHandWeapon.mesh.visible = s.getLevel('fireball') > 0;
|
||||
this.uiFireballContainer.style.display = s.getLevel('fireball') > 0 ? '' : 'none';
|
||||
this.uiTeleportContainer.style.display = s.getLevel('teleport') > 0 ? '' : 'none';
|
||||
this.uiLightningContainer.style.display = s.getLevel('chainlightning') > 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
private spawnEnemy() {
|
||||
const x = (Math.random() - 0.5) * 80;
|
||||
const z = (Math.random() - 0.5) * 80;
|
||||
enemy.body.position.set(x, isGhost ? 1.0 : 0.1, z);
|
||||
|
||||
const weights = this.enemyWeights(this.spawns);
|
||||
let total = 0;
|
||||
for (const w of weights.values()) total += w;
|
||||
let roll = Math.random() * total;
|
||||
let kind = 'spider';
|
||||
for (const [k, w] of weights) {
|
||||
roll -= w;
|
||||
if (roll <= 0) {
|
||||
kind = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case 'spider': this.spawnEnemyInstance(new Spider(), x, z, 0.1); break;
|
||||
case 'ghost': this.spawnEnemyInstance(new Ghost(), x, z, 1.0); break;
|
||||
case 'frog': this.spawnEnemyInstance(new Frog(), x, z, 0.1); break;
|
||||
case 'bat': this.spawnEnemyInstance(new Bat(), x, z, 1.5); break;
|
||||
case 'bee': this.spawnEnemyInstance(new BeeSwarm(), x, z, 1.2); break;
|
||||
case 'slime': this.spawnEnemyInstance(new Slime(), x, z, 0.1); break;
|
||||
case 'brute': this.spawnEnemyInstance(new Brute(), x, z, 0.15); break;
|
||||
case 'crab': this.spawnEnemyInstance(new Crab(), x, z, 0.1); break;
|
||||
case 'turret': this.spawnEnemyInstance(new Turret(), x, z, 0.1); break;
|
||||
}
|
||||
}
|
||||
|
||||
private enemyWeights(wave: number): Map<string, number> {
|
||||
const w = new Map<string, number>();
|
||||
if (wave >= 7) {
|
||||
w.set('spider', 25).set('ghost', 15).set('frog', 15).set('slime', 10)
|
||||
.set('bat', 10).set('turret', 10).set('crab', 10).set('bee', 5);
|
||||
} else if (wave >= 6) {
|
||||
w.set('spider', 35).set('ghost', 20).set('frog', 15).set('slime', 10)
|
||||
.set('bat', 10).set('turret', 10);
|
||||
} else if (wave >= 5) {
|
||||
w.set('spider', 45).set('ghost', 20).set('frog', 15).set('slime', 10)
|
||||
.set('bat', 10);
|
||||
} else if (wave >= 4) {
|
||||
w.set('spider', 55).set('ghost', 25).set('frog', 20);
|
||||
} else if (wave >= 3) {
|
||||
w.set('spider', 75).set('ghost', 25);
|
||||
} else {
|
||||
w.set('spider', 100);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
spawnEnemyInstance(enemy: Enemy, x: number, z: number, y: number) {
|
||||
enemy.body.position.set(x, y, z);
|
||||
this.scene.add(enemy.body);
|
||||
this.enemies.push(enemy);
|
||||
}
|
||||
|
||||
spawnEnemyProjectile(from: THREE.Vector3, target: THREE.Vector3, damage: number) {
|
||||
const projectile = new EnemyProjectile(from, target, damage);
|
||||
this.scene.add(projectile.body);
|
||||
this.projectiles.push(projectile);
|
||||
}
|
||||
|
||||
destroyProjectilesInArc(pos: THREE.Vector3, aim: THREE.Vector3, range: number) {
|
||||
for (let i = this.projectiles.length - 1; i >= 0; i--) {
|
||||
const p = this.projectiles[i];
|
||||
if (isInSlashArc(pos, aim, p.body.position, range)) {
|
||||
p.dispose();
|
||||
this.projectiles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnCross(position: THREE.Vector3) {
|
||||
if (Math.random() > 0.9) {
|
||||
const cross = new Cross(position.clone());
|
||||
@@ -452,7 +713,7 @@ export class Game {
|
||||
if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) moveDir.add(camLeft);
|
||||
if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) moveDir.sub(camLeft);
|
||||
|
||||
let speed = 7.5;
|
||||
let speed = this.player.stats.moveSpeed;
|
||||
let isMoving = false;
|
||||
if (moveDir.length() > 0) {
|
||||
moveDir.normalize();
|
||||
@@ -521,8 +782,8 @@ export class Game {
|
||||
for (const enemy of this.enemies) {
|
||||
if (enemy.dead) continue;
|
||||
const dist = enemy.body.position.distanceTo(fb.body.position);
|
||||
if (dist < 8) {
|
||||
enemy.takeDamage(100, this);
|
||||
if (dist < fb.explosionRadius) {
|
||||
enemy.takeDamage(fb.damage, this, true, fb.body.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,15 +814,37 @@ export class Game {
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
const healthPct = this.player.health / this.player.MAX_HEALTH;
|
||||
const maxHealth = this.player.stats.maxHealth;
|
||||
const healthPct = this.player.health / maxHealth;
|
||||
this.uiHealthBar.style.width = `${Math.max(0, healthPct * 250)}px`;
|
||||
this.uiHealthText.textContent = `${Math.ceil(this.player.health)}%`;
|
||||
this.uiHealthText.textContent = `${Math.ceil(this.player.health)}`;
|
||||
|
||||
const shieldMax = this.player.stats.shield;
|
||||
this.uiShieldBarBg.style.display = shieldMax > 0 ? '' : 'none';
|
||||
this.uiShieldBar.style.display = shieldMax > 0 ? '' : 'none';
|
||||
this.uiShieldText.style.display = shieldMax > 0 ? '' : 'none';
|
||||
if (shieldMax > 0) {
|
||||
const shieldPct = this.player.shieldHp / shieldMax;
|
||||
this.uiShieldBar.style.width = `${Math.max(0, shieldPct * 250)}px`;
|
||||
this.uiShieldText.textContent = `${Math.ceil(this.player.shieldHp)}`;
|
||||
}
|
||||
|
||||
this.uiWaveLabel.textContent = `Wave ${this.spawns}`;
|
||||
this.uiKillsLabel.textContent = `Kills: ${this.killed}`;
|
||||
this.uiLevelLabel.textContent = `Level ${this.skillSystem.level}`;
|
||||
this.uiSeedLabel.textContent = this.seed === null
|
||||
? ''
|
||||
: `Seed: ${this.seed}`;
|
||||
|
||||
const xpPct = this.skillSystem.xpProgress();
|
||||
this.uiXpBarFill.style.width = `${Math.min(1, xpPct) * 100}%`;
|
||||
this.uiXpBarLabel.textContent = this.skillSystem.hasAnyUpgradeLeft()
|
||||
? `XP: ${Math.floor(this.skillSystem.xp)}/${this.skillSystem.xpNeeded()}`
|
||||
: 'MAX';
|
||||
|
||||
const sword = this.player.rightHandWeapon as Sword;
|
||||
const staff = this.player.leftHandWeapon as Staff;
|
||||
const lightning = this.player.skillLightning;
|
||||
|
||||
const fbPct = Math.max(0, (staff.COOLDOWN1_TIME - staff.cooldown1) / staff.COOLDOWN1_TIME);
|
||||
this.uiFireballCD.style.width = `${fbPct * 100}%`;
|
||||
@@ -572,6 +855,9 @@ export class Game {
|
||||
const tpPct = Math.max(0, (staff.COOLDOWN2_TIME - staff.cooldown2) / staff.COOLDOWN2_TIME);
|
||||
this.uiTeleportCD.style.width = `${tpPct * 100}%`;
|
||||
|
||||
const lnPct = Math.max(0, (lightning.COOLDOWN1_TIME - lightning.cooldown1) / lightning.COOLDOWN1_TIME);
|
||||
this.uiLightningCD.style.width = `${lnPct * 100}%`;
|
||||
|
||||
const wavePct = this.spawnTimer / SPAWN_TIME;
|
||||
this.uiWaveBarFill.style.width = `${wavePct * 100}%`;
|
||||
this.uiWaveBarLabel.textContent = `Next Wave: ${Math.ceil(SPAWN_TIME - this.spawnTimer)}s`;
|
||||
@@ -608,8 +894,15 @@ export class Game {
|
||||
this.spawnTimer += dt;
|
||||
if (this.spawnTimer >= SPAWN_TIME) {
|
||||
this.spawns++;
|
||||
for (let i = 0; i < this.spawns; i++) {
|
||||
this.spawnEnemy();
|
||||
if (this.spawns % 5 === 0) {
|
||||
const x = (Math.random() - 0.5) * 80;
|
||||
const z = (Math.random() - 0.5) * 80;
|
||||
this.spawnEnemyInstance(new Boss(), x, z, 0.15);
|
||||
playSound('spawn', 0.8);
|
||||
} else {
|
||||
for (let i = 0; i < this.spawns; i++) {
|
||||
this.spawnEnemy();
|
||||
}
|
||||
}
|
||||
this.spawnTimer = 0;
|
||||
}
|
||||
@@ -617,11 +910,28 @@ export class Game {
|
||||
|
||||
// Check close encounters (enemy touching player)
|
||||
if (this.state === 'playing') {
|
||||
const thorns = this.player.stats.thornDamage;
|
||||
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;
|
||||
if (dist < enemy.contactRadius) {
|
||||
this.player.damage(enemy.contactDps * dt);
|
||||
if (thorns > 0) {
|
||||
enemy.takeDamage(thorns * dt, this, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.player.updatePassives(dt);
|
||||
}
|
||||
|
||||
// Enemy projectiles
|
||||
if (this.state === 'playing') {
|
||||
for (let i = this.projectiles.length - 1; i >= 0; i--) {
|
||||
const p = this.projectiles[i];
|
||||
const alive = p.update(dt, this);
|
||||
if (!alive || p.dead) {
|
||||
p.dispose();
|
||||
this.projectiles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,6 +953,7 @@ export class Game {
|
||||
// Update cooldowns
|
||||
this.player.rightHandWeapon.reduceCooldowns(dt);
|
||||
this.player.leftHandWeapon.reduceCooldowns(dt);
|
||||
this.player.skillLightning.reduceCooldowns(dt);
|
||||
|
||||
// Camera follows player
|
||||
this.camera.position.set(
|
||||
@@ -675,6 +986,23 @@ export class Game {
|
||||
removeEnemy(enemy: Enemy) {
|
||||
enemy.dead = true;
|
||||
this.killed++;
|
||||
this.spawnCross(enemy.body.position);
|
||||
if (enemy.guaranteedDrop) {
|
||||
const cross = new Cross(enemy.body.position.clone());
|
||||
this.scene.add(cross.body);
|
||||
this.crosses.push(cross);
|
||||
} else {
|
||||
this.spawnCross(enemy.body.position);
|
||||
}
|
||||
|
||||
if (this.state === 'playing' || this.state === 'levelup') {
|
||||
const leveled = this.skillSystem.addXp(enemy.xp);
|
||||
if (leveled) {
|
||||
if (this.state === 'playing') {
|
||||
this.openLevelUp();
|
||||
} else {
|
||||
this.pendingPicks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user