feat: Implement basic horror world with standard materials, ambient lighting, pillars, and a target object, alongside general code cleanup and logging improvements.
This commit is contained in:
55
index.html
55
index.html
@@ -10,63 +10,38 @@
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
const logDiv = document.getElementById('debug-log');
|
||||
if (logDiv) {
|
||||
const err = document.createElement('div');
|
||||
err.style.color = 'red';
|
||||
err.style.fontWeight = 'bold';
|
||||
err.style.background = 'white';
|
||||
err.style.padding = '5px';
|
||||
err.style.margin = '5px 0';
|
||||
err.textContent = `CRITICAL ERROR: ${msg} [Line: ${lineNo}]`;
|
||||
logDiv.appendChild(err);
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
logDiv.style.display = 'block';
|
||||
logDiv.innerHTML += `<div style="color:red; background:white">CRITICAL: ${msg} [Line: ${lineNo}]</div>`;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function checkWebGL() {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
if (gl && gl instanceof WebGLRenderingContext) return "WebGL Supported";
|
||||
else return "WebGL NOT Supported";
|
||||
} catch (e) { return "WebGL Error: " + e.message; }
|
||||
}
|
||||
function test2D() {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 100;
|
||||
canvas.height = 100;
|
||||
canvas.style.position = 'fixed';
|
||||
canvas.style.bottom = '10px';
|
||||
canvas.style.right = '10px';
|
||||
canvas.style.zIndex = '10001';
|
||||
canvas.style.border = '2px solid white';
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, 100, 100);
|
||||
window.log("2D Canvas Test: Rendered Red Square");
|
||||
} catch (e) { window.log("2D Canvas Error: " + e.message); }
|
||||
}
|
||||
window.addEventListener('load', test2D);
|
||||
window.test2D = function () {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = 50; c.height = 50;
|
||||
c.style.position = 'fixed'; c.bottom = '0'; c.right = '0'; c.zIndex = '10001';
|
||||
document.body.appendChild(c);
|
||||
const ctx = c.getContext('2d');
|
||||
ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 50, 50);
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
<div id="debug-log"
|
||||
style="position: absolute; top: 10px; left: 10px; z-index: 10000; color: lime; font-family: monospace; pointer-events: none; background: rgba(0,0,0,0.8); max-height: 80%; overflow-y: auto; font-size: 14px; padding: 10px; width: 300px;">
|
||||
</div>
|
||||
|
||||
<div id="ui-layer">
|
||||
<div id="start-screen">
|
||||
<h1>ECHOES</h1>
|
||||
<p>Click to Start</p>
|
||||
<p class="controls">WASD to Move | Mouse to Look | E to Interact</p>
|
||||
<p class="controls">WASD to Move | Mouse to Look | F Flashlight | P Debug</p>
|
||||
</div>
|
||||
<div id="hud" style="display: none;">
|
||||
<div id="battery">Battery: <span id="battery-level">100%</span></div>
|
||||
</div>
|
||||
<div id="debug-log"
|
||||
style="position: absolute; top: 10px; left: 10px; z-index: 10000; color: lime; font-family: monospace; pointer-events: none; background: rgba(0,0,0,0.8); max-height: 80%; overflow-y: auto; font-size: 14px; padding: 10px; width: 300px; display: block;">
|
||||
<div>> Logger Initialized</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
|
||||
34
src/Game.js
34
src/Game.js
@@ -1,19 +1,12 @@
|
||||
import { Graphics } from './Graphics.js';
|
||||
import { World } from './World.js';
|
||||
import { Player } from './Player.js';
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class Game {
|
||||
constructor() {
|
||||
window.log('Game constructor start');
|
||||
try {
|
||||
this.graphics = new Graphics();
|
||||
this.world = new World(this.graphics.scene);
|
||||
this.player = new Player(this.graphics.camera, this.world.colliders);
|
||||
window.log('All components created successfully');
|
||||
} catch (e) {
|
||||
window.log('CRITICAL ERROR during setup: ' + e.message);
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
this.lastTime = 0;
|
||||
@@ -23,38 +16,21 @@ export class Game {
|
||||
setupUI() {
|
||||
const startScreen = document.getElementById('start-screen');
|
||||
const hud = document.getElementById('hud');
|
||||
if (!startScreen) {
|
||||
window.log('ERROR: start-screen not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startScreen) {
|
||||
startScreen.addEventListener('click', () => {
|
||||
window.log('Start screen clicked');
|
||||
if (this.player) {
|
||||
this.player.lockControls();
|
||||
}
|
||||
if (this.player) this.player.lockControls();
|
||||
startScreen.style.display = 'none';
|
||||
if (hud) hud.style.display = 'block';
|
||||
this.isRunning = true;
|
||||
window.log('Game isRunning = true');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
window.log('Game.start() begin');
|
||||
try {
|
||||
if (this.world) this.world.load();
|
||||
if (this.player) {
|
||||
const playerObj = this.player.getObject();
|
||||
this.graphics.scene.add(playerObj);
|
||||
}
|
||||
window.log('World/Player loading complete');
|
||||
} catch (e) {
|
||||
window.log('ERROR in Game.start(): ' + e.message);
|
||||
}
|
||||
|
||||
this.world.load();
|
||||
this.graphics.scene.add(this.player.getObject());
|
||||
requestAnimationFrame(this.loop.bind(this));
|
||||
window.log('Animation loop requested');
|
||||
}
|
||||
|
||||
loop(time) {
|
||||
|
||||
@@ -4,44 +4,22 @@ export class Graphics {
|
||||
constructor() {
|
||||
// Main scene rendering
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.fog = new THREE.Fog(0x111122, 2, 15);
|
||||
this.scene.background = new THREE.Color(0x111122); // Dark blue background instead of black
|
||||
this.scene.fog = new THREE.Fog(0x000000, 2, 12);
|
||||
this.scene.background = new THREE.Color(0x000000);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||
|
||||
// Real Screen Renderer - Force 400x300 for diagnostic visibility
|
||||
const w = 400;
|
||||
const h = 300;
|
||||
// Real Screen Renderer
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: false });
|
||||
this.renderer.setSize(w, h);
|
||||
this.renderer.setClearColor(0xff00ff);
|
||||
this.renderer.domElement.style.position = 'fixed';
|
||||
this.renderer.domElement.style.top = '50%';
|
||||
this.renderer.domElement.style.left = '50%';
|
||||
this.renderer.domElement.style.transform = 'translate(-50%, -50%)';
|
||||
this.renderer.domElement.style.border = '5px solid yellow';
|
||||
this.renderer.domElement.style.zIndex = '1000';
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.domElement.id = 'three-canvas';
|
||||
|
||||
document.body.appendChild(this.renderer.domElement);
|
||||
window.log(`GL Vendor: ${this.renderer.getContext().getParameter(this.renderer.getContext().VENDOR)}`);
|
||||
window.log(`Canvas forced to ${w}x${h} (centered)`);
|
||||
|
||||
this.camera.position.set(5, 5, 5);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
// Add a guaranteed object
|
||||
const testCube = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2, 2, 2),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||
);
|
||||
this.scene.add(testCube);
|
||||
window.log('Green Cube added to scene');
|
||||
// Append to the correct container, not body directly
|
||||
const container = document.getElementById('game-container');
|
||||
if (container) container.appendChild(this.renderer.domElement);
|
||||
else document.body.appendChild(this.renderer.domElement);
|
||||
|
||||
// --- Retro Pipeline Setup ---
|
||||
|
||||
// 1. Off-screen Render Target (Small Resolution)
|
||||
// We render the 3D scene here first.
|
||||
this.targetWidth = 320;
|
||||
this.targetHeight = 240;
|
||||
|
||||
@@ -51,18 +29,15 @@ export class Graphics {
|
||||
format: THREE.RGBAFormat
|
||||
});
|
||||
|
||||
// 2. Post-Processing Quad
|
||||
// We render this quad to the screen, using the texture from the Render Target.
|
||||
this.postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
this.postScene = new THREE.Scene();
|
||||
|
||||
const planeGeometry = new THREE.PlaneGeometry(2, 2);
|
||||
|
||||
// Custom Shader for Color Quantization
|
||||
const postMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
tDiffuse: { value: this.renderTarget.texture },
|
||||
colorDepth: { value: 16.0 } // 16 levels per channel (4-bit per channel - 4096 colors)
|
||||
colorDepth: { value: 24.0 } // 24 levels
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
@@ -77,7 +52,6 @@ export class Graphics {
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vec4 tex = texture2D(tDiffuse, vUv);
|
||||
// Quantize color and ensure it's not absolutely zeroed if there's light
|
||||
vec3 color = floor(tex.rgb * colorDepth + 0.5) / colorDepth;
|
||||
gl_FragColor = vec4(color, tex.a);
|
||||
}
|
||||
@@ -87,27 +61,17 @@ export class Graphics {
|
||||
this.postQuad = new THREE.Mesh(planeGeometry, postMaterial);
|
||||
this.postScene.add(this.postQuad);
|
||||
|
||||
// Input & Resize handling
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize.bind(this));
|
||||
}
|
||||
|
||||
init() {
|
||||
// Standard init placeholder
|
||||
}
|
||||
init() { }
|
||||
|
||||
handleResize() {
|
||||
const aspect = window.innerWidth / window.innerHeight;
|
||||
|
||||
// Update 3D Camera
|
||||
this.camera.aspect = aspect;
|
||||
this.camera.updateProjectionMatrix();
|
||||
|
||||
// Update Screen Renderer
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// Update Render Target Size (Fixed low height, aspect-correct width? Or Fixed Width?)
|
||||
// Let's stick to fixed width 320 for that specific PSX feel, height auto.
|
||||
this.targetHeight = Math.floor(this.targetWidth / aspect);
|
||||
this.renderTarget.setSize(this.targetWidth, this.targetHeight);
|
||||
}
|
||||
@@ -115,19 +79,13 @@ export class Graphics {
|
||||
render() {
|
||||
if (!this.renderCount) this.renderCount = 0;
|
||||
this.renderCount++;
|
||||
if (this.renderCount % 500 === 0) window.log(`Frame: ${this.renderCount}`);
|
||||
|
||||
if (this.renderCount % 500 === 0) {
|
||||
window.log(`F:${this.renderCount} | Scene: ${this.scene.children.length} obj | Cam: ${this.camera.position.x.toFixed(1)},${this.camera.position.y.toFixed(1)},${this.camera.position.z.toFixed(1)}`);
|
||||
}
|
||||
|
||||
this.renderer.setClearColor(0xff00ff);
|
||||
this.renderer.clear();
|
||||
|
||||
// 1. Render 3D Scene directly
|
||||
// Diagnostic: Render 3D Scene directly to Screen
|
||||
this.renderer.setRenderTarget(null);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
/*
|
||||
// Original Post-Processing Pipeline
|
||||
// 1. Render 3D Scene to RenderTarget
|
||||
this.renderer.setRenderTarget(this.renderTarget);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
@@ -71,7 +71,11 @@ export class Player {
|
||||
}
|
||||
|
||||
toggleFlashlight() {
|
||||
if (!this.controls || !this.controls.isLocked) return; // Only toggle when game is active
|
||||
if (!this.controls) return;
|
||||
// Note: isLocked might be false if user escaped, but we allow 'F' if controls exist?
|
||||
// Better to allow F only when locked to avoid confusion.
|
||||
if (!this.controls.isLocked) return;
|
||||
|
||||
this.flashlightOn = !this.flashlightOn;
|
||||
if (this.flashlight) {
|
||||
this.flashlight.visible = this.flashlightOn;
|
||||
@@ -79,48 +83,53 @@ export class Player {
|
||||
}
|
||||
|
||||
lockControls() {
|
||||
if (this.controls) this.controls.lock();
|
||||
else window.log('WARNING: Controls not initialized for locking');
|
||||
if (this.controls) {
|
||||
this.controls.lock();
|
||||
window.log('Player.lockControls() called');
|
||||
} else {
|
||||
window.log('WARNING: Controls not initialized for locking');
|
||||
}
|
||||
}
|
||||
|
||||
getObject() {
|
||||
return this.controls ? this.controls.getObject() : new THREE.Group();
|
||||
// PointerLockControls in this version operates directly on the camera
|
||||
return this.camera;
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
if (!this.controls || !this.controls.isLocked) return;
|
||||
if (!this.controls) return;
|
||||
|
||||
// Friction-like dampening
|
||||
// Simple direct velocity for now
|
||||
this.velocity.x = 0;
|
||||
this.velocity.z = 0;
|
||||
// Debug logging for lock state (once per second roughly)
|
||||
if (Math.random() < 0.01) {
|
||||
// window.log(`Controls Locked: ${this.controls.isLocked}`);
|
||||
}
|
||||
|
||||
if (!this.controls.isLocked) return;
|
||||
|
||||
// Friction-like dampening (simple decay)
|
||||
this.velocity.x -= this.velocity.x * 10.0 * dt;
|
||||
this.velocity.z -= this.velocity.z * 10.0 * dt;
|
||||
|
||||
this.direction.z = Number(this.moveForward) - Number(this.moveBackward);
|
||||
this.direction.x = Number(this.moveRight) - Number(this.moveLeft);
|
||||
this.direction.normalize(); // Ensure consistent speed in all directions
|
||||
this.direction.normalize();
|
||||
|
||||
if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * this.speed * dt;
|
||||
if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * this.speed * dt;
|
||||
if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * 100.0 * dt;
|
||||
if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * 100.0 * dt;
|
||||
|
||||
// Apply movement
|
||||
this.controls.moveRight(-this.velocity.x);
|
||||
this.controls.moveForward(-this.velocity.z);
|
||||
this.controls.moveRight(-this.velocity.x * dt);
|
||||
this.controls.moveForward(-this.velocity.z * dt);
|
||||
|
||||
// Simple Collision: Push back
|
||||
const playerPos = this.controls.getObject().position;
|
||||
const playerRadius = 0.5; // approximated radius
|
||||
const playerPos = this.camera.position;
|
||||
const playerRadius = 0.5;
|
||||
|
||||
for (const collider of this.colliders) {
|
||||
// Assume colliders are BoxGeometry meshes aligned with axes for now
|
||||
const box = new THREE.Box3().setFromObject(collider);
|
||||
|
||||
// Check if player is inside the box (expanded by radius)
|
||||
// We only check X/Z for walls
|
||||
if (playerPos.x > box.min.x - playerRadius && playerPos.x < box.max.x + playerRadius &&
|
||||
playerPos.z > box.min.z - playerRadius && playerPos.z < box.max.z + playerRadius) {
|
||||
|
||||
// Very simple resolution: determine closest edge and push out
|
||||
// This is a naive implementation but works for static orthogonal walls
|
||||
const dx1 = Math.abs(playerPos.x - (box.min.x - playerRadius));
|
||||
const dx2 = Math.abs(playerPos.x - (box.max.x + playerRadius));
|
||||
const dz1 = Math.abs(playerPos.z - (box.min.z - playerRadius));
|
||||
|
||||
56
src/World.js
56
src/World.js
@@ -7,45 +7,59 @@ export class World {
|
||||
}
|
||||
|
||||
load() {
|
||||
window.log('World.load() start');
|
||||
// Grid helper for depth
|
||||
this.scene.add(new THREE.GridHelper(20, 20));
|
||||
// Standard lighting for horror
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 1.0); // Full brightness ambient for testing
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
// Floor (Basic Material - no light needed)
|
||||
const floorGeo = new THREE.PlaneGeometry(20, 20);
|
||||
const floorMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x333333,
|
||||
side: THREE.DoubleSide
|
||||
// Floor
|
||||
const floorGeo = new THREE.PlaneGeometry(30, 30);
|
||||
const floorMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x222222,
|
||||
roughness: 0.9
|
||||
});
|
||||
const floor = new THREE.Mesh(floorGeo, floorMat);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(floor);
|
||||
|
||||
// Simple walls
|
||||
this.createWall(0, 2.5, -10, 20, 5); // Back
|
||||
this.createWall(0, 2.5, 10, 20, 5); // Front
|
||||
this.createWall(-10, 2.5, 0, 20, 5, true); // Left
|
||||
this.createWall(10, 2.5, 0, 20, 5, true); // Right
|
||||
this.createWall(0, 2.5, -15, 30, 5); // Back
|
||||
this.createWall(0, 2.5, 15, 30, 5); // Front
|
||||
this.createWall(-15, 2.5, 0, 30, 5, true); // Left
|
||||
this.createWall(15, 2.5, 0, 30, 5, true); // Right
|
||||
|
||||
// Add a reference cube (Bright Yellow Basic Material)
|
||||
const cube = new THREE.Mesh(
|
||||
// Add some "horror" pillars
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const px = (Math.random() - 0.5) * 20;
|
||||
const pz = (Math.random() - 0.5) * 20;
|
||||
this.createPillar(px, 2.5, pz);
|
||||
}
|
||||
|
||||
// Add a red object to find
|
||||
const target = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffff00 })
|
||||
new THREE.MeshStandardMaterial({ color: 0x880000 })
|
||||
);
|
||||
cube.position.set(0, 0.5, -5);
|
||||
this.scene.add(cube);
|
||||
this.colliders.push(cube);
|
||||
target.position.set(5, 0.5, -5);
|
||||
this.scene.add(target);
|
||||
this.colliders.push(target);
|
||||
}
|
||||
|
||||
createWall(x, y, z, width, height, rotate = false) {
|
||||
window.log(`Creating wall at ${x},${z}`);
|
||||
const geo = new THREE.BoxGeometry(width, height, 0.5);
|
||||
const mat = new THREE.MeshBasicMaterial({ color: 0x555555 });
|
||||
const mat = new THREE.MeshStandardMaterial({ color: 0x333333 });
|
||||
const wall = new THREE.Mesh(geo, mat);
|
||||
wall.position.set(x, y, z);
|
||||
if (rotate) wall.rotation.y = Math.PI / 2;
|
||||
|
||||
this.scene.add(wall);
|
||||
this.colliders.push(wall);
|
||||
}
|
||||
|
||||
createPillar(x, y, z) {
|
||||
const geo = new THREE.BoxGeometry(1, 5, 1);
|
||||
const mat = new THREE.MeshStandardMaterial({ color: 0x444444 });
|
||||
const pillar = new THREE.Mesh(geo, mat);
|
||||
pillar.position.set(x, y, z);
|
||||
this.scene.add(pillar);
|
||||
this.colliders.push(pillar);
|
||||
}
|
||||
}
|
||||
|
||||
18
src/main.js
18
src/main.js
@@ -1,3 +1,5 @@
|
||||
import { Game } from './Game.js';
|
||||
|
||||
window.log = (msg) => {
|
||||
const logDiv = document.getElementById('debug-log');
|
||||
if (logDiv) {
|
||||
@@ -9,6 +11,14 @@ window.log = (msg) => {
|
||||
console.log(msg);
|
||||
};
|
||||
|
||||
if (window.test2D) window.test2D();
|
||||
window.log(`Searching for WebGL...`);
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
window.log(gl ? "WebGL Found" : "WebGL NOT Found");
|
||||
} catch (e) { window.log("WebGL Check Error: " + e.message); }
|
||||
|
||||
// Toggle debug log with 'P'
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyP') {
|
||||
@@ -19,15 +29,7 @@ window.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
import * as THREE from 'three';
|
||||
window.log(`THREE Revision: ${THREE.REVISION}`);
|
||||
if (window.checkWebGL) window.log(`System Check: ${window.checkWebGL()}`);
|
||||
|
||||
import { Game } from './Game.js';
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.log('DOM Content Loaded');
|
||||
const game = new Game();
|
||||
game.start();
|
||||
window.log('Game Started');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user