feat: add templates/games support with subpath routing and Docker build integration

This commit is contained in:
Plena Finance Dev
2026-07-09 13:09:22 +05:30
parent 65fdc108a8
commit b842bc678d
12 changed files with 342 additions and 0 deletions

17
games/.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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
View 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()],
}));