Rework dragon model with bat wings, detailed head and curved tail

This commit is contained in:
2026-08-20 18:12:54 +02:00
parent 1f311d3208
commit d1ba3fc9c3
+285 -92
View File
@@ -66,6 +66,7 @@ export class Dragon extends Enemy {
private wingL!: THREE.Group; private wingL!: THREE.Group;
private wingR!: THREE.Group; private wingR!: THREE.Group;
private jaw!: THREE.Group; private jaw!: THREE.Group;
private tailGroup!: THREE.Group;
private breathGroup!: THREE.Group; private breathGroup!: THREE.Group;
private breathMat!: THREE.MeshBasicMaterial; private breathMat!: THREE.MeshBasicMaterial;
private telegraphArc!: THREE.Mesh; private telegraphArc!: THREE.Mesh;
@@ -108,131 +109,316 @@ export class Dragon extends Enemy {
this.skinMat = new THREE.MeshToonMaterial({ color: 0xbb4433 }); this.skinMat = new THREE.MeshToonMaterial({ color: 0xbb4433 });
this.darkMat = new THREE.MeshToonMaterial({ color: 0x772a1e }); this.darkMat = new THREE.MeshToonMaterial({ color: 0x772a1e });
this.bellyMat = new THREE.MeshToonMaterial({ color: 0xe8c890 }); this.bellyMat = new THREE.MeshToonMaterial({ color: 0xe8c890 });
this.wingMat = new THREE.MeshToonMaterial({ color: 0x993322 }); this.wingMat = new THREE.MeshToonMaterial({ color: 0x993322, side: THREE.DoubleSide });
this.eyeMat = new THREE.MeshBasicMaterial({ color: 0xffcc33 }); this.eyeMat = new THREE.MeshBasicMaterial({ color: 0xffcc33 });
const boneMat = new THREE.MeshToonMaterial({ color: 0xf0e6cf });
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;
}
};
// Hilfspunkt: Strebe/Knochen zwischen zwei Punkten
const strut = (
a: THREE.Vector3, b: THREE.Vector3, r1: number, r2: number
): THREE.Mesh => {
const dir = new THREE.Vector3().subVectors(b, a);
const len = dir.length();
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(r2, r1, len, 6), this.darkMat);
mesh.position.copy(a).addScaledVector(dir, 0.5);
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize());
return mesh;
};
// --- Körper (Blickrichtung +Z) --- // --- Körper (Blickrichtung +Z) ---
// Rumpf: gestreckte Kugel // Brust, Rumpfmitte, Hüfte (überlappende Kugeln für organische Silhouette)
const chest = new THREE.Mesh(new THREE.SphereGeometry(0.8, 18, 12), this.skinMat);
chest.scale.set(0.95, 0.92, 1.05);
chest.position.set(0, 1.4, 0.85);
chest.castShadow = true;
this.registerFadeMesh(chest);
this.model.add(chest);
const torso = new THREE.Mesh(new THREE.SphereGeometry(1.0, 20, 14), this.skinMat); const torso = new THREE.Mesh(new THREE.SphereGeometry(1.0, 20, 14), this.skinMat);
torso.scale.set(1, 0.85, 1.7); torso.scale.set(1, 0.85, 1.5);
torso.position.y = 1.4; torso.position.set(0, 1.5, -0.1);
torso.castShadow = true; torso.castShadow = true;
this.registerFadeMesh(torso); this.registerFadeMesh(torso);
this.model.add(torso); this.model.add(torso);
// Bauch (hell, unten) const hips = new THREE.Mesh(new THREE.SphereGeometry(0.85, 18, 12), this.skinMat);
const belly = new THREE.Mesh(new THREE.SphereGeometry(0.75, 16, 10), this.bellyMat); hips.scale.set(1.0, 0.9, 1.1);
belly.scale.set(1, 0.55, 1.6); hips.position.set(0, 1.45, -0.85);
belly.position.set(0, 1.05, 0.1); hips.castShadow = true;
this.registerFadeMesh(belly); this.registerFadeMesh(hips);
this.model.add(belly); this.model.add(hips);
// Schwanz (-Z, verjüngt) // Bauchplatten (hell, segmentiert)
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const seg = new THREE.Mesh( const plate = new THREE.Mesh(
new THREE.SphereGeometry(0.5 - i * 0.08, 10, 8), new THREE.SphereGeometry(0.58 - i * 0.06, 14, 8),
i % 2 === 0 ? this.skinMat : this.darkMat this.bellyMat
); );
seg.position.set(0, 1.4 - i * 0.06, -1.7 - i * 0.6); plate.scale.set(0.95, 0.5, 0.8);
this.registerFadeMesh(seg); plate.position.set(0, 1.02 - i * 0.02, 1.15 - i * 0.62);
this.model.add(seg); this.registerFadeMesh(plate);
this.model.add(plate);
} }
// Schwanzspitze (Pfeil) // Schwanz (geschwungen, mit Zacken und Spadel-Spitze)
const tailTip = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.8, 6), this.darkMat); this.tailGroup = new THREE.Group();
this.tailGroup.position.set(0, 1.35, -1.5);
this.model.add(this.tailGroup);
for (let i = 0; i < 7; i++) {
const radius = 0.46 - i * 0.055;
const seg = new THREE.Mesh(
new THREE.SphereGeometry(radius, 12, 8),
i % 2 === 0 ? this.skinMat : this.darkMat
);
const y = -0.13 * i + Math.max(0, i - 3) * 0.18;
seg.position.set(0, y, -0.15 - i * 0.45);
seg.scale.set(1, 0.92, 1.15);
this.registerFadeMesh(seg);
this.tailGroup.add(seg);
if (i % 2 === 0 && i < 5) {
const spike = new THREE.Mesh(
new THREE.ConeGeometry(0.09 - i * 0.012, 0.32 - i * 0.03, 5),
this.darkMat
);
spike.position.set(0, y + radius * 0.9 + 0.1, -0.15 - i * 0.45);
spike.rotation.x = -0.3;
this.registerFadeMesh(spike);
this.tailGroup.add(spike);
}
}
const tailTip = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.9, 4), this.darkMat);
tailTip.rotation.x = -Math.PI / 2; tailTip.rotation.x = -Math.PI / 2;
tailTip.position.set(0, 1.1, -4.8); tailTip.scale.x = 0.4;
tailTip.position.set(0, -0.35, -3.3);
this.registerFadeMesh(tailTip); this.registerFadeMesh(tailTip);
this.model.add(tailTip); this.tailGroup.add(tailTip);
// Kopf (+Z) // Hals (S-Kurve nach oben)
const head = new THREE.Mesh(new THREE.SphereGeometry(0.7, 16, 12), this.skinMat); for (const [y, z, r] of [[1.95, 1.3, 0.46], [2.2, 1.55, 0.4]]) {
head.scale.set(1, 0.9, 1.2); const neck = new THREE.Mesh(new THREE.SphereGeometry(r, 14, 10), this.skinMat);
head.position.set(0, 2.0, 1.7); neck.scale.set(0.85, 0.95, 1.1);
head.castShadow = true; neck.position.set(0, y, z);
this.registerFadeMesh(head); this.registerFadeMesh(neck);
this.model.add(head); this.model.add(neck);
}
// Kopf-Gruppe (+Z)
const headGroup = new THREE.Group();
headGroup.position.set(0, 2.4, 1.8);
headGroup.rotation.x = 0.06;
this.model.add(headGroup);
const skull = new THREE.Mesh(new THREE.SphereGeometry(0.55, 16, 12), this.skinMat);
skull.scale.set(0.95, 0.9, 1.15);
skull.castShadow = true;
this.registerFadeMesh(skull);
headGroup.add(skull);
// Schnauze (+Z) // Schnauze (+Z)
const snout = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.35, 0.6), this.skinMat); const snout = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.32, 0.7), this.skinMat);
snout.position.set(0, 1.85, 2.5); snout.position.set(0, -0.1, 0.72);
this.registerFadeMesh(snout); this.registerFadeMesh(snout);
this.model.add(snout); headGroup.add(snout);
// Kiefer (beweglich, öffnet sich beim Feuerspeer) // Stirnleiste (grimmiger Blick)
this.jaw = new THREE.Group(); const brow = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.14, 0.26), this.darkMat);
const jawMesh = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.22, 0.55), this.darkMat); brow.position.set(0, 0.3, 0.36);
jawMesh.position.set(0, -0.1, 0.22); brow.rotation.x = 0.12;
this.registerFadeMesh(jawMesh); this.registerFadeMesh(brow);
this.jaw.add(jawMesh); headGroup.add(brow);
this.jaw.position.set(0, 1.75, 2.4);
this.jaw.rotation.x = 0.15;
this.model.add(this.jaw);
// Augen (+Z, ragen aus dem Kopf heraus) // Augen (+Z, ragen aus dem Kopf heraus)
for (const side of [-1, 1]) { for (const side of [-1, 1]) {
const eye = new THREE.Mesh(new THREE.SphereGeometry(0.13, 8, 6), this.eyeMat); const eye = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 6), this.eyeMat);
eye.position.set(side * 0.32, 2.3, 2.42); eye.position.set(side * 0.3, 0.2, 0.48);
this.model.add(eye); headGroup.add(eye);
} }
// Hörner // Hörner (nach hinten geschwungen, zwei Paare)
for (const side of [-1, 1]) { for (const side of [-1, 1]) {
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.09, 0.6, 6), this.darkMat); const horn = new THREE.Mesh(new THREE.ConeGeometry(0.09, 0.75, 6), this.darkMat);
horn.position.set(side * 0.28, 2.6, 1.4); horn.position.set(side * 0.28, 0.42, -0.12);
horn.rotation.set(-0.9, 0, side * 0.5); horn.rotation.set(-1.15, 0, side * 0.35);
this.registerFadeMesh(horn); this.registerFadeMesh(horn);
this.model.add(horn); headGroup.add(horn);
const horn2 = new THREE.Mesh(new THREE.ConeGeometry(0.06, 0.4, 6), this.darkMat);
horn2.position.set(side * 0.17, 0.3, -0.34);
horn2.rotation.set(-1.3, 0, side * 0.25);
this.registerFadeMesh(horn2);
headGroup.add(horn2);
} }
// Rückenstacheln // Ohr-Fransen (seitlich nach hinten)
for (let i = 0; i < 4; i++) {
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.14, 0.5, 5), this.darkMat);
spike.position.set(0, 1.9, 0.8 - i * 0.9);
this.registerFadeMesh(spike);
this.model.add(spike);
}
// Beine (4, unter dem Rumpf)
for (const side of [-1, 1]) { for (const side of [-1, 1]) {
for (const back of [true, false]) { const frill = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.45, 5), this.darkMat);
const leg = new THREE.Mesh( frill.position.set(side * 0.48, 0.1, -0.18);
new THREE.CylinderGeometry(0.16, 0.22, 0.7, 8), frill.rotation.set(0.35, 0, side * -1.75);
this.darkMat frill.scale.set(1, 1, 0.5);
); this.registerFadeMesh(frill);
leg.position.set(side * 0.65, 0.35, back ? -0.8 : 0.7); headGroup.add(frill);
this.registerFadeMesh(leg); }
this.model.add(leg);
const claw = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.14, 0.5), this.skinMat); // Nüstern
claw.position.set(side * 0.65, 0.08, back ? -0.8 : 0.7); for (const side of [-1, 1]) {
this.registerFadeMesh(claw); const nostril = new THREE.Mesh(new THREE.SphereGeometry(0.04, 6, 4), this.darkMat);
nostril.position.set(side * 0.1, 0.06, 1.0);
headGroup.add(nostril);
}
// Zähne im Oberkiefer (zeigen nach unten)
for (const side of [-1, 1]) {
for (const z of [0.72, 0.88, 1.02]) {
const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.032, 0.11, 5), boneMat);
tooth.rotation.x = Math.PI;
tooth.position.set(side * 0.15, -0.3, z);
registerBone(tooth);
headGroup.add(tooth);
}
}
// Kiefer (beweglich, öffnet sich beim Feuerspeer)
this.jaw = new THREE.Group();
this.jaw.position.set(0, -0.28, 0.28);
const jawMesh = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.16, 0.62), this.darkMat);
jawMesh.position.set(0, -0.05, 0.3);
this.registerFadeMesh(jawMesh);
this.jaw.add(jawMesh);
for (const side of [-1, 1]) {
for (const z of [0.42, 0.56]) {
const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.028, 0.09, 5), boneMat);
tooth.position.set(side * 0.11, 0.06, z);
registerBone(tooth);
this.jaw.add(tooth);
}
}
this.jaw.rotation.x = 0.12;
headGroup.add(this.jaw);
// Rückenplatten (vom Nacken bis in den Schwanz)
const backPlates: [number, number, number, number][] = [
[2.25, 0.95, 0.15, 0.48],
[2.38, 0.35, 0.19, 0.6],
[2.4, -0.25, 0.2, 0.64],
[2.32, -0.85, 0.17, 0.52],
[1.9, -1.5, 0.14, 0.44],
[1.62, -2.4, 0.12, 0.38],
[1.45, -3.3, 0.1, 0.32],
];
for (const [y, z, r, h] of backPlates) {
const plate = new THREE.Mesh(new THREE.ConeGeometry(r, h, 5), this.darkMat);
plate.position.set(0, y, z);
plate.rotation.x = -0.22;
this.registerFadeMesh(plate);
this.model.add(plate);
}
// Beine (Hinterbeine kräftiger, Vorderbeine schlanker)
for (const side of [-1, 1]) {
// Hinterbein
const thigh = new THREE.Mesh(new THREE.SphereGeometry(0.5, 12, 10), this.skinMat);
thigh.scale.set(0.75, 1.1, 0.95);
thigh.position.set(side * 0.72, 1.0, -0.8);
this.registerFadeMesh(thigh);
this.model.add(thigh);
const shin = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.19, 0.75, 8), this.darkMat);
shin.position.set(side * 0.8, 0.45, -0.9);
shin.rotation.x = 0.12;
this.registerFadeMesh(shin);
this.model.add(shin);
const hindFoot = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.16, 0.6), this.skinMat);
hindFoot.position.set(side * 0.8, 0.09, -0.78);
this.registerFadeMesh(hindFoot);
this.model.add(hindFoot);
for (const dx of [-0.14, 0, 0.14]) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.2, 5), boneMat);
claw.rotation.x = Math.PI / 2;
claw.position.set(side * 0.8 + dx, 0.09, -0.44);
registerBone(claw);
this.model.add(claw);
}
// Vorderbein
const upperArm = new THREE.Mesh(new THREE.SphereGeometry(0.3, 12, 8), this.skinMat);
upperArm.scale.set(0.8, 1.15, 0.9);
upperArm.position.set(side * 0.62, 1.05, 0.8);
this.registerFadeMesh(upperArm);
this.model.add(upperArm);
const foreArm = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.15, 0.7, 8), this.darkMat);
foreArm.position.set(side * 0.6, 0.5, 0.85);
this.registerFadeMesh(foreArm);
this.model.add(foreArm);
const foreFoot = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.14, 0.5), this.skinMat);
foreFoot.position.set(side * 0.6, 0.08, 0.95);
this.registerFadeMesh(foreFoot);
this.model.add(foreFoot);
for (const dx of [-0.11, 0, 0.11]) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.18, 5), boneMat);
claw.rotation.x = Math.PI / 2;
claw.position.set(side * 0.6 + dx, 0.08, 1.24);
registerBone(claw);
this.model.add(claw); this.model.add(claw);
} }
} }
// --- Flügel (animiertes Flattern) --- // --- Flügel (Fledermaus-Silhouette mit Membran und Fingerknochen) ---
const wingGeom = new THREE.BoxGeometry(0.1, 2.4, 1.5); const wingShape = new THREE.Shape();
this.wingL = new THREE.Group(); wingShape.moveTo(0.2, 0.1);
this.wingL.position.set(-0.7, 2.2, -0.2); wingShape.quadraticCurveTo(0.8, -0.12, 1.5, 0.0); // Vorderkante zum Handgelenk
const wingLmesh = new THREE.Mesh(wingGeom, this.wingMat); wingShape.quadraticCurveTo(2.5, 0.0, 3.05, 0.95); // zur Flügelspitze
wingLmesh.position.set(-1.6, 0, 0); // Hinterkante: Fingerspitzen mit konkaven Bögen dazwischen
wingLmesh.rotation.z = Math.PI / 2; wingShape.quadraticCurveTo(2.2, 1.0, 2.7, 1.65);
wingLmesh.castShadow = true; wingShape.quadraticCurveTo(1.7, 1.45, 2.0, 2.1);
this.registerFadeMesh(wingLmesh); wingShape.quadraticCurveTo(1.05, 1.6, 1.1, 2.25);
this.wingL.add(wingLmesh); wingShape.quadraticCurveTo(0.4, 1.2, 0.15, 1.5);
this.model.add(this.wingL); wingShape.closePath();
const wingGeom = new THREE.ShapeGeometry(wingShape, 10);
this.wingR = new THREE.Group(); const buildWing = (): THREE.Group => {
this.wingR.position.set(0.7, 2.2, -0.2); const wing = new THREE.Group();
const wingRmesh = new THREE.Mesh(wingGeom, this.wingMat); const membrane = new THREE.Mesh(wingGeom, this.wingMat);
wingRmesh.position.set(1.6, 0, 0); membrane.rotation.x = -Math.PI / 2;
wingRmesh.rotation.z = -Math.PI / 2; membrane.castShadow = true;
wingRmesh.castShadow = true; this.registerFadeMesh(membrane);
this.registerFadeMesh(wingRmesh); wing.add(membrane);
this.wingR.add(wingRmesh);
// Flügelknochen: Arm, Außenkante, ein Fingerstrahl
const bones = [
strut(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1.5, 0.06, 0), 0.07, 0.05),
strut(new THREE.Vector3(1.5, 0.06, 0), new THREE.Vector3(3.05, 0.1, -0.95), 0.05, 0.028),
strut(new THREE.Vector3(1.3, 0.03, -0.25), new THREE.Vector3(2.0, 0.05, -2.1), 0.04, 0.02),
];
for (const bone of bones) {
this.registerFadeMesh(bone);
wing.add(bone);
}
// Daumenkralle am Handgelenk
const thumb = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.22, 5), boneMat);
thumb.rotation.x = Math.PI / 2;
thumb.position.set(1.55, 0.06, 0.14);
registerBone(thumb);
wing.add(thumb);
return wing;
};
this.wingR = buildWing();
this.wingR.position.set(0.7, 2.25, -0.2);
this.model.add(this.wingR); this.model.add(this.wingR);
this.wingL = buildWing();
this.wingL.position.set(-0.7, 2.25, -0.2);
this.wingL.scale.x = -1; // spiegeln für linke Seite
this.model.add(this.wingL);
// --- Effekte --- // --- Effekte ---
// Boden-Schatten (folgt dem Drachen) // Boden-Schatten (folgt dem Drachen)
@@ -289,8 +475,10 @@ export class Dragon extends Enemy {
const flame = new THREE.Mesh(new THREE.ConeGeometry(1.8, BREATH_RANGE, 16, 1, true), this.breathMat); const flame = new THREE.Mesh(new THREE.ConeGeometry(1.8, BREATH_RANGE, 16, 1, true), this.breathMat);
flame.rotation.x = Math.PI / 2; flame.rotation.x = Math.PI / 2;
flame.position.z = BREATH_RANGE / 2; flame.position.z = BREATH_RANGE / 2;
flame.position.y = 1.2;
this.breathGroup.add(flame); this.breathGroup.add(flame);
// auf Maulhöhe und leicht nach unten geneigt
this.breathGroup.position.y = 3.3;
this.breathGroup.rotation.x = 0.3;
this.breathGroup.visible = false; this.breathGroup.visible = false;
this.body.add(this.breathGroup); this.body.add(this.breathGroup);
} }
@@ -332,10 +520,15 @@ export class Dragon extends Enemy {
const flap = Math.sin(this.waveTime * flapSpeed) * (this.state === 'fly' ? 0.7 : 0.2); const flap = Math.sin(this.waveTime * flapSpeed) * (this.state === 'fly' ? 0.7 : 0.2);
this.wingL.rotation.z = flap; this.wingL.rotation.z = flap;
this.wingR.rotation.z = -flap; this.wingR.rotation.z = -flap;
// Flügel-Einstellwinkel // Flügel-Einstellwinkel (wegen scale.x-Spiegelung auf beiden Seiten gleich)
this.wingL.rotation.x = -0.3; this.wingL.rotation.x = 0.3;
this.wingR.rotation.x = 0.3; this.wingR.rotation.x = 0.3;
// Schwebe-Wippen im Flug und sanftes Schwanz-Schwenken
const airborne = this.state === 'fly' || this.state === 'takeoff';
this.model.position.y = airborne ? Math.sin(this.waveTime * 2.3) * 0.15 : 0;
this.tailGroup.rotation.y = Math.sin(this.waveTime * 1.7) * 0.12;
// Schatten bleibt am Boden // Schatten bleibt am Boden
this.shadow.position.y = 0.06 - this.body.position.y; this.shadow.position.y = 0.06 - this.body.position.y;
const shadowScale = 0.6 + this.body.position.y * 0.15; const shadowScale = 0.6 + this.body.position.y * 0.15;
@@ -559,7 +752,7 @@ export class Dragon extends Enemy {
this.telegraphArc.visible = false; this.telegraphArc.visible = false;
this.breathGroup.visible = true; this.breathGroup.visible = true;
this.breathActive = true; this.breathActive = true;
this.jaw.rotation.x = -0.5; this.jaw.rotation.x = 0.55;
} }
} else if (this.breathPhase === 'active') { } else if (this.breathPhase === 'active') {
this.breathTime -= dt; this.breathTime -= dt;
@@ -589,7 +782,7 @@ export class Dragon extends Enemy {
this.breathPhase = 'done'; this.breathPhase = 'done';
this.breathGroup.visible = false; this.breathGroup.visible = false;
this.breathMat.opacity = 0; this.breathMat.opacity = 0;
this.jaw.rotation.x = 0.15; this.jaw.rotation.x = 0.12;
} }
} }