awacke1's picture
Update game.js
fce2d60 verified
raw
history blame
15.9 kB
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
// Uncomment for physics debugging:
// import CannonDebugger from 'cannon-es-debugger';
// --- DOM Elements ---
const sceneContainer = document.getElementById('scene-container');
const statsElement = document.getElementById('stats-display');
const inventoryElement = document.getElementById('inventory-display');
const logElement = document.getElementById('log-display');
// --- Config ---
const ROOM_SIZE = 10;
const WALL_HEIGHT = 4;
const WALL_THICKNESS = 0.5;
const CAMERA_Y_OFFSET = 20; // Increased camera height for better overview
const PLAYER_SPEED = 5;
const PLAYER_RADIUS = 0.5;
const PLAYER_HEIGHT = 1.8;
const PROJECTILE_SPEED = 15;
const PROJECTILE_RADIUS = 0.2;
// --- Three.js Setup ---
let scene, camera, renderer;
let playerMesh;
const meshesToSync = [];
const projectiles = [];
let axesHelper; // For debugging orientation
// --- Physics Setup ---
let world;
let playerBody;
const physicsBodies = [];
let cannonDebugger = null; // Optional physics debugger instance
// --- Game State ---
let gameState = {}; // Will be initialized in init()
const keysPressed = {};
let gameLoopActive = false; // Control the game loop
// --- Game Data (Ensure starting point 0,0 exists!) ---
const gameData = {
"0,0": { type: 'city', features: ['door_north', 'item_Key'], name: "City Square"}, // Added item
"0,1": { type: 'forest', features: ['path_north', 'door_south', 'monster_goblin'], name: "Forest Path" }, // Added monster
"0,2": { type: 'forest', features: ['path_south'], name: "Deep Forest"},
"1,1": { type: 'forest', features: ['river', 'path_west'], name: "River Bend" },
"-1,1": { type: 'ruins', features: ['path_east', 'item_Healing Potion'], name: "Old Ruins"}, // Added item
// ... Add many more locations ...
};
const itemsData = {
"Healing Potion": { type: "consumable", description: "Restores 10 HP.", hpRestore: 10, model: 'sphere_red' },
"Key": { type: "quest", description: "A rusty key.", model: 'box_gold'},
// ...
};
const monstersData = {
"goblin": { hp: 15, attack: 4, defense: 1, speed: 2, model: 'capsule_green', xp: 5 },
// ...
};
// --- Initialization ---
function init() {
console.log("Initializing Game...");
// Reset state cleanly
gameState = {
inventory: [],
stats: { hp: 30, maxHp: 30, strength: 7, wisdom: 5, courage: 6 },
position: { x: 0, z: 0 },
monsters: [],
items: [],
};
// Clear existing sync/physics lists if restarting
meshesToSync.length = 0;
projectiles.length = 0;
physicsBodies.length = 0;
initThreeJS();
initPhysics();
initPlayer(); // Must be after physics
generateMap(); // Generate map based on gameData
setupInputListeners();
updateUI(); // Initial UI update
addLog("Welcome! Move with WASD/Arrows. Space to Attack.", "info");
console.log("Initialization Complete.");
gameLoopActive = true; // Start the loop AFTER setup
animate(); // Start the game loop
}
function initThreeJS() {
console.log("Initializing Three.js...");
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
camera = new THREE.PerspectiveCamera(60, sceneContainer.clientWidth / sceneContainer.clientHeight, 0.1, 1000);
camera.position.set(0, CAMERA_Y_OFFSET, 5); // Start above origin
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(sceneContainer.clientWidth, sceneContainer.clientHeight);
renderer.shadowMap.enabled = true;
sceneContainer.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0); // Brighter directional
dirLight.position.set(15, 25, 10); // Higher angle
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
// Optional: Improve shadow quality/range
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -ROOM_SIZE * 2;
dirLight.shadow.camera.right = ROOM_SIZE * 2;
dirLight.shadow.camera.top = ROOM_SIZE * 2;
dirLight.shadow.camera.bottom = -ROOM_SIZE * 2;
scene.add(dirLight);
// scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); // Debug shadow camera
// Axes Helper
axesHelper = new THREE.AxesHelper(ROOM_SIZE / 2); // Size relative to room
axesHelper.position.set(0, 0.1, 0); // Slightly above ground
scene.add(axesHelper);
console.log("Three.js Initialized.");
window.addEventListener('resize', onWindowResize, false);
}
function initPhysics() {
console.log("Initializing Cannon-es...");
world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
world.broadphase = new CANNON.SAPBroadphase(world); // Generally better than Naive
world.allowSleep = true; // Allow bodies to sleep to save performance
// Ground plane
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0, material: new CANNON.Material("ground") });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
console.log("Physics World Initialized.");
// Optional: Physics Debugger
// try {
// cannonDebugger = new CannonDebugger(scene, world, {
// color: 0x00ff00, // Optional: specify color
// scale: 1.0, // Optional: scale
// });
// console.log("Cannon-es Debugger Initialized.");
// } catch (e) {
// console.error("Failed to initialize CannonDebugger. Did you include it?", e);
// }
}
// --- Primitive Creation Functions (Keep as before) ---
function createPlayerMesh() { /* ... same as before ... */
const group = new THREE.Group();
const bodyMat = new THREE.MeshLambertMaterial({ color: 0x0077ff });
const bodyGeom = new THREE.CylinderGeometry(PLAYER_RADIUS, PLAYER_RADIUS, PLAYER_HEIGHT - (PLAYER_RADIUS * 2), 16);
const body = new THREE.Mesh(bodyGeom, bodyMat);
body.position.y = PLAYER_RADIUS;
body.castShadow = true;
const headGeom = new THREE.SphereGeometry(PLAYER_RADIUS, 16, 16);
const head = new THREE.Mesh(headGeom, bodyMat);
head.position.y = PLAYER_HEIGHT - PLAYER_RADIUS;
head.castShadow = true;
group.add(body); group.add(head);
const noseGeom = new THREE.ConeGeometry(PLAYER_RADIUS * 0.3, PLAYER_RADIUS * 0.5, 8);
const noseMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const nose = new THREE.Mesh(noseGeom, noseMat);
nose.position.set(0, PLAYER_HEIGHT * 0.7, -PLAYER_RADIUS * 0.7); // Point along negative Z initially
nose.rotation.x = Math.PI / 2 + Math.PI; // Adjust rotation to point forward (-Z)
group.add(nose);
return group;
}
function createSimpleMonsterMesh(modelType = 'capsule_green') { /* ... same as before ... */
const group = new THREE.Group();
let color = 0xff0000;
let geom; let mat;
if (modelType === 'capsule_green') {
color = 0x00ff00;
mat = new THREE.MeshLambertMaterial({ color: color });
const bodyGeom = new THREE.CylinderGeometry(0.4, 0.4, 1.0, 12);
const headGeom = new THREE.SphereGeometry(0.4, 12, 12);
const body = new THREE.Mesh(bodyGeom, mat);
const head = new THREE.Mesh(headGeom, mat);
body.position.y = 0.5; head.position.y = 1.0 + 0.4;
group.add(body); group.add(head);
} else { geom = new THREE.BoxGeometry(0.8, 1.2, 0.8); mat = new THREE.MeshLambertMaterial({ color: color }); const mesh = new THREE.Mesh(geom, mat); mesh.position.y = 0.6; group.add(mesh); }
group.traverse(child => { if (child.isMesh) child.castShadow = true; });
return group;
}
function createSimpleItemMesh(modelType = 'sphere_red') { /* ... same as before ... */
let geom, mat; let color = 0xffffff;
if(modelType === 'sphere_red') { color = 0xff0000; geom = new THREE.SphereGeometry(0.3, 16, 16); }
else if (modelType === 'box_gold') { color = 0xffd700; geom = new THREE.BoxGeometry(0.4, 0.4, 0.4); }
else { geom = new THREE.SphereGeometry(0.3, 16, 16); }
mat = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.6 });
const mesh = new THREE.Mesh(geom, mat); mesh.position.y = PLAYER_RADIUS; mesh.castShadow = true;
return mesh;
}
function createProjectileMesh() { /* ... same as before ... */
const geom = new THREE.SphereGeometry(PROJECTILE_RADIUS, 8, 8);
const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const mesh = new THREE.Mesh(geom, mat);
return mesh;
}
// Shared Geometries / Materials for walls/floors
const floorGeometry = new THREE.PlaneGeometry(ROOM_SIZE, ROOM_SIZE);
const wallN SGeometry = new THREE.BoxGeometry(ROOM_SIZE, WALL_HEIGHT, WALL_THICKNESS);
const wallEWGeometry = new THREE.BoxGeometry(WALL_THICKNESS, WALL_HEIGHT, ROOM_SIZE);
const floorMaterials = {
city: new THREE.MeshLambertMaterial({ color: 0xdeb887 }),
forest: new THREE.MeshLambertMaterial({ color: 0x228B22 }),
cave: new THREE.MeshLambertMaterial({ color: 0x696969 }),
ruins: new THREE.MeshLambertMaterial({ color: 0x778899 }),
plains: new THREE.MeshLambertMaterial({ color: 0x90EE90 }),
default: new THREE.MeshLambertMaterial({ color: 0xaaaaaa })
};
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 });
// --- Player Setup ---
function initPlayer() {
console.log("Initializing Player...");
// Visual Mesh
playerMesh = createPlayerMesh();
// Start slightly above ground to avoid initial collision issues
playerMesh.position.set(0, PLAYER_HEIGHT, 0); // Adjust initial Y based on model pivot
scene.add(playerMesh);
console.log("Player mesh added to scene.");
// Physics Body
const playerShape = new CANNON.Sphere(PLAYER_RADIUS);
playerBody = new CANNON.Body({
mass: 70, // Player mass in kg (approx)
shape: playerShape,
position: new CANNON.Vec3(0, PLAYER_HEIGHT, 0), // Match mesh Y
linearDamping: 0.95, // High damping for tight control
angularDamping: 1.0, // Prevent any spinning
material: new CANNON.Material("player") // Assign physics material
});
// playerBody.fixedRotation = true; // Might not be needed with angular damping 1.0
playerBody.allowSleep = false; // Player should always be active
playerBody.addEventListener("collide", handlePlayerCollision);
world.addBody(playerBody);
console.log("Player physics body added.");
// Add to sync list
meshesToSync.push({ mesh: playerMesh, body: playerBody });
}
// --- Map Generation ---
function generateMap() {
console.log("Generating Map...");
const staticMaterial = new CANNON.Material("static"); // Physics material for walls/floor
// Add physics ground plane (redundant with initPhysics but ensures material)
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0, material: staticMaterial });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
physicsBodies.push(groundBody);
// Add visual grid helper (optional)
// const gridHelper = new THREE.GridHelper(ROOM_SIZE * 5, 5, 0x444444, 0x444444); // Size, Divisions
// scene.add(gridHelper);
for (const coordString in gameData) {
const [xStr, yStr] = coordString.split(',');
const x = parseInt(xStr);
const z = parseInt(yStr); // Map Y is World Z
const data = gameData[coordString];
const type = data.type || 'default';
// Create Floor Mesh
const floorMat = floorMaterials[type] || floorMaterials.default;
const floorMesh = new THREE.Mesh(floorGeometry, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.set(x * ROOM_SIZE, 0, z * ROOM_SIZE);
floorMesh.receiveShadow = true;
scene.add(floorMesh);
// console.log(`Added floor at <span class="math-inline">\{x\},</span>{z}`);
// Create Wall Meshes and Physics Bodies
const features = data.features || [];
const wallDefs = [ // [direction, geometry, xOffset, zOffset]
['north', wallNSGeometry, 0, -0.5],
['south', wallNSGeometry, 0, 0.5],
['east', wallEWGeometry, 0.5, 0],
['west', wallEWGeometry, -0.5, 0],
];
wallDefs.forEach(([dir, geom, xOff, zOff]) => {
const doorFeature = `door_${dir}`;
const pathFeature = `path_${dir}`; // Consider paths as openings
if (!features.includes(doorFeature) && !features.includes(pathFeature)) {
const wallX = x * ROOM_SIZE + xOff * ROOM_SIZE;
const wallZ = z * ROOM_SIZE + zOff * ROOM_SIZE;
const wallY = WALL_HEIGHT / 2;
// Visual Wall
const wallMesh = new THREE.Mesh(geom, wallMaterial);
wallMesh.position.set(wallX, wallY, wallZ);
wallMesh.castShadow = true;
wallMesh.receiveShadow = true;
scene.add(wallMesh);
// Physics Wall
const wallShape = new CANNON.Box(new CANNON.Vec3(geom.parameters.width / 2, geom.parameters.height / 2, geom.parameters.depth / 2));
const wallBody = new CANNON.Body({ mass: 0, shape: wallShape, position: new CANNON.Vec3(wallX, wallY, wallZ), material: staticMaterial });
world.addBody(wallBody);
physicsBodies.push(wallBody);
}
});
// Spawn Items based on features like 'item_Key'
features.forEach(feature => {
if (feature.startsWith('item_')) {
const itemName = feature.substring(5).replace('_', ' '); // e.g., item_Healing_Potion -> Healing Potion
if(itemsData[itemName]) {
spawnItem(itemName, x, z);
} else {
console.warn(`Item feature found but no data for: ${itemName}`);
}
} else if (feature.startsWith('monster_')) {
const monsterType = feature.substring(8); // e.g., monster_goblin -> goblin
if(monstersData[monsterType]) {
spawnMonster(monsterType, x, z);
} else {
console.warn(`Monster feature found but no data for: ${monsterType}`);
}
}
// Handle other visual features (rivers etc.) - currently no physics
else if (feature === 'river') {
const riverMesh = createFeature(feature, x, z);
if (riverMesh) scene.add(riverMesh);
}
});
}
console.log("Map Generation Complete.");
}
function spawnItem(itemName, gridX, gridZ) { /* ... Mostly same as before ... */
const itemData = itemsData[itemName];
if (!itemData) return;
const x = gridX * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.4);
const z = gridZ * ROOM_SIZE + (Math.random() - 0.5) * (ROOM_SIZE * 0.4);
const y = PLAYER_RADIUS;
const mesh = createSimpleItemMesh(itemData.model);
mesh.position.set(x, y, z);
mesh.userData = { type: 'item', name: itemName };
scene.add(mesh);
const shape = new CANNON.Sphere(PLAYER_RADIUS); // Make pickup radius same as player
const body = new CANNON.Body({ mass: 0, isTrigger: true, shape: shape, position: new CANNON.Vec3(x, y, z)});
body.userData = { type: 'item', name: itemName, mesh: mesh }; // Link back
world.addBody(body);
gameState.items.push({ id: body.id, name: itemName, body: body, mesh: mesh });
physicsBodies.push(body);
console.log(`Spawned item ${