Add aura, boomerang, trap and gas cloud skills with hotbar and ESC pause
This commit is contained in:
+215
-8
@@ -18,12 +18,20 @@ import { Fireball } from './Fireball';
|
||||
import { Cross, initCrossModel } from './Cross';
|
||||
import { Arena } from './Arena';
|
||||
import { SlashEffect, isInSlashArc } from './SwordTrail';
|
||||
import { SkillSystem, getSkill, type SkillOffer } from './Skills';
|
||||
import { SkillSystem, getSkill, HOTBAR_SKILLS, type SkillOffer } from './Skills';
|
||||
import { skillIcon } from './SkillIcons';
|
||||
import { initSounds, playSound, resumeContext } from './SoundManager';
|
||||
import { Aura } from './Aura';
|
||||
import { Boomerang } from './Boomerang';
|
||||
import { Trap } from './Trap';
|
||||
import { GasCloud } from './GasCloud';
|
||||
import { Trapper } from './Trapper';
|
||||
|
||||
const SPAWN_TIME = 10;
|
||||
|
||||
const HOTBAR_KEY_CODES = ['KeyQ', 'KeyE', 'Digit1', 'Digit2', 'Digit3', 'Digit4'];
|
||||
const HOTBAR_KEY_LABELS = ['Q', 'E', '1', '2', '3', '4'];
|
||||
|
||||
type GameState = 'menu' | 'playing' | 'gameover' | 'levelup';
|
||||
|
||||
export class Game {
|
||||
@@ -38,8 +46,15 @@ export class Game {
|
||||
crosses: Cross[] = [];
|
||||
slashEffects: SlashEffect[] = [];
|
||||
projectiles: EnemyProjectile[] = [];
|
||||
traps: Trap[] = [];
|
||||
gasClouds: GasCloud[] = [];
|
||||
private aura: Aura | null = null;
|
||||
private boomerang: Boomerang | null = null;
|
||||
private trapper = new Trapper();
|
||||
private hotbar: (string | null)[] = Array(HOTBAR_KEY_CODES.length).fill(null);
|
||||
|
||||
state: GameState = 'menu';
|
||||
private paused = false;
|
||||
skillSystem: SkillSystem = new SkillSystem(0);
|
||||
private seed: number | null = null;
|
||||
private offers: SkillOffer[] = [];
|
||||
@@ -98,6 +113,15 @@ export class Game {
|
||||
private uiOfferDescs: HTMLElement[] = [];
|
||||
private uiDamageVignette!: HTMLElement;
|
||||
private uiLowHpVignette!: HTMLElement;
|
||||
private uiTrapContainer!: HTMLElement;
|
||||
private uiTrapCD!: HTMLElement;
|
||||
private uiGasContainer!: HTMLElement;
|
||||
private uiGasCD!: HTMLElement;
|
||||
private uiLightningLabel!: HTMLElement;
|
||||
private uiTeleportLabel!: HTMLElement;
|
||||
private uiTrapLabel!: HTMLElement;
|
||||
private uiGasLabel!: HTMLElement;
|
||||
private uiPauseOverlay!: HTMLElement;
|
||||
private mouseOnCanvas = false;
|
||||
|
||||
private bloodParticles: {
|
||||
@@ -189,6 +213,14 @@ export class Game {
|
||||
this.uiConfirmBtn.addEventListener('click', () => this.confirmOffer());
|
||||
this.uiDamageVignette = document.getElementById('damage-vignette')!;
|
||||
this.uiLowHpVignette = document.getElementById('low-hp-vignette')!;
|
||||
this.uiTrapContainer = document.getElementById('trap-cooldown')!;
|
||||
this.uiTrapCD = document.querySelector('#trap-cooldown .cooldown-fill')!;
|
||||
this.uiGasContainer = document.getElementById('gas-cooldown')!;
|
||||
this.uiGasCD = document.querySelector('#gas-cooldown .cooldown-fill')!;
|
||||
this.uiLightningLabel = document.querySelector('#lightning-cooldown .cooldown-label')!;
|
||||
this.uiTeleportLabel = document.querySelector('#teleport-cooldown .cooldown-label')!;
|
||||
this.uiTrapLabel = document.querySelector('#trap-cooldown .cooldown-label')!;
|
||||
this.uiGasLabel = document.querySelector('#gas-cooldown .cooldown-label')!;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const card = document.getElementById(`offer-${i}`)!;
|
||||
@@ -201,6 +233,9 @@ export class Game {
|
||||
document.getElementById('btn-start')!.addEventListener('click', () => this.startGame());
|
||||
document.getElementById('btn-restart')!.addEventListener('click', () => this.restart());
|
||||
document.getElementById('btn-menu')!.addEventListener('click', () => this.showMenu());
|
||||
this.uiPauseOverlay = document.getElementById('pause-overlay')!;
|
||||
document.getElementById('btn-resume')!.addEventListener('click', () => this.resume());
|
||||
document.getElementById('btn-pause-menu')!.addEventListener('click', () => this.showMenu());
|
||||
|
||||
// Diagnostics: detect a full page reload that wiped a running game
|
||||
if (sessionStorage.getItem('wackelpeter-run') === '1') {
|
||||
@@ -224,6 +259,20 @@ export class Game {
|
||||
private setupInput() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
this.keys.add(e.code);
|
||||
if (this.paused) {
|
||||
if (e.code === 'Escape') this.resume();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Escape') {
|
||||
if (this.state === 'playing') {
|
||||
this.pause();
|
||||
} else if (this.state === 'gameover') {
|
||||
this.showMenu();
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Space' && this.state === 'playing') {
|
||||
this.player.jump();
|
||||
e.preventDefault();
|
||||
@@ -231,8 +280,9 @@ 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);
|
||||
const slotIdx = HOTBAR_KEY_CODES.indexOf(e.code);
|
||||
if (slotIdx >= 0 && this.state === 'playing' && this.hotbar[slotIdx]) {
|
||||
this.triggerHotbarSkill(this.hotbar[slotIdx]!);
|
||||
}
|
||||
if (this.state === 'levelup') {
|
||||
if (e.code === 'Digit1' || e.code === 'Numpad1') this.selectOffer(0);
|
||||
@@ -254,9 +304,6 @@ export class Game {
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
this.keys.delete(e.code);
|
||||
if (e.code === 'KeyE' && this.state === 'playing') {
|
||||
this.player.leftHandWeapon.skill2(this.groundPoint, this.player);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('mousedown', (e) => {
|
||||
@@ -276,7 +323,7 @@ export class Game {
|
||||
}
|
||||
|
||||
private handleMouseRelease(button: number) {
|
||||
if (this.state !== 'playing') return;
|
||||
if (this.state !== 'playing' || this.paused) return;
|
||||
if (button === 0) {
|
||||
this.player.rightHandWeapon.skill1(this.groundPoint, this.player);
|
||||
} else if (button === 2) {
|
||||
@@ -284,6 +331,33 @@ export class Game {
|
||||
}
|
||||
}
|
||||
|
||||
private triggerHotbarSkill(id: string) {
|
||||
switch (id) {
|
||||
case 'chainlightning':
|
||||
this.player.skillLightning.skill1(this.groundPoint, this.player);
|
||||
break;
|
||||
case 'teleport':
|
||||
this.player.leftHandWeapon.skill2(this.groundPoint, this.player);
|
||||
break;
|
||||
case 'trap':
|
||||
this.trapper.skill1(this.groundPoint, this.player);
|
||||
break;
|
||||
case 'gascloud':
|
||||
this.trapper.skill2(this.groundPoint, this.player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private hotbarKeyLabel(skillId: string): string {
|
||||
const idx = this.hotbar.indexOf(skillId);
|
||||
return idx >= 0 ? HOTBAR_KEY_LABELS[idx] : '';
|
||||
}
|
||||
|
||||
private nextFreeHotbarLabel(): string {
|
||||
const idx = this.hotbar.indexOf(null);
|
||||
return idx >= 0 ? HOTBAR_KEY_LABELS[idx] : '';
|
||||
}
|
||||
|
||||
private setupLighting() {
|
||||
const ambient = new THREE.AmbientLight(0x556688, 0.35);
|
||||
this.scene.add(ambient);
|
||||
@@ -416,13 +490,17 @@ export class Game {
|
||||
this.resetWorld();
|
||||
this.logState('playing', 'startGame');
|
||||
this.state = 'playing';
|
||||
this.paused = false;
|
||||
this.hidePauseOverlay();
|
||||
sessionStorage.setItem('wackelpeter-run', '1');
|
||||
this.uiMainMenu.style.opacity = '0';
|
||||
this.uiMainMenu.style.pointerEvents = 'none';
|
||||
this.uiMainMenu.style.visibility = 'hidden';
|
||||
this.uiGameOver.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.3s ease';
|
||||
this.uiGameOverPanel.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'none';
|
||||
this.uiGameOverPanel.style.visibility = 'hidden';
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
@@ -435,7 +513,34 @@ export class Game {
|
||||
this.startGame();
|
||||
}
|
||||
|
||||
private pause() {
|
||||
if (this.state !== 'playing' || this.paused) return;
|
||||
this.paused = true;
|
||||
this.uiPauseOverlay.style.visibility = 'visible';
|
||||
this.uiPauseOverlay.style.opacity = '1';
|
||||
this.uiPauseOverlay.style.pointerEvents = 'auto';
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
private resume() {
|
||||
if (!this.paused) return;
|
||||
this.paused = false;
|
||||
this.hidePauseOverlay();
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
private hidePauseOverlay() {
|
||||
this.uiPauseOverlay.style.opacity = '0';
|
||||
this.uiPauseOverlay.style.pointerEvents = 'none';
|
||||
this.uiPauseOverlay.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
showMenu() {
|
||||
if ((this.state === 'playing' || this.state === 'levelup') && !this.paused) {
|
||||
return;
|
||||
}
|
||||
this.paused = false;
|
||||
this.hidePauseOverlay();
|
||||
this.resetWorld();
|
||||
this.logState('menu', 'showMenu');
|
||||
this.state = 'menu';
|
||||
@@ -445,8 +550,10 @@ export class Game {
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.3s ease';
|
||||
this.uiGameOverPanel.style.opacity = '0';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'none';
|
||||
this.uiGameOverPanel.style.visibility = 'hidden';
|
||||
this.uiMainMenu.style.opacity = '1';
|
||||
this.uiMainMenu.style.pointerEvents = 'auto';
|
||||
this.uiMainMenu.style.visibility = 'visible';
|
||||
this.blurFocus();
|
||||
}
|
||||
|
||||
@@ -461,6 +568,7 @@ export class Game {
|
||||
this.uiGameOverPanel.style.transition = 'opacity 0.8s ease-in 1.5s';
|
||||
this.uiGameOverPanel.style.opacity = '1';
|
||||
this.uiGameOverPanel.style.pointerEvents = 'auto';
|
||||
this.uiGameOverPanel.style.visibility = 'visible';
|
||||
playSound('gameover', 0.8);
|
||||
}
|
||||
|
||||
@@ -483,6 +591,25 @@ export class Game {
|
||||
for (const p of this.projectiles) p.dispose();
|
||||
this.projectiles = [];
|
||||
|
||||
for (const trap of this.traps) trap.dispose();
|
||||
this.traps = [];
|
||||
|
||||
for (const cloud of this.gasClouds) cloud.dispose();
|
||||
this.gasClouds = [];
|
||||
|
||||
if (this.aura) {
|
||||
this.aura.dispose(this.scene);
|
||||
this.aura = null;
|
||||
}
|
||||
if (this.boomerang) {
|
||||
this.boomerang.dispose(this.scene);
|
||||
this.boomerang = null;
|
||||
}
|
||||
this.hotbar = Array(HOTBAR_KEY_CODES.length).fill(null);
|
||||
this.trapper.cooldown1 = 0;
|
||||
this.trapper.cooldown2 = 0;
|
||||
this.trapper.cooldown3 = 0;
|
||||
|
||||
for (const [enemy, ring] of this.enemyHitRings) {
|
||||
this.scene.remove(ring);
|
||||
ring.geometry.dispose();
|
||||
@@ -556,7 +683,12 @@ export class Game {
|
||||
const isNew = offer.nextLevel === 1;
|
||||
this.uiOfferIcons[i].innerHTML = skillIcon(offer.id);
|
||||
title.textContent = `${skill.name}${isNew ? ' (NEU)' : ` · Stufe ${offer.nextLevel}`}`;
|
||||
desc.textContent = skill.describe(offer.nextLevel);
|
||||
let descText = skill.describe(offer.nextLevel);
|
||||
if (isNew && HOTBAR_SKILLS.includes(offer.id)) {
|
||||
const key = this.nextFreeHotbarLabel();
|
||||
if (key) descText += ` · Taste [${key}]`;
|
||||
}
|
||||
desc.textContent = descText;
|
||||
card.style.display = '';
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
@@ -564,6 +696,7 @@ export class Game {
|
||||
}
|
||||
}
|
||||
this.uiConfirmBtn.disabled = true;
|
||||
this.uiLevelUp.style.visibility = 'visible';
|
||||
this.uiLevelUp.style.opacity = '1';
|
||||
this.uiLevelUp.style.pointerEvents = 'auto';
|
||||
}
|
||||
@@ -571,6 +704,7 @@ export class Game {
|
||||
private hideLevelUp() {
|
||||
this.uiLevelUp.style.opacity = '0';
|
||||
this.uiLevelUp.style.pointerEvents = 'none';
|
||||
this.uiLevelUp.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
private selectOffer(index: number) {
|
||||
@@ -588,6 +722,10 @@ export class Game {
|
||||
const offer = this.offers[this.selectedOffer];
|
||||
this.selectedOffer = -1;
|
||||
this.skillSystem.apply(offer.id);
|
||||
if (HOTBAR_SKILLS.includes(offer.id) && !this.hotbar.includes(offer.id)) {
|
||||
const free = this.hotbar.indexOf(null);
|
||||
if (free >= 0) this.hotbar[free] = offer.id;
|
||||
}
|
||||
this.player.applySkillSystem(this.skillSystem);
|
||||
this.syncSkillVisibility();
|
||||
this.blurFocus();
|
||||
@@ -610,6 +748,30 @@ export class Game {
|
||||
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';
|
||||
this.uiTrapContainer.style.display = s.getLevel('trap') > 0 ? '' : 'none';
|
||||
this.uiGasContainer.style.display = s.getLevel('gascloud') > 0 ? '' : 'none';
|
||||
|
||||
const auraLevel = s.getLevel('aura');
|
||||
if (auraLevel > 0 && !this.aura) {
|
||||
this.aura = new Aura(this.scene);
|
||||
} else if (auraLevel <= 0 && this.aura) {
|
||||
this.aura.dispose(this.scene);
|
||||
this.aura = null;
|
||||
}
|
||||
|
||||
const boomerangLevel = s.getLevel('boomerang');
|
||||
if (boomerangLevel > 0 && !this.boomerang) {
|
||||
this.boomerang = new Boomerang(this.scene);
|
||||
} else if (boomerangLevel <= 0 && this.boomerang) {
|
||||
this.boomerang.dispose(this.scene);
|
||||
this.boomerang = null;
|
||||
}
|
||||
if (this.boomerang) this.boomerang.setLevel(boomerangLevel);
|
||||
|
||||
this.uiLightningLabel.textContent = `${getSkill('chainlightning').name} [${this.hotbarKeyLabel('chainlightning')}]`;
|
||||
this.uiTeleportLabel.textContent = `${getSkill('teleport').name} [${this.hotbarKeyLabel('teleport')}]`;
|
||||
this.uiTrapLabel.textContent = `${getSkill('trap').name} [${this.hotbarKeyLabel('trap')}]`;
|
||||
this.uiGasLabel.textContent = `${getSkill('gascloud').name} [${this.hotbarKeyLabel('gascloud')}]`;
|
||||
}
|
||||
|
||||
private spawnEnemy() {
|
||||
@@ -780,6 +942,11 @@ export class Game {
|
||||
enemy.updateAnimation(dt);
|
||||
continue;
|
||||
}
|
||||
if (enemy.rootTime > 0) {
|
||||
enemy.rootTime -= dt;
|
||||
enemy.updateAnimation(dt);
|
||||
continue;
|
||||
}
|
||||
enemy.update(dt, this);
|
||||
enemy.updateAnimation(dt);
|
||||
}
|
||||
@@ -873,6 +1040,14 @@ export class Game {
|
||||
const lnPct = Math.max(0, (lightning.COOLDOWN1_TIME - lightning.cooldown1) / lightning.COOLDOWN1_TIME);
|
||||
this.uiLightningCD.style.width = `${lnPct * 100}%`;
|
||||
|
||||
const trapTotal = this.trapper.COOLDOWN1_TIME || 1;
|
||||
const trapPct = Math.max(0, (trapTotal - this.trapper.cooldown1) / trapTotal);
|
||||
this.uiTrapCD.style.width = `${trapPct * 100}%`;
|
||||
|
||||
const gasTotal = this.trapper.COOLDOWN2_TIME || 1;
|
||||
const gasPct = Math.max(0, (gasTotal - this.trapper.cooldown2) / gasTotal);
|
||||
this.uiGasCD.style.width = `${gasPct * 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`;
|
||||
@@ -947,11 +1122,37 @@ export class Game {
|
||||
}
|
||||
}
|
||||
|
||||
private updateTraps(dt: number) {
|
||||
for (let i = this.traps.length - 1; i >= 0; i--) {
|
||||
const trap = this.traps[i];
|
||||
if (!trap.update(dt, this)) {
|
||||
trap.dispose();
|
||||
this.traps.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateGasClouds(dt: number) {
|
||||
for (let i = this.gasClouds.length - 1; i >= 0; i--) {
|
||||
const cloud = this.gasClouds[i];
|
||||
if (!cloud.update(dt, this)) {
|
||||
cloud.dispose();
|
||||
this.gasClouds.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
const dt = Math.min(this.clock.getDelta(), 0.1);
|
||||
|
||||
if (this.paused) {
|
||||
this.updateUI();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateGroundPoint();
|
||||
this.updatePlayerMovement(dt);
|
||||
this.updateEnemies(dt);
|
||||
@@ -967,6 +1168,11 @@ export class Game {
|
||||
|
||||
this.updateBloodParticles(dt);
|
||||
|
||||
if (this.aura) this.aura.update(dt, this);
|
||||
if (this.boomerang) this.boomerang.update(dt, this);
|
||||
this.updateTraps(dt);
|
||||
this.updateGasClouds(dt);
|
||||
|
||||
// Update animations
|
||||
this.player.updateAnimations(dt);
|
||||
this.player.rightHandWeapon.updateAnimation(dt);
|
||||
@@ -1036,6 +1242,7 @@ export class Game {
|
||||
this.player.rightHandWeapon.reduceCooldowns(dt);
|
||||
this.player.leftHandWeapon.reduceCooldowns(dt);
|
||||
this.player.skillLightning.reduceCooldowns(dt);
|
||||
this.trapper.reduceCooldowns(dt);
|
||||
|
||||
// Camera follows player
|
||||
this.camera.position.set(
|
||||
|
||||
Reference in New Issue
Block a user