From 7d41f6db6ccb8ad02930cb1282e2332c22ded377 Mon Sep 17 00:00:00 2001 From: nico Date: Wed, 19 Aug 2026 14:05:24 +0200 Subject: [PATCH] Add boss hit feedback with flash and floating damage numbers --- web/src/DamageNumber.ts | 53 +++++++++++++++++++++++++++++++++++++++++ web/src/Enemy.ts | 2 +- web/src/Game.ts | 24 ++++++++++++++++++- web/src/Krake.ts | 24 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 web/src/DamageNumber.ts diff --git a/web/src/DamageNumber.ts b/web/src/DamageNumber.ts new file mode 100644 index 0000000..f203d99 --- /dev/null +++ b/web/src/DamageNumber.ts @@ -0,0 +1,53 @@ +import * as THREE from 'three'; + +const LIFE = 0.8; + +export class DamageNumber { + sprite: THREE.Sprite; + private mat: THREE.SpriteMaterial; + private life = LIFE; + + constructor( + scene: THREE.Scene, + text: string, + pos: THREE.Vector3, + color = '#ffffff' + ) { + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 64; + const ctx = canvas.getContext('2d')!; + ctx.font = 'bold 40px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 6; + ctx.strokeStyle = '#000'; + ctx.strokeText(text, 64, 32); + ctx.fillStyle = color; + ctx.fillText(text, 64, 32); + + this.mat = new THREE.SpriteMaterial({ + map: new THREE.CanvasTexture(canvas), + transparent: true, + depthWrite: false, + }); + this.sprite = new THREE.Sprite(this.mat); + this.sprite.position.copy(pos); + this.sprite.position.y += 2.6; + this.sprite.scale.set(2, 1, 1); + scene.add(this.sprite); + } + + update(dt: number): boolean { + this.life -= dt; + this.sprite.position.y += dt * 1.6; + this.mat.opacity = this.life < 0.3 ? this.life / 0.3 : 1; + return this.life > 0; + } + + dispose(scene: THREE.Scene) { + scene.remove(this.sprite); + this.mat.map?.dispose(); + this.mat.dispose(); + } +} diff --git a/web/src/Enemy.ts b/web/src/Enemy.ts index a65ecd0..361c61d 100644 --- a/web/src/Enemy.ts +++ b/web/src/Enemy.ts @@ -97,7 +97,7 @@ export abstract class Enemy { } playSound('damage', 0.7); - game.onDamageDealt(damage); + game.onDamageDealt(damage, this); if (this.health <= 0) { this.die(game); diff --git a/web/src/Game.ts b/web/src/Game.ts index 0422a3d..e586d66 100644 --- a/web/src/Game.ts +++ b/web/src/Game.ts @@ -27,6 +27,7 @@ import { Boomerang } from './Boomerang'; import { Trap } from './Trap'; import { GasCloud } from './GasCloud'; import { Trapper } from './Trapper'; +import { DamageNumber } from './DamageNumber'; const SPAWN_TIME = 10; @@ -50,6 +51,7 @@ export class Game { projectiles: EnemyProjectile[] = []; traps: Trap[] = []; gasClouds: GasCloud[] = []; + private damageNumbers: DamageNumber[] = []; private aura: Aura | null = null; private boomerang: Boomerang | null = null; private trapper = new Trapper(); @@ -687,6 +689,9 @@ export class Game { for (const cloud of this.gasClouds) cloud.dispose(); this.gasClouds = []; + for (const num of this.damageNumbers) num.dispose(this.scene); + this.damageNumbers = []; + if (this.aura) { this.aura.dispose(this.scene); this.aura = null; @@ -743,10 +748,19 @@ export class Game { this.updateUI(); } - onDamageDealt(damage: number) { + onDamageDealt(damage: number, target?: Enemy) { if (this.state === 'playing' && this.player.stats.lifesteal > 0) { this.player.heal(damage * this.player.stats.lifesteal); } + // Treffer-Feedback: Schadenszahl bei Bossen einblenden + if (target?.isBoss && !target.dead) { + this.spawnDamageNumber(Math.round(damage), target.body.position); + } + } + + spawnDamageNumber(damage: number, pos: THREE.Vector3, color?: string) { + const num = new DamageNumber(this.scene, `${damage}`, pos, color); + this.damageNumbers.push(num); } private openLevelUp() { @@ -1314,6 +1328,14 @@ export class Game { this.updateBloodParticles(dt); + // Schadenszahlen aktualisieren + for (let i = this.damageNumbers.length - 1; i >= 0; i--) { + if (!this.damageNumbers[i].update(dt)) { + this.damageNumbers[i].dispose(this.scene); + this.damageNumbers.splice(i, 1); + } + } + if (this.aura) this.aura.update(dt, this); if (this.boomerang) this.boomerang.update(dt, this); this.updateTraps(dt); diff --git a/web/src/Krake.ts b/web/src/Krake.ts index 107eb00..4af46d3 100644 --- a/web/src/Krake.ts +++ b/web/src/Krake.ts @@ -15,6 +15,7 @@ const PULL_CHARGE_TIME = 2.5; const PULL_DAMAGE = 25; const PULL_TIME = 0.6; const SLAM_TIME = 0.5; +const HIT_FLASH_TIME = 0.14; const SWEEP_RADIUS = 11; const SWEEP_DURATION = 3.6; const SWEEP_HIT_WIDTH = 1.2; @@ -56,6 +57,7 @@ export class Krake extends Enemy { private puddles: InkPuddle[] = []; private skinMat!: THREE.MeshToonMaterial; private darkMat!: THREE.MeshToonMaterial; + private hitFlash = 0; constructor() { super(); @@ -212,6 +214,18 @@ export class Krake extends Enemy { this.ensureEffects(game.scene); this.waveTime += dt; + // Hit-Feedback: kurz weiss aufblitzen + leichtes Pulsieren + if (this.hitFlash > 0) { + this.hitFlash -= dt; + this.skinMat.color.set(0xffffff); + this.darkMat.color.set(0xffffff); + this.model.scale.setScalar(1 + (this.hitFlash / HIT_FLASH_TIME) * 0.05); + } else { + this.skinMat.color.set(this.enraged ? 0x8a2233 : 0x8a4a9a); + this.darkMat.color.set(this.enraged ? 0x551520 : 0x5a2a6a); + this.model.scale.setScalar(1); + } + // Tentakel wippen for (let i = 0; i < this.tentacles.length; i++) { this.tentacles[i].rotation.z = Math.sin(this.waveTime * 2.2 + i * 0.9) * 0.25; @@ -469,6 +483,16 @@ export class Krake extends Enemy { } } + override takeDamage( + damage: number, + game: Game, + withKnockback = true, + sourcePos?: THREE.Vector3 + ) { + super.takeDamage(damage, game, withKnockback, sourcePos); + this.hitFlash = HIT_FLASH_TIME; + } + protected onDeath(_game: Game) { this.cleanupEffects(); this.beginFadeDeath();