feat: initialize project with core dependencies and game entry point

This commit is contained in:
2026-01-03 01:24:51 -05:00
commit 45d46ddac6
1382 changed files with 844553 additions and 0 deletions

52
src/Game.js Normal file
View File

@@ -0,0 +1,52 @@
import { Graphics } from './Graphics.js';
import { World } from './World.js';
import { Player } from './Player.js';
export class Game {
constructor() {
this.graphics = new Graphics();
this.world = new World(this.graphics.scene);
this.player = new Player(this.graphics.camera, this.world.colliders);
this.isRunning = false;
this.lastTime = 0;
this.setupUI();
}
setupUI() {
const startScreen = document.getElementById('start-screen');
const hud = document.getElementById('hud');
startScreen.addEventListener('click', () => {
this.player.lockControls();
startScreen.style.display = 'none';
hud.style.display = 'block';
this.isRunning = true;
});
}
start() {
this.graphics.init();
this.world.load();
// Add player to scene if needed, but usually player moves camera
this.graphics.scene.add(this.player.getObject());
requestAnimationFrame(this.loop.bind(this));
}
loop(time) {
requestAnimationFrame(this.loop.bind(this));
const dt = Math.min((time - this.lastTime) / 1000, 0.1); // Cap dt
this.lastTime = time;
if (this.isRunning) {
this.player.update(dt);
// this.world.update(dt);
}
this.graphics.render();
}
}

100
src/Graphics.js Normal file
View File

@@ -0,0 +1,100 @@
import * as THREE from 'three';
export class Graphics {
constructor() {
// Main scene rendering
this.scene = new THREE.Scene();
this.scene.fog = new THREE.Fog(0x000000, 2, 15);
this.scene.background = new THREE.Color(0x000000);
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
// Real Screen Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: false });
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('game-container').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;
this.renderTarget = new THREE.WebGLRenderTarget(this.targetWidth, this.targetHeight, {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
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: 32.0 } // 32 levels per channel (5-bit)
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float colorDepth;
varying vec2 vUv;
void main() {
vec4 tex = texture2D(tDiffuse, vUv);
// Quantize color
vec3 color = floor(tex.rgb * colorDepth) / colorDepth;
gl_FragColor = vec4(color, tex.a);
}
`
});
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
}
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);
}
render() {
// 1. Render 3D Scene to RenderTarget
this.renderer.setRenderTarget(this.renderTarget);
this.renderer.render(this.scene, this.camera);
// 2. Render Post-Processing Quad to Screen
this.renderer.setRenderTarget(null);
this.renderer.render(this.postScene, this.postCamera);
}
}

109
src/Player.js Normal file
View File

@@ -0,0 +1,109 @@
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
export class Player {
constructor(camera, colliders) {
this.camera = camera;
this.colliders = colliders;
// Player stats
this.speed = 5.0;
this.height = 1.7; // Eyes height
// Init controls
this.controls = new PointerLockControls(camera, document.body);
// Movement state
this.moveForward = false;
this.moveBackward = false;
this.moveLeft = false;
this.moveRight = false;
this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.setupInput();
}
setupInput() {
const onKeyDown = (event) => {
switch (event.code) {
case 'KeyW': this.moveForward = true; break;
case 'KeyA': this.moveLeft = true; break;
case 'KeyS': this.moveBackward = true; break;
case 'KeyD': this.moveRight = true; break;
}
};
const onKeyUp = (event) => {
switch (event.code) {
case 'KeyW': this.moveForward = false; break;
case 'KeyA': this.moveLeft = false; break;
case 'KeyS': this.moveBackward = false; break;
case 'KeyD': this.moveRight = false; break;
}
};
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
}
lockControls() {
this.controls.lock();
}
getObject() {
return this.controls.getObject();
}
update(dt) {
if (!this.controls.isLocked) return;
// Friction-like dampening
// Simple direct velocity for now
this.velocity.x = 0;
this.velocity.z = 0;
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
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;
// Apply movement
this.controls.moveRight(-this.velocity.x);
this.controls.moveForward(-this.velocity.z);
// Simple Collision: Push back
const playerPos = this.controls.getObject().position;
const playerRadius = 0.5; // approximated radius
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));
const dz2 = Math.abs(playerPos.z - (box.max.z + playerRadius));
const minOverlap = Math.min(dx1, dx2, dz1, dz2);
if (minOverlap === dx1) playerPos.x = box.min.x - playerRadius;
else if (minOverlap === dx2) playerPos.x = box.max.x + playerRadius;
else if (minOverlap === dz1) playerPos.z = box.min.z - playerRadius;
else if (minOverlap === dz2) playerPos.z = box.max.z + playerRadius;
}
}
// Keep player on ground
playerPos.y = this.height;
}
}

50
src/World.js Normal file
View File

@@ -0,0 +1,50 @@
import * as THREE from 'three';
export class World {
constructor(scene) {
this.scene = scene;
this.colliders = [];
}
load() {
// Basic lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.5); // Dim ambient
this.scene.add(ambientLight);
// Floor
const floorGeo = new THREE.PlaneGeometry(20, 20);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x333333,
roughness: 0.8
});
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
// Add a reference cube to see depth
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0xff0000 })
);
cube.position.set(0, 0.5, -5);
this.scene.add(cube);
this.colliders.push(cube);
}
createWall(x, y, z, width, height, rotate = false) {
const geo = new THREE.BoxGeometry(width, height, 0.5);
const mat = new THREE.MeshStandardMaterial({ color: 0x555555 });
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);
}
}

6
src/main.js Normal file
View File

@@ -0,0 +1,6 @@
import { Game } from './Game.js';
window.addEventListener('DOMContentLoaded', () => {
const game = new Game();
game.start();
});

1
src/utils/MathUtils.js Normal file
View File

@@ -0,0 +1 @@
export const clamp = (val, min, max) => Math.max(min, Math.min(max, val));