feat: initialize project with core dependencies and game entry point
This commit is contained in:
52
src/Game.js
Normal file
52
src/Game.js
Normal 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
100
src/Graphics.js
Normal 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
109
src/Player.js
Normal 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
50
src/World.js
Normal 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
6
src/main.js
Normal 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
1
src/utils/MathUtils.js
Normal file
@@ -0,0 +1 @@
|
||||
export const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
|
||||
Reference in New Issue
Block a user