Rework necromancer, archer, shaman and golem models with procedural animations

This commit is contained in:
2026-08-21 11:25:50 +02:00
parent b05835df74
commit 8065104d8a
5 changed files with 692 additions and 103 deletions
+198 -39
View File
@@ -17,6 +17,14 @@ export class Archer extends Enemy {
private boneMat!: THREE.MeshToonMaterial;
private darkMat!: THREE.MeshToonMaterial;
private bowGroup!: THREE.Group;
private legL!: THREE.Group;
private legR!: THREE.Group;
private walkPhase = 0;
private walkAmp = 0;
private handL!: THREE.Mesh;
private arrowNock!: THREE.Mesh;
private draw = 0; // 0 = Bogen entspannt, 1 = voll ausgezogen
private recoil = 0; // klingt nach dem Abschuss ab
private shootTimer = 1 + Math.random();
private telegraphs: { mesh: THREE.Mesh; mat: THREE.MeshBasicMaterial; timer: number }[] = [];
private time = Math.random() * Math.PI * 2;
@@ -36,53 +44,177 @@ export class Archer extends Enemy {
this.boneMat = new THREE.MeshToonMaterial({ color: 0xe8dcc0 });
this.darkMat = new THREE.MeshToonMaterial({ color: 0x8a7a5a });
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff4422 });
// Torso
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.55, 0.35), this.boneMat);
torso.position.y = 1.0;
torso.castShadow = true;
this.registerFadeMesh(torso);
this.model.add(torso);
// --- Skelett (Blickrichtung +Z) ---
// Beine
for (const side of [-1, 1]) {
const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.09, 0.7, 6), this.boneMat);
leg.position.set(side * 0.15, 0.35, 0);
this.registerFadeMesh(leg);
this.model.add(leg);
// Becken + Gürtel
const pelvis = new THREE.Mesh(new THREE.SphereGeometry(0.16, 10, 8), this.boneMat);
pelvis.scale.set(1.2, 0.7, 0.9);
pelvis.position.y = 0.78;
this.registerFadeMesh(pelvis);
this.model.add(pelvis);
const belt = new THREE.Mesh(new THREE.TorusGeometry(0.17, 0.03, 6, 12), this.darkMat);
belt.rotation.x = Math.PI / 2;
belt.position.y = 0.82;
this.registerFadeMesh(belt);
this.model.add(belt);
// Wirbelsäule + Rippenbögen
const spine = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.045, 0.55, 6), this.boneMat);
spine.position.y = 1.08;
this.registerFadeMesh(spine);
this.model.add(spine);
for (const [y, r] of [[1.02, 0.2], [1.14, 0.23], [1.26, 0.19]] as const) {
const rib = new THREE.Mesh(new THREE.TorusGeometry(r, 0.025, 6, 12), this.boneMat);
rib.rotation.x = Math.PI / 2;
rib.scale.set(1, 1, 0.8);
rib.position.y = y;
this.registerFadeMesh(rib);
this.model.add(rib);
}
// Kopf (Schädel)
const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 10, 8), this.boneMat);
head.position.y = 1.5;
this.registerFadeMesh(head);
this.model.add(head);
// Augen (+Z, rot)
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff4422 });
// Schädel mit Kiefer, Augenhöhlen und roten Augen (+Z)
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 10), this.boneMat);
skull.scale.set(0.95, 1.05, 1);
skull.position.y = 1.52;
skull.castShadow = true;
this.registerFadeMesh(skull);
this.model.add(skull);
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.09, 0.16), this.boneMat);
jaw.position.set(0, 1.38, 0.1);
this.registerFadeMesh(jaw);
this.model.add(jaw);
for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 5), eyeMat);
eye.position.set(side * 0.1, 1.53, 0.2);
const socket = new THREE.Mesh(new THREE.SphereGeometry(0.055, 8, 6), this.darkMat);
socket.position.set(side * 0.09, 1.55, 0.17);
this.registerFadeMesh(socket);
this.model.add(socket);
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.035, 6, 5), eyeMat);
eye.position.set(side * 0.09, 1.55, 0.2);
this.model.add(eye);
}
// Bogen (hängt an Gruppe, wird beim Schuss angehoben)
// Schultern + Arme (rechts hält den Bogen, links zieht die Sehne)
for (const side of [-1, 1]) {
const shoulder = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), this.boneMat);
shoulder.position.set(side * 0.28, 1.34, 0);
this.registerFadeMesh(shoulder);
this.model.add(shoulder);
}
const upperArmR = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.05, 0.32, 6), this.boneMat);
upperArmR.position.set(0.34, 1.26, 0.08);
upperArmR.rotation.z = -1.0;
this.registerFadeMesh(upperArmR);
this.model.add(upperArmR);
const foreArmR = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.04, 0.3, 6), this.boneMat);
foreArmR.position.set(0.4, 1.18, 0.25);
foreArmR.rotation.x = -1.2;
this.registerFadeMesh(foreArmR);
this.model.add(foreArmR);
const handR = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8, 6), this.boneMat);
handR.position.set(0.42, 1.15, 0.4);
this.registerFadeMesh(handR);
this.model.add(handR);
const upperArmL = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.05, 0.32, 6), this.boneMat);
upperArmL.position.set(-0.34, 1.24, 0.05);
upperArmL.rotation.z = 1.1;
this.registerFadeMesh(upperArmL);
this.model.add(upperArmL);
const foreArmL = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.04, 0.3, 6), this.boneMat);
foreArmL.position.set(-0.24, 1.25, 0.15);
foreArmL.rotation.x = -1.6;
foreArmL.rotation.z = 0.6;
this.registerFadeMesh(foreArmL);
this.model.add(foreArmL);
this.handL = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8, 6), this.boneMat);
this.handL.position.set(-0.1, 1.3, 0.28);
this.registerFadeMesh(this.handL);
this.model.add(this.handL);
// Beine mit Kniegelenken und Füßen (Gruppen mit Hüft-Pivot für Laufanimation)
for (const side of [-1, 1]) {
const leg = new THREE.Group();
leg.position.set(side * 0.15, 0.72, 0);
this.model.add(leg);
const thigh = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.07, 0.4, 6), this.boneMat);
thigh.position.y = -0.17;
this.registerFadeMesh(thigh);
leg.add(thigh);
const knee = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 6), this.boneMat);
knee.position.y = -0.37;
this.registerFadeMesh(knee);
leg.add(knee);
const shin = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 0.35, 6), this.boneMat);
shin.position.y = -0.54;
this.registerFadeMesh(shin);
leg.add(shin);
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.06, 0.22), this.boneMat);
foot.position.set(0, -0.68, 0.06);
this.registerFadeMesh(foot);
leg.add(foot);
if (side < 0) {
this.legL = leg;
} else {
this.legR = leg;
}
}
// Zerfetzter Umhang auf dem Rücken
const cape = new THREE.Mesh(new THREE.ConeGeometry(0.32, 0.75, 8, 1, true), this.darkMat);
cape.position.set(0, 1.0, -0.2);
cape.scale.set(1.1, 1, 0.5);
this.registerFadeMesh(cape);
this.model.add(cape);
for (const [x, z] of [[-0.15, -0.28], [0, -0.32], [0.15, -0.28]] as const) {
const tatter = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.25, 4), this.darkMat);
tatter.rotation.x = Math.PI;
tatter.position.set(x, 0.62, z);
this.registerFadeMesh(tatter);
this.model.add(tatter);
}
// Köcher mit Pfeilen am Rücken
const quiver = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.11, 0.5, 8), this.darkMat);
quiver.position.set(0.12, 1.15, -0.28);
quiver.rotation.x = 0.5;
this.registerFadeMesh(quiver);
this.model.add(quiver);
for (const dx of [-0.03, 0.02, 0.06]) {
const qArrow = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.28, 5), this.boneMat);
qArrow.position.set(0.12 + dx, 1.42, -0.4);
qArrow.rotation.x = 0.5;
this.registerFadeMesh(qArrow);
this.model.add(qArrow);
}
// Bogen (hängt an Gruppe, wird beim Schuss angehoben):
// Halbkreis-Bogen mit Sehne und eingelegtem Pfeil
this.bowGroup = new THREE.Group();
this.bowGroup.position.set(0.4, 1.05, 0.15);
this.bowGroup.position.set(0.42, 1.15, 0.45);
this.model.add(this.bowGroup);
const bow = new THREE.Mesh(new THREE.TorusGeometry(0.28, 0.03, 6, 14, Math.PI * 1.6), this.darkMat);
bow.rotation.y = -Math.PI / 2;
bow.rotation.z = -0.8;
const bowOrient = new THREE.Group();
bowOrient.rotation.y = -Math.PI / 2; // Bogenebene in Blickrichtung
this.bowGroup.add(bowOrient);
const bow = new THREE.Mesh(new THREE.TorusGeometry(0.4, 0.028, 6, 16, Math.PI), this.darkMat);
bow.rotation.z = -Math.PI / 2; // Bogenbauch zeigt nach +Z
this.registerFadeMesh(bow);
this.bowGroup.add(bow);
const string = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.5, 0.02), this.darkMat);
string.position.set(0.02, 0, 0);
bowOrient.add(bow);
const string = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.8, 5), this.darkMat);
this.registerFadeMesh(string);
this.bowGroup.add(string);
const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.06, 0.5, 6), this.boneMat);
arm.position.set(0.02, -0.1, 0);
this.registerFadeMesh(arm);
this.bowGroup.add(arm);
bowOrient.add(string);
// Pfeil sitzt auf Sehne und Bogen: Schaft entlang lokaler X-Achse
// (zeigt nach Welt-+Z), Nocke an der Sehne
this.arrowNock = new THREE.Mesh(new THREE.CylinderGeometry(0.014, 0.014, 0.75, 5), this.boneMat);
this.arrowNock.rotation.z = -Math.PI / 2;
this.arrowNock.position.x = 0.28;
this.registerFadeMesh(this.arrowNock);
bowOrient.add(this.arrowNock);
const arrowHead = new THREE.Mesh(new THREE.ConeGeometry(0.035, 0.1, 6), this.darkMat);
arrowHead.position.y = 0.42;
this.registerFadeMesh(arrowHead);
this.arrowNock.add(arrowHead);
}
playAnim() {
@@ -112,6 +244,7 @@ export class Archer extends Enemy {
toPlayer.y = 0;
const dist = toPlayer.length();
let moved = false;
if (dist > 0.1) {
const dir = toPlayer.normalize();
this.body.lookAt(
@@ -138,9 +271,20 @@ export class Archer extends Enemy {
this.body.position.x += move.x * dt;
this.body.position.z += move.z * dt;
game.clampToArena(this.body.position);
moved = true;
}
}
// Laufanimation: Beine gegengleich schwingen, Amplitude weich ein-/ausblenden
this.walkAmp += ((moved ? 0.55 : 0) - this.walkAmp) * Math.min(1, dt * 8);
if (this.walkAmp > 0.01) {
this.walkPhase += dt * 11;
}
const swing = Math.sin(this.walkPhase) * this.walkAmp;
this.legL.rotation.x = swing;
this.legR.rotation.x = -swing;
this.model.position.y = Math.abs(Math.cos(this.walkPhase)) * this.walkAmp * 0.12;
// Schuss mit Telegraf
this.shootTimer -= dt;
if (dist < SHOOT_RANGE && this.shootTimer <= 0 && game.player.health > 0) {
@@ -163,8 +307,23 @@ export class Archer extends Enemy {
}
}
// Bogen-Schwung
this.bowGroup.rotation.z = Math.sin(this.time * 2) * 0.1;
// Schießanimation: während des Telegrafs Bogen heben und Sehne spannen,
// beim Abschuss Recoil-Kick und schnelles Entspannen
const aiming = this.telegraphs.length > 0;
const drawRate = aiming ? 6 : this.recoil > 0 ? 25 : 10;
this.draw += ((aiming ? 1 : 0) - this.draw) * Math.min(1, dt * drawRate);
this.recoil = Math.max(0, this.recoil - dt);
const aim = this.draw;
this.bowGroup.position.set(
0.42 - aim * 0.17,
1.15 + aim * 0.2,
0.45 + aim * 0.1
);
this.bowGroup.rotation.x = -aim * 0.15;
this.bowGroup.rotation.z =
Math.sin(this.time * 2) * 0.1 * (1 - aim) - this.recoil * 2;
this.handL.position.z = 0.28 - aim * 0.16;
this.arrowNock.position.x = 0.28 - aim * 0.14;
}
private startShot(game: Game) {
@@ -192,8 +351,8 @@ export class Archer extends Enemy {
const target = targetPos.clone();
target.y += 0.6;
game.spawnEnemyProjectile(from, target, ARROW_DAMAGE);
// Bogen anheben als Recoil
this.bowGroup.rotation.z = -0.6;
// Recoil-Kick, klingt in update() ab
this.recoil = 0.25;
}
override takeDamage(
+40 -1
View File
@@ -4,7 +4,12 @@ import { Enemy } from './Enemy';
export class Brute extends Enemy {
protected model: THREE.Group;
protected legL!: THREE.Group;
protected legR!: THREE.Group;
protected armL: THREE.Group | null = null;
protected armR: THREE.Group | null = null;
private stepTime = 0;
private walkAmp = 0;
constructor() {
super();
@@ -21,7 +26,11 @@ export class Brute extends Enemy {
const rockMat = new THREE.MeshToonMaterial({ color: 0x6a6a78 });
const darkMat = new THREE.MeshToonMaterial({ color: 0x3c3c46 });
// Torso
// Becken + Torso
const pelvis = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.35, 0.55), darkMat);
pelvis.position.y = 0.62;
this.registerFadeMesh(pelvis);
this.model.add(pelvis);
const torso = new THREE.Mesh(new THREE.BoxGeometry(1.3, 1.2, 0.95), rockMat);
torso.position.y = 0.95;
torso.castShadow = true;
@@ -61,6 +70,26 @@ export class Brute extends Enemy {
this.registerFadeMesh(eye);
this.model.add(eye);
}
// Beine (Gruppen mit Hüft-Pivot für die Laufanimation)
for (const side of [-1, 1]) {
const leg = new THREE.Group();
leg.position.set(side * 0.35, 0.6, 0);
this.model.add(leg);
const thigh = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.18, 0.35, 6), rockMat);
thigh.position.y = -0.17;
this.registerFadeMesh(thigh);
leg.add(thigh);
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.12, 0.4), darkMat);
foot.position.set(0, -0.5, 0.06);
this.registerFadeMesh(foot);
leg.add(foot);
if (side < 0) {
this.legL = leg;
} else {
this.legR = leg;
}
}
}
playAnim() {
@@ -81,6 +110,7 @@ export class Brute extends Enemy {
.subVectors(game.player.body.position, this.body.position);
toPlayer.y = 0;
const dist = toPlayer.length();
let moved = false;
if (dist > 0.1) {
const dir = toPlayer.normalize();
this.body.lookAt(
@@ -96,7 +126,16 @@ export class Brute extends Enemy {
this.body.position.x += moveDir.x * dt;
this.body.position.z += moveDir.z * dt;
game.clampToArena(this.body.position);
moved = true;
}
// Laufanimation: Beine gegengleich im Takt der Schritte, Arme leicht mit
this.walkAmp += ((moved ? 0.5 : 0) - this.walkAmp) * Math.min(1, dt * 8);
const swing = Math.sin(this.stepTime * 3) * this.walkAmp;
this.legL.rotation.x = swing;
this.legR.rotation.x = -swing;
if (this.armL) this.armL.rotation.x = -swing * 0.4;
if (this.armR) this.armR.rotation.x = swing * 0.4;
}
protected onDeath(_game: Game) {
+139 -2
View File
@@ -15,15 +15,152 @@ export class Golem extends Brute {
this.contactRadius = 3.4;
this.guaranteedDrop = true;
// Brute-Standardmodell verwerfen und als Fels-Golem neu aufbauen
this.model.clear();
const rockMat = new THREE.MeshToonMaterial({ color: 0x6a6a78 });
const darkMat = new THREE.MeshToonMaterial({ color: 0x3c3c46 });
const mossMat = new THREE.MeshToonMaterial({ color: 0x4a8a4a });
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff6622 });
// Facettierte Fels-Optik: flache Normalen auf der unindizierten Geometrie
const rockGeom = (r: number) => {
const g = new THREE.DodecahedronGeometry(r);
g.computeVertexNormals();
return g;
};
// Becken + Rumpf (facettierte Felsbrocken)
const pelvis = new THREE.Mesh(rockGeom(0.42), darkMat);
pelvis.scale.set(1.2, 0.8, 0.9);
pelvis.position.y = 0.75;
this.registerFadeMesh(pelvis);
this.model.add(pelvis);
const torso = new THREE.Mesh(rockGeom(0.72), rockMat);
torso.scale.set(1.25, 1.05, 0.95);
torso.position.y = 1.4;
torso.castShadow = true;
this.registerFadeMesh(torso);
this.model.add(torso);
// Glühender Kern in der Brust (+Z)
const core = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.36, 0.08), eyeMat);
core.position.set(0, 1.45, 0.66);
this.model.add(core);
// Kopf mit Stirnplatte, Augen und Kiefer (+Z)
const head = new THREE.Mesh(rockGeom(0.38), rockMat);
head.position.y = 2.15;
head.castShadow = true;
this.registerFadeMesh(head);
this.model.add(head);
const brow = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.1, 0.12), darkMat);
brow.position.set(0, 2.3, 0.3);
this.registerFadeMesh(brow);
this.model.add(brow);
for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.09, 0.05), eyeMat);
eye.position.set(side * 0.15, 2.15, 0.33);
this.model.add(eye);
}
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.12, 0.2), darkMat);
jaw.position.set(0, 1.93, 0.25);
this.registerFadeMesh(jaw);
this.model.add(jaw);
// Felsstacheln auf dem Rücken
for (const side of [-1, 1]) {
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.14, 0.45, 5), darkMat);
spike.position.set(side * 0.3, 1.75, -0.6);
spike.rotation.x = 0.6;
this.registerFadeMesh(spike);
this.model.add(spike);
}
// Moosflecken
const mossSpots: [number, number, number, number][] = [
[-0.9, 2.15, 0.1, 0.12],
[0.5, 1.95, 0.3, 0.1],
[0.3, 2.4, -0.1, 0.09],
];
for (const [x, y, z, r] of mossSpots) {
const moss = new THREE.Mesh(rockGeom(r), mossMat);
moss.position.set(x, y, z);
this.registerFadeMesh(moss);
this.model.add(moss);
}
// Schultern + gegliederte Arme (Schulter-Pivot für Laufanimation)
for (const side of [-1, 1]) {
const shoulder = new THREE.Mesh(rockGeom(0.48), rockMat);
shoulder.position.set(side * 0.95, 1.75, 0);
shoulder.castShadow = true;
this.registerFadeMesh(shoulder);
this.model.add(shoulder);
const arm = new THREE.Group();
arm.position.set(side * 0.95, 1.7, 0);
this.model.add(arm);
const upperArm = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.2, 0.55, 6), rockMat);
upperArm.position.y = -0.3;
this.registerFadeMesh(upperArm);
arm.add(upperArm);
const elbow = new THREE.Mesh(rockGeom(0.18), darkMat);
elbow.position.y = -0.6;
this.registerFadeMesh(elbow);
arm.add(elbow);
const foreArm = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.18, 0.5, 6), darkMat);
foreArm.position.y = -0.85;
this.registerFadeMesh(foreArm);
arm.add(foreArm);
const fist = new THREE.Mesh(rockGeom(0.24), rockMat);
fist.scale.set(1, 0.8, 1.1);
fist.position.y = -1.15;
this.registerFadeMesh(fist);
arm.add(fist);
if (side < 0) {
this.armL = arm;
} else {
this.armR = arm;
}
}
// Beine (Hüft-Pivot für Laufanimation)
for (const side of [-1, 1]) {
const leg = new THREE.Group();
leg.position.set(side * 0.4, 0.75, 0);
this.model.add(leg);
const thigh = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.24, 0.4, 6), rockMat);
thigh.position.y = -0.18;
this.registerFadeMesh(thigh);
leg.add(thigh);
const knee = new THREE.Mesh(rockGeom(0.16), darkMat);
knee.position.y = -0.38;
this.registerFadeMesh(knee);
leg.add(knee);
const shin = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.18, 0.35, 6), darkMat);
shin.position.y = -0.52;
this.registerFadeMesh(shin);
leg.add(shin);
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.16, 0.5), rockMat);
foot.position.set(0, -0.67, 0.08);
this.registerFadeMesh(foot);
leg.add(foot);
if (side < 0) {
this.legL = leg;
} else {
this.legR = leg;
}
}
this.model.scale.setScalar(2.1);
// Crown
// Krone (Boss-Markierung)
const crownMat = new THREE.MeshToonMaterial({ color: 0xffcc33 });
const crown = new THREE.Mesh(
new THREE.ConeGeometry(0.3, 0.4, 8, 1, true),
crownMat
);
crown.position.y = 2.25;
crown.position.y = 2.55;
crown.castShadow = true;
this.registerFadeMesh(crown);
this.model.add(crown);
+153 -38
View File
@@ -90,21 +90,66 @@ export class Necromancer extends Enemy {
this.darkMat = new THREE.MeshToonMaterial({ color: 0x2a1445 });
this.glowMat = new THREE.MeshBasicMaterial({ color: 0xbb66ff });
this.eyeMat = new THREE.MeshBasicMaterial({ color: 0xdd88ff });
const boneMat = new THREE.MeshToonMaterial({ color: 0xf0e6cf });
const faceMat = new THREE.MeshBasicMaterial({ color: 0x0a0614 });
let boneFadeRegistered = false;
const registerBone = (mesh: THREE.Mesh) => {
// Knochen-Material einmal fürs Fade-Death registrieren (wird geteilt)
if (!boneFadeRegistered) {
this.registerFadeMesh(mesh);
boneFadeRegistered = true;
}
};
// Robe (umgedrehter Kegel, schwingt beim Schweben)
const robe = new THREE.Mesh(new THREE.ConeGeometry(0.9, 1.8, 12, 1, true), this.robeMat);
robe.position.y = -0.2;
robe.rotation.x = Math.PI;
// --- Robe (Blickrichtung +Z, schwebt: unten offen und zerfetzt) ---
const robeProfile = [
new THREE.Vector2(0.75, -1.15), // Saum
new THREE.Vector2(0.62, -0.75),
new THREE.Vector2(0.45, -0.2),
new THREE.Vector2(0.38, 0.25),
new THREE.Vector2(0.5, 0.55), // Schulteransatz
new THREE.Vector2(0.32, 0.78), // Hals
];
const robe = new THREE.Mesh(new THREE.LatheGeometry(robeProfile, 14), this.robeMat);
robe.castShadow = true;
this.registerFadeMesh(robe);
this.model.add(robe);
// Rocksaum
const hem = new THREE.Mesh(new THREE.CircleGeometry(0.95, 12), this.darkMat);
hem.rotation.x = -Math.PI / 2;
hem.position.y = -1.05;
this.registerFadeMesh(hem);
this.model.add(hem);
// Innenschatten im Saum (von unten sichtbar)
const hemInner = new THREE.Mesh(new THREE.CircleGeometry(0.7, 14), this.darkMat);
hemInner.rotation.x = Math.PI / 2;
hemInner.position.y = -1.08;
this.registerFadeMesh(hemInner);
this.model.add(hemInner);
// Zerfetzte Saumfetzen
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
const tatter = new THREE.Mesh(
new THREE.ConeGeometry(0.08, 0.4, 4),
i % 2 === 0 ? this.robeMat : this.darkMat
);
tatter.rotation.x = Math.PI;
tatter.position.set(Math.sin(a) * 0.68, -1.28, Math.cos(a) * 0.68);
this.registerFadeMesh(tatter);
this.model.add(tatter);
}
// Gürtelkordel + Quaste
const belt = new THREE.Mesh(new THREE.TorusGeometry(0.42, 0.035, 6, 16), this.darkMat);
belt.rotation.x = Math.PI / 2;
belt.position.y = 0.12;
this.registerFadeMesh(belt);
this.model.add(belt);
const tasselCord = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.28, 5), this.darkMat);
tasselCord.position.set(-0.3, -0.05, 0.36);
this.registerFadeMesh(tasselCord);
this.model.add(tasselCord);
const tassel = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.14, 5), boneMat);
tassel.rotation.x = Math.PI;
tassel.position.set(-0.3, -0.24, 0.38);
registerBone(tassel);
this.model.add(tassel);
// Oberkörper
const body = new THREE.Mesh(new THREE.SphereGeometry(0.55, 16, 12), this.robeMat);
@@ -114,46 +159,116 @@ export class Necromancer extends Enemy {
this.registerFadeMesh(body);
this.model.add(body);
// Kapuze
const hood = new THREE.Mesh(new THREE.ConeGeometry(0.42, 0.8, 12, 1, true), this.darkMat);
hood.position.set(0, 1.1, 0.1);
hood.rotation.x = 0.15;
// Kragen
const collar = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.045, 6, 14), this.darkMat);
collar.rotation.x = Math.PI / 2 - 0.2;
collar.position.set(0, 0.85, 0.08);
this.registerFadeMesh(collar);
this.model.add(collar);
// Schulterpanzer mit Spitzen
for (const side of [-1, 1]) {
const pauldron = new THREE.Mesh(new THREE.SphereGeometry(0.26, 12, 8), this.darkMat);
pauldron.scale.set(1.25, 0.65, 1.25);
pauldron.position.set(side * 0.45, 0.78, 0);
this.registerFadeMesh(pauldron);
this.model.add(pauldron);
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.22, 5), this.darkMat);
spike.position.set(side * 0.55, 0.95, 0);
spike.rotation.z = side * -0.5;
this.registerFadeMesh(spike);
this.model.add(spike);
}
// Kapuze mit Krempe und schwarzem Gesichts-Leerraum
const hood = new THREE.Mesh(new THREE.ConeGeometry(0.5, 0.95, 12, 1, true), this.darkMat);
hood.position.set(0, 1.18, 0.02);
hood.rotation.x = 0.22;
hood.castShadow = true;
this.registerFadeMesh(hood);
this.model.add(hood);
// Kapuzen-Öffnung (+Z, dunkel)
const hoodFace = new THREE.Mesh(new THREE.CircleGeometry(0.36, 12), this.darkMat);
hoodFace.position.set(0, 1.02, 0.42);
hoodFace.rotation.y = Math.PI / 2 - 0.1;
hoodFace.rotation.x = -0.35;
this.registerFadeMesh(hoodFace);
this.model.add(hoodFace);
const hoodRim = new THREE.Mesh(new THREE.TorusGeometry(0.34, 0.05, 8, 16), this.darkMat);
hoodRim.rotation.x = -0.5;
hoodRim.position.set(0, 1.05, 0.34);
this.registerFadeMesh(hoodRim);
this.model.add(hoodRim);
const face = new THREE.Mesh(new THREE.SphereGeometry(0.3, 12, 10), faceMat);
face.scale.set(1, 1.1, 0.8);
face.position.set(0, 1.04, 0.26);
this.registerFadeMesh(face);
this.model.add(face);
// Leuchtende Augen in der Kapuze (+Z)
for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), this.eyeMat);
eye.position.set(side * 0.13, 1.05, 0.44);
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.075, 8, 6), this.eyeMat);
eye.position.set(side * 0.12, 1.08, 0.48);
this.model.add(eye);
}
// Glühender Amulett-Kern
// Amulett + Runen auf der Robe (glühen, färben sich im Enrage)
const amulet = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 6), this.glowMat);
amulet.position.set(0, 0.62, 0.52);
amulet.position.set(0, 0.6, 0.5);
this.model.add(amulet);
const runeSpots: [number, number, number][] = [
[0, -0.15, 0.5],
[-0.2, -0.5, 0.5],
[0.2, -0.5, 0.5],
];
for (const [x, y, z] of runeSpots) {
const rune = new THREE.Mesh(new THREE.OctahedronGeometry(0.06), this.glowMat);
rune.scale.set(1, 1.7, 0.5);
rune.position.set(x, y, z);
this.model.add(rune);
}
// Arm mit Stab
const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.12, 0.6, 8), this.darkMat);
arm.position.set(0.55, 0.7, 0.15);
arm.rotation.z = -0.6;
this.registerFadeMesh(arm);
this.model.add(arm);
// Rechter Arm: Ärmel zum Stab
const sleeveR = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.16, 0.55, 8), this.robeMat);
sleeveR.position.set(0.65, 0.84, 0.14);
sleeveR.rotation.z = 1.2;
this.registerFadeMesh(sleeveR);
this.model.add(sleeveR);
const handR = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 6), boneMat);
handR.position.set(0.85, 0.9, 0.23);
registerBone(handR);
this.model.add(handR);
const staff = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 2.0, 8), this.darkMat);
staff.position.set(0.85, 1.3, 0.25);
staff.rotation.z = 0.35;
staff.rotation.x = 0.2;
// Linker Arm: Ärmel nach vorn (Beschwörungs-Geste) + Energiekugel
const sleeveL = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.18, 0.6, 8), this.robeMat);
sleeveL.position.set(-0.55, 0.75, 0.32);
sleeveL.rotation.x = -1.25;
sleeveL.rotation.z = -0.35;
this.registerFadeMesh(sleeveL);
this.model.add(sleeveL);
const handL = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 6), boneMat);
handL.position.set(-0.68, 0.72, 0.62);
registerBone(handL);
this.model.add(handL);
const castOrb = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 6), this.glowMat);
castOrb.position.set(-0.7, 0.92, 0.68);
this.model.add(castOrb);
// Stab mit Manschetten, Totenkopf und schwebendem Orb
const staff = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 2.4, 8), this.darkMat);
staff.position.set(0.8, 1.2, 0.28);
staff.rotation.z = 0.18;
staff.rotation.x = 0.15;
this.registerFadeMesh(staff);
this.model.add(staff);
for (const [x, y, z] of [[0.88, 0.76, 0.21], [0.96, 0.31, 0.15]] as const) {
const collarM = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.07, 8), this.darkMat);
collarM.position.set(x, y, z);
this.registerFadeMesh(collarM);
this.model.add(collarM);
}
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.15, 10, 8), boneMat);
skull.position.set(0.58, 2.42, 0.47);
registerBone(skull);
this.model.add(skull);
for (const side of [-1, 1]) {
const socket = new THREE.Mesh(new THREE.SphereGeometry(0.03, 6, 4), this.darkMat);
socket.position.set(0.58 + side * 0.05, 2.44, 0.6);
this.model.add(socket);
}
const orbMat = new THREE.MeshBasicMaterial({
color: 0xbb66ff,
@@ -162,8 +277,8 @@ export class Necromancer extends Enemy {
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.16, 10, 8), orbMat);
orb.position.set(1.2, 1.95, 0.35);
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.14, 10, 8), orbMat);
orb.position.set(0.58, 2.66, 0.47);
this.model.add(orb);
this.orbMat = orbMat;
this.orb = orb;
+162 -23
View File
@@ -33,40 +33,149 @@ export class Shaman extends Enemy {
this.robeMat = new THREE.MeshToonMaterial({ color: 0x33aa55, emissive: 0x0a1f10 });
this.darkMat = new THREE.MeshToonMaterial({ color: 0x1c5c2e });
const boneMat = new THREE.MeshToonMaterial({ color: 0xe8dcc0 });
const faceMat = new THREE.MeshBasicMaterial({ color: 0x06120a });
let boneFadeRegistered = false;
const registerBone = (mesh: THREE.Mesh) => {
// Knochen-Material einmal fürs Fade-Death registrieren (wird geteilt)
if (!boneFadeRegistered) {
this.registerFadeMesh(mesh);
boneFadeRegistered = true;
}
};
// Robe (Kegel)
const robe = new THREE.Mesh(new THREE.ConeGeometry(0.45, 1.1, 10, 1, true), this.robeMat);
robe.position.y = 0.5;
// --- Robe (Blickrichtung +Z, bis zum Boden) ---
const robeProfile = [
new THREE.Vector2(0.52, 0.02), // Saum
new THREE.Vector2(0.44, 0.35),
new THREE.Vector2(0.32, 0.7),
new THREE.Vector2(0.3, 0.95),
new THREE.Vector2(0.38, 1.1), // Schulteransatz
new THREE.Vector2(0.24, 1.25), // Hals
];
const robe = new THREE.Mesh(new THREE.LatheGeometry(robeProfile, 12), this.robeMat);
robe.castShadow = true;
this.registerFadeMesh(robe);
this.model.add(robe);
// Oberkörper
const body = new THREE.Mesh(new THREE.SphereGeometry(0.3, 12, 10), this.robeMat);
body.position.y = 1.0;
this.registerFadeMesh(body);
this.model.add(body);
// Zerfetzter Saum
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
const tatter = new THREE.Mesh(
new THREE.ConeGeometry(0.06, 0.28, 4),
i % 2 === 0 ? this.robeMat : this.darkMat
);
tatter.rotation.x = Math.PI;
tatter.position.set(Math.sin(a) * 0.48, -0.02, Math.cos(a) * 0.48);
this.registerFadeMesh(tatter);
this.model.add(tatter);
}
// Kopf mit Kapuze
const hood = new THREE.Mesh(new THREE.ConeGeometry(0.26, 0.5, 10, 1, true), this.darkMat);
hood.position.set(0, 1.4, 0.08);
// Gürtel mit hängenden Knochen-Amuletten
const belt = new THREE.Mesh(new THREE.TorusGeometry(0.33, 0.03, 6, 14), this.darkMat);
belt.rotation.x = Math.PI / 2;
belt.position.y = 0.75;
this.registerFadeMesh(belt);
this.model.add(belt);
for (const [x, z] of [[-0.14, 0.3], [0, 0.34], [0.14, 0.3]] as const) {
const charm = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.16, 5), boneMat);
charm.position.set(x, 0.62, z);
registerBone(charm);
this.model.add(charm);
}
// Brust + Fellkragen mit Zacken
const chest = new THREE.Mesh(new THREE.SphereGeometry(0.28, 12, 10), this.robeMat);
chest.position.y = 1.12;
chest.scale.set(1, 1.15, 1);
this.registerFadeMesh(chest);
this.model.add(chest);
const collar = new THREE.Mesh(new THREE.TorusGeometry(0.26, 0.09, 8, 14), this.darkMat);
collar.rotation.x = Math.PI / 2;
collar.scale.set(1, 1, 0.8);
collar.position.y = 1.28;
this.registerFadeMesh(collar);
this.model.add(collar);
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
const fur = new THREE.Mesh(new THREE.ConeGeometry(0.04, 0.16, 4), this.darkMat);
fur.position.set(Math.sin(a) * 0.28, 1.36, Math.cos(a) * 0.24);
fur.rotation.x = -0.4;
fur.rotation.y = a;
this.registerFadeMesh(fur);
this.model.add(fur);
}
// Knochenkette mit Zähnen auf der Brust
const necklace = new THREE.Mesh(new THREE.TorusGeometry(0.16, 0.02, 6, 12), this.darkMat);
necklace.rotation.x = Math.PI / 2 - 0.3;
necklace.position.set(0, 1.2, 0.2);
this.registerFadeMesh(necklace);
this.model.add(necklace);
for (const [x, z] of [[-0.08, 0.32], [0, 0.35], [0.08, 0.32]] as const) {
const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.03, 0.1, 4), boneMat);
tooth.rotation.x = Math.PI;
tooth.position.set(x, 1.1, z);
registerBone(tooth);
this.model.add(tooth);
}
// Kapuze mit Krempe, schwarzem Gesicht und grünen Augen (+Z)
const hood = new THREE.Mesh(new THREE.ConeGeometry(0.32, 0.6, 10, 1, true), this.darkMat);
hood.position.set(0, 1.55, 0.02);
hood.rotation.x = 0.2;
hood.castShadow = true;
this.registerFadeMesh(hood);
this.model.add(hood);
// Leuchtende Augen (+Z)
const hoodRim = new THREE.Mesh(new THREE.TorusGeometry(0.22, 0.04, 8, 14), this.darkMat);
hoodRim.rotation.x = -0.5;
hoodRim.position.set(0, 1.44, 0.22);
this.registerFadeMesh(hoodRim);
this.model.add(hoodRim);
const face = new THREE.Mesh(new THREE.SphereGeometry(0.2, 12, 10), faceMat);
face.scale.set(1, 1.1, 0.8);
face.position.set(0, 1.44, 0.16);
this.registerFadeMesh(face);
this.model.add(face);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x66ff88 });
for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 5), eyeMat);
eye.position.set(side * 0.11, 1.32, 0.26);
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.055, 6, 5), eyeMat);
eye.position.set(side * 0.08, 1.47, 0.35);
this.model.add(eye);
}
// Stab mit Glühmund
const staff = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 1.2, 6), this.darkMat);
staff.position.set(0.45, 0.8, 0.1);
staff.rotation.z = 0.35;
this.registerFadeMesh(staff);
this.model.add(staff);
// Federn an der Kapuze (hängen nach hinten)
for (const side of [-1, 1]) {
const feather = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.4, 4), boneMat);
feather.rotation.x = Math.PI + side * 0.15;
feather.scale.set(1, 1, 0.3);
feather.position.set(side * 0.14, 1.35, -0.22);
registerBone(feather);
this.model.add(feather);
}
// Rechter Arm zum Stab
const sleeveR = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.12, 0.45, 8), this.robeMat);
sleeveR.position.set(0.32, 1.05, 0.1);
sleeveR.rotation.z = 1.0;
this.registerFadeMesh(sleeveR);
this.model.add(sleeveR);
const handR = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), boneMat);
handR.position.set(0.45, 0.95, 0.18);
registerBone(handR);
this.model.add(handR);
// Linker Arm erhoben mit kleiner Energiekugel
const sleeveL = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.12, 0.45, 8), this.robeMat);
sleeveL.position.set(-0.34, 1.12, 0.18);
sleeveL.rotation.x = -1.1;
sleeveL.rotation.z = -0.5;
this.registerFadeMesh(sleeveL);
this.model.add(sleeveL);
const handL = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), boneMat);
handL.position.set(-0.4, 1.22, 0.34);
registerBone(handL);
this.model.add(handL);
const orbMat = new THREE.MeshBasicMaterial({
color: 0x66ff88,
@@ -75,9 +184,39 @@ export class Shaman extends Enemy {
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 6), orbMat);
orb.position.set(0.62, 1.45, 0.12);
const castOrb = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), orbMat);
castOrb.position.set(-0.42, 1.38, 0.38);
this.model.add(castOrb);
// Totem-Stab: Schädel mit Klauenkrone, Glüh-Orb, hängender Anhänger
const staff = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.04, 1.7, 6), this.darkMat);
staff.position.set(0.5, 0.85, 0.2);
staff.rotation.z = 0.15;
staff.rotation.x = 0.1;
this.registerFadeMesh(staff);
this.model.add(staff);
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.11, 10, 8), boneMat);
skull.position.set(0.37, 1.72, 0.29);
registerBone(skull);
this.model.add(skull);
for (const side of [-1, 1]) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.03, 0.22, 5), boneMat);
claw.position.set(0.37 + side * 0.1, 1.84, 0.29);
claw.rotation.z = side * -0.5;
registerBone(claw);
this.model.add(claw);
const socket = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 4), this.darkMat);
socket.position.set(0.37 + side * 0.04, 1.74, 0.39);
this.model.add(socket);
}
const orb = new THREE.Mesh(new THREE.SphereGeometry(0.09, 8, 6), orbMat);
orb.position.set(0.37, 1.94, 0.29);
this.model.add(orb);
const dangle = new THREE.Mesh(new THREE.CylinderGeometry(0.015, 0.015, 0.14, 5), boneMat);
dangle.position.set(0.44, 1.58, 0.27);
dangle.rotation.z = 0.4;
registerBone(dangle);
this.model.add(dangle);
// Heil-Aura-Ring am Boden
this.healRingMat = new THREE.MeshBasicMaterial({