diff --git a/web/src/Dragon.ts b/web/src/Dragon.ts index d62de39..227435b 100644 --- a/web/src/Dragon.ts +++ b/web/src/Dragon.ts @@ -66,6 +66,7 @@ export class Dragon extends Enemy { private wingL!: THREE.Group; private wingR!: THREE.Group; private jaw!: THREE.Group; + private tailGroup!: THREE.Group; private breathGroup!: THREE.Group; private breathMat!: THREE.MeshBasicMaterial; private telegraphArc!: THREE.Mesh; @@ -108,131 +109,316 @@ export class Dragon extends Enemy { this.skinMat = new THREE.MeshToonMaterial({ color: 0xbb4433 }); this.darkMat = new THREE.MeshToonMaterial({ color: 0x772a1e }); 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 }); + 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) --- - // 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); - torso.scale.set(1, 0.85, 1.7); - torso.position.y = 1.4; + torso.scale.set(1, 0.85, 1.5); + torso.position.set(0, 1.5, -0.1); torso.castShadow = true; this.registerFadeMesh(torso); this.model.add(torso); - // Bauch (hell, unten) - const belly = new THREE.Mesh(new THREE.SphereGeometry(0.75, 16, 10), this.bellyMat); - belly.scale.set(1, 0.55, 1.6); - belly.position.set(0, 1.05, 0.1); - this.registerFadeMesh(belly); - this.model.add(belly); + const hips = new THREE.Mesh(new THREE.SphereGeometry(0.85, 18, 12), this.skinMat); + hips.scale.set(1.0, 0.9, 1.1); + hips.position.set(0, 1.45, -0.85); + hips.castShadow = true; + this.registerFadeMesh(hips); + this.model.add(hips); - // Schwanz (-Z, verjüngt) + // Bauchplatten (hell, segmentiert) for (let i = 0; i < 5; i++) { - const seg = new THREE.Mesh( - new THREE.SphereGeometry(0.5 - i * 0.08, 10, 8), - i % 2 === 0 ? this.skinMat : this.darkMat + const plate = new THREE.Mesh( + new THREE.SphereGeometry(0.58 - i * 0.06, 14, 8), + this.bellyMat ); - seg.position.set(0, 1.4 - i * 0.06, -1.7 - i * 0.6); - this.registerFadeMesh(seg); - this.model.add(seg); + plate.scale.set(0.95, 0.5, 0.8); + plate.position.set(0, 1.02 - i * 0.02, 1.15 - i * 0.62); + this.registerFadeMesh(plate); + this.model.add(plate); } - // Schwanzspitze (Pfeil) - const tailTip = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.8, 6), this.darkMat); + // Schwanz (geschwungen, mit Zacken und Spadel-Spitze) + 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.position.set(0, 1.1, -4.8); + tailTip.scale.x = 0.4; + tailTip.position.set(0, -0.35, -3.3); this.registerFadeMesh(tailTip); - this.model.add(tailTip); + this.tailGroup.add(tailTip); - // Kopf (+Z) - const head = new THREE.Mesh(new THREE.SphereGeometry(0.7, 16, 12), this.skinMat); - head.scale.set(1, 0.9, 1.2); - head.position.set(0, 2.0, 1.7); - head.castShadow = true; - this.registerFadeMesh(head); - this.model.add(head); + // Hals (S-Kurve nach oben) + for (const [y, z, r] of [[1.95, 1.3, 0.46], [2.2, 1.55, 0.4]]) { + const neck = new THREE.Mesh(new THREE.SphereGeometry(r, 14, 10), this.skinMat); + neck.scale.set(0.85, 0.95, 1.1); + neck.position.set(0, y, z); + this.registerFadeMesh(neck); + 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) - const snout = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.35, 0.6), this.skinMat); - snout.position.set(0, 1.85, 2.5); + const snout = new THREE.Mesh(new THREE.BoxGeometry(0.44, 0.32, 0.7), this.skinMat); + snout.position.set(0, -0.1, 0.72); this.registerFadeMesh(snout); - this.model.add(snout); + headGroup.add(snout); - // Kiefer (beweglich, öffnet sich beim Feuerspeer) - this.jaw = new THREE.Group(); - const jawMesh = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.22, 0.55), this.darkMat); - jawMesh.position.set(0, -0.1, 0.22); - this.registerFadeMesh(jawMesh); - this.jaw.add(jawMesh); - this.jaw.position.set(0, 1.75, 2.4); - this.jaw.rotation.x = 0.15; - this.model.add(this.jaw); + // Stirnleiste (grimmiger Blick) + const brow = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.14, 0.26), this.darkMat); + brow.position.set(0, 0.3, 0.36); + brow.rotation.x = 0.12; + this.registerFadeMesh(brow); + headGroup.add(brow); // Augen (+Z, ragen aus dem Kopf heraus) for (const side of [-1, 1]) { - const eye = new THREE.Mesh(new THREE.SphereGeometry(0.13, 8, 6), this.eyeMat); - eye.position.set(side * 0.32, 2.3, 2.42); - this.model.add(eye); + const eye = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 6), this.eyeMat); + eye.position.set(side * 0.3, 0.2, 0.48); + headGroup.add(eye); } - // Hörner + // Hörner (nach hinten geschwungen, zwei Paare) for (const side of [-1, 1]) { - const horn = new THREE.Mesh(new THREE.ConeGeometry(0.09, 0.6, 6), this.darkMat); - horn.position.set(side * 0.28, 2.6, 1.4); - horn.rotation.set(-0.9, 0, side * 0.5); + const horn = new THREE.Mesh(new THREE.ConeGeometry(0.09, 0.75, 6), this.darkMat); + horn.position.set(side * 0.28, 0.42, -0.12); + horn.rotation.set(-1.15, 0, side * 0.35); 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 - 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) + // Ohr-Fransen (seitlich nach hinten) for (const side of [-1, 1]) { - for (const back of [true, false]) { - const leg = new THREE.Mesh( - new THREE.CylinderGeometry(0.16, 0.22, 0.7, 8), - this.darkMat - ); - leg.position.set(side * 0.65, 0.35, back ? -0.8 : 0.7); - this.registerFadeMesh(leg); - this.model.add(leg); - const claw = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.14, 0.5), this.skinMat); - claw.position.set(side * 0.65, 0.08, back ? -0.8 : 0.7); - this.registerFadeMesh(claw); + const frill = new THREE.Mesh(new THREE.ConeGeometry(0.12, 0.45, 5), this.darkMat); + frill.position.set(side * 0.48, 0.1, -0.18); + frill.rotation.set(0.35, 0, side * -1.75); + frill.scale.set(1, 1, 0.5); + this.registerFadeMesh(frill); + headGroup.add(frill); + } + + // Nüstern + for (const side of [-1, 1]) { + 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); } } - // --- Flügel (animiertes Flattern) --- - const wingGeom = new THREE.BoxGeometry(0.1, 2.4, 1.5); - this.wingL = new THREE.Group(); - this.wingL.position.set(-0.7, 2.2, -0.2); - const wingLmesh = new THREE.Mesh(wingGeom, this.wingMat); - wingLmesh.position.set(-1.6, 0, 0); - wingLmesh.rotation.z = Math.PI / 2; - wingLmesh.castShadow = true; - this.registerFadeMesh(wingLmesh); - this.wingL.add(wingLmesh); - this.model.add(this.wingL); + // --- Flügel (Fledermaus-Silhouette mit Membran und Fingerknochen) --- + const wingShape = new THREE.Shape(); + wingShape.moveTo(0.2, 0.1); + wingShape.quadraticCurveTo(0.8, -0.12, 1.5, 0.0); // Vorderkante zum Handgelenk + wingShape.quadraticCurveTo(2.5, 0.0, 3.05, 0.95); // zur Flügelspitze + // Hinterkante: Fingerspitzen mit konkaven Bögen dazwischen + wingShape.quadraticCurveTo(2.2, 1.0, 2.7, 1.65); + wingShape.quadraticCurveTo(1.7, 1.45, 2.0, 2.1); + wingShape.quadraticCurveTo(1.05, 1.6, 1.1, 2.25); + wingShape.quadraticCurveTo(0.4, 1.2, 0.15, 1.5); + wingShape.closePath(); + const wingGeom = new THREE.ShapeGeometry(wingShape, 10); - this.wingR = new THREE.Group(); - this.wingR.position.set(0.7, 2.2, -0.2); - const wingRmesh = new THREE.Mesh(wingGeom, this.wingMat); - wingRmesh.position.set(1.6, 0, 0); - wingRmesh.rotation.z = -Math.PI / 2; - wingRmesh.castShadow = true; - this.registerFadeMesh(wingRmesh); - this.wingR.add(wingRmesh); + const buildWing = (): THREE.Group => { + const wing = new THREE.Group(); + const membrane = new THREE.Mesh(wingGeom, this.wingMat); + membrane.rotation.x = -Math.PI / 2; + membrane.castShadow = true; + this.registerFadeMesh(membrane); + wing.add(membrane); + + // 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.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 --- // 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); flame.rotation.x = Math.PI / 2; flame.position.z = BREATH_RANGE / 2; - flame.position.y = 1.2; 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.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); this.wingL.rotation.z = flap; this.wingR.rotation.z = -flap; - // Flügel-Einstellwinkel - this.wingL.rotation.x = -0.3; + // Flügel-Einstellwinkel (wegen scale.x-Spiegelung auf beiden Seiten gleich) + this.wingL.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 this.shadow.position.y = 0.06 - this.body.position.y; const shadowScale = 0.6 + this.body.position.y * 0.15; @@ -559,7 +752,7 @@ export class Dragon extends Enemy { this.telegraphArc.visible = false; this.breathGroup.visible = true; this.breathActive = true; - this.jaw.rotation.x = -0.5; + this.jaw.rotation.x = 0.55; } } else if (this.breathPhase === 'active') { this.breathTime -= dt; @@ -589,7 +782,7 @@ export class Dragon extends Enemy { this.breathPhase = 'done'; this.breathGroup.visible = false; this.breathMat.opacity = 0; - this.jaw.rotation.x = 0.15; + this.jaw.rotation.x = 0.12; } }