feat: add templates/games support with subpath routing and Docker build integration
This commit is contained in:
17
games/.gitignore
vendored
Normal file
17
games/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Lockfiles
|
||||
bun.lock
|
||||
|
||||
# Vite
|
||||
.vite
|
||||
|
||||
# Build output
|
||||
dist
|
||||
|
||||
# Cache
|
||||
cache
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
29
games/eslint.config.js
Normal file
29
games/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
14
games/index.html
Normal file
14
games/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Three.js Game</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
<script src="https://content.trynoah.ai/mg-utils.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
games/package.json
Normal file
30
games/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "noah-threejs-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"watch": "vite build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "2.57.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"three": "0.160.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.9.1",
|
||||
"@types/react": "19.0.0",
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@vitejs/plugin-react-swc": "4.3.0",
|
||||
"eslint": "9.9.1",
|
||||
"eslint-plugin-react-hooks": "5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "0.4.11",
|
||||
"globals": "15.9.0",
|
||||
"vite": "8.0.8"
|
||||
}
|
||||
}
|
||||
18
games/src/App.jsx
Normal file
18
games/src/App.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ThreeScene } from "./game/ThreeScene";
|
||||
|
||||
function App() {
|
||||
const containerRef = useRef(null);
|
||||
const sceneRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current && !sceneRef.current) {
|
||||
sceneRef.current = new ThreeScene(containerRef.current);
|
||||
sceneRef.current.init();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div id="game-container" ref={containerRef}></div>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
171
games/src/game/ThreeScene.js
Normal file
171
games/src/game/ThreeScene.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
|
||||
|
||||
export class ThreeScene {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
this.container.clientWidth / this.container.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.controls = null;
|
||||
this.clock = new THREE.Clock();
|
||||
|
||||
this.playerObject = null;
|
||||
this.planetObject = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.renderer.setSize(
|
||||
this.container.clientWidth,
|
||||
this.container.clientHeight
|
||||
);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setClearColor(0x101020);
|
||||
this.container.appendChild(this.renderer.domElement);
|
||||
|
||||
this.camera.position.set(0, 10, 20);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0x404040);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(10, 15, 10);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
this.addStars();
|
||||
|
||||
this.createPlanet();
|
||||
this.createPlayer();
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.screenSpacePanning = false;
|
||||
this.controls.minDistance = 5;
|
||||
this.controls.maxDistance = 100;
|
||||
this.controls.target.set(0, 0, 0);
|
||||
|
||||
window.addEventListener("resize", () => this.onWindowResize());
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
addStars() {
|
||||
const starsGeometry = new THREE.BufferGeometry();
|
||||
const starsMaterial = new THREE.PointsMaterial({
|
||||
color: 0xffffff,
|
||||
size: 0.05,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
const starsVertices = [];
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
const x = THREE.MathUtils.randFloatSpread(200);
|
||||
const y = THREE.MathUtils.randFloatSpread(200);
|
||||
const z = THREE.MathUtils.randFloatSpread(200);
|
||||
if (Math.sqrt(x * x + y * y + z * z) > 20) {
|
||||
starsVertices.push(x, y, z);
|
||||
} else {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
starsGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(starsVertices, 3)
|
||||
);
|
||||
const stars = new THREE.Points(starsGeometry, starsMaterial);
|
||||
this.scene.add(stars);
|
||||
}
|
||||
|
||||
createPlanet() {
|
||||
const planetGeometry = new THREE.SphereGeometry(4, 32, 32);
|
||||
const planetMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0x4a6f8a,
|
||||
shininess: 10,
|
||||
});
|
||||
this.planetObject = new THREE.Mesh(planetGeometry, planetMaterial);
|
||||
this.scene.add(this.planetObject);
|
||||
}
|
||||
|
||||
createPlayer() {
|
||||
const playerGroup = new THREE.Group();
|
||||
|
||||
const bodyGeometry = new THREE.ConeGeometry(0.8, 2, 8);
|
||||
const bodyMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0xe88a5b,
|
||||
shininess: 30,
|
||||
});
|
||||
const playerBody = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
||||
playerBody.rotation.x = Math.PI / 2;
|
||||
playerGroup.add(playerBody);
|
||||
|
||||
const cockpitGeometry = new THREE.SphereGeometry(
|
||||
0.4,
|
||||
16,
|
||||
16,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
0,
|
||||
Math.PI / 2
|
||||
);
|
||||
const cockpitMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0xadd8e6,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
shininess: 50,
|
||||
});
|
||||
const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial);
|
||||
cockpit.rotation.x = Math.PI;
|
||||
cockpit.position.set(0, 0, -0.6);
|
||||
playerBody.add(cockpit);
|
||||
|
||||
this.playerObject = playerGroup;
|
||||
this.playerObject.position.set(0, 0, 10);
|
||||
this.scene.add(this.playerObject);
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect =
|
||||
this.container.clientWidth / this.container.clientHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(
|
||||
this.container.clientWidth,
|
||||
this.container.clientHeight
|
||||
);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
}
|
||||
|
||||
animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
const delta = this.clock.getDelta();
|
||||
const elapsedTime = this.clock.getElapsedTime();
|
||||
|
||||
if (this.planetObject) {
|
||||
this.planetObject.rotation.y += 0.2 * delta;
|
||||
}
|
||||
|
||||
if (this.playerObject) {
|
||||
const orbitRadius = 10;
|
||||
const orbitSpeed = 0.5;
|
||||
this.playerObject.position.x =
|
||||
Math.cos(elapsedTime * orbitSpeed) * orbitRadius;
|
||||
this.playerObject.position.z =
|
||||
Math.sin(elapsedTime * orbitSpeed) * orbitRadius;
|
||||
this.playerObject.lookAt(this.planetObject.position);
|
||||
this.playerObject.rotateY(Math.PI);
|
||||
}
|
||||
|
||||
if (this.controls) {
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
13
games/src/index.css
Normal file
13
games/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#game-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
10
games/src/main.jsx
Normal file
10
games/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
21
games/vite.config.js
Normal file
21
games/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
base: "/games/",
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 3000,
|
||||
allowedHosts: true,
|
||||
hmr: {
|
||||
overlay: false,
|
||||
timeout: 15000,
|
||||
},
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 500,
|
||||
binaryInterval: 500,
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
}));
|
||||
Reference in New Issue
Block a user