Add Minotaurus boss with front armor, dash stun and jumpable axe swing
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import * as THREE from 'three';
|
||||
import type { Game } from './Game';
|
||||
|
||||
const TELEGRAPH_TIME = 0.9;
|
||||
const IMPACT_RADIUS = 1.8;
|
||||
const ROCK_DAMAGE = 30;
|
||||
const DUST_TIME = 0.4;
|
||||
|
||||
export class FallingRock {
|
||||
body: THREE.Group;
|
||||
private shadow: THREE.Mesh;
|
||||
private rock: THREE.Mesh;
|
||||
private phase: 'telegraph' | 'fall' | 'dust' = 'telegraph';
|
||||
private timer = TELEGRAPH_TIME;
|
||||
private dust: THREE.Mesh[] = [];
|
||||
private dustDirs: THREE.Vector3[] = [];
|
||||
private dustMat: THREE.MeshBasicMaterial;
|
||||
|
||||
constructor(pos: THREE.Vector3) {
|
||||
this.body = new THREE.Group();
|
||||
this.body.position.set(pos.x, 0, pos.z);
|
||||
|
||||
// Schatten-Telegraf (waechst kurz vor dem Aufprall)
|
||||
const shadowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x000000,
|
||||
transparent: true,
|
||||
opacity: 0.3,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.shadow = new THREE.Mesh(new THREE.CircleGeometry(1.6, 24), shadowMat);
|
||||
this.shadow.rotation.x = -Math.PI / 2;
|
||||
this.shadow.position.y = 0.06;
|
||||
this.shadow.scale.setScalar(0.4);
|
||||
this.body.add(this.shadow);
|
||||
|
||||
// Fels von der Decke
|
||||
const rockMat = new THREE.MeshToonMaterial({ color: 0x777788 });
|
||||
this.rock = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.9, 0.9), rockMat);
|
||||
this.rock.position.y = 16;
|
||||
this.rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
|
||||
this.body.add(this.rock);
|
||||
|
||||
this.dustMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x888899,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
});
|
||||
}
|
||||
|
||||
update(dt: number, game: Game): boolean {
|
||||
this.timer -= dt;
|
||||
|
||||
if (this.phase === 'telegraph') {
|
||||
const t = 1 - this.timer / TELEGRAPH_TIME;
|
||||
this.shadow.scale.setScalar(0.4 + t * 0.6);
|
||||
(this.shadow.material as THREE.MeshBasicMaterial).opacity = 0.2 + t * 0.2;
|
||||
if (this.timer <= 0) {
|
||||
this.phase = 'fall';
|
||||
this.timer = 2;
|
||||
}
|
||||
} else if (this.phase === 'fall') {
|
||||
this.rock.position.y -= 26 * dt;
|
||||
this.rock.rotation.x += dt * 3;
|
||||
this.rock.rotation.z += dt * 2;
|
||||
if (this.rock.position.y <= 0.15) {
|
||||
this.rock.position.y = 0.15;
|
||||
this.rock.visible = false;
|
||||
this.shadow.visible = false;
|
||||
|
||||
if (game.player.health > 0) {
|
||||
const dx = game.player.body.position.x - this.body.position.x;
|
||||
const dz = game.player.body.position.z - this.body.position.z;
|
||||
if (Math.hypot(dx, dz) < IMPACT_RADIUS) {
|
||||
game.player.damage(ROCK_DAMAGE, this.body.position);
|
||||
}
|
||||
}
|
||||
|
||||
// Staubwolke
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const a = (i / 5) * Math.PI * 2;
|
||||
const dir = new THREE.Vector3(Math.cos(a), 0, Math.sin(a));
|
||||
const puff = new THREE.Mesh(new THREE.SphereGeometry(0.25, 6, 5), this.dustMat);
|
||||
this.body.add(puff);
|
||||
this.dust.push(puff);
|
||||
this.dustDirs.push(dir);
|
||||
}
|
||||
this.phase = 'dust';
|
||||
this.timer = DUST_TIME;
|
||||
}
|
||||
} else {
|
||||
const t = 1 - this.timer / DUST_TIME;
|
||||
for (let i = 0; i < this.dust.length; i++) {
|
||||
this.dust[i].position.copy(this.dustDirs[i]).multiplyScalar(0.5 + t * 1.8);
|
||||
this.dust[i].position.y = 0.2 + t * 0.8;
|
||||
}
|
||||
this.dustMat.opacity = 0.5 * (1 - t);
|
||||
if (this.timer <= 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.body.removeFromParent();
|
||||
this.body.traverse((obj) => {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+30
-6
@@ -10,6 +10,7 @@ import { Slime } from './Slime';
|
||||
import { Brute } from './Brute';
|
||||
import { Golem } from './Golem';
|
||||
import { Krake } from './Krake';
|
||||
import { Minotaurus } from './Minotaurus';
|
||||
import { Crab } from './Crab';
|
||||
import { Turret } from './Turret';
|
||||
import { EnemyProjectile } from './EnemyProjectile';
|
||||
@@ -81,8 +82,11 @@ export class Game {
|
||||
private bossWaves = new Map<number, { name: string; spawn: () => Enemy }>([
|
||||
[10, { name: 'Golem', spawn: () => new Golem() }],
|
||||
[20, { name: 'Krake', spawn: () => new Krake() }],
|
||||
[30, { name: 'Minotaurus', spawn: () => new Minotaurus() }],
|
||||
]);
|
||||
private bossFightActive = false;
|
||||
private shakeTime = 0;
|
||||
private shakeAmp = 0;
|
||||
|
||||
// Debug hitzone visualization
|
||||
private showHitzones = false;
|
||||
@@ -298,7 +302,8 @@ export class Game {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === 'Space' && this.state === 'playing' && this.player.pullTime <= 0) {
|
||||
if (e.code === 'Space' && this.state === 'playing'
|
||||
&& this.player.pullTime <= 0 && this.player.stunTime <= 0) {
|
||||
this.player.jump();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -728,6 +733,7 @@ export class Game {
|
||||
this.spawns = 0;
|
||||
this.killed = 0;
|
||||
this.bossFightActive = false;
|
||||
this.shakeTime = 0;
|
||||
this.pendingPicks = 0;
|
||||
this.hideLevelUp();
|
||||
this.skillSystem = new SkillSystem(this.seed ?? 0);
|
||||
@@ -754,15 +760,24 @@ export class Game {
|
||||
}
|
||||
// Treffer-Feedback: Schadenszahl bei Bossen einblenden
|
||||
if (target?.isBoss && !target.dead) {
|
||||
this.spawnDamageNumber(Math.round(damage), target.body.position);
|
||||
this.spawnDamageText(`${Math.round(damage)}`, target.body.position);
|
||||
}
|
||||
}
|
||||
|
||||
spawnDamageNumber(damage: number, pos: THREE.Vector3, color?: string) {
|
||||
const num = new DamageNumber(this.scene, `${damage}`, pos, color);
|
||||
spawnDamageText(text: string, pos: THREE.Vector3, color?: string) {
|
||||
const num = new DamageNumber(this.scene, text, pos, color);
|
||||
this.damageNumbers.push(num);
|
||||
}
|
||||
|
||||
spawnDamageNumber(damage: number, pos: THREE.Vector3, color?: string) {
|
||||
this.spawnDamageText(`${damage}`, pos, color);
|
||||
}
|
||||
|
||||
triggerCameraShake(duration: number, amp: number) {
|
||||
this.shakeTime = duration;
|
||||
this.shakeAmp = amp;
|
||||
}
|
||||
|
||||
private openLevelUp() {
|
||||
this.offers = this.skillSystem.rollOffers();
|
||||
if (this.offers.length === 0) {
|
||||
@@ -1012,7 +1027,10 @@ export class Game {
|
||||
if (p.slowTimer > 0) speed *= p.slowFactor;
|
||||
let isMoving = false;
|
||||
|
||||
if (p.pullTime > 0) {
|
||||
if (p.stunTime > 0) {
|
||||
// Gestunnt (Stampf-Schockwelle): keine Eingaben
|
||||
p.velocity.set(0, 0, 0);
|
||||
} else if (p.pullTime > 0) {
|
||||
// Tentakel-Griff: Spieler wird komplett zum Boss gezogen
|
||||
p.pullTime -= dt;
|
||||
p.velocity.copy(p.pullDir).multiplyScalar(20);
|
||||
@@ -1042,7 +1060,7 @@ export class Game {
|
||||
}
|
||||
|
||||
// Jump / gravity
|
||||
if (!p.onGround && p.pullTime <= 0) {
|
||||
if (!p.onGround && p.pullTime <= 0 && p.stunTime <= 0) {
|
||||
p.jumpVelocity -= 20 * dt;
|
||||
p.body.position.y += p.jumpVelocity * dt;
|
||||
if (p.body.position.y <= 0.5) {
|
||||
@@ -1430,6 +1448,12 @@ export class Game {
|
||||
25,
|
||||
this.player.body.position.z - 15
|
||||
);
|
||||
if (this.shakeTime > 0) {
|
||||
this.shakeTime -= dt;
|
||||
const k = this.shakeTime * this.shakeAmp;
|
||||
this.camera.position.x += (Math.random() - 0.5) * k;
|
||||
this.camera.position.z += (Math.random() - 0.5) * k;
|
||||
}
|
||||
this.camera.lookAt(
|
||||
this.player.body.position.x,
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
import * as THREE from 'three';
|
||||
import type { Game } from './Game';
|
||||
import { Enemy } from './Enemy';
|
||||
import { playSound } from './SoundManager';
|
||||
import { FallingRock } from './FallingRock';
|
||||
|
||||
const MAX_HEALTH = 6000;
|
||||
const HIT_FLASH_TIME = 0.14;
|
||||
const MODEL_SCALE = 1.7;
|
||||
const IDEAL_DIST = 4.5;
|
||||
const MIN_DIST = 3.2;
|
||||
const MOVE_SPEED = 5;
|
||||
const TURN_SPEED_1 = 2.2;
|
||||
const TURN_SPEED_2 = 3.5;
|
||||
const DASH_COOLDOWN = 8;
|
||||
const DASH_COOLDOWN_2 = 4;
|
||||
const DASH_WINDUP = 0.6;
|
||||
const DASH_SPEED = 34;
|
||||
const DASH_MAX_DIST = 30;
|
||||
const DASH_DAMAGE = 35;
|
||||
const WALL_LIMIT = 46;
|
||||
const BOSS_STUN_TIME = 3.5;
|
||||
const SWING_COOLDOWN = 5;
|
||||
const SWING_COOLDOWN_2 = 3.5;
|
||||
const SWING_WINDUP = 0.45;
|
||||
const SWING_TIME = 0.3;
|
||||
const SWING_RANGE = 5.2;
|
||||
const SWING_ARC = (90 * Math.PI) / 180;
|
||||
const SWING_DAMAGE = 60;
|
||||
const STOMP_COOLDOWN = 10;
|
||||
const STOMP_COOLDOWN_2 = 8;
|
||||
const STOMP_JUMP_TIME = 1.0;
|
||||
const STOMP_JUMP_HEIGHT = 4;
|
||||
const STOMP_RADIUS = 11;
|
||||
const STOMP_DAMAGE = 15;
|
||||
const STOMP_STUN = 1.2;
|
||||
const ENRAGE_RATIO = 0.4;
|
||||
const ROCK_COUNT = 7;
|
||||
const ROCK_COUNT_2 = 10;
|
||||
|
||||
type State = 'idle' | 'dashwindup' | 'dash' | 'dashstop' | 'stunned' | 'swingwindup' | 'swing' | 'jump';
|
||||
|
||||
export class Minotaurus extends Enemy {
|
||||
private model: THREE.Group;
|
||||
private game: Game | null = null;
|
||||
private waveTime = 0;
|
||||
private state: State = 'idle';
|
||||
private stateTime = 0;
|
||||
private yaw = 0;
|
||||
private enraged = false;
|
||||
private hitFlash = 0;
|
||||
|
||||
private dashTimer = 3.5;
|
||||
private swingTimer = 2;
|
||||
private stompTimer = 6;
|
||||
private doubleDashLeft = 0;
|
||||
|
||||
private dashDir = new THREE.Vector3();
|
||||
private dashDist = 0;
|
||||
private dashHitPlayer = false;
|
||||
|
||||
private swingHit = false;
|
||||
|
||||
private jumpFrom = new THREE.Vector3();
|
||||
private jumpTo = new THREE.Vector3();
|
||||
|
||||
private axeGroup: THREE.Group;
|
||||
private eyeMat!: THREE.MeshBasicMaterial;
|
||||
private furMat!: THREE.MeshToonMaterial;
|
||||
private darkMat!: THREE.MeshToonMaterial;
|
||||
private starsGroup: THREE.Group;
|
||||
private telegraphArc!: THREE.Mesh;
|
||||
private swingArc!: THREE.Mesh;
|
||||
private dashLine!: THREE.Mesh;
|
||||
private shockRing!: THREE.Mesh;
|
||||
private shockRingMat!: THREE.MeshBasicMaterial;
|
||||
private legs: THREE.Group[] = [];
|
||||
private moving = false;
|
||||
|
||||
private rocks: FallingRock[] = [];
|
||||
private cracks: { group: THREE.Group; life: number }[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.health = MAX_HEALTH;
|
||||
this.maxHealth = MAX_HEALTH;
|
||||
this.isBoss = true;
|
||||
this.displayName = 'Minotaurus';
|
||||
this.speed = 0;
|
||||
this.xp = 200;
|
||||
this.contactDps = 0;
|
||||
this.contactRadius = 0;
|
||||
this.guaranteedDrop = true;
|
||||
|
||||
this.model = new THREE.Group();
|
||||
this.model.scale.setScalar(MODEL_SCALE);
|
||||
// Kein fadeOutModel: beim Tod schrumpft der ganze Body inkl. Effekten
|
||||
this.body.add(this.model);
|
||||
|
||||
this.furMat = new THREE.MeshToonMaterial({ color: 0x6b4a2f });
|
||||
this.darkMat = new THREE.MeshToonMaterial({ color: 0x4a3220 });
|
||||
const hornMat = new THREE.MeshToonMaterial({ color: 0xd8c8a8 });
|
||||
const axeMat = new THREE.MeshToonMaterial({ color: 0x9999aa });
|
||||
const metalMat = new THREE.MeshToonMaterial({ color: 0x8a8a99 });
|
||||
const darkMetalMat = new THREE.MeshToonMaterial({ color: 0x555566 });
|
||||
this.eyeMat = new THREE.MeshBasicMaterial({ color: 0xffcc33 });
|
||||
|
||||
// Beine (Gruppen mit Hueft-Gelenk, laufen animiert)
|
||||
for (const side of [-1, 1]) {
|
||||
const legGroup = new THREE.Group();
|
||||
legGroup.position.set(side * 0.45, 0.75, 0);
|
||||
const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.3, 0.8, 10), this.darkMat);
|
||||
leg.position.y = -0.4;
|
||||
leg.castShadow = true;
|
||||
this.registerFadeMesh(leg);
|
||||
legGroup.add(leg);
|
||||
const hoof = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.16, 0.42), metalMat);
|
||||
hoof.position.set(0, -0.78, 0.06);
|
||||
this.registerFadeMesh(hoof);
|
||||
legGroup.add(hoof);
|
||||
const guard = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.4, 0.1), metalMat);
|
||||
guard.position.set(0, -0.4, 0.2);
|
||||
this.registerFadeMesh(guard);
|
||||
legGroup.add(guard);
|
||||
this.model.add(legGroup);
|
||||
this.legs.push(legGroup);
|
||||
}
|
||||
|
||||
// Torso
|
||||
const torso = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.3, 1.0), this.furMat);
|
||||
torso.position.y = 1.15;
|
||||
torso.castShadow = true;
|
||||
this.registerFadeMesh(torso);
|
||||
this.model.add(torso);
|
||||
|
||||
// Vorder-Panzerung: Brustplatte
|
||||
const chest = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.8, 0.1), metalMat);
|
||||
chest.position.set(0, 1.18, 0.48);
|
||||
chest.castShadow = true;
|
||||
this.registerFadeMesh(chest);
|
||||
this.model.add(chest);
|
||||
|
||||
// Schultern (gepanzert vorn)
|
||||
for (const side of [-1, 1]) {
|
||||
const shoulder = new THREE.Mesh(new THREE.SphereGeometry(0.42, 12, 10), this.furMat);
|
||||
shoulder.position.set(side * 0.85, 1.6, 0);
|
||||
shoulder.castShadow = true;
|
||||
this.registerFadeMesh(shoulder);
|
||||
this.model.add(shoulder);
|
||||
const pauldron = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.3, 0.4), metalMat);
|
||||
pauldron.position.set(side * 0.85, 1.68, 0.3);
|
||||
this.registerFadeMesh(pauldron);
|
||||
this.model.add(pauldron);
|
||||
}
|
||||
|
||||
// Brustpanzer-Gürtel
|
||||
const belt = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.16, 0.75), darkMetalMat);
|
||||
belt.position.set(0, 0.68, 0);
|
||||
this.registerFadeMesh(belt);
|
||||
this.model.add(belt);
|
||||
|
||||
// Linker Arm
|
||||
const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.26, 0.9, 8), this.furMat);
|
||||
arm.position.set(-0.85, 0.85, 0);
|
||||
arm.castShadow = true;
|
||||
this.registerFadeMesh(arm);
|
||||
this.model.add(arm);
|
||||
|
||||
// Rechter Arm mit Axt (schwingt beim Angriff)
|
||||
this.axeGroup = new THREE.Group();
|
||||
this.axeGroup.position.set(0.85, 1.0, 0);
|
||||
this.model.add(this.axeGroup);
|
||||
const axeArm = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.26, 0.9, 8), this.furMat);
|
||||
axeArm.position.y = -0.15;
|
||||
axeArm.castShadow = true;
|
||||
this.registerFadeMesh(axeArm);
|
||||
this.axeGroup.add(axeArm);
|
||||
const handle = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.8, 8), this.darkMat);
|
||||
handle.rotation.x = Math.PI / 2;
|
||||
handle.position.z = 0.6;
|
||||
handle.castShadow = true;
|
||||
this.registerFadeMesh(handle);
|
||||
this.axeGroup.add(handle);
|
||||
const blade = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.7, 0.12), axeMat);
|
||||
blade.position.z = 1.35;
|
||||
blade.castShadow = true;
|
||||
this.registerFadeMesh(blade);
|
||||
this.axeGroup.add(blade);
|
||||
|
||||
// Kopf
|
||||
const head = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.6, 0.75), this.furMat);
|
||||
head.position.y = 2.05;
|
||||
head.castShadow = true;
|
||||
this.registerFadeMesh(head);
|
||||
this.model.add(head);
|
||||
|
||||
// Schnauze (+Z)
|
||||
const snout = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.24, 0.4), this.darkMat);
|
||||
snout.position.set(0, 1.92, 0.55);
|
||||
this.registerFadeMesh(snout);
|
||||
this.model.add(snout);
|
||||
|
||||
// Hörner
|
||||
for (const side of [-1, 1]) {
|
||||
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.09, 0.55, 8), hornMat);
|
||||
horn.position.set(side * 0.28, 2.35, 0.1);
|
||||
horn.rotation.set(-1.1, 0, side * 0.45);
|
||||
horn.castShadow = true;
|
||||
this.registerFadeMesh(horn);
|
||||
this.model.add(horn);
|
||||
}
|
||||
|
||||
// Augen (+Z, gelb -> rot in Phase 2)
|
||||
for (const side of [-1, 1]) {
|
||||
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 6), this.eyeMat);
|
||||
eye.position.set(side * 0.2, 2.12, 0.4);
|
||||
this.registerFadeMesh(eye);
|
||||
this.model.add(eye);
|
||||
}
|
||||
|
||||
// Stirnpanzer vorn
|
||||
const brow = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.22, 0.08), metalMat);
|
||||
brow.position.set(0, 2.24, 0.38);
|
||||
this.registerFadeMesh(brow);
|
||||
this.model.add(brow);
|
||||
|
||||
// --- Effekte an den Body haengen (nicht skaliert, drehen aber mit) ---
|
||||
|
||||
// Stun-Sterne um den Kopf
|
||||
this.starsGroup = new THREE.Group();
|
||||
const starGeom = new THREE.OctahedronGeometry(0.14);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: 0xffdd33 });
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const a = (i / 4) * Math.PI * 2;
|
||||
const star = new THREE.Mesh(starGeom, starMat);
|
||||
star.position.set(Math.cos(a) * 1.4, 3.7, Math.sin(a) * 1.4);
|
||||
this.starsGroup.add(star);
|
||||
}
|
||||
this.starsGroup.visible = false;
|
||||
this.body.add(this.starsGroup);
|
||||
|
||||
// Axt-Telegraf-Bogen (Trefferzone) auf dem Boden.
|
||||
// Achtung: RingGeometry-Winkel starten an lokaler +X -> um 90° versetzen,
|
||||
// damit der Halbkreis nach vorn (+Z, Blickrichtung) zeigt.
|
||||
const arcMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xff4444,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const arcGeom = new THREE.RingGeometry(
|
||||
SWING_RANGE - 0.7, SWING_RANGE - 0.2, 48, 1,
|
||||
-SWING_ARC - Math.PI / 2, SWING_ARC * 2
|
||||
);
|
||||
this.telegraphArc = new THREE.Mesh(arcGeom, arcMat);
|
||||
this.telegraphArc.rotation.x = -Math.PI / 2;
|
||||
this.telegraphArc.position.y = 0.05;
|
||||
this.telegraphArc.visible = false;
|
||||
this.body.add(this.telegraphArc);
|
||||
|
||||
// Axt-Sweep-Visual: heller Bogen fegt von rechts nach links ueber den Boden
|
||||
const swingArcMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xffaa88,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.swingArc = new THREE.Mesh(arcGeom, swingArcMat);
|
||||
this.swingArc.rotation.x = -Math.PI / 2;
|
||||
this.swingArc.position.y = 0.05;
|
||||
this.swingArc.visible = false;
|
||||
this.body.add(this.swingArc);
|
||||
|
||||
// Dash-Richtungslinie
|
||||
const lineMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xff3333,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.dashLine = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.02, 12), lineMat);
|
||||
this.dashLine.position.y = 0.06;
|
||||
this.dashLine.position.z = 8;
|
||||
this.dashLine.visible = false;
|
||||
this.body.add(this.dashLine);
|
||||
|
||||
// Schockwellen-Ring
|
||||
this.shockRingMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xff5533,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.shockRing = new THREE.Mesh(new THREE.RingGeometry(0.9, 1.0, 48), this.shockRingMat);
|
||||
this.shockRing.rotation.x = -Math.PI / 2;
|
||||
this.shockRing.position.y = 0.06;
|
||||
this.shockRing.visible = false;
|
||||
this.body.add(this.shockRing);
|
||||
}
|
||||
|
||||
playAnim() {
|
||||
// Prozedurale Optik
|
||||
}
|
||||
|
||||
updateAnimation(dt: number) {
|
||||
this.updateFadeDeath(dt, { duration: 1.2, sink: 0.5 });
|
||||
}
|
||||
|
||||
update(dt: number, game: Game) {
|
||||
this.game = game;
|
||||
this.waveTime += dt;
|
||||
|
||||
// Hit-Feedback: kurz weiss aufblitzen + Puls
|
||||
if (this.hitFlash > 0) {
|
||||
this.hitFlash -= dt;
|
||||
this.furMat.color.set(0xffffff);
|
||||
this.darkMat.color.set(0xffffff);
|
||||
this.model.scale.setScalar(MODEL_SCALE * (1 + (this.hitFlash / HIT_FLASH_TIME) * 0.05));
|
||||
} else {
|
||||
this.furMat.color.set(0x6b4a2f);
|
||||
this.darkMat.color.set(0x4a3220);
|
||||
this.model.scale.setScalar(MODEL_SCALE);
|
||||
}
|
||||
|
||||
// Phase 2 ab 40% HP
|
||||
if (!this.enraged && this.health < MAX_HEALTH * ENRAGE_RATIO) {
|
||||
this.enraged = true;
|
||||
this.eyeMat.color.set(0xff2222);
|
||||
playSound('damage', 0.9);
|
||||
game.triggerCameraShake(0.35, 0.5);
|
||||
}
|
||||
|
||||
switch (this.state) {
|
||||
case 'idle': this.updateIdle(dt, game); break;
|
||||
case 'dashwindup': this.updateDashWindup(dt, game); break;
|
||||
case 'dash': this.updateDash(dt, game); break;
|
||||
case 'dashstop': this.updateDashStop(dt); break;
|
||||
case 'stunned': this.updateStunned(dt); break;
|
||||
case 'swingwindup': this.updateSwingWindup(dt, game); break;
|
||||
case 'swing': this.updateSwing(dt, game); break;
|
||||
case 'jump': this.updateJump(dt, game); break;
|
||||
}
|
||||
|
||||
// Stun-Sterne animieren
|
||||
this.starsGroup.visible = this.state === 'stunned';
|
||||
if (this.starsGroup.visible) {
|
||||
this.starsGroup.rotation.y += dt * 3;
|
||||
for (const star of this.starsGroup.children) {
|
||||
star.rotation.x += dt * 5;
|
||||
star.rotation.y += dt * 4;
|
||||
star.position.y = 3.7 + Math.sin(this.waveTime * 6) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
// Bein-Animation: Laufen / Dash-Haltung / sonst still
|
||||
if (this.state === 'dash' || this.state === 'dashwindup') {
|
||||
for (const leg of this.legs) leg.rotation.x = 0.5;
|
||||
} else if (this.state === 'jump') {
|
||||
for (const leg of this.legs) leg.rotation.x = 0.25;
|
||||
} else if (this.state === 'idle' && this.moving) {
|
||||
const step = Math.sin(this.waveTime * 9);
|
||||
this.legs[0].rotation.x = step * 0.55;
|
||||
this.legs[1].rotation.x = -step * 0.55;
|
||||
} else {
|
||||
for (const leg of this.legs) leg.rotation.x = 0;
|
||||
}
|
||||
|
||||
// Schockwellen-Ring expandieren
|
||||
if (this.shockRing.visible) {
|
||||
const s = this.shockRing.scale.x + dt * 26;
|
||||
this.shockRing.scale.setScalar(s);
|
||||
this.shockRingMat.opacity = Math.max(0, 0.5 - (s / 11) * 0.5);
|
||||
if (s >= 11) this.shockRing.visible = false;
|
||||
}
|
||||
|
||||
// Felsen
|
||||
for (let i = this.rocks.length - 1; i >= 0; i--) {
|
||||
if (!this.rocks[i].update(dt, game)) {
|
||||
this.rocks[i].dispose();
|
||||
this.rocks.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Wandrisse verblassen
|
||||
for (let i = this.cracks.length - 1; i >= 0; i--) {
|
||||
const c = this.cracks[i];
|
||||
c.life -= dt;
|
||||
const op = Math.max(0, c.life / 2.5) * 0.6;
|
||||
c.group.traverse((obj) => {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
(obj.material as THREE.Material & { opacity: number }).opacity = op;
|
||||
}
|
||||
});
|
||||
if (c.life <= 0) {
|
||||
game.scene.remove(c.group);
|
||||
this.disposeGroup(c.group);
|
||||
this.cracks.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateIdle(dt: number, game: Game) {
|
||||
const playerPos = game.player.body.position;
|
||||
const dx = playerPos.x - this.body.position.x;
|
||||
const dz = playerPos.z - this.body.position.z;
|
||||
const dist = Math.hypot(dx, dz);
|
||||
|
||||
// Turn-Lag: langsam zum Spieler drehen
|
||||
const target = Math.atan2(dx, dz);
|
||||
const turnSpeed = this.enraged ? TURN_SPEED_2 : TURN_SPEED_1;
|
||||
this.yaw = this.lerpAngle(this.yaw, target, Math.min(1, turnSpeed * dt));
|
||||
this.body.rotation.y = this.yaw;
|
||||
|
||||
// Abstand halten: nie zu nah, nie zu weit
|
||||
this.moving = false;
|
||||
const moveDir = new THREE.Vector3();
|
||||
if (dist > IDEAL_DIST + 1 && dist > 0.01) {
|
||||
moveDir.set(dx / dist, 0, dz / dist);
|
||||
this.moving = true;
|
||||
} else if (dist < MIN_DIST && dist > 0.01) {
|
||||
moveDir.set(-dx / dist, 0, -dz / dist);
|
||||
this.moving = true;
|
||||
}
|
||||
if (moveDir.lengthSq() > 0) {
|
||||
this.body.position.x += moveDir.x * MOVE_SPEED * dt;
|
||||
this.body.position.z += moveDir.z * MOVE_SPEED * dt;
|
||||
game.clampToArena(this.body.position);
|
||||
}
|
||||
|
||||
this.dashTimer -= dt;
|
||||
this.swingTimer -= dt;
|
||||
this.stompTimer -= dt;
|
||||
if (this.dashTimer <= 0) {
|
||||
this.dashTimer = this.enraged ? DASH_COOLDOWN_2 : DASH_COOLDOWN;
|
||||
this.doubleDashLeft = this.enraged ? 1 : 0;
|
||||
this.startDashWindup(game);
|
||||
} else if (this.swingTimer <= 0) {
|
||||
this.swingTimer = this.enraged ? SWING_COOLDOWN_2 : SWING_COOLDOWN;
|
||||
this.startSwingWindup();
|
||||
} else if (this.stompTimer <= 0) {
|
||||
this.stompTimer = this.enraged ? STOMP_COOLDOWN_2 : STOMP_COOLDOWN;
|
||||
this.startJump(game);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dash ---
|
||||
|
||||
private startDashWindup(game: Game) {
|
||||
this.state = 'dashwindup';
|
||||
this.stateTime = DASH_WINDUP;
|
||||
this.dashDir.set(
|
||||
game.player.body.position.x - this.body.position.x,
|
||||
0,
|
||||
game.player.body.position.z - this.body.position.z
|
||||
);
|
||||
if (this.dashDir.lengthSq() < 0.01) this.dashDir.set(0, 0, 1);
|
||||
this.dashDir.normalize();
|
||||
this.dashHitPlayer = false;
|
||||
this.yaw = Math.atan2(this.dashDir.x, this.dashDir.z);
|
||||
this.body.rotation.y = this.yaw;
|
||||
this.dashLine.visible = true;
|
||||
playSound('spawn', 0.6);
|
||||
}
|
||||
|
||||
private updateDashWindup(dt: number, game: Game) {
|
||||
this.stateTime -= dt;
|
||||
(this.dashLine.material as THREE.MeshBasicMaterial).opacity =
|
||||
0.3 + 0.4 * Math.abs(Math.sin(this.waveTime * 14));
|
||||
if (this.stateTime <= 0) {
|
||||
this.dashLine.visible = false;
|
||||
this.state = 'dash';
|
||||
this.dashDist = 0;
|
||||
playSound('spawn', 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
private updateDash(dt: number, game: Game) {
|
||||
const step = DASH_SPEED * dt;
|
||||
this.dashDist += step;
|
||||
this.body.position.x += this.dashDir.x * step;
|
||||
this.body.position.z += this.dashDir.z * step;
|
||||
|
||||
if (!this.dashHitPlayer && game.player.health > 0) {
|
||||
const dx = game.player.body.position.x - this.body.position.x;
|
||||
const dz = game.player.body.position.z - this.body.position.z;
|
||||
if (Math.hypot(dx, dz) < 1.7) {
|
||||
this.dashHitPlayer = true;
|
||||
game.player.damage(DASH_DAMAGE, this.body.position);
|
||||
playSound('damage', 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.abs(this.body.position.x) > WALL_LIMIT || Math.abs(this.body.position.z) > WALL_LIMIT) {
|
||||
game.clampToArena(this.body.position);
|
||||
this.hitWall(game);
|
||||
return;
|
||||
}
|
||||
if (this.dashDist >= DASH_MAX_DIST) {
|
||||
this.endDash(game);
|
||||
}
|
||||
}
|
||||
|
||||
private endDash(game: Game) {
|
||||
if (this.doubleDashLeft > 0) {
|
||||
this.doubleDashLeft--;
|
||||
this.startDashWindup(game);
|
||||
} else {
|
||||
this.state = 'dashstop';
|
||||
this.stateTime = 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
private updateDashStop(dt: number) {
|
||||
this.stateTime -= dt;
|
||||
if (this.stateTime <= 0) this.state = 'idle';
|
||||
}
|
||||
|
||||
private hitWall(game: Game) {
|
||||
this.state = 'stunned';
|
||||
this.stateTime = BOSS_STUN_TIME;
|
||||
this.spawnWallCracks(game);
|
||||
playSound('explosion', 0.5);
|
||||
}
|
||||
|
||||
private updateStunned(dt: number) {
|
||||
this.stateTime -= dt;
|
||||
// Schwindel-Wackeln
|
||||
this.model.rotation.z = Math.sin(this.waveTime * 20) * 0.08;
|
||||
if (this.stateTime <= 0) {
|
||||
this.model.rotation.z = 0;
|
||||
this.state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
private spawnWallCracks(game: Game) {
|
||||
const group = new THREE.Group();
|
||||
const crackMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x111111,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
depthWrite: false,
|
||||
});
|
||||
const onXWall = Math.abs(this.body.position.x) > Math.abs(this.body.position.z);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const len = 0.8 + Math.random() * 1.2;
|
||||
const geo = new THREE.BoxGeometry(
|
||||
onXWall ? 0.06 : len,
|
||||
0.06,
|
||||
onXWall ? len : 0.06
|
||||
);
|
||||
const crack = new THREE.Mesh(geo, crackMat);
|
||||
if (onXWall) {
|
||||
crack.position.set(
|
||||
Math.sign(this.body.position.x) * 49.2,
|
||||
0.5 + Math.random() * 1.4,
|
||||
this.body.position.z + (Math.random() - 0.5) * 2.4
|
||||
);
|
||||
} else {
|
||||
crack.position.set(
|
||||
this.body.position.x + (Math.random() - 0.5) * 2.4,
|
||||
0.5 + Math.random() * 1.4,
|
||||
Math.sign(this.body.position.z) * 49.2
|
||||
);
|
||||
}
|
||||
crack.rotation.y = Math.random() * Math.PI;
|
||||
group.add(crack);
|
||||
}
|
||||
game.scene.add(group);
|
||||
this.cracks.push({ group, life: 2.5 });
|
||||
}
|
||||
|
||||
// --- Axt-Schwung ---
|
||||
|
||||
private startSwingWindup() {
|
||||
this.state = 'swingwindup';
|
||||
this.stateTime = SWING_WINDUP;
|
||||
this.swingHit = false;
|
||||
// Axt anheben (leicht), bereit fuer den horizontalen Sweep
|
||||
this.axeGroup.rotation.x = -0.35;
|
||||
this.axeGroup.rotation.y = 1.0;
|
||||
this.telegraphArc.visible = true;
|
||||
playSound('spawn', 0.5);
|
||||
}
|
||||
|
||||
private updateSwingWindup(dt: number, game: Game) {
|
||||
this.stateTime -= dt;
|
||||
(this.telegraphArc.material as THREE.MeshBasicMaterial).opacity =
|
||||
0.3 + 0.4 * Math.abs(Math.sin(this.waveTime * 12));
|
||||
if (this.stateTime <= 0) {
|
||||
this.telegraphArc.visible = false;
|
||||
this.state = 'swing';
|
||||
this.stateTime = SWING_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
private updateSwing(dt: number, game: Game) {
|
||||
this.stateTime -= dt;
|
||||
const t = 1 - this.stateTime / SWING_TIME;
|
||||
|
||||
// Axt fegt horizontal von rechts nach links vor dem Koerper
|
||||
this.axeGroup.rotation.y = 1.0 - t * 2.0;
|
||||
this.axeGroup.rotation.x = -0.35 + t * 0.35;
|
||||
|
||||
// Hell: Halbkreis fegt ueber den Boden
|
||||
this.swingArc.visible = true;
|
||||
this.swingArc.rotation.y = SWING_ARC * 0.85 * (1 - 2 * t);
|
||||
const fade = Math.min(1, t * 6, (1 - t) * 6);
|
||||
(this.swingArc.material as THREE.MeshBasicMaterial).opacity = fade * 0.7;
|
||||
|
||||
// Treffer in der Mitte des Schwungs
|
||||
if (!this.swingHit && t >= 0.35) {
|
||||
this.swingHit = true;
|
||||
const playerPos = game.player.body.position;
|
||||
const dx = playerPos.x - this.body.position.x;
|
||||
const dz = playerPos.z - this.body.position.z;
|
||||
const dist = Math.hypot(dx, dz);
|
||||
if (dist < SWING_RANGE) {
|
||||
const ang = Math.atan2(dx, dz);
|
||||
// Ueberspringbar: Treffer nur am Boden
|
||||
if (Math.abs(this.angleDiff(this.yaw, ang)) < SWING_ARC && playerPos.y <= 0.6) {
|
||||
game.player.damage(SWING_DAMAGE, this.body.position);
|
||||
playSound('damage', 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stateTime <= 0) {
|
||||
this.axeGroup.rotation.x = 0;
|
||||
this.axeGroup.rotation.y = 0;
|
||||
this.swingArc.visible = false;
|
||||
this.state = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sprung + Stampf ---
|
||||
|
||||
private startJump(game: Game) {
|
||||
this.state = 'jump';
|
||||
this.stateTime = STOMP_JUMP_TIME;
|
||||
this.jumpFrom.copy(this.body.position);
|
||||
this.jumpTo.set(game.player.body.position.x, 0, game.player.body.position.z);
|
||||
playSound('spawn', 0.7);
|
||||
}
|
||||
|
||||
private updateJump(dt: number, game: Game) {
|
||||
this.stateTime -= dt;
|
||||
const t = 1 - Math.max(0, this.stateTime) / STOMP_JUMP_TIME;
|
||||
this.body.position.x = this.jumpFrom.x + (this.jumpTo.x - this.jumpFrom.x) * t;
|
||||
this.body.position.z = this.jumpFrom.z + (this.jumpTo.z - this.jumpFrom.z) * t;
|
||||
this.body.position.y = 0.15 + Math.sin(t * Math.PI) * STOMP_JUMP_HEIGHT;
|
||||
|
||||
if (this.stateTime <= 0) {
|
||||
this.body.position.y = 0.15;
|
||||
this.landStomp(game);
|
||||
}
|
||||
}
|
||||
|
||||
private landStomp(game: Game) {
|
||||
// Schockwelle: stunnt den Spieler (ueberspringbar)
|
||||
this.shockRing.visible = true;
|
||||
this.shockRing.scale.setScalar(0.2);
|
||||
this.shockRingMat.opacity = 0.5;
|
||||
playSound('explosion', 0.7);
|
||||
|
||||
const playerPos = game.player.body.position;
|
||||
const dist = Math.hypot(
|
||||
playerPos.x - this.body.position.x,
|
||||
playerPos.z - this.body.position.z
|
||||
);
|
||||
if (dist < STOMP_RADIUS && playerPos.y <= 0.6) {
|
||||
game.player.stunTime = STOMP_STUN;
|
||||
game.player.damage(STOMP_DAMAGE, this.body.position);
|
||||
}
|
||||
|
||||
// Steinschlag um den Spieler
|
||||
const count = this.enraged ? ROCK_COUNT_2 : ROCK_COUNT;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
const r = 2 + Math.random() * 6;
|
||||
const rock = new FallingRock(new THREE.Vector3(
|
||||
playerPos.x + Math.cos(a) * r,
|
||||
0,
|
||||
playerPos.z + Math.sin(a) * r
|
||||
));
|
||||
game.scene.add(rock.body);
|
||||
this.rocks.push(rock);
|
||||
}
|
||||
|
||||
this.state = 'idle';
|
||||
}
|
||||
|
||||
// --- Schaden ---
|
||||
|
||||
override takeDamage(
|
||||
damage: number,
|
||||
game: Game,
|
||||
withKnockback = true,
|
||||
sourcePos?: THREE.Vector3
|
||||
) {
|
||||
const src = sourcePos ?? game.player.body.position;
|
||||
const fwd = new THREE.Vector3(Math.sin(this.yaw), 0, Math.cos(this.yaw));
|
||||
const incoming = new THREE.Vector3().subVectors(src, this.body.position);
|
||||
incoming.y = 0;
|
||||
if (incoming.lengthSq() < 0.0001) incoming.copy(fwd);
|
||||
incoming.normalize();
|
||||
|
||||
// Nur von hinten verletzbar: vorn wird geblockt
|
||||
if (fwd.dot(incoming) > 0.15) {
|
||||
game.spawnDamageText('BLOCK', this.body.position, '#99aabb');
|
||||
playSound('sword_hit1', 0.6);
|
||||
return;
|
||||
}
|
||||
|
||||
super.takeDamage(damage, game, withKnockback, sourcePos);
|
||||
this.hitFlash = HIT_FLASH_TIME;
|
||||
}
|
||||
|
||||
protected onDeath(_game: Game) {
|
||||
this.cleanupEffects();
|
||||
this.beginFadeDeath();
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
this.cleanupEffects();
|
||||
}
|
||||
|
||||
private cleanupEffects() {
|
||||
this.starsGroup.visible = false;
|
||||
this.dashLine.visible = false;
|
||||
this.telegraphArc.visible = false;
|
||||
this.swingArc.visible = false;
|
||||
this.shockRing.visible = false;
|
||||
this.furMat.color.set(0x6b4a2f);
|
||||
this.darkMat.color.set(0x4a3220);
|
||||
for (const rock of this.rocks) rock.dispose();
|
||||
this.rocks = [];
|
||||
if (this.game) {
|
||||
for (const c of this.cracks) {
|
||||
this.game.scene.remove(c.group);
|
||||
this.disposeGroup(c.group);
|
||||
}
|
||||
}
|
||||
this.cracks = [];
|
||||
}
|
||||
|
||||
private disposeGroup(group: THREE.Group) {
|
||||
group.traverse((obj) => {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private lerpAngle(a: number, b: number, t: number): number {
|
||||
return a + this.angleDiff(a, b) * t;
|
||||
}
|
||||
|
||||
private angleDiff(a: number, b: number): number {
|
||||
let diff = b - a;
|
||||
while (diff > Math.PI) diff -= Math.PI * 2;
|
||||
while (diff < -Math.PI) diff += Math.PI * 2;
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export class Player {
|
||||
slowFactor = 0.6;
|
||||
pullTime = 0;
|
||||
pullDir = new THREE.Vector3();
|
||||
stunTime = 0;
|
||||
stats: PlayerStats = {
|
||||
maxHealth: 100,
|
||||
regen: 0,
|
||||
@@ -52,6 +53,7 @@ export class Player {
|
||||
private bodyMaterials: THREE.MeshToonMaterial[] = [];
|
||||
private hurtCooldown = 0;
|
||||
private flashTime = 0;
|
||||
private stunStars: THREE.Group;
|
||||
private static readonly BASE_COLOR = 0x44aa44;
|
||||
private static readonly HURT_COLOR = 0xff5555;
|
||||
private static readonly SLOW_COLOR = 0x9944cc;
|
||||
@@ -60,6 +62,19 @@ export class Player {
|
||||
constructor() {
|
||||
this.body = new THREE.Group();
|
||||
|
||||
// Stun-Sterne: kreisen um den Kopf, solange der Spieler gestunnt ist
|
||||
this.stunStars = new THREE.Group();
|
||||
const starGeom = new THREE.OctahedronGeometry(0.13);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: 0xffdd33 });
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a = (i / 3) * Math.PI * 2;
|
||||
const star = new THREE.Mesh(starGeom, starMat);
|
||||
star.position.set(Math.cos(a) * 0.55, 1.35, Math.sin(a) * 0.55);
|
||||
this.stunStars.add(star);
|
||||
}
|
||||
this.stunStars.visible = false;
|
||||
this.body.add(this.stunStars);
|
||||
|
||||
// Helmet
|
||||
const helmetGeom = new THREE.SphereGeometry(0.45, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2);
|
||||
const helmetMat = new THREE.MeshToonMaterial({ color: 0x888888 });
|
||||
@@ -223,6 +238,18 @@ export class Player {
|
||||
if (this.mixer) this.mixer.update(dt);
|
||||
if (this.hurtCooldown > 0) this.hurtCooldown -= dt;
|
||||
if (this.slowTimer > 0) this.slowTimer -= dt;
|
||||
if (this.stunTime > 0) this.stunTime -= dt;
|
||||
|
||||
// Stun-Sterne animieren
|
||||
this.stunStars.visible = this.stunTime > 0;
|
||||
if (this.stunStars.visible) {
|
||||
this.stunStars.rotation.y += dt * 5;
|
||||
for (const star of this.stunStars.children) {
|
||||
star.rotation.x += dt * 8;
|
||||
star.rotation.y += dt * 6;
|
||||
star.position.y = 1.35 + Math.sin(this.stunTime * 10) * 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
let color: THREE.Color;
|
||||
if (this.flashTime > 0) {
|
||||
@@ -332,6 +359,7 @@ export class Player {
|
||||
this.shieldTimer = 0;
|
||||
this.slowTimer = 0;
|
||||
this.pullTime = 0;
|
||||
this.stunTime = 0;
|
||||
this.body.position.set(0, 0.5, 0);
|
||||
this.velocity.set(0, 0, 0);
|
||||
this.jumpVelocity = 0;
|
||||
|
||||
Reference in New Issue
Block a user