Ai_Assistant/client/_archive/obstacleCourse.js

449 lines
16 KiB
JavaScript
Raw Normal View History

2026-05-24 13:31:30 +02:00
import * as THREE from 'three';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
/**
* Creates an obstacle course with:
* - Tall wall (jump over)
* - Laser wires (crouch/crawl under)
* - Balance beam
* - "OBSTACLE COURSE" title text
* - Goal platform with "GOAL" sign
*/
export function createObstacleCourse(scene) {
const courseGroup = new THREE.Group();
courseGroup.name = 'obstacleCourse';
// Course layout along negative Z axis (character faces -Z to go through course)
const courseStartZ = -2;
const obstacleSpacing = 4;
// Materials
const wallMaterial = new THREE.MeshStandardMaterial({
color: 0x8B4513,
roughness: 0.8
});
const laserMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 0.8
});
const beamMaterial = new THREE.MeshStandardMaterial({
color: 0xDEB887,
roughness: 0.6
});
const platformMaterial = new THREE.MeshStandardMaterial({
color: 0x228B22,
roughness: 0.5
});
const postMaterial = new THREE.MeshStandardMaterial({
color: 0x444444,
metalness: 0.8,
roughness: 0.2
});
const signMaterial = new THREE.MeshStandardMaterial({
color: 0xFFD700
});
// ============================================
// OBSTACLE 1: Tall Wall (Jump Over)
// ============================================
const wallZ = courseStartZ;
const wallHeight = 0.8;
const wallWidth = 2;
const wallThickness = 0.15;
const wallGeometry = new THREE.BoxGeometry(wallWidth, wallHeight, wallThickness);
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(0, wallHeight / 2, wallZ);
wall.castShadow = true;
wall.receiveShadow = true;
wall.name = 'obstacle_wall';
courseGroup.add(wall);
// Wall side supports
const supportGeometry = new THREE.BoxGeometry(0.1, wallHeight, 0.3);
const leftSupport = new THREE.Mesh(supportGeometry, wallMaterial);
leftSupport.position.set(-wallWidth / 2 - 0.05, wallHeight / 2, wallZ);
leftSupport.castShadow = true;
courseGroup.add(leftSupport);
const rightSupport = new THREE.Mesh(supportGeometry, wallMaterial);
rightSupport.position.set(wallWidth / 2 + 0.05, wallHeight / 2, wallZ);
rightSupport.castShadow = true;
courseGroup.add(rightSupport);
// ============================================
// OBSTACLE 2: Laser Wires (Crouch/Crawl)
// ============================================
const laserZ = wallZ - obstacleSpacing;
const laserCount = 3;
const laserSpacing = 0.8;
const laserWidth = 2;
const laserHeight = 0.4; // Low enough to require crawling
// Posts for laser wires
const postHeight = 1.0;
const postRadius = 0.05;
const postGeometry = new THREE.CylinderGeometry(postRadius, postRadius, postHeight, 8);
const leftPost = new THREE.Mesh(postGeometry, postMaterial);
leftPost.position.set(-laserWidth / 2 - 0.1, postHeight / 2, laserZ);
leftPost.castShadow = true;
courseGroup.add(leftPost);
const rightPost = new THREE.Mesh(postGeometry, postMaterial);
rightPost.position.set(laserWidth / 2 + 0.1, postHeight / 2, laserZ);
rightPost.castShadow = true;
courseGroup.add(rightPost);
// Laser wires (thin glowing cylinders)
for (let i = 0; i < laserCount; i++) {
const wireZ = laserZ + (i - 1) * laserSpacing;
const wireGeometry = new THREE.CylinderGeometry(0.01, 0.01, laserWidth, 8);
const wire = new THREE.Mesh(wireGeometry, laserMaterial);
wire.rotation.z = Math.PI / 2; // Rotate to horizontal
wire.position.set(0, laserHeight, wireZ);
wire.name = `laser_wire_${i}`;
courseGroup.add(wire);
// Add glow effect with a slightly larger transparent cylinder
const glowGeometry = new THREE.CylinderGeometry(0.03, 0.03, laserWidth, 8);
const glowMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.3
});
const glow = new THREE.Mesh(glowGeometry, glowMaterial);
glow.rotation.z = Math.PI / 2;
glow.position.set(0, laserHeight, wireZ);
courseGroup.add(glow);
// Additional posts for each wire row
if (i !== 1) { // Skip middle, already have posts
const miniPostGeometry = new THREE.CylinderGeometry(0.03, 0.03, postHeight * 0.6, 6);
const leftMiniPost = new THREE.Mesh(miniPostGeometry, postMaterial);
leftMiniPost.position.set(-laserWidth / 2 - 0.1, postHeight * 0.3, wireZ);
courseGroup.add(leftMiniPost);
const rightMiniPost = new THREE.Mesh(miniPostGeometry, postMaterial);
rightMiniPost.position.set(laserWidth / 2 + 0.1, postHeight * 0.3, wireZ);
courseGroup.add(rightMiniPost);
}
}
// ============================================
// OBSTACLE 3: Balance Beam over Spike Pit
// ============================================
const beamZ = laserZ - obstacleSpacing;
const beamLength = 3;
const beamWidth = 0.18;
const pitDepth = 1.2;
const pitWidth = 2.5;
const pitLength = beamLength + 1;
// Pit materials
const pitWallMaterial = new THREE.MeshStandardMaterial({
color: 0x3d3d3d,
roughness: 0.9
});
const pitFloorMaterial = new THREE.MeshStandardMaterial({
color: 0x1a1a1a,
roughness: 1.0
});
const spikeMaterial = new THREE.MeshStandardMaterial({
color: 0x888888,
metalness: 0.7,
roughness: 0.3
});
// Pit floor (bottom of the pit)
const pitFloorGeometry = new THREE.BoxGeometry(pitWidth, 0.1, pitLength);
const pitFloor = new THREE.Mesh(pitFloorGeometry, pitFloorMaterial);
pitFloor.position.set(0, -pitDepth, beamZ);
pitFloor.receiveShadow = true;
pitFloor.name = 'pit_floor';
courseGroup.add(pitFloor);
// Pit walls (4 sides)
const wallThicknessPit = 0.15;
// Left wall
const leftWallGeometry = new THREE.BoxGeometry(wallThicknessPit, pitDepth, pitLength);
const leftWall = new THREE.Mesh(leftWallGeometry, pitWallMaterial);
leftWall.position.set(-pitWidth / 2 - wallThicknessPit / 2, -pitDepth / 2, beamZ);
leftWall.castShadow = true;
leftWall.receiveShadow = true;
courseGroup.add(leftWall);
// Right wall
const rightWall = new THREE.Mesh(leftWallGeometry, pitWallMaterial);
rightWall.position.set(pitWidth / 2 + wallThicknessPit / 2, -pitDepth / 2, beamZ);
rightWall.castShadow = true;
rightWall.receiveShadow = true;
courseGroup.add(rightWall);
// Front wall (with gap for beam)
const frontWallSideWidth = (pitWidth - beamWidth) / 2;
const frontWallGeometry = new THREE.BoxGeometry(frontWallSideWidth, pitDepth, wallThicknessPit);
const frontWallLeft = new THREE.Mesh(frontWallGeometry, pitWallMaterial);
frontWallLeft.position.set(-beamWidth / 2 - frontWallSideWidth / 2, -pitDepth / 2, beamZ + pitLength / 2 + wallThicknessPit / 2);
frontWallLeft.castShadow = true;
courseGroup.add(frontWallLeft);
const frontWallRight = new THREE.Mesh(frontWallGeometry, pitWallMaterial);
frontWallRight.position.set(beamWidth / 2 + frontWallSideWidth / 2, -pitDepth / 2, beamZ + pitLength / 2 + wallThicknessPit / 2);
frontWallRight.castShadow = true;
courseGroup.add(frontWallRight);
// Back wall (with gap for beam)
const backWallLeft = new THREE.Mesh(frontWallGeometry, pitWallMaterial);
backWallLeft.position.set(-beamWidth / 2 - frontWallSideWidth / 2, -pitDepth / 2, beamZ - pitLength / 2 - wallThicknessPit / 2);
backWallLeft.castShadow = true;
courseGroup.add(backWallLeft);
const backWallRight = new THREE.Mesh(frontWallGeometry, pitWallMaterial);
backWallRight.position.set(beamWidth / 2 + frontWallSideWidth / 2, -pitDepth / 2, beamZ - pitLength / 2 - wallThicknessPit / 2);
backWallRight.castShadow = true;
courseGroup.add(backWallRight);
// Spikes at the bottom of the pit
const spikeHeight = 0.4;
const spikeRadius = 0.06;
const spikeGeometry = new THREE.ConeGeometry(spikeRadius, spikeHeight, 6);
// Create grid of spikes
const spikeSpacingX = 0.25;
const spikeSpacingZ = 0.3;
const spikesPerRowX = Math.floor((pitWidth - 0.3) / spikeSpacingX);
const spikesPerRowZ = Math.floor((pitLength - 0.3) / spikeSpacingZ);
for (let ix = 0; ix < spikesPerRowX; ix++) {
for (let iz = 0; iz < spikesPerRowZ; iz++) {
const spikeX = -pitWidth / 2 + 0.2 + ix * spikeSpacingX;
const spikeZPos = beamZ - pitLength / 2 + 0.2 + iz * spikeSpacingZ;
// Skip spikes directly under the beam
if (Math.abs(spikeX) < beamWidth / 2 + 0.05) continue;
const spike = new THREE.Mesh(spikeGeometry, spikeMaterial);
spike.position.set(spikeX, -pitDepth + spikeHeight / 2 + 0.05, spikeZPos);
spike.castShadow = true;
spike.name = 'spike';
courseGroup.add(spike);
}
}
// Balance beam (at floor level, spanning the pit)
const beamGeometry = new THREE.BoxGeometry(beamWidth, 0.08, beamLength + 0.6);
const beam = new THREE.Mesh(beamGeometry, beamMaterial);
beam.position.set(0, 0.04, beamZ);
beam.castShadow = true;
beam.receiveShadow = true;
beam.name = 'balance_beam';
courseGroup.add(beam);
// Entry/exit ramps (slight incline to beam)
const rampLength = 0.4;
const rampGeometry = new THREE.BoxGeometry(beamWidth + 0.1, 0.08, rampLength);
const entryRamp = new THREE.Mesh(rampGeometry, beamMaterial);
entryRamp.position.set(0, 0.04, beamZ + beamLength / 2 + 0.5);
entryRamp.castShadow = true;
courseGroup.add(entryRamp);
const exitRamp = new THREE.Mesh(rampGeometry, beamMaterial);
exitRamp.position.set(0, 0.04, beamZ - beamLength / 2 - 0.5);
exitRamp.castShadow = true;
courseGroup.add(exitRamp);
// Warning stripes on the edges
const warningMaterial = new THREE.MeshStandardMaterial({
color: 0xffcc00,
roughness: 0.6
});
const warningGeometry = new THREE.BoxGeometry(pitWidth + wallThicknessPit * 2, 0.02, 0.1);
const frontWarning = new THREE.Mesh(warningGeometry, warningMaterial);
frontWarning.position.set(0, 0.01, beamZ + pitLength / 2 + wallThicknessPit + 0.05);
courseGroup.add(frontWarning);
const backWarning = new THREE.Mesh(warningGeometry, warningMaterial);
backWarning.position.set(0, 0.01, beamZ - pitLength / 2 - wallThicknessPit - 0.05);
courseGroup.add(backWarning);
// ============================================
// GOAL: Finish Platform with Button
// ============================================
const goalZ = beamZ - obstacleSpacing;
// Main platform
const platformGeometry = new THREE.CylinderGeometry(0.8, 0.9, 0.15, 16);
const platform = new THREE.Mesh(platformGeometry, platformMaterial);
platform.position.set(0, 0.075, goalZ);
platform.receiveShadow = true;
platform.name = 'goal_platform';
courseGroup.add(platform);
// Button on platform
const buttonBaseGeometry = new THREE.CylinderGeometry(0.25, 0.3, 0.1, 16);
const buttonBaseMaterial = new THREE.MeshStandardMaterial({
color: 0x333333,
metalness: 0.5
});
const buttonBase = new THREE.Mesh(buttonBaseGeometry, buttonBaseMaterial);
buttonBase.position.set(0, 0.2, goalZ);
buttonBase.castShadow = true;
courseGroup.add(buttonBase);
const buttonTopGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.08, 16);
const buttonTopMaterial = new THREE.MeshStandardMaterial({
color: 0xff3333,
emissive: 0xff0000,
emissiveIntensity: 0.3
});
const buttonTop = new THREE.Mesh(buttonTopGeometry, buttonTopMaterial);
buttonTop.position.set(0, 0.29, goalZ);
buttonTop.castShadow = true;
buttonTop.name = 'goal_button';
courseGroup.add(buttonTop);
// Goal sign post
const signPostGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1.5, 8);
const signPost = new THREE.Mesh(signPostGeometry, postMaterial);
signPost.position.set(0, 0.75, goalZ - 0.8);
signPost.castShadow = true;
courseGroup.add(signPost);
// Goal sign board
const signBoardGeometry = new THREE.BoxGeometry(0.8, 0.3, 0.05);
const signBoard = new THREE.Mesh(signBoardGeometry, signMaterial);
signBoard.position.set(0, 1.4, goalZ - 0.8);
signBoard.castShadow = true;
signBoard.name = 'goal_sign';
courseGroup.add(signBoard);
// ============================================
// TITLE TEXT: "OBSTACLE COURSE" in distance
// ============================================
const titleZ = goalZ - 5;
// Load font and create 3D text
const fontLoader = new FontLoader();
fontLoader.load(
'https://threejs.org/examples/fonts/helvetiker_bold.typeface.json',
(font) => {
// Main title
const titleGeometry = new TextGeometry('OBSTACLE COURSE', {
font: font,
size: 0.4,
depth: 0.1,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.02,
bevelSize: 0.01,
bevelOffset: 0,
bevelSegments: 3
});
titleGeometry.computeBoundingBox();
titleGeometry.center();
const titleMaterial = new THREE.MeshStandardMaterial({
color: 0xFFD700,
metalness: 0.6,
roughness: 0.3
});
const titleMesh = new THREE.Mesh(titleGeometry, titleMaterial);
titleMesh.position.set(0, 2, titleZ);
titleMesh.castShadow = true;
titleMesh.name = 'title_text';
courseGroup.add(titleMesh);
// GOAL text on the sign
const goalTextGeometry = new TextGeometry('GOAL', {
font: font,
size: 0.12,
depth: 0.02,
curveSegments: 8,
bevelEnabled: false
});
goalTextGeometry.computeBoundingBox();
goalTextGeometry.center();
const goalTextMaterial = new THREE.MeshStandardMaterial({
color: 0x222222
});
const goalTextMesh = new THREE.Mesh(goalTextGeometry, goalTextMaterial);
goalTextMesh.position.set(0, 1.4, goalZ - 0.75);
goalTextMesh.name = 'goal_text';
courseGroup.add(goalTextMesh);
console.log('✅ Obstacle course text loaded');
},
undefined,
(error) => {
console.warn('Could not load font for obstacle course text:', error);
// Fallback: Create simple plane signs
createFallbackSigns(courseGroup, goalZ, titleZ);
}
);
// Add to scene
scene.add(courseGroup);
console.log('✅ Obstacle course created');
return {
group: courseGroup,
// Expose obstacle positions for collision detection
obstacles: {
wall: { position: new THREE.Vector3(0, wallHeight / 2, wallZ), size: new THREE.Vector3(wallWidth, wallHeight, wallThickness) },
lasers: { position: new THREE.Vector3(0, laserHeight, laserZ), height: laserHeight, width: laserWidth },
beam: { position: new THREE.Vector3(0, 0.04, beamZ), length: beamLength, width: beamWidth, height: 0.08 },
pit: { position: new THREE.Vector3(0, -pitDepth / 2, beamZ), width: pitWidth, length: pitLength, depth: pitDepth },
goal: { position: new THREE.Vector3(0, 0, goalZ), radius: 0.8 }
}
};
}
// Fallback signs if font fails to load
function createFallbackSigns(group, goalZ, titleZ) {
// Title sign (plane with color)
const titleSignGeometry = new THREE.PlaneGeometry(3, 0.6);
const titleSignMaterial = new THREE.MeshStandardMaterial({
color: 0xFFD700,
side: THREE.DoubleSide
});
const titleSign = new THREE.Mesh(titleSignGeometry, titleSignMaterial);
titleSign.position.set(0, 2, titleZ);
titleSign.name = 'title_fallback';
group.add(titleSign);
// Goal sign already exists as signBoard
console.log('⚠️ Using fallback signs (font not loaded)');
}
/**
* Optional: Animate laser wires for visual effect
*/
export function animateObstacleCourse(courseGroup, deltaTime, elapsedTime) {
if (!courseGroup) return;
// Pulse the laser wires
courseGroup.children.forEach(child => {
if (child.name && child.name.startsWith('laser_wire_')) {
const pulse = 0.8 + 0.2 * Math.sin(elapsedTime * 5 + parseInt(child.name.split('_')[2]) * 0.5);
if (child.material.emissiveIntensity !== undefined) {
child.material.emissiveIntensity = pulse;
}
}
});
// Gently pulse the goal button
const goalButton = courseGroup.getObjectByName('goal_button');
if (goalButton && goalButton.material.emissiveIntensity !== undefined) {
goalButton.material.emissiveIntensity = 0.2 + 0.15 * Math.sin(elapsedTime * 3);
}
}