feat: Figurenauswahl, Autos, Debug-Panel und Namensnennung
Task 11: Figuren (Mannequin/Superhero Male/Female) mit auf ual.glb retargeteten Animationen, Frisuren, vier SUVs auf angehefteten Chunks, ein lil-gui-Debug-Panel (Figur/Frisur/Höhe/Hitbox/Chunk-Grenzen/ Kartenbild/Nebel/Wahrzeichen-Sprung) und die verpflichtende Namensnennung fürs HUD (Kartenkacheln, Wahrzeichen, SUV, Texturen, Palme, Figuren — jede Quelle mit ihrer eigenen Lizenz). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
02d0f36580
commit
d30aae68a1
42 files changed
+13143
-4
No files matched your search
+122
-1
@@ -7,6 +7,10 @@ import { playerController } from "three-player-controller";
|
||||
import { ChunkStreamer } from "./world/streaming.js";
|
||||
import { createMaterials } from "./world/materials.js";
|
||||
import { PropLayer } from "./world/props.js";
|
||||
import { chunkForWorld, threeToWorld } from "./world/coords.js";
|
||||
import { createCharacterModule } from "./characters/index.js";
|
||||
import { createPanel } from "./ui/panel.js";
|
||||
import { buildAttribution, mountHud } from "./ui/hud.js";
|
||||
|
||||
const container = document.querySelector("#container");
|
||||
const scene = new Scene();
|
||||
@@ -31,6 +35,11 @@ scene.add(sun);
|
||||
// die dort erzeugten Vertexfarben (Pastellton pro Gebäude) über der Stuckputz-Textur.
|
||||
const materials = await createMaterials(renderer.capabilities.getMaxAnisotropy());
|
||||
|
||||
// Task 11: erster Laufzeit-Verbraucher von manifest.json (Task 6) — bisher las nur die
|
||||
// Extraktions-Pipeline es. Trägt die Namensnennung fürs HUD und die Chunk-Liste fürs
|
||||
// Kartenbild im Debug-Panel.
|
||||
const manifest = await fetch("./data/vice-a/manifest.json").then((r) => r.json());
|
||||
|
||||
// Himmel und Nebel über der Szene; sunPosition zeigt in dieselbe Richtung wie das Sonnenlicht.
|
||||
const sky = new Sky();
|
||||
sky.scale.setScalar(45000);
|
||||
@@ -50,6 +59,47 @@ scene.add(water);
|
||||
// Chunk anfragt statt erst nach dem späteren reset() umzuschwenken.
|
||||
const SPAWN = { x: 1699, y: 3, z: -743 };
|
||||
|
||||
// Task 11, Schritt 5: vier SUVs an echten Mittellinienpunkten aus chunk_10_7.json (derselbe
|
||||
// Chunk wie SPAWN), auf der Straße verteilt und weit genug von SPAWN entfernt, um den Spieler
|
||||
// beim Start nicht zu überlappen. y=1: kleine Fallhöhe bis zur Fahrbahn (y=0), die Federung
|
||||
// übernimmt den Rest — wie schon bei SPAWN selbst braucht es keine exakte Bodenhöhe im Voraus.
|
||||
const VEHICLE_SPAWNS = [
|
||||
new Vector3(1656, 1, -739),
|
||||
new Vector3(1676, 1, -742),
|
||||
new Vector3(1726, 1, -746),
|
||||
new Vector3(1745, 1, -729),
|
||||
];
|
||||
// Konfiguration unverändert aus spawnShowcaseVehicles/VEHICLE_TUNING in
|
||||
// three-player-controller/example/showcase.js — dieselbe suv.glb, dieselbe Abstimmung.
|
||||
const VEHICLE_SCALE = 0.5;
|
||||
const VEHICLE_TUNING = {
|
||||
followVehicleDirection: true,
|
||||
debug: { showPhysicsBox: false, showWheelRays: false, showWheelTravel: false, showWheelSpheres: false },
|
||||
chassis: { density: 1, linearDamping: 0.05, angularDamping: 0.5, clearance: 0.8, sizeScale: { x: 0.9, y: 0.55, z: 0.9 } },
|
||||
suspension: {
|
||||
restLength: 0.15, maxTravel: 0.15, stiffness: 25, compression: 2.1, relaxation: 2.5,
|
||||
maxForce: 6000, frictionSlip: 8, sideFrictionStiffness: 1, rollInfluence: 0.12,
|
||||
},
|
||||
steering: {
|
||||
maxSteerAngle: Math.PI / 5, steerTime: 0.45, steerReturnTimeSlow: 0.55,
|
||||
steerReturnTimeFast: 0.4, highSpeedSteerScale: 0.3,
|
||||
},
|
||||
grip: {
|
||||
maxG: 1.2, sideFrictionIdle: 1, sideFrictionFrontMin: 0.55, sideFrictionRearMin: 0.45,
|
||||
handbrakeRearFriction: 0.35, handbrakeRearDriveScale: 0.65, handbrakeReleaseTime: 0.15,
|
||||
wheelbaseRatio: 0.55,
|
||||
},
|
||||
power: { maxSpeed: 100, acceleration: 5, deceleration: 5 },
|
||||
};
|
||||
// Die Chunks unter den vier Spawnpunkten hält ChunkStreamer dauerhaft fest (this.pinned,
|
||||
// siehe streaming.js): ein Fahrzeug ohne Boden fällt ins Nichts, sobald der Spieler sich weit
|
||||
// genug entfernt und der normale Radius-Abbau die Kollision unter ihm abbaut.
|
||||
const VEHICLE_PINNED_CHUNKS = [...new Set(VEHICLE_SPAWNS.map((v) => {
|
||||
const { X, Y } = threeToWorld(v.x, v.z);
|
||||
const { i, j } = chunkForWorld(X, Y);
|
||||
return `${i}_${j}`;
|
||||
}))];
|
||||
|
||||
const gltf = await new GLTFLoader().loadAsync("./models/ual.glb");
|
||||
const player = new playerController();
|
||||
await player.init({
|
||||
@@ -63,6 +113,57 @@ await player.init({
|
||||
},
|
||||
});
|
||||
|
||||
// Figurenauswahl (Task 11): player.playerModel ist hier noch das frisch geladene ual.glb
|
||||
// (Mannequin) — genau das braucht createCharacterModule als Quellskelett/-animation für
|
||||
// retargetClips, bevor die erste Figur gewechselt wird.
|
||||
const characterModule = createCharacterModule(player);
|
||||
|
||||
// Namensnennung dauerhaft unten links (Layout in index.html, .attribution).
|
||||
mountHud(document.body, buildAttribution(manifest));
|
||||
|
||||
async function spawnVehicles() {
|
||||
const suvGltf = await new GLTFLoader().loadAsync("./models/vehicles/suv.glb");
|
||||
for (const spawnPos of VEHICLE_SPAWNS) {
|
||||
const model = suvGltf.scene.clone(true);
|
||||
await player.loadVehicleModel({
|
||||
model,
|
||||
scale: VEHICLE_SCALE,
|
||||
position: spawnPos.clone(),
|
||||
wheelsNames: ["Wheel_LF", "Wheel_RF", "Wheel_LR", "Wheel_RR"],
|
||||
driverSeatPosition: new Vector3(-0.4, 1.0, 0.3),
|
||||
driverSeatRotation: -Math.PI / 2,
|
||||
modelRotation: -Math.PI / 2,
|
||||
followVehicleDirection: VEHICLE_TUNING.followVehicleDirection,
|
||||
debug: VEHICLE_TUNING.debug,
|
||||
chassis: VEHICLE_TUNING.chassis,
|
||||
suspension: VEHICLE_TUNING.suspension,
|
||||
steering: VEHICLE_TUNING.steering,
|
||||
grip: VEHICLE_TUNING.grip,
|
||||
power: VEHICLE_TUNING.power,
|
||||
});
|
||||
const vehicle = player.getAllVehicles().at(-1);
|
||||
vehicle?.vehicleGroup?.traverse((child) => {
|
||||
if (!child.isMesh) return;
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ein hängender Sprung wartet auf den Collider am Ziel, statt sofort auf normale Kollision zu
|
||||
// schalten (Ruling 53 gilt hier genauso wie beim Ersteinstieg unten): ohne das könnte der
|
||||
// Spieler bei einem Sprung auf ein weit entferntes, noch nicht gestreamtes Wahrzeichen durch
|
||||
// den ungeladenen Boden fallen und vom Auffangnetz (p.y < -30) zurück an lastSafe geschickt
|
||||
// werden — der Sprung sähe dann aus, als wäre er wirkungslos.
|
||||
let pendingTeleport = null;
|
||||
function requestTeleport(position) {
|
||||
pendingTeleport = { x: position.x, y: position.y, z: position.z };
|
||||
player.setSkipCapsuleCollision(true);
|
||||
player.reset(new Vector3(position.x, position.y, position.z));
|
||||
}
|
||||
|
||||
createPanel({ player, characterModule, scene, manifest, requestTeleport });
|
||||
|
||||
// Meeresboden: man watet hinein, statt endlos zu fallen. Einmalig, nicht je Chunk.
|
||||
const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor);
|
||||
seafloor.position.set(500, -3.0, -1000); // Oberkante bei y = -2,5
|
||||
@@ -94,12 +195,13 @@ function updateShadow(target) {
|
||||
// Zeitschranke, die danach die Kollision ohne vorhandenen Boden einschaltet, würde in eine
|
||||
// endlose Fall-Auffang-Schleife laufen, sobald der Chunk dauerhaft ausbleibt. Nach 15s gibt
|
||||
// es nur noch eine einmalige Warnung, kein Verhaltenswechsel.
|
||||
const streamer = new ChunkStreamer({ scene, player, materials, props });
|
||||
const streamer = new ChunkStreamer({ scene, player, materials, props, pinned: VEHICLE_PINNED_CHUNKS });
|
||||
player.setSkipCapsuleCollision(true);
|
||||
let settled = false;
|
||||
let warnedSlowSpawn = false;
|
||||
const startedAt = performance.now();
|
||||
let lastSafe = { ...SPAWN };
|
||||
let vehiclesSpawned = false;
|
||||
|
||||
function tick(delta) {
|
||||
streamer.update(player.playerCapsule.position);
|
||||
@@ -117,6 +219,25 @@ function tick(delta) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Erst spawnen, sobald der Boden unter allen vier Punkten wirklich da ist (Task 11,
|
||||
// Schritt 5) — sonst fiele ein SUV durch einen Chunk, dessen Collider noch nicht fertig ist.
|
||||
if (!vehiclesSpawned && VEHICLE_SPAWNS.every((v) => streamer.isReadyAt(v))) {
|
||||
vehiclesSpawned = true;
|
||||
void spawnVehicles();
|
||||
}
|
||||
|
||||
// Sprungziel wird gehalten, bis sein Chunk bereit ist (s. Kommentar bei requestTeleport).
|
||||
if (pendingTeleport) {
|
||||
if (streamer.isReadyAt(player.playerCapsule.position)) {
|
||||
player.setSkipCapsuleCollision(false);
|
||||
pendingTeleport = null;
|
||||
} else {
|
||||
player.reset(new Vector3(pendingTeleport.x, pendingTeleport.y, pendingTeleport.z));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const p = player.playerCapsule.position;
|
||||
// Ruling 52: an ein echtes Bodensignal binden statt an die Höhe — sonst würde auch der
|
||||
// Scheitelpunkt eines Sprungs, ein Dach oder ein Moment im freien Fall als "sicher" gelten.
|
||||
|
||||
Reference in new issue
Block a user