feat: App-Gerüst mit Spieler auf Testfläche
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a523dce676
commit
b27b9b7a61
10 files changed
+3630
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "three-player-controller"]
|
||||||
|
path = three-player-controller
|
||||||
|
url = https://github.com/hh-hang/three-player-controller.git
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Leonida</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; height: 100%; overflow: hidden; }
|
||||||
|
#container { width: 100%; height: 100%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3478
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "leonida",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"three": "0.184.0",
|
||||||
|
"three-mesh-bvh": "^0.9.14",
|
||||||
|
"three-player-controller": "file:three-player-controller",
|
||||||
|
"lil-gui": "^0.21.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^7.3.0",
|
||||||
|
"vitest": "^3.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import { ACESFilmicToneMapping, AmbientLight, DirectionalLight, Mesh, MeshStandardMaterial,
|
||||||
|
PerspectiveCamera, PlaneGeometry, Scene, Timer, Vector3, WebGLRenderer } from "three";
|
||||||
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
import { playerController } from "three-player-controller";
|
||||||
|
|
||||||
|
const container = document.querySelector("#container");
|
||||||
|
const scene = new Scene();
|
||||||
|
const renderer = new WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
renderer.toneMapping = ACESFilmicToneMapping;
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
const camera = new PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 2500);
|
||||||
|
scene.add(new AmbientLight(0xffffff, 1.5));
|
||||||
|
const sun = new DirectionalLight(0xffffff, 2);
|
||||||
|
sun.position.set(60, 120, 40);
|
||||||
|
sun.castShadow = true;
|
||||||
|
scene.add(sun);
|
||||||
|
|
||||||
|
const ground = new Mesh(new PlaneGeometry(400, 400), new MeshStandardMaterial({ color: 0x9aa88a }));
|
||||||
|
ground.rotation.x = -Math.PI / 2;
|
||||||
|
ground.receiveShadow = true;
|
||||||
|
scene.add(ground);
|
||||||
|
|
||||||
|
const gltf = await new GLTFLoader().loadAsync("./models/ual.glb");
|
||||||
|
const player = new playerController();
|
||||||
|
await player.init({
|
||||||
|
scene, camera,
|
||||||
|
colliders: [{ motion: "static", shape: { kind: "mesh", source: ground } }],
|
||||||
|
initPos: new Vector3(0, 2, 0),
|
||||||
|
playerModelConfig: {
|
||||||
|
model: gltf.scene, animations: gltf.animations, scale: 0.005,
|
||||||
|
idleAnim: "Idle_Loop", walkAnim: "Walk_Loop", runAnim: "Sprint_Loop",
|
||||||
|
jumpAnim: ["Jump_Start", "Jump_Loop", "Jump_Land"],
|
||||||
|
headBoneName: "Head", rotateY: -Math.PI / 2, speed: 100, runSpeed: 600,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = new Timer();
|
||||||
|
timer.connect(document);
|
||||||
|
renderer.setAnimationLoop((t) => {
|
||||||
|
timer.update(t);
|
||||||
|
player.update(Math.min(timer.getDelta(), 1 / 40));
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
});
|
||||||
|
addEventListener("resize", () => {
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
camera.aspect = container.clientWidth / container.clientHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export const TILE_PX = 256;
|
||||||
|
export const WORLD_HALF = 16384; // gtadb-Weltgrenze in Metern
|
||||||
|
export const CHUNK_SIZE = 250;
|
||||||
|
export const SLICE = { x0: -1000, x1: 2000, y0: -500, y1: 2500 };
|
||||||
|
export const CHUNKS_PER_SIDE = (SLICE.x1 - SLICE.x0) / CHUNK_SIZE; // 12
|
||||||
|
|
||||||
|
export const worldToThree = (X, Y) => ({ x: X, z: -Y });
|
||||||
|
export const threeToWorld = (x, z) => ({ X: x, Y: -z });
|
||||||
|
|
||||||
|
export function tileForWorld(X, Y) {
|
||||||
|
return {
|
||||||
|
row: Math.floor((WORLD_HALF - Y) / TILE_PX),
|
||||||
|
col: Math.floor((X + WORLD_HALF) / TILE_PX),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chunkForWorld(X, Y) {
|
||||||
|
return {
|
||||||
|
i: Math.floor((X - SLICE.x0) / CHUNK_SIZE),
|
||||||
|
j: Math.floor((SLICE.y1 - Y) / CHUNK_SIZE),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nordwest-Ecke eines Chunks in Weltkoordinaten. */
|
||||||
|
export function chunkOrigin(i, j) {
|
||||||
|
return { X: SLICE.x0 + i * CHUNK_SIZE, Y: SLICE.y1 - j * CHUNK_SIZE };
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { CHUNK_SIZE, chunkForWorld, threeToWorld, tileForWorld, worldToThree } from "../src/world/coords.js";
|
||||||
|
|
||||||
|
describe("coords", () => {
|
||||||
|
it("dreht Y zur three-z-Achse", () => {
|
||||||
|
expect(worldToThree(1699, 743)).toEqual({ x: 1699, z: -743 });
|
||||||
|
expect(threeToWorld(1699, -743)).toEqual({ X: 1699, Y: 743 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("findet die Kachel von Vice Beach", () => {
|
||||||
|
expect(tileForWorld(1409.4, 2803.2)).toEqual({ row: 53, col: 69 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deckt den Ausschnitt mit Spalten 60..71 und Zeilen 54..65 ab", () => {
|
||||||
|
expect(tileForWorld(-1000, 2500)).toEqual({ row: 54, col: 60 });
|
||||||
|
expect(tileForWorld(1999, -499)).toEqual({ row: 65, col: 71 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("teilt den Ausschnitt in 250-m-Chunks", () => {
|
||||||
|
expect(CHUNK_SIZE).toBe(250);
|
||||||
|
expect(chunkForWorld(-1000, 2500)).toEqual({ i: 0, j: 0 });
|
||||||
|
expect(chunkForWorld(1999, -499)).toEqual({ i: 11, j: 11 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Submodule
+1
Submodule three-player-controller added at ac396825a7.
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Ohne dedupe lädt der Controller three aus seinem eigenen node_modules -> zwei Kopien.
|
||||||
|
resolve: { dedupe: ["three", "three-mesh-bvh"] },
|
||||||
|
// Der BVH-Worker wird dynamisch aus three-mesh-bvh/src/workers/ geladen und darf nicht vorgebündelt werden.
|
||||||
|
optimizeDeps: { exclude: ["three-mesh-bvh", "three-player-controller"] },
|
||||||
|
server: { host: true },
|
||||||
|
});
|
||||||
Reference in new issue
Block a user