Add boss hit feedback with flash and floating damage numbers
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+23
-1
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user