Spaces:
Running
Running
bep40 commited on
Commit ·
dfdcf94
1
Parent(s): e2ddd27
Switch to self-contained esbuild bundle: no CDN/import map (fix zustand pulling react@19 crash); drop drei; DOM name tags via projection; Docker multi-stage build
Browse files- .gitignore +2 -0
- Dockerfile +16 -6
- build/esbuild.config.mjs +18 -0
- build/package-lock.json +810 -0
- build/package.json +18 -0
- frontend/app.js +407 -348
- frontend/index.html +12 -17
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
build/node_modules/
|
| 2 |
+
build/frontend/
|
Dockerfile
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
|
| 6 |
-
RUN pip install --no-cache-dir fastapi uvicorn[standard] websockets
|
| 7 |
|
| 8 |
-
# Copy backend
|
| 9 |
COPY backend.py .
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
COPY frontend/ ./frontend/
|
| 13 |
|
| 14 |
EXPOSE 7860
|
| 15 |
|
|
|
|
| 1 |
+
FROM node:20-slim AS build
|
| 2 |
+
WORKDIR /build
|
| 3 |
+
|
| 4 |
+
# Frontend build deps
|
| 5 |
+
COPY build/package.json build/package-lock.json* ./
|
| 6 |
+
COPY frontend/app.js ./frontend/app.js
|
| 7 |
+
COPY build/esbuild.config.mjs ./
|
| 8 |
+
|
| 9 |
+
RUN npm install --no-audit --no-fund 2>&1 | tail -2 && \
|
| 10 |
+
node esbuild.config.mjs && \
|
| 11 |
+
ls -la frontend/app.bundle.js
|
| 12 |
+
|
| 13 |
+
# ---- runtime ----
|
| 14 |
FROM python:3.11-slim
|
| 15 |
|
| 16 |
WORKDIR /app
|
| 17 |
|
| 18 |
+
RUN pip install --no-cache-dir fastapi uvicorn[standard]
|
|
|
|
| 19 |
|
|
|
|
| 20 |
COPY backend.py .
|
| 21 |
+
COPY frontend/index.html ./frontend/index.html
|
| 22 |
+
COPY --from=build /build/frontend/app.bundle.js ./frontend/app.bundle.js
|
|
|
|
| 23 |
|
| 24 |
EXPOSE 7860
|
| 25 |
|
build/esbuild.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// esbuild bundle config: bundles app.js + all deps (react, three, etc.) into ONE file
|
| 2 |
+
import { build } from 'esbuild'
|
| 3 |
+
|
| 4 |
+
await build({
|
| 5 |
+
entryPoints: ['frontend/app.js'],
|
| 6 |
+
bundle: true,
|
| 7 |
+
outfile: 'frontend/app.bundle.js',
|
| 8 |
+
format: 'iife',
|
| 9 |
+
platform: 'browser',
|
| 10 |
+
target: ['es2020', 'chrome110', 'firefox115', 'safari14'],
|
| 11 |
+
minify: false,
|
| 12 |
+
sourcemap: false,
|
| 13 |
+
loader: { '.js': 'jsx' }, // allow JSX in app.js if needed
|
| 14 |
+
define: { 'process.env.NODE_ENV': '"production"' },
|
| 15 |
+
external: [],
|
| 16 |
+
logLevel: 'info',
|
| 17 |
+
})
|
| 18 |
+
console.log('bundle OK -> frontend/app.bundle.js')
|
build/package-lock.json
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hangmu-frontend-build",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {
|
| 6 |
+
"": {
|
| 7 |
+
"name": "hangmu-frontend-build",
|
| 8 |
+
"dependencies": {
|
| 9 |
+
"@react-three/fiber": "8.17.0",
|
| 10 |
+
"react": "18.3.1",
|
| 11 |
+
"react-dom": "18.3.1",
|
| 12 |
+
"three": "0.170.0",
|
| 13 |
+
"zustand": "5.0.0"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"esbuild": "^0.24.0"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"node_modules/@babel/runtime": {
|
| 20 |
+
"version": "7.29.13",
|
| 21 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
|
| 22 |
+
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
|
| 23 |
+
"license": "MIT",
|
| 24 |
+
"engines": {
|
| 25 |
+
"node": ">=6.9.0"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 29 |
+
"version": "0.24.2",
|
| 30 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
| 31 |
+
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
| 32 |
+
"cpu": [
|
| 33 |
+
"ppc64"
|
| 34 |
+
],
|
| 35 |
+
"dev": true,
|
| 36 |
+
"license": "MIT",
|
| 37 |
+
"optional": true,
|
| 38 |
+
"os": [
|
| 39 |
+
"aix"
|
| 40 |
+
],
|
| 41 |
+
"engines": {
|
| 42 |
+
"node": ">=18"
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"node_modules/@esbuild/android-arm": {
|
| 46 |
+
"version": "0.24.2",
|
| 47 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
| 48 |
+
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
| 49 |
+
"cpu": [
|
| 50 |
+
"arm"
|
| 51 |
+
],
|
| 52 |
+
"dev": true,
|
| 53 |
+
"license": "MIT",
|
| 54 |
+
"optional": true,
|
| 55 |
+
"os": [
|
| 56 |
+
"android"
|
| 57 |
+
],
|
| 58 |
+
"engines": {
|
| 59 |
+
"node": ">=18"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"node_modules/@esbuild/android-arm64": {
|
| 63 |
+
"version": "0.24.2",
|
| 64 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
| 65 |
+
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
| 66 |
+
"cpu": [
|
| 67 |
+
"arm64"
|
| 68 |
+
],
|
| 69 |
+
"dev": true,
|
| 70 |
+
"license": "MIT",
|
| 71 |
+
"optional": true,
|
| 72 |
+
"os": [
|
| 73 |
+
"android"
|
| 74 |
+
],
|
| 75 |
+
"engines": {
|
| 76 |
+
"node": ">=18"
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
"node_modules/@esbuild/android-x64": {
|
| 80 |
+
"version": "0.24.2",
|
| 81 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
| 82 |
+
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
| 83 |
+
"cpu": [
|
| 84 |
+
"x64"
|
| 85 |
+
],
|
| 86 |
+
"dev": true,
|
| 87 |
+
"license": "MIT",
|
| 88 |
+
"optional": true,
|
| 89 |
+
"os": [
|
| 90 |
+
"android"
|
| 91 |
+
],
|
| 92 |
+
"engines": {
|
| 93 |
+
"node": ">=18"
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 97 |
+
"version": "0.24.2",
|
| 98 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
| 99 |
+
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
| 100 |
+
"cpu": [
|
| 101 |
+
"arm64"
|
| 102 |
+
],
|
| 103 |
+
"dev": true,
|
| 104 |
+
"license": "MIT",
|
| 105 |
+
"optional": true,
|
| 106 |
+
"os": [
|
| 107 |
+
"darwin"
|
| 108 |
+
],
|
| 109 |
+
"engines": {
|
| 110 |
+
"node": ">=18"
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 114 |
+
"version": "0.24.2",
|
| 115 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
| 116 |
+
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
| 117 |
+
"cpu": [
|
| 118 |
+
"x64"
|
| 119 |
+
],
|
| 120 |
+
"dev": true,
|
| 121 |
+
"license": "MIT",
|
| 122 |
+
"optional": true,
|
| 123 |
+
"os": [
|
| 124 |
+
"darwin"
|
| 125 |
+
],
|
| 126 |
+
"engines": {
|
| 127 |
+
"node": ">=18"
|
| 128 |
+
}
|
| 129 |
+
},
|
| 130 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 131 |
+
"version": "0.24.2",
|
| 132 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
| 133 |
+
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
| 134 |
+
"cpu": [
|
| 135 |
+
"arm64"
|
| 136 |
+
],
|
| 137 |
+
"dev": true,
|
| 138 |
+
"license": "MIT",
|
| 139 |
+
"optional": true,
|
| 140 |
+
"os": [
|
| 141 |
+
"freebsd"
|
| 142 |
+
],
|
| 143 |
+
"engines": {
|
| 144 |
+
"node": ">=18"
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 148 |
+
"version": "0.24.2",
|
| 149 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
| 150 |
+
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
| 151 |
+
"cpu": [
|
| 152 |
+
"x64"
|
| 153 |
+
],
|
| 154 |
+
"dev": true,
|
| 155 |
+
"license": "MIT",
|
| 156 |
+
"optional": true,
|
| 157 |
+
"os": [
|
| 158 |
+
"freebsd"
|
| 159 |
+
],
|
| 160 |
+
"engines": {
|
| 161 |
+
"node": ">=18"
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"node_modules/@esbuild/linux-arm": {
|
| 165 |
+
"version": "0.24.2",
|
| 166 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
| 167 |
+
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
| 168 |
+
"cpu": [
|
| 169 |
+
"arm"
|
| 170 |
+
],
|
| 171 |
+
"dev": true,
|
| 172 |
+
"license": "MIT",
|
| 173 |
+
"optional": true,
|
| 174 |
+
"os": [
|
| 175 |
+
"linux"
|
| 176 |
+
],
|
| 177 |
+
"engines": {
|
| 178 |
+
"node": ">=18"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 182 |
+
"version": "0.24.2",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
| 184 |
+
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
| 185 |
+
"cpu": [
|
| 186 |
+
"arm64"
|
| 187 |
+
],
|
| 188 |
+
"dev": true,
|
| 189 |
+
"license": "MIT",
|
| 190 |
+
"optional": true,
|
| 191 |
+
"os": [
|
| 192 |
+
"linux"
|
| 193 |
+
],
|
| 194 |
+
"engines": {
|
| 195 |
+
"node": ">=18"
|
| 196 |
+
}
|
| 197 |
+
},
|
| 198 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 199 |
+
"version": "0.24.2",
|
| 200 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
| 201 |
+
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
| 202 |
+
"cpu": [
|
| 203 |
+
"ia32"
|
| 204 |
+
],
|
| 205 |
+
"dev": true,
|
| 206 |
+
"license": "MIT",
|
| 207 |
+
"optional": true,
|
| 208 |
+
"os": [
|
| 209 |
+
"linux"
|
| 210 |
+
],
|
| 211 |
+
"engines": {
|
| 212 |
+
"node": ">=18"
|
| 213 |
+
}
|
| 214 |
+
},
|
| 215 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 216 |
+
"version": "0.24.2",
|
| 217 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
| 218 |
+
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
| 219 |
+
"cpu": [
|
| 220 |
+
"loong64"
|
| 221 |
+
],
|
| 222 |
+
"dev": true,
|
| 223 |
+
"license": "MIT",
|
| 224 |
+
"optional": true,
|
| 225 |
+
"os": [
|
| 226 |
+
"linux"
|
| 227 |
+
],
|
| 228 |
+
"engines": {
|
| 229 |
+
"node": ">=18"
|
| 230 |
+
}
|
| 231 |
+
},
|
| 232 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 233 |
+
"version": "0.24.2",
|
| 234 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
| 235 |
+
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
| 236 |
+
"cpu": [
|
| 237 |
+
"mips64el"
|
| 238 |
+
],
|
| 239 |
+
"dev": true,
|
| 240 |
+
"license": "MIT",
|
| 241 |
+
"optional": true,
|
| 242 |
+
"os": [
|
| 243 |
+
"linux"
|
| 244 |
+
],
|
| 245 |
+
"engines": {
|
| 246 |
+
"node": ">=18"
|
| 247 |
+
}
|
| 248 |
+
},
|
| 249 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 250 |
+
"version": "0.24.2",
|
| 251 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
| 252 |
+
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
| 253 |
+
"cpu": [
|
| 254 |
+
"ppc64"
|
| 255 |
+
],
|
| 256 |
+
"dev": true,
|
| 257 |
+
"license": "MIT",
|
| 258 |
+
"optional": true,
|
| 259 |
+
"os": [
|
| 260 |
+
"linux"
|
| 261 |
+
],
|
| 262 |
+
"engines": {
|
| 263 |
+
"node": ">=18"
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 267 |
+
"version": "0.24.2",
|
| 268 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
| 269 |
+
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
| 270 |
+
"cpu": [
|
| 271 |
+
"riscv64"
|
| 272 |
+
],
|
| 273 |
+
"dev": true,
|
| 274 |
+
"license": "MIT",
|
| 275 |
+
"optional": true,
|
| 276 |
+
"os": [
|
| 277 |
+
"linux"
|
| 278 |
+
],
|
| 279 |
+
"engines": {
|
| 280 |
+
"node": ">=18"
|
| 281 |
+
}
|
| 282 |
+
},
|
| 283 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 284 |
+
"version": "0.24.2",
|
| 285 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
| 286 |
+
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
| 287 |
+
"cpu": [
|
| 288 |
+
"s390x"
|
| 289 |
+
],
|
| 290 |
+
"dev": true,
|
| 291 |
+
"license": "MIT",
|
| 292 |
+
"optional": true,
|
| 293 |
+
"os": [
|
| 294 |
+
"linux"
|
| 295 |
+
],
|
| 296 |
+
"engines": {
|
| 297 |
+
"node": ">=18"
|
| 298 |
+
}
|
| 299 |
+
},
|
| 300 |
+
"node_modules/@esbuild/linux-x64": {
|
| 301 |
+
"version": "0.24.2",
|
| 302 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
| 303 |
+
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
| 304 |
+
"cpu": [
|
| 305 |
+
"x64"
|
| 306 |
+
],
|
| 307 |
+
"dev": true,
|
| 308 |
+
"license": "MIT",
|
| 309 |
+
"optional": true,
|
| 310 |
+
"os": [
|
| 311 |
+
"linux"
|
| 312 |
+
],
|
| 313 |
+
"engines": {
|
| 314 |
+
"node": ">=18"
|
| 315 |
+
}
|
| 316 |
+
},
|
| 317 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 318 |
+
"version": "0.24.2",
|
| 319 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
| 320 |
+
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
| 321 |
+
"cpu": [
|
| 322 |
+
"arm64"
|
| 323 |
+
],
|
| 324 |
+
"dev": true,
|
| 325 |
+
"license": "MIT",
|
| 326 |
+
"optional": true,
|
| 327 |
+
"os": [
|
| 328 |
+
"netbsd"
|
| 329 |
+
],
|
| 330 |
+
"engines": {
|
| 331 |
+
"node": ">=18"
|
| 332 |
+
}
|
| 333 |
+
},
|
| 334 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 335 |
+
"version": "0.24.2",
|
| 336 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
| 337 |
+
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
| 338 |
+
"cpu": [
|
| 339 |
+
"x64"
|
| 340 |
+
],
|
| 341 |
+
"dev": true,
|
| 342 |
+
"license": "MIT",
|
| 343 |
+
"optional": true,
|
| 344 |
+
"os": [
|
| 345 |
+
"netbsd"
|
| 346 |
+
],
|
| 347 |
+
"engines": {
|
| 348 |
+
"node": ">=18"
|
| 349 |
+
}
|
| 350 |
+
},
|
| 351 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 352 |
+
"version": "0.24.2",
|
| 353 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
| 354 |
+
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
| 355 |
+
"cpu": [
|
| 356 |
+
"arm64"
|
| 357 |
+
],
|
| 358 |
+
"dev": true,
|
| 359 |
+
"license": "MIT",
|
| 360 |
+
"optional": true,
|
| 361 |
+
"os": [
|
| 362 |
+
"openbsd"
|
| 363 |
+
],
|
| 364 |
+
"engines": {
|
| 365 |
+
"node": ">=18"
|
| 366 |
+
}
|
| 367 |
+
},
|
| 368 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 369 |
+
"version": "0.24.2",
|
| 370 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
| 371 |
+
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
| 372 |
+
"cpu": [
|
| 373 |
+
"x64"
|
| 374 |
+
],
|
| 375 |
+
"dev": true,
|
| 376 |
+
"license": "MIT",
|
| 377 |
+
"optional": true,
|
| 378 |
+
"os": [
|
| 379 |
+
"openbsd"
|
| 380 |
+
],
|
| 381 |
+
"engines": {
|
| 382 |
+
"node": ">=18"
|
| 383 |
+
}
|
| 384 |
+
},
|
| 385 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 386 |
+
"version": "0.24.2",
|
| 387 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
| 388 |
+
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
| 389 |
+
"cpu": [
|
| 390 |
+
"x64"
|
| 391 |
+
],
|
| 392 |
+
"dev": true,
|
| 393 |
+
"license": "MIT",
|
| 394 |
+
"optional": true,
|
| 395 |
+
"os": [
|
| 396 |
+
"sunos"
|
| 397 |
+
],
|
| 398 |
+
"engines": {
|
| 399 |
+
"node": ">=18"
|
| 400 |
+
}
|
| 401 |
+
},
|
| 402 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 403 |
+
"version": "0.24.2",
|
| 404 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
| 405 |
+
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
| 406 |
+
"cpu": [
|
| 407 |
+
"arm64"
|
| 408 |
+
],
|
| 409 |
+
"dev": true,
|
| 410 |
+
"license": "MIT",
|
| 411 |
+
"optional": true,
|
| 412 |
+
"os": [
|
| 413 |
+
"win32"
|
| 414 |
+
],
|
| 415 |
+
"engines": {
|
| 416 |
+
"node": ">=18"
|
| 417 |
+
}
|
| 418 |
+
},
|
| 419 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 420 |
+
"version": "0.24.2",
|
| 421 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
| 422 |
+
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
| 423 |
+
"cpu": [
|
| 424 |
+
"ia32"
|
| 425 |
+
],
|
| 426 |
+
"dev": true,
|
| 427 |
+
"license": "MIT",
|
| 428 |
+
"optional": true,
|
| 429 |
+
"os": [
|
| 430 |
+
"win32"
|
| 431 |
+
],
|
| 432 |
+
"engines": {
|
| 433 |
+
"node": ">=18"
|
| 434 |
+
}
|
| 435 |
+
},
|
| 436 |
+
"node_modules/@esbuild/win32-x64": {
|
| 437 |
+
"version": "0.24.2",
|
| 438 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
| 439 |
+
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
| 440 |
+
"cpu": [
|
| 441 |
+
"x64"
|
| 442 |
+
],
|
| 443 |
+
"dev": true,
|
| 444 |
+
"license": "MIT",
|
| 445 |
+
"optional": true,
|
| 446 |
+
"os": [
|
| 447 |
+
"win32"
|
| 448 |
+
],
|
| 449 |
+
"engines": {
|
| 450 |
+
"node": ">=18"
|
| 451 |
+
}
|
| 452 |
+
},
|
| 453 |
+
"node_modules/@react-three/fiber": {
|
| 454 |
+
"version": "8.17.0",
|
| 455 |
+
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.17.0.tgz",
|
| 456 |
+
"integrity": "sha512-b/xxrehBQc/fZwDNRhdlG7Ik69Mv+P1RkHohRsbpkDY+rSC3ChtC7Di6e7Ueu5Mx1/b5FE8QYEzcVVU8rdBfKg==",
|
| 457 |
+
"license": "MIT",
|
| 458 |
+
"dependencies": {
|
| 459 |
+
"@babel/runtime": "^7.17.8",
|
| 460 |
+
"@types/debounce": "^1.2.1",
|
| 461 |
+
"@types/react-reconciler": "^0.26.7",
|
| 462 |
+
"@types/webxr": "*",
|
| 463 |
+
"base64-js": "^1.5.1",
|
| 464 |
+
"buffer": "^6.0.3",
|
| 465 |
+
"debounce": "^1.2.1",
|
| 466 |
+
"its-fine": "^1.0.6",
|
| 467 |
+
"react-reconciler": "^0.27.0",
|
| 468 |
+
"scheduler": "^0.21.0",
|
| 469 |
+
"suspend-react": "^0.1.3",
|
| 470 |
+
"zustand": "^3.7.1"
|
| 471 |
+
},
|
| 472 |
+
"peerDependencies": {
|
| 473 |
+
"expo": ">=43.0",
|
| 474 |
+
"expo-asset": ">=8.4",
|
| 475 |
+
"expo-file-system": ">=11.0",
|
| 476 |
+
"expo-gl": ">=11.0",
|
| 477 |
+
"react": ">=18.0",
|
| 478 |
+
"react-dom": ">=18.0",
|
| 479 |
+
"react-native": ">=0.64",
|
| 480 |
+
"three": ">=0.133"
|
| 481 |
+
},
|
| 482 |
+
"peerDependenciesMeta": {
|
| 483 |
+
"expo": {
|
| 484 |
+
"optional": true
|
| 485 |
+
},
|
| 486 |
+
"expo-asset": {
|
| 487 |
+
"optional": true
|
| 488 |
+
},
|
| 489 |
+
"expo-file-system": {
|
| 490 |
+
"optional": true
|
| 491 |
+
},
|
| 492 |
+
"expo-gl": {
|
| 493 |
+
"optional": true
|
| 494 |
+
},
|
| 495 |
+
"react-dom": {
|
| 496 |
+
"optional": true
|
| 497 |
+
},
|
| 498 |
+
"react-native": {
|
| 499 |
+
"optional": true
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
},
|
| 503 |
+
"node_modules/@react-three/fiber/node_modules/zustand": {
|
| 504 |
+
"version": "3.7.2",
|
| 505 |
+
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
|
| 506 |
+
"integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
|
| 507 |
+
"license": "MIT",
|
| 508 |
+
"engines": {
|
| 509 |
+
"node": ">=12.7.0"
|
| 510 |
+
},
|
| 511 |
+
"peerDependencies": {
|
| 512 |
+
"react": ">=16.8"
|
| 513 |
+
},
|
| 514 |
+
"peerDependenciesMeta": {
|
| 515 |
+
"react": {
|
| 516 |
+
"optional": true
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
},
|
| 520 |
+
"node_modules/@types/debounce": {
|
| 521 |
+
"version": "1.2.4",
|
| 522 |
+
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz",
|
| 523 |
+
"integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==",
|
| 524 |
+
"license": "MIT"
|
| 525 |
+
},
|
| 526 |
+
"node_modules/@types/react": {
|
| 527 |
+
"version": "19.3.0",
|
| 528 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
| 529 |
+
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
| 530 |
+
"license": "MIT",
|
| 531 |
+
"dependencies": {
|
| 532 |
+
"csstype": "^3.2.2"
|
| 533 |
+
}
|
| 534 |
+
},
|
| 535 |
+
"node_modules/@types/react-reconciler": {
|
| 536 |
+
"version": "0.26.7",
|
| 537 |
+
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
|
| 538 |
+
"integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
|
| 539 |
+
"license": "MIT",
|
| 540 |
+
"dependencies": {
|
| 541 |
+
"@types/react": "*"
|
| 542 |
+
}
|
| 543 |
+
},
|
| 544 |
+
"node_modules/@types/webxr": {
|
| 545 |
+
"version": "0.5.24",
|
| 546 |
+
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
| 547 |
+
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
| 548 |
+
"license": "MIT"
|
| 549 |
+
},
|
| 550 |
+
"node_modules/base64-js": {
|
| 551 |
+
"version": "1.5.1",
|
| 552 |
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 553 |
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 554 |
+
"funding": [
|
| 555 |
+
{
|
| 556 |
+
"type": "github",
|
| 557 |
+
"url": "https://github.com/sponsors/feross"
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"type": "patreon",
|
| 561 |
+
"url": "https://www.patreon.com/feross"
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"type": "consulting",
|
| 565 |
+
"url": "https://feross.org/support"
|
| 566 |
+
}
|
| 567 |
+
],
|
| 568 |
+
"license": "MIT"
|
| 569 |
+
},
|
| 570 |
+
"node_modules/buffer": {
|
| 571 |
+
"version": "6.0.3",
|
| 572 |
+
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
| 573 |
+
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
| 574 |
+
"funding": [
|
| 575 |
+
{
|
| 576 |
+
"type": "github",
|
| 577 |
+
"url": "https://github.com/sponsors/feross"
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"type": "patreon",
|
| 581 |
+
"url": "https://www.patreon.com/feross"
|
| 582 |
+
},
|
| 583 |
+
{
|
| 584 |
+
"type": "consulting",
|
| 585 |
+
"url": "https://feross.org/support"
|
| 586 |
+
}
|
| 587 |
+
],
|
| 588 |
+
"license": "MIT",
|
| 589 |
+
"dependencies": {
|
| 590 |
+
"base64-js": "^1.3.1",
|
| 591 |
+
"ieee754": "^1.2.1"
|
| 592 |
+
}
|
| 593 |
+
},
|
| 594 |
+
"node_modules/csstype": {
|
| 595 |
+
"version": "3.2.3",
|
| 596 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 597 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 598 |
+
"license": "MIT"
|
| 599 |
+
},
|
| 600 |
+
"node_modules/debounce": {
|
| 601 |
+
"version": "1.2.1",
|
| 602 |
+
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
| 603 |
+
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
| 604 |
+
"license": "MIT"
|
| 605 |
+
},
|
| 606 |
+
"node_modules/esbuild": {
|
| 607 |
+
"version": "0.24.2",
|
| 608 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
| 609 |
+
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
| 610 |
+
"dev": true,
|
| 611 |
+
"hasInstallScript": true,
|
| 612 |
+
"license": "MIT",
|
| 613 |
+
"bin": {
|
| 614 |
+
"esbuild": "bin/esbuild"
|
| 615 |
+
},
|
| 616 |
+
"engines": {
|
| 617 |
+
"node": ">=18"
|
| 618 |
+
},
|
| 619 |
+
"optionalDependencies": {
|
| 620 |
+
"@esbuild/aix-ppc64": "0.24.2",
|
| 621 |
+
"@esbuild/android-arm": "0.24.2",
|
| 622 |
+
"@esbuild/android-arm64": "0.24.2",
|
| 623 |
+
"@esbuild/android-x64": "0.24.2",
|
| 624 |
+
"@esbuild/darwin-arm64": "0.24.2",
|
| 625 |
+
"@esbuild/darwin-x64": "0.24.2",
|
| 626 |
+
"@esbuild/freebsd-arm64": "0.24.2",
|
| 627 |
+
"@esbuild/freebsd-x64": "0.24.2",
|
| 628 |
+
"@esbuild/linux-arm": "0.24.2",
|
| 629 |
+
"@esbuild/linux-arm64": "0.24.2",
|
| 630 |
+
"@esbuild/linux-ia32": "0.24.2",
|
| 631 |
+
"@esbuild/linux-loong64": "0.24.2",
|
| 632 |
+
"@esbuild/linux-mips64el": "0.24.2",
|
| 633 |
+
"@esbuild/linux-ppc64": "0.24.2",
|
| 634 |
+
"@esbuild/linux-riscv64": "0.24.2",
|
| 635 |
+
"@esbuild/linux-s390x": "0.24.2",
|
| 636 |
+
"@esbuild/linux-x64": "0.24.2",
|
| 637 |
+
"@esbuild/netbsd-arm64": "0.24.2",
|
| 638 |
+
"@esbuild/netbsd-x64": "0.24.2",
|
| 639 |
+
"@esbuild/openbsd-arm64": "0.24.2",
|
| 640 |
+
"@esbuild/openbsd-x64": "0.24.2",
|
| 641 |
+
"@esbuild/sunos-x64": "0.24.2",
|
| 642 |
+
"@esbuild/win32-arm64": "0.24.2",
|
| 643 |
+
"@esbuild/win32-ia32": "0.24.2",
|
| 644 |
+
"@esbuild/win32-x64": "0.24.2"
|
| 645 |
+
}
|
| 646 |
+
},
|
| 647 |
+
"node_modules/ieee754": {
|
| 648 |
+
"version": "1.2.1",
|
| 649 |
+
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
| 650 |
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
| 651 |
+
"funding": [
|
| 652 |
+
{
|
| 653 |
+
"type": "github",
|
| 654 |
+
"url": "https://github.com/sponsors/feross"
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"type": "patreon",
|
| 658 |
+
"url": "https://www.patreon.com/feross"
|
| 659 |
+
},
|
| 660 |
+
{
|
| 661 |
+
"type": "consulting",
|
| 662 |
+
"url": "https://feross.org/support"
|
| 663 |
+
}
|
| 664 |
+
],
|
| 665 |
+
"license": "BSD-3-Clause"
|
| 666 |
+
},
|
| 667 |
+
"node_modules/its-fine": {
|
| 668 |
+
"version": "1.2.5",
|
| 669 |
+
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
|
| 670 |
+
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
|
| 671 |
+
"license": "MIT",
|
| 672 |
+
"dependencies": {
|
| 673 |
+
"@types/react-reconciler": "^0.28.0"
|
| 674 |
+
},
|
| 675 |
+
"peerDependencies": {
|
| 676 |
+
"react": ">=18.0"
|
| 677 |
+
}
|
| 678 |
+
},
|
| 679 |
+
"node_modules/its-fine/node_modules/@types/react-reconciler": {
|
| 680 |
+
"version": "0.28.9",
|
| 681 |
+
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
| 682 |
+
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
| 683 |
+
"license": "MIT",
|
| 684 |
+
"peerDependencies": {
|
| 685 |
+
"@types/react": "*"
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
"node_modules/js-tokens": {
|
| 689 |
+
"version": "4.0.0",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 691 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 692 |
+
"license": "MIT"
|
| 693 |
+
},
|
| 694 |
+
"node_modules/loose-envify": {
|
| 695 |
+
"version": "1.4.0",
|
| 696 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 697 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 698 |
+
"license": "MIT",
|
| 699 |
+
"dependencies": {
|
| 700 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 701 |
+
},
|
| 702 |
+
"bin": {
|
| 703 |
+
"loose-envify": "cli.js"
|
| 704 |
+
}
|
| 705 |
+
},
|
| 706 |
+
"node_modules/react": {
|
| 707 |
+
"version": "18.3.1",
|
| 708 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 709 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 710 |
+
"license": "MIT",
|
| 711 |
+
"dependencies": {
|
| 712 |
+
"loose-envify": "^1.1.0"
|
| 713 |
+
},
|
| 714 |
+
"engines": {
|
| 715 |
+
"node": ">=0.10.0"
|
| 716 |
+
}
|
| 717 |
+
},
|
| 718 |
+
"node_modules/react-dom": {
|
| 719 |
+
"version": "18.3.1",
|
| 720 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 721 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 722 |
+
"license": "MIT",
|
| 723 |
+
"dependencies": {
|
| 724 |
+
"loose-envify": "^1.1.0",
|
| 725 |
+
"scheduler": "^0.23.2"
|
| 726 |
+
},
|
| 727 |
+
"peerDependencies": {
|
| 728 |
+
"react": "^18.3.1"
|
| 729 |
+
}
|
| 730 |
+
},
|
| 731 |
+
"node_modules/react-dom/node_modules/scheduler": {
|
| 732 |
+
"version": "0.23.2",
|
| 733 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 734 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 735 |
+
"license": "MIT",
|
| 736 |
+
"dependencies": {
|
| 737 |
+
"loose-envify": "^1.1.0"
|
| 738 |
+
}
|
| 739 |
+
},
|
| 740 |
+
"node_modules/react-reconciler": {
|
| 741 |
+
"version": "0.27.0",
|
| 742 |
+
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
|
| 743 |
+
"integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
|
| 744 |
+
"license": "MIT",
|
| 745 |
+
"dependencies": {
|
| 746 |
+
"loose-envify": "^1.1.0",
|
| 747 |
+
"scheduler": "^0.21.0"
|
| 748 |
+
},
|
| 749 |
+
"engines": {
|
| 750 |
+
"node": ">=0.10.0"
|
| 751 |
+
},
|
| 752 |
+
"peerDependencies": {
|
| 753 |
+
"react": "^18.0.0"
|
| 754 |
+
}
|
| 755 |
+
},
|
| 756 |
+
"node_modules/scheduler": {
|
| 757 |
+
"version": "0.21.0",
|
| 758 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
| 759 |
+
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
| 760 |
+
"license": "MIT",
|
| 761 |
+
"dependencies": {
|
| 762 |
+
"loose-envify": "^1.1.0"
|
| 763 |
+
}
|
| 764 |
+
},
|
| 765 |
+
"node_modules/suspend-react": {
|
| 766 |
+
"version": "0.1.3",
|
| 767 |
+
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
| 768 |
+
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
| 769 |
+
"license": "MIT",
|
| 770 |
+
"peerDependencies": {
|
| 771 |
+
"react": ">=17.0"
|
| 772 |
+
}
|
| 773 |
+
},
|
| 774 |
+
"node_modules/three": {
|
| 775 |
+
"version": "0.170.0",
|
| 776 |
+
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
| 777 |
+
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
| 778 |
+
"license": "MIT"
|
| 779 |
+
},
|
| 780 |
+
"node_modules/zustand": {
|
| 781 |
+
"version": "5.0.0",
|
| 782 |
+
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz",
|
| 783 |
+
"integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==",
|
| 784 |
+
"license": "MIT",
|
| 785 |
+
"engines": {
|
| 786 |
+
"node": ">=12.20.0"
|
| 787 |
+
},
|
| 788 |
+
"peerDependencies": {
|
| 789 |
+
"@types/react": ">=18.0.0",
|
| 790 |
+
"immer": ">=9.0.6",
|
| 791 |
+
"react": ">=18.0.0",
|
| 792 |
+
"use-sync-external-store": ">=1.2.0"
|
| 793 |
+
},
|
| 794 |
+
"peerDependenciesMeta": {
|
| 795 |
+
"@types/react": {
|
| 796 |
+
"optional": true
|
| 797 |
+
},
|
| 798 |
+
"immer": {
|
| 799 |
+
"optional": true
|
| 800 |
+
},
|
| 801 |
+
"react": {
|
| 802 |
+
"optional": true
|
| 803 |
+
},
|
| 804 |
+
"use-sync-external-store": {
|
| 805 |
+
"optional": true
|
| 806 |
+
}
|
| 807 |
+
}
|
| 808 |
+
}
|
| 809 |
+
}
|
| 810 |
+
}
|
build/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hangmu-frontend-build",
|
| 3 |
+
"private": true,
|
| 4 |
+
"type": "module",
|
| 5 |
+
"scripts": {
|
| 6 |
+
"build": "node esbuild.config.mjs"
|
| 7 |
+
},
|
| 8 |
+
"dependencies": {
|
| 9 |
+
"react": "18.3.1",
|
| 10 |
+
"react-dom": "18.3.1",
|
| 11 |
+
"three": "0.170.0",
|
| 12 |
+
"@react-three/fiber": "8.17.0",
|
| 13 |
+
"zustand": "5.0.0"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"esbuild": "^0.24.0"
|
| 17 |
+
}
|
| 18 |
+
}
|
frontend/app.js
CHANGED
|
@@ -1,479 +1,538 @@
|
|
| 1 |
import React from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
| 3 |
-
import { Canvas } from '@react-three/fiber'
|
| 4 |
-
import { OrbitControls, Html } from '@react-three/drei'
|
| 5 |
import * as THREE from 'three'
|
|
|
|
| 6 |
import { create } from 'zustand'
|
| 7 |
-
import { useFrame } from '@react-three/fiber'
|
| 8 |
|
| 9 |
-
/
|
| 10 |
-
|
|
|
|
| 11 |
|
|
|
|
| 12 |
const useGameStore = create((set, get) => ({
|
| 13 |
isConnected: false,
|
| 14 |
ws: null,
|
| 15 |
localPlayerId: null,
|
| 16 |
playerName: '',
|
| 17 |
-
players: {},
|
| 18 |
messages: [],
|
| 19 |
-
|
| 20 |
-
setConnected: (
|
| 21 |
setLocalPlayerId: (id) => set({ localPlayerId: id }),
|
| 22 |
-
setPlayerName: (
|
| 23 |
-
|
| 24 |
initWebSocket: (playerId) => {
|
| 25 |
-
|
| 26 |
-
const
|
| 27 |
-
const wsUrl = `${protocol}//${window.location.host}/ws/${playerId}`
|
| 28 |
-
const ws = new WebSocket(wsUrl)
|
| 29 |
-
|
| 30 |
ws.onopen = () => {
|
| 31 |
-
console.log('
|
| 32 |
set({ isConnected: true })
|
| 33 |
-
ws.send(JSON.stringify({
|
| 34 |
-
type: 'join_room',
|
| 35 |
-
room_id: 'lobby',
|
| 36 |
-
player_name: get().playerName || `Player_${playerId.slice(0,4)}`
|
| 37 |
-
}))
|
| 38 |
}
|
| 39 |
ws.onclose = () => {
|
| 40 |
-
console.log('
|
| 41 |
set({ isConnected: false })
|
| 42 |
-
setTimeout(() => {
|
| 43 |
-
if (get().localPlayerId) get().initWebSocket(get().localPlayerId)
|
| 44 |
-
}, 3000)
|
| 45 |
}
|
| 46 |
-
ws.onerror = (e) => console.error('WS error
|
| 47 |
ws.onmessage = (e) => {
|
| 48 |
-
try { get().handleMessage(JSON.parse(e.data)) } catch (err) { console.error('
|
| 49 |
}
|
| 50 |
set({ ws })
|
| 51 |
},
|
| 52 |
-
|
| 53 |
closeWebSocket: () => { const { ws } = get(); if (ws) { ws.close(); set({ ws: null, isConnected: false }) } },
|
| 54 |
-
|
| 55 |
sendPosition: (position, rotation, animation) => {
|
| 56 |
const { ws } = get()
|
| 57 |
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 58 |
-
ws.send(JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
},
|
| 61 |
-
|
| 62 |
sendChat: (message) => {
|
| 63 |
const { ws } = get()
|
| 64 |
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 65 |
-
ws.send(JSON.stringify({ type: 'chat', message }))
|
| 66 |
-
}
|
| 67 |
},
|
| 68 |
-
|
| 69 |
handleMessage: (data) => {
|
|
|
|
| 70 |
switch (data.type) {
|
| 71 |
-
case 'connected':
|
| 72 |
-
break
|
| 73 |
case 'room_joined':
|
| 74 |
set({ players: data.players, localPlayerId: data.player_id })
|
| 75 |
break
|
| 76 |
-
case 'player_joined':
|
| 77 |
-
|
|
|
|
| 78 |
break
|
|
|
|
| 79 |
case 'player_left':
|
| 80 |
set(s => { const np = { ...s.players }; delete np[data.player_id]; return { players: np } })
|
| 81 |
break
|
| 82 |
-
case 'player_update':
|
| 83 |
-
set(s =>
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
}
|
| 94 |
-
})
|
| 95 |
break
|
|
|
|
| 96 |
case 'chat':
|
| 97 |
set(s => ({ messages: [...s.messages, { playerId: data.player_id, playerName: data.player_name, message: data.message, timestamp: data.timestamp }].slice(-100) }))
|
| 98 |
break
|
| 99 |
case 'pong': break
|
| 100 |
-
case 'error': console.error('
|
| 101 |
}
|
| 102 |
}
|
| 103 |
}))
|
| 104 |
|
| 105 |
-
/
|
|
|
|
|
|
|
|
|
|
| 106 |
function CaveScene() {
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
const
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
})
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
)
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
React.createElement('mesh', { castShadow: true, rotation: [-Math.sin(bob * animSpeed) * 0.8, 0, 0] },
|
| 207 |
-
React.createElement('capsuleGeometry', { args: [0.18, 0.9, 4, 8] }),
|
| 208 |
-
React.createElement('meshStandardMaterial', { color: 0x1a1a2e, roughness: 0.8, metalness: 0.1 })
|
| 209 |
-
)
|
| 210 |
-
),
|
| 211 |
-
// Name tag
|
| 212 |
-
React.createElement(Html, { position: [0, 3.2, 0], prepend: true, fullscreen: true, wrapperClass: 'player-name-tag', distanceFactor: 10 }, name)
|
| 213 |
)
|
| 214 |
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
| 226 |
)
|
| 227 |
}
|
| 228 |
|
| 229 |
function ConnectionStatus({ status }) {
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
)
|
| 237 |
}
|
| 238 |
|
| 239 |
function LoadingScreen({ visible }) {
|
| 240 |
if (!visible) return null
|
| 241 |
-
return
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
| 244 |
)
|
| 245 |
}
|
| 246 |
|
| 247 |
function LoginModal({ onLogin }) {
|
| 248 |
const [name, setName] = React.useState('')
|
|
|
|
| 249 |
const [error, setError] = React.useState('')
|
| 250 |
-
|
| 251 |
-
const
|
| 252 |
e.preventDefault()
|
| 253 |
-
|
| 254 |
-
setError('')
|
| 255 |
-
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
-
|
| 258 |
-
return
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
| 276 |
)
|
| 277 |
}
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
| 281 |
const [input, setInput] = React.useState('')
|
| 282 |
-
const
|
| 283 |
-
|
| 284 |
-
React.useEffect(() => {
|
| 285 |
-
|
| 286 |
-
const
|
| 287 |
e.preventDefault()
|
| 288 |
if (!input.trim()) return
|
| 289 |
-
|
| 290 |
setInput('')
|
| 291 |
}
|
| 292 |
-
|
| 293 |
-
return
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
)
|
| 320 |
-
)
|
| 321 |
-
)
|
| 322 |
)
|
| 323 |
}
|
| 324 |
|
|
|
|
| 325 |
function BottomControls({ onChatToggle, showChat }) {
|
| 326 |
-
const
|
|
|
|
|
|
|
| 327 |
const lp = localPlayerId ? players[localPlayerId] : null
|
| 328 |
-
const
|
| 329 |
-
const
|
| 330 |
-
|
| 331 |
-
const rotRef = React.useRef(0)
|
| 332 |
-
|
| 333 |
-
// Keyboard controls
|
| 334 |
React.useEffect(() => {
|
| 335 |
const down = (e) => {
|
| 336 |
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
| 337 |
-
|
| 338 |
-
if (e.code === '
|
| 339 |
-
if (e.code === 'KeyS' || e.code === 'ArrowDown') { keys.current.s = true; if (anim === 'idle') setAnim('walk') }
|
| 340 |
-
if (e.code === 'KeyA' || e.code === 'ArrowLeft') { keys.current.a = true; if (anim === 'idle') setAnim('walk') }
|
| 341 |
-
if (e.code === 'KeyD' || e.code === 'ArrowRight') { keys.current.d = true; if (anim === 'idle') setAnim('walk') }
|
| 342 |
}
|
| 343 |
const up = (e) => {
|
| 344 |
-
|
| 345 |
-
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight')
|
| 346 |
-
if (e.code === 'KeyW' || e.code === 'ArrowUp') { keys.current.w = false }
|
| 347 |
-
if (e.code === 'KeyS' || e.code === 'ArrowDown') { keys.current.s = false }
|
| 348 |
-
if (e.code === 'KeyA' || e.code === 'ArrowLeft') { keys.current.a = false }
|
| 349 |
-
if (e.code === 'KeyD' || e.code === 'ArrowRight') { keys.current.d = false }
|
| 350 |
-
if (!keys.current.w && !keys.current.s && !keys.current.a && !keys.current.d && !keys.current.shift) setAnim('idle')
|
| 351 |
}
|
| 352 |
window.addEventListener('keydown', down)
|
| 353 |
window.addEventListener('keyup', up)
|
| 354 |
return () => { window.removeEventListener('keydown', down); window.removeEventListener('keyup', up) }
|
| 355 |
-
}, [
|
| 356 |
-
|
| 357 |
-
//
|
| 358 |
React.useEffect(() => {
|
| 359 |
-
let
|
| 360 |
const loop = (now) => {
|
| 361 |
-
const dt = (now -
|
| 362 |
-
lastTime = now
|
| 363 |
-
|
| 364 |
if (lp) {
|
| 365 |
-
|
| 366 |
-
const
|
| 367 |
-
const
|
| 368 |
-
const
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
const
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
const newZ = lp.position.z + Math.cos(targetRot) * speed * dt
|
| 378 |
-
|
| 379 |
-
// Clamp to cave bounds
|
| 380 |
-
const clampedX = Math.max(-45, Math.min(45, newX))
|
| 381 |
-
const clampedZ = Math.max(-45, Math.min(45, newZ))
|
| 382 |
-
|
| 383 |
-
lp.position.x = clampedX
|
| 384 |
-
lp.position.z = clampedZ
|
| 385 |
-
sendPosition({ x: clampedX, y: 0, z: clampedZ }, targetRot, anim)
|
| 386 |
-
moved = true
|
| 387 |
-
}
|
| 388 |
-
if (!moved) {
|
| 389 |
-
sendPosition({ x: lp.position.x, y: 0, z: lp.position.z }, rotRef.current, anim)
|
| 390 |
}
|
|
|
|
|
|
|
| 391 |
}
|
| 392 |
-
requestAnimationFrame(loop)
|
| 393 |
}
|
| 394 |
-
|
| 395 |
-
return () => cancelAnimationFrame(
|
| 396 |
-
}, [
|
| 397 |
-
|
| 398 |
-
const
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
),
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
React.createElement('path', { d: 'M15 19 12 15 9 19 10 15 6 11 14 5 14 2 18 6 18 9 10 13 10 17 6 11 14 10 15 9' })
|
| 413 |
-
),
|
| 414 |
-
React.createElement('span', null, anim==='idle'?'Đi':'Đứng')
|
| 415 |
-
),
|
| 416 |
-
React.createElement('button', { className: `control-btn ${showChat?'active':''}`, onClick: onChatToggle },
|
| 417 |
-
React.createElement('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
| 418 |
-
React.createElement('path', { d: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' })
|
| 419 |
-
),
|
| 420 |
-
React.createElement('span', null, 'Chat')
|
| 421 |
-
)
|
| 422 |
)
|
| 423 |
}
|
| 424 |
|
|
|
|
| 425 |
function Experience() {
|
| 426 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
const [showLogin, setShowLogin] = React.useState(true)
|
| 428 |
const [showChat, setShowChat] = React.useState(false)
|
| 429 |
const [loading, setLoading] = React.useState(true)
|
| 430 |
-
|
| 431 |
-
React.useEffect(() => { const t = setTimeout(() => setLoading(false),
|
| 432 |
-
|
| 433 |
const handleLogin = async (name) => {
|
| 434 |
try {
|
| 435 |
const res = await fetch('/join', {
|
| 436 |
-
method: 'POST',
|
|
|
|
| 437 |
body: JSON.stringify({ room_id: 'lobby', player_name: name })
|
| 438 |
})
|
|
|
|
| 439 |
const data = await res.json()
|
| 440 |
if (data.player_id) {
|
| 441 |
-
useGameStore.getState().setLocalPlayerId(data.player_id)
|
| 442 |
-
useGameStore.getState().setPlayerName(name)
|
| 443 |
-
// Load existing players (including self) from /join response
|
| 444 |
const existing = {}
|
| 445 |
for (const [pid, p] of Object.entries(data.players || {})) {
|
| 446 |
-
existing[pid] = { ...p, position:
|
| 447 |
}
|
| 448 |
-
useGameStore.setState({ players: existing })
|
| 449 |
initWebSocket(data.player_id)
|
| 450 |
setShowLogin(false)
|
| 451 |
}
|
| 452 |
-
} catch (e) { console.error('
|
| 453 |
}
|
| 454 |
-
|
| 455 |
-
return
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
)
|
| 476 |
}
|
| 477 |
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import React from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import { Canvas, useFrame, useThree } from '@react-three/fiber'
|
|
|
|
| 4 |
import * as THREE from 'three'
|
| 5 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
| 6 |
import { create } from 'zustand'
|
|
|
|
| 7 |
|
| 8 |
+
/* =========================================================================
|
| 9 |
+
Hang MU — frontend (bundle qua esbuild, KHÔNG dùng CDN/import map)
|
| 10 |
+
========================================================================= */
|
| 11 |
|
| 12 |
+
/* ---------------- Store ---------------- */
|
| 13 |
const useGameStore = create((set, get) => ({
|
| 14 |
isConnected: false,
|
| 15 |
ws: null,
|
| 16 |
localPlayerId: null,
|
| 17 |
playerName: '',
|
| 18 |
+
players: {}, // id -> { id, name, position: Vector3, rotation, animation, extra }
|
| 19 |
messages: [],
|
| 20 |
+
|
| 21 |
+
setConnected: (c) => set({ isConnected: c }),
|
| 22 |
setLocalPlayerId: (id) => set({ localPlayerId: id }),
|
| 23 |
+
setPlayerName: (n) => set({ playerName: n }),
|
| 24 |
+
|
| 25 |
initWebSocket: (playerId) => {
|
| 26 |
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
| 27 |
+
const ws = new WebSocket(`${proto}//${window.location.host}/ws/${playerId}`)
|
|
|
|
|
|
|
|
|
|
| 28 |
ws.onopen = () => {
|
| 29 |
+
console.log('[hangmu] WS connected')
|
| 30 |
set({ isConnected: true })
|
| 31 |
+
ws.send(JSON.stringify({ type: 'join_room', room_id: 'lobby', player_name: get().playerName || `Player_${playerId.slice(0, 4)}` }))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
ws.onclose = () => {
|
| 34 |
+
console.log('[hangmu] WS disconnected — reconnect in 3s')
|
| 35 |
set({ isConnected: false })
|
| 36 |
+
setTimeout(() => { if (get().localPlayerId) get().initWebSocket(get().localPlayerId) }, 3000)
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
+
ws.onerror = (e) => console.error('[hangmu] WS error', e)
|
| 39 |
ws.onmessage = (e) => {
|
| 40 |
+
try { get().handleMessage(JSON.parse(e.data)) } catch (err) { console.error('[hangmu] parse error', err) }
|
| 41 |
}
|
| 42 |
set({ ws })
|
| 43 |
},
|
| 44 |
+
|
| 45 |
closeWebSocket: () => { const { ws } = get(); if (ws) { ws.close(); set({ ws: null, isConnected: false }) } },
|
| 46 |
+
|
| 47 |
sendPosition: (position, rotation, animation) => {
|
| 48 |
const { ws } = get()
|
| 49 |
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 50 |
+
ws.send(JSON.stringify({
|
| 51 |
+
type: 'update',
|
| 52 |
+
position: { x: position.x, y: position.y, z: position.z },
|
| 53 |
+
rotation, animation
|
| 54 |
+
}))
|
| 55 |
}
|
| 56 |
},
|
| 57 |
+
|
| 58 |
sendChat: (message) => {
|
| 59 |
const { ws } = get()
|
| 60 |
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'chat', message }))
|
|
|
|
|
|
|
| 61 |
},
|
| 62 |
+
|
| 63 |
handleMessage: (data) => {
|
| 64 |
+
const v3 = (p) => new THREE.Vector3(p.x, p.y, p.z)
|
| 65 |
switch (data.type) {
|
| 66 |
+
case 'connected': break
|
|
|
|
| 67 |
case 'room_joined':
|
| 68 |
set({ players: data.players, localPlayerId: data.player_id })
|
| 69 |
break
|
| 70 |
+
case 'player_joined': {
|
| 71 |
+
const p = data.player
|
| 72 |
+
set(s => ({ players: { ...s.players, [p.id]: { ...p, position: v3(p.position) } } }))
|
| 73 |
break
|
| 74 |
+
}
|
| 75 |
case 'player_left':
|
| 76 |
set(s => { const np = { ...s.players }; delete np[data.player_id]; return { players: np } })
|
| 77 |
break
|
| 78 |
+
case 'player_update': {
|
| 79 |
+
set(s => {
|
| 80 |
+
const cur = s.players[data.player_id]
|
| 81 |
+
if (!cur) return s
|
| 82 |
+
return {
|
| 83 |
+
players: {
|
| 84 |
+
...s.players,
|
| 85 |
+
[data.player_id]: {
|
| 86 |
+
...cur,
|
| 87 |
+
position: data.position ? v3(data.position) : cur.position,
|
| 88 |
+
rotation: data.rotation ?? cur.rotation,
|
| 89 |
+
animation: data.animation ?? cur.animation,
|
| 90 |
+
lastUpdate: Date.now()
|
| 91 |
+
}
|
| 92 |
}
|
| 93 |
}
|
| 94 |
+
})
|
| 95 |
break
|
| 96 |
+
}
|
| 97 |
case 'chat':
|
| 98 |
set(s => ({ messages: [...s.messages, { playerId: data.player_id, playerName: data.player_name, message: data.message, timestamp: data.timestamp }].slice(-100) }))
|
| 99 |
break
|
| 100 |
case 'pong': break
|
| 101 |
+
case 'error': console.error('[hangmu] server:', data.message); break
|
| 102 |
}
|
| 103 |
}
|
| 104 |
}))
|
| 105 |
|
| 106 |
+
/* ---------------- Shared projection buffer (Canvas -> DOM overlay) ---------------- */
|
| 107 |
+
const projBuffer = { points: {} } // id -> {x, y, visible}
|
| 108 |
+
|
| 109 |
+
/* ---------------- Cave scene ---------------- */
|
| 110 |
function CaveScene() {
|
| 111 |
+
return (
|
| 112 |
+
<>
|
| 113 |
+
{/* floor */}
|
| 114 |
+
<mesh receiveShadow position={[0, -0.5, 0]}>
|
| 115 |
+
<cylinderGeometry args={[50, 50, 1, 32]} />
|
| 116 |
+
<meshStandardMaterial color={0x14141f} roughness={0.9} metalness={0.1} />
|
| 117 |
+
</mesh>
|
| 118 |
+
{/* walls */}
|
| 119 |
+
{[[0, 15, -50, 0, 0, 0], [0, 15, 50, 0, 0, 0], [-50, 15, 0, 0, Math.PI / 2, 0], [50, 15, 0, 0, Math.PI / 2, 0]].map(([x, y, z, rx, ry, rz], i) => (
|
| 120 |
+
<mesh key={`wall${i}`} receiveShadow position={[x, y, z]} rotation={[rx, ry, rz]} scale={[100, 30, 1]}>
|
| 121 |
+
<boxGeometry />
|
| 122 |
+
<meshStandardMaterial color={0x241626} roughness={0.85} metalness={0.1} side={THREE.BackSide} />
|
| 123 |
+
</mesh>
|
| 124 |
+
))}
|
| 125 |
+
{/* ceiling */}
|
| 126 |
+
<mesh receiveShadow position={[0, 30, 0]}>
|
| 127 |
+
<cylinderGeometry args={[50, 50, 1, 32]} />
|
| 128 |
+
<meshStandardMaterial color={0x14141f} roughness={0.9} metalness={0.1} />
|
| 129 |
+
</mesh>
|
| 130 |
+
{/* MU logo on floor */}
|
| 131 |
+
<mesh position={[0, -0.4, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
| 132 |
+
<circleGeometry args={[8, 64]} />
|
| 133 |
+
<meshBasicMaterial color={0xc8102e} transparent opacity={0.35} side={THREE.DoubleSide} />
|
| 134 |
+
</mesh>
|
| 135 |
+
{/* pillars + torch lights */}
|
| 136 |
+
{[[-30, -30], [30, -30], [-30, 30], [30, 30]].map(([x, z], i) => (
|
| 137 |
+
<group key={`p${i}`}>
|
| 138 |
+
<mesh position={[x, 7, z]} castShadow receiveShadow>
|
| 139 |
+
<cylinderGeometry args={[2, 2, 30, 16]} />
|
| 140 |
+
<meshStandardMaterial color={0x332435} roughness={0.7} metalness={0.25} />
|
| 141 |
+
</mesh>
|
| 142 |
+
<pointLight position={[x, 14, z]} color={0xffaa33} intensity={60} distance={35} decay={2} />
|
| 143 |
+
</group>
|
| 144 |
+
))}
|
| 145 |
+
<pointLight position={[0, 20, 0]} color={0xffddaa} intensity={90} distance={70} decay={2} />
|
| 146 |
+
<ambientLight intensity={0.35} color={0x444466} />
|
| 147 |
+
</>
|
| 148 |
+
)
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/* ---------------- Player avatar ---------------- */
|
| 152 |
+
const COLORS = ['#c8102e', '#fdbd10', '#ffffff', '#4ade80', '#60a5fa', '#f472b6', '#a78bfa', '#fb923c']
|
| 153 |
+
|
| 154 |
+
function PlayerAvatar({ id, position, rotation = 0, animation = 'idle', name = 'Player' }) {
|
| 155 |
+
const color = COLORS[parseInt(id.slice(-1), 16) % COLORS.length]
|
| 156 |
+
const bob = React.useRef(0)
|
| 157 |
+
const anim = animation === 'run' ? 14 : (animation === 'walk' ? 8 : 0)
|
| 158 |
+
|
| 159 |
+
useFrame((_, dt) => { bob.current += dt * (anim || 1) })
|
| 160 |
+
const yOff = anim ? Math.sin(bob.current * 1.2) * 0.12 : 0
|
| 161 |
+
|
| 162 |
+
return (
|
| 163 |
+
<group position={position} rotation={[0, rotation, 0]}>
|
| 164 |
+
{/* body */}
|
| 165 |
+
<mesh castShadow position={[0, 1 + yOff, 0]}>
|
| 166 |
+
<capsuleGeometry args={[0.5, 1.5, 4, 8]} />
|
| 167 |
+
<meshStandardMaterial color={color} roughness={0.7} metalness={0.1} />
|
| 168 |
+
</mesh>
|
| 169 |
+
{/* head */}
|
| 170 |
+
<mesh castShadow position={[0, 2.5 + yOff, 0]}>
|
| 171 |
+
<sphereGeometry args={[0.45, 16, 16]} />
|
| 172 |
+
<meshStandardMaterial color={0xfdbcb4} roughness={0.9} metalness={0} />
|
| 173 |
+
</mesh>
|
| 174 |
+
{/* arms */}
|
| 175 |
+
<group position={[-0.78, 2 + yOff, 0]}>
|
| 176 |
+
<mesh castShadow rotation={[Math.sin(bob.current * anim / 4) * 0.9, 0, 0.35]}>
|
| 177 |
+
<capsuleGeometry args={[0.16, 0.85, 4, 8]} />
|
| 178 |
+
<meshStandardMaterial color={0xfdbcb4} roughness={0.9} />
|
| 179 |
+
</mesh>
|
| 180 |
+
</group>
|
| 181 |
+
<group position={[0.78, 2 + yOff, 0]}>
|
| 182 |
+
<mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, -0.35]}>
|
| 183 |
+
<capsuleGeometry args={[0.16, 0.85, 4, 8]} />
|
| 184 |
+
<meshStandardMaterial color={0xfdbcb4} roughness={0.9} />
|
| 185 |
+
</mesh>
|
| 186 |
+
</group>
|
| 187 |
+
{/* legs */}
|
| 188 |
+
<group position={[-0.22, 0.95 + yOff, 0]}>
|
| 189 |
+
<mesh castShadow rotation={[Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}>
|
| 190 |
+
<capsuleGeometry args={[0.18, 0.95, 4, 8]} />
|
| 191 |
+
<meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} />
|
| 192 |
+
</mesh>
|
| 193 |
+
</group>
|
| 194 |
+
<group position={[0.22, 0.95 + yOff, 0]}>
|
| 195 |
+
<mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}>
|
| 196 |
+
<capsuleGeometry args={[0.18, 0.95, 4, 8]} />
|
| 197 |
+
<meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} />
|
| 198 |
+
</mesh>
|
| 199 |
+
</group>
|
| 200 |
+
{/* name plate (3D sprite-ish) */}
|
| 201 |
+
<mesh position={[0, 3.4 + yOff, 0]}>
|
| 202 |
+
<planeGeometry args={[2.6, 0.62]} />
|
| 203 |
+
<meshBasicMaterial color={0x000000} transparent opacity={0.55} depthTest={false} />
|
| 204 |
+
</mesh>
|
| 205 |
+
</group>
|
| 206 |
)
|
| 207 |
}
|
| 208 |
|
| 209 |
+
/* ---------------- OrbitControls rig (three/addons, no drei) ---------------- */
|
| 210 |
+
function CameraRig() {
|
| 211 |
+
const { camera, gl } = useThree()
|
| 212 |
+
React.useEffect(() => {
|
| 213 |
+
const ctrl = new OrbitControls(camera, gl.domElement)
|
| 214 |
+
ctrl.enableDamping = true
|
| 215 |
+
ctrl.dampingFactor = 0.08
|
| 216 |
+
ctrl.enablePan = true
|
| 217 |
+
ctrl.enableZoom = true
|
| 218 |
+
ctrl.minDistance = 3
|
| 219 |
+
ctrl.maxDistance = 35
|
| 220 |
+
ctrl.target.set(0, 1.5, 0)
|
| 221 |
+
ctrl.update()
|
| 222 |
+
return () => ctrl.dispose()
|
| 223 |
+
}, [camera, gl])
|
| 224 |
+
return null
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* ---------------- Projector: each frame push player pos -> screen coords ---------------- */
|
| 228 |
+
function Projector() {
|
| 229 |
+
const { camera, size } = useThree()
|
| 230 |
+
const players = useGameStore(s => s.players)
|
| 231 |
+
const v = React.useMemo(() => new THREE.Vector3(), [])
|
| 232 |
+
useFrame(() => {
|
| 233 |
+
const pts = projBuffer.points
|
| 234 |
+
// mark all invisible first (for stale ids)
|
| 235 |
+
for (const k in pts) pts[k].visible = false
|
| 236 |
+
for (const id in players) {
|
| 237 |
+
const p = players[id]
|
| 238 |
+
v.set(p.position.x, p.position.y + 3.5, p.position.z).project(camera)
|
| 239 |
+
const visible = v.z < 1 && v.z > -1
|
| 240 |
+
pts[id] = {
|
| 241 |
+
x: (v.x * 0.5 + 0.5) * size.width,
|
| 242 |
+
y: (-v.y * 0.5 + 0.5) * size.height,
|
| 243 |
+
visible
|
| 244 |
+
}
|
| 245 |
}
|
| 246 |
})
|
| 247 |
+
return null
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* ---------------- DOM overlay: name tags read projBuffer at 60fps ---------------- */
|
| 251 |
+
function NameTagOverlay() {
|
| 252 |
+
const players = useGameStore(s => s.players)
|
| 253 |
+
const refs = React.useRef({})
|
| 254 |
+
React.useEffect(() => {
|
| 255 |
+
let raf
|
| 256 |
+
const loop = () => {
|
| 257 |
+
for (const id in refs.current) {
|
| 258 |
+
const el = refs.current[id]
|
| 259 |
+
const pt = projBuffer.points[id]
|
| 260 |
+
if (el && pt) {
|
| 261 |
+
el.style.display = pt.visible ? 'block' : 'none'
|
| 262 |
+
el.style.transform = `translate(calc(-50% + ${pt.x}px), calc(-100% + ${pt.y}px))`
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
raf = requestAnimationFrame(loop)
|
| 266 |
+
}
|
| 267 |
+
raf = requestAnimationFrame(loop)
|
| 268 |
+
return () => cancelAnimationFrame(raf)
|
| 269 |
+
}, [])
|
| 270 |
+
|
| 271 |
+
return (
|
| 272 |
+
<div className="name-tag-layer">
|
| 273 |
+
{Object.entries(players).map(([id, p]) => (
|
| 274 |
+
<div
|
| 275 |
+
key={id}
|
| 276 |
+
ref={el => { refs.current[id] = el }}
|
| 277 |
+
className={`name-tag${id === useGameStore.getState().localPlayerId ? ' self' : ''}`}
|
| 278 |
+
>
|
| 279 |
+
{p.name}
|
| 280 |
+
</div>
|
| 281 |
+
))}
|
| 282 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
)
|
| 284 |
}
|
| 285 |
|
| 286 |
+
/* ---------------- UI: top bar ---------------- */
|
| 287 |
+
function TopBar({ playerCount }) {
|
| 288 |
+
return (
|
| 289 |
+
<div className="top-bar">
|
| 290 |
+
<div className="logo">
|
| 291 |
+
<div className="logo-icon">MU</div>
|
| 292 |
+
<span>Hang MU</span>
|
| 293 |
+
</div>
|
| 294 |
+
<div className="room-info">
|
| 295 |
+
<div className="player-count">{playerCount} / 50</div>
|
| 296 |
+
<span>Sảnh chính</span>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
)
|
| 300 |
}
|
| 301 |
|
| 302 |
function ConnectionStatus({ status }) {
|
| 303 |
+
const label = status === 'connected' ? 'Đã kết nối' : (status === 'connecting' ? 'Đang kết nối…' : 'Mất kết nối — đang thử lại')
|
| 304 |
+
return (
|
| 305 |
+
<div className={`connection-status ${status}`}>
|
| 306 |
+
<span className="status-dot" />
|
| 307 |
+
<span>{label}</span>
|
| 308 |
+
</div>
|
| 309 |
)
|
| 310 |
}
|
| 311 |
|
| 312 |
function LoadingScreen({ visible }) {
|
| 313 |
if (!visible) return null
|
| 314 |
+
return (
|
| 315 |
+
<div className="loading-screen">
|
| 316 |
+
<div className="loading-spinner" />
|
| 317 |
+
<div className="loading-text">Đang vào hang…</div>
|
| 318 |
+
</div>
|
| 319 |
)
|
| 320 |
}
|
| 321 |
|
| 322 |
function LoginModal({ onLogin }) {
|
| 323 |
const [name, setName] = React.useState('')
|
| 324 |
+
const [busy, setBusy] = React.useState(false)
|
| 325 |
const [error, setError] = React.useState('')
|
| 326 |
+
|
| 327 |
+
const submit = async (e) => {
|
| 328 |
e.preventDefault()
|
| 329 |
+
const n = name.trim()
|
| 330 |
+
if (!n) { setError('Nhập tên để vào hang'); return }
|
| 331 |
+
setBusy(true); setError('')
|
| 332 |
+
await onLogin(n)
|
| 333 |
+
setBusy(false)
|
| 334 |
}
|
| 335 |
+
|
| 336 |
+
return (
|
| 337 |
+
<div className="login-modal">
|
| 338 |
+
<div className="login-box">
|
| 339 |
+
<h1>🏰 Hang MU</h1>
|
| 340 |
+
<p>Nhập tên để vào hang cùng anh em Quỷ Đỏ</p>
|
| 341 |
+
{error && <p className="login-error">{error}</p>}
|
| 342 |
+
<form className="login-form" onSubmit={submit}>
|
| 343 |
+
<input
|
| 344 |
+
type="text" className="login-input" placeholder="Tên của bạn"
|
| 345 |
+
value={name} maxLength={20} autoFocus
|
| 346 |
+
onChange={(e) => setName(e.target.value)}
|
| 347 |
+
/>
|
| 348 |
+
<button type="submit" className="login-btn" disabled={busy}>
|
| 349 |
+
{busy ? 'Đang vào…' : 'Vào Hang'}
|
| 350 |
+
</button>
|
| 351 |
+
</form>
|
| 352 |
+
<p className="login-hint">Di chuyển: WASD / phím mũi tên · Chạy: Shift · Xoay camera: kéo chuột</p>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
)
|
| 356 |
}
|
| 357 |
|
| 358 |
+
/* ---------------- UI: chat ---------------- */
|
| 359 |
+
function ChatPanel({ onClose }) {
|
| 360 |
+
const messages = useGameStore(s => s.messages)
|
| 361 |
+
const sendChat = useGameStore(s => s.sendChat)
|
| 362 |
const [input, setInput] = React.useState('')
|
| 363 |
+
const endRef = React.useRef(null)
|
| 364 |
+
|
| 365 |
+
React.useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages])
|
| 366 |
+
|
| 367 |
+
const send = (e) => {
|
| 368 |
e.preventDefault()
|
| 369 |
if (!input.trim()) return
|
| 370 |
+
sendChat(input.trim())
|
| 371 |
setInput('')
|
| 372 |
}
|
| 373 |
+
|
| 374 |
+
return (
|
| 375 |
+
<div className="chat-panel">
|
| 376 |
+
<div className="chat-header">
|
| 377 |
+
<span>💬 Chat</span>
|
| 378 |
+
<button className="chat-close" onClick={onClose}>×</button>
|
| 379 |
+
</div>
|
| 380 |
+
<div className="chat-messages">
|
| 381 |
+
{messages.length === 0 && <div className="chat-empty">Chưa có tin nhắn — hãy gửi lời chào! 👋</div>}
|
| 382 |
+
{messages.map((m, i) => (
|
| 383 |
+
<div key={i} className="chat-message">
|
| 384 |
+
<span className="sender">{m.playerName}</span>
|
| 385 |
+
<span className="text">{m.message}</span>
|
| 386 |
+
<span className="time">{new Date(m.timestamp * 1000).toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' })}</span>
|
| 387 |
+
</div>
|
| 388 |
+
))}
|
| 389 |
+
<div ref={endRef} />
|
| 390 |
+
</div>
|
| 391 |
+
<form className="chat-input-area" onSubmit={send}>
|
| 392 |
+
<input type="text" className="chat-input" placeholder="Nhập tin nhắn…" value={input} maxLength={200} onChange={(e) => setInput(e.target.value)} />
|
| 393 |
+
<button type="submit" className="chat-send-btn" aria-label="Gửi">
|
| 394 |
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
| 395 |
+
<path d="M22 2L11 13" /><path d="M22 2l-7 20-4-9-9-4 20-7z" />
|
| 396 |
+
</svg>
|
| 397 |
+
</button>
|
| 398 |
+
</form>
|
| 399 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 400 |
)
|
| 401 |
}
|
| 402 |
|
| 403 |
+
/* ---------------- UI: bottom controls ---------------- */
|
| 404 |
function BottomControls({ onChatToggle, showChat }) {
|
| 405 |
+
const players = useGameStore(s => s.players)
|
| 406 |
+
const localPlayerId = useGameStore(s => s.localPlayerId)
|
| 407 |
+
const sendPosition = useGameStore(s => s.sendPosition)
|
| 408 |
const lp = localPlayerId ? players[localPlayerId] : null
|
| 409 |
+
const keys = React.useRef({})
|
| 410 |
+
const [mode, setMode] = React.useState('walk') // idle | walk | run
|
| 411 |
+
|
|
|
|
|
|
|
|
|
|
| 412 |
React.useEffect(() => {
|
| 413 |
const down = (e) => {
|
| 414 |
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
| 415 |
+
keys.current[e.code] = true
|
| 416 |
+
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') setMode('run')
|
|
|
|
|
|
|
|
|
|
| 417 |
}
|
| 418 |
const up = (e) => {
|
| 419 |
+
keys.current[e.code] = false
|
| 420 |
+
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') setMode('walk')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
}
|
| 422 |
window.addEventListener('keydown', down)
|
| 423 |
window.addEventListener('keyup', up)
|
| 424 |
return () => { window.removeEventListener('keydown', down); window.removeEventListener('keyup', up) }
|
| 425 |
+
}, [])
|
| 426 |
+
|
| 427 |
+
// movement loop
|
| 428 |
React.useEffect(() => {
|
| 429 |
+
let raf, last = performance.now()
|
| 430 |
const loop = (now) => {
|
| 431 |
+
const dt = Math.min((now - last) / 1000, 0.05); last = now
|
|
|
|
|
|
|
| 432 |
if (lp) {
|
| 433 |
+
const k = keys.current
|
| 434 |
+
const moving = k.KeyW || k.KeyS || k.KeyA || k.KeyD || k.ArrowUp || k.ArrowDown || k.ArrowLeft || k.ArrowRight
|
| 435 |
+
const anim = mode === 'run' ? 'run' : (moving ? 'walk' : 'idle')
|
| 436 |
+
const speed = mode === 'run' ? 11 : (moving ? 5 : 0)
|
| 437 |
+
if (moving) {
|
| 438 |
+
const mx = ((k.KeyD || k.ArrowRight) ? 1 : 0) - ((k.KeyA || k.ArrowLeft) ? 1 : 0)
|
| 439 |
+
const mz = ((k.KeyS || k.ArrowDown) ? 1 : 0) - ((k.KeyW || k.ArrowUp) ? 1 : 0)
|
| 440 |
+
const rot = Math.atan2(mx, mz)
|
| 441 |
+
const nx = Math.max(-46, Math.min(46, lp.position.x + Math.sin(rot) * speed * dt))
|
| 442 |
+
const nz = Math.max(-46, Math.min(46, lp.position.z + Math.cos(rot) * speed * dt))
|
| 443 |
+
lp.position.x = nx; lp.position.z = nz
|
| 444 |
+
lp.rotation = rot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
}
|
| 446 |
+
lp.animation = anim
|
| 447 |
+
sendPosition(lp.position, lp.rotation, anim)
|
| 448 |
}
|
| 449 |
+
raf = requestAnimationFrame(loop)
|
| 450 |
}
|
| 451 |
+
raf = requestAnimationFrame(loop)
|
| 452 |
+
return () => cancelAnimationFrame(raf)
|
| 453 |
+
}, [lp, mode, sendPosition])
|
| 454 |
+
|
| 455 |
+
const btn = (label, emoji, onClick, active) => (
|
| 456 |
+
<button className={`control-btn${active ? ' active' : ''}`} onClick={onClick}>
|
| 457 |
+
<span className="ctrl-emoji">{emoji}</span>
|
| 458 |
+
<span>{label}</span>
|
| 459 |
+
</button>
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
return (
|
| 463 |
+
<div className="bottom-controls">
|
| 464 |
+
{btn('Đi', '🚶', () => setMode(mode === 'walk' ? 'idle' : 'walk'), mode === 'walk')}
|
| 465 |
+
{btn('Chạy', '🏃', () => setMode('run'), mode === 'run')}
|
| 466 |
+
{btn(mode === 'idle' ? 'Đứng' : 'Dừng', '🛑', () => setMode('idle'), mode === 'idle')}
|
| 467 |
+
{btn('Chat', '💬', onChatToggle, showChat)}
|
| 468 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
)
|
| 470 |
}
|
| 471 |
|
| 472 |
+
/* ---------------- Main Experience ---------------- */
|
| 473 |
function Experience() {
|
| 474 |
+
const isConnected = useGameStore(s => s.isConnected)
|
| 475 |
+
const players = useGameStore(s => s.players)
|
| 476 |
+
const localPlayerId = useGameStore(s => s.localPlayerId)
|
| 477 |
+
const initWebSocket = useGameStore(s => s.initWebSocket)
|
| 478 |
+
|
| 479 |
const [showLogin, setShowLogin] = React.useState(true)
|
| 480 |
const [showChat, setShowChat] = React.useState(false)
|
| 481 |
const [loading, setLoading] = React.useState(true)
|
| 482 |
+
|
| 483 |
+
React.useEffect(() => { const t = setTimeout(() => setLoading(false), 1200); return () => clearTimeout(t) }, [])
|
| 484 |
+
|
| 485 |
const handleLogin = async (name) => {
|
| 486 |
try {
|
| 487 |
const res = await fetch('/join', {
|
| 488 |
+
method: 'POST',
|
| 489 |
+
headers: { 'Content-Type': 'application/json' },
|
| 490 |
body: JSON.stringify({ room_id: 'lobby', player_name: name })
|
| 491 |
})
|
| 492 |
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
| 493 |
const data = await res.json()
|
| 494 |
if (data.player_id) {
|
|
|
|
|
|
|
|
|
|
| 495 |
const existing = {}
|
| 496 |
for (const [pid, p] of Object.entries(data.players || {})) {
|
| 497 |
+
existing[pid] = { ...p, position: new THREE.Vector3(p.position.x, p.position.y, p.position.z) }
|
| 498 |
}
|
| 499 |
+
useGameStore.setState({ players: existing, localPlayerId: data.player_id, playerName: name })
|
| 500 |
initWebSocket(data.player_id)
|
| 501 |
setShowLogin(false)
|
| 502 |
}
|
| 503 |
+
} catch (e) { console.error('[hangmu] login failed:', e) }
|
| 504 |
}
|
| 505 |
+
|
| 506 |
+
return (
|
| 507 |
+
<>
|
| 508 |
+
<LoadingScreen visible={loading} />
|
| 509 |
+
<Canvas
|
| 510 |
+
camera={{ position: [0, 6, 16], fov: 60 }}
|
| 511 |
+
shadows
|
| 512 |
+
gl={{ antialias: true, alpha: true, preserveDrawingBuffer: true }}
|
| 513 |
+
onCreated={({ gl }) => { gl.setClearColor(0x0a0a0c, 1) }}
|
| 514 |
+
>
|
| 515 |
+
<CaveScene />
|
| 516 |
+
{Object.entries(players).map(([id, p]) => (
|
| 517 |
+
<PlayerAvatar key={id} id={id} position={p.position} rotation={p.rotation} animation={p.animation} name={p.name} />
|
| 518 |
+
))}
|
| 519 |
+
<CameraRig />
|
| 520 |
+
<Projector />
|
| 521 |
+
</Canvas>
|
| 522 |
+
<NameTagOverlay />
|
| 523 |
+
<div className="ui-overlay">
|
| 524 |
+
<TopBar playerCount={Object.keys(players).length} />
|
| 525 |
+
<ConnectionStatus status={showLogin ? 'disconnected' : (isConnected ? 'connected' : 'connecting')} />
|
| 526 |
+
<BottomControls onChatToggle={() => setShowChat(!showChat)} showChat={showChat} />
|
| 527 |
+
{showChat && <ChatPanel onClose={() => setShowChat(false)} />}
|
| 528 |
+
</div>
|
| 529 |
+
{showLogin && <LoginModal onLogin={handleLogin} />}
|
| 530 |
+
</>
|
| 531 |
)
|
| 532 |
}
|
| 533 |
|
| 534 |
+
createRoot(document.getElementById('root')).render(
|
| 535 |
+
<React.StrictMode>
|
| 536 |
+
<Experience />
|
| 537 |
+
</React.StrictMode>
|
| 538 |
+
)
|
frontend/index.html
CHANGED
|
@@ -8,20 +8,7 @@
|
|
| 8 |
<meta name="theme-color" content="#c8102e" />
|
| 9 |
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏰</text></svg>" />
|
| 10 |
|
| 11 |
-
<!--
|
| 12 |
-
<script type="importmap">
|
| 13 |
-
{
|
| 14 |
-
"imports": {
|
| 15 |
-
"react": "https://esm.sh/react@18.3.1",
|
| 16 |
-
"react-dom": "https://esm.sh/react-dom@18.3.1",
|
| 17 |
-
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
|
| 18 |
-
"three": "https://esm.sh/three@0.170.0",
|
| 19 |
-
"@react-three/fiber": "https://esm.sh/@react-three/fiber@8.17.0",
|
| 20 |
-
"@react-three/drei": "https://esm.sh/@react-three/drei@9.117.0",
|
| 21 |
-
"zustand": "https://esm.sh/zustand@5.0.0"
|
| 22 |
-
}
|
| 23 |
-
}
|
| 24 |
-
</script>
|
| 25 |
|
| 26 |
<style>
|
| 27 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -79,7 +66,15 @@
|
|
| 79 |
.loading-spinner { width: 48px; height: 48px; border: 3px solid rgba(253,189,16,0.2); border-top-color: var(--mu-gold); border-radius: 50%; animation: spin 1s linear infinite; }
|
| 80 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 81 |
.loading-text { color: var(--mu-gold); font-weight: 600; font-size: 18px; }
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
@media (max-width: 768px) {
|
| 84 |
.top-bar { padding: 12px 16px; } .logo span { display: none; }
|
| 85 |
.chat-panel { width: calc(100% - 32px); right: 16px; left: 16px; }
|
|
@@ -92,7 +87,7 @@
|
|
| 92 |
<body>
|
| 93 |
<div id="root"></div>
|
| 94 |
|
| 95 |
-
<!--
|
| 96 |
-
<script
|
| 97 |
</body>
|
| 98 |
</html>
|
|
|
|
| 8 |
<meta name="theme-color" content="#c8102e" />
|
| 9 |
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏰</text></svg>" />
|
| 10 |
|
| 11 |
+
<!-- Bundle: app.bundle.js tự chứa toàn bộ (react, three, ...), không cần CDN/import map -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
<style>
|
| 14 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
| 66 |
.loading-spinner { width: 48px; height: 48px; border: 3px solid rgba(253,189,16,0.2); border-top-color: var(--mu-gold); border-radius: 50%; animation: spin 1s linear infinite; }
|
| 67 |
@keyframes spin { to { transform: rotate(360deg); } }
|
| 68 |
.loading-text { color: var(--mu-gold); font-weight: 600; font-size: 18px; }
|
| 69 |
+
/* Name tag DOM overlay (projection từ 3D -> 2D) */
|
| 70 |
+
.name-tag-layer { position: fixed; inset: 0; pointer-events: none; z-index: 5; overflow: hidden; }
|
| 71 |
+
.name-tag { position: absolute; left: 0; top: 0; display: none; background: rgba(0,0,0,0.78); padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; color: var(--mu-gold); white-space: nowrap; border: 1px solid rgba(253,189,16,0.5); pointer-events: none; }
|
| 72 |
+
.name-tag.self { color: #fff; border-color: #4ade80; background: rgba(16,60,40,0.8); }
|
| 73 |
+
.login-error { color: #ff6b6b; margin-top: -16px; margin-bottom: 16px; font-size: 14px; }
|
| 74 |
+
.login-hint { margin-top: 24px; color: var(--text-secondary); font-size: 12px; line-height: 1.6; }
|
| 75 |
+
.chat-empty { color: var(--text-secondary); text-align: center; font-size: 13px; padding: 12px; }
|
| 76 |
+
.chat-close { background: none; border: none; color: var(--mu-gold); font-size: 22px; cursor: pointer; line-height: 1; }
|
| 77 |
+
.ctrl-emoji { font-size: 20px; }
|
| 78 |
@media (max-width: 768px) {
|
| 79 |
.top-bar { padding: 12px 16px; } .logo span { display: none; }
|
| 80 |
.chat-panel { width: calc(100% - 32px); right: 16px; left: 16px; }
|
|
|
|
| 87 |
<body>
|
| 88 |
<div id="root"></div>
|
| 89 |
|
| 90 |
+
<!-- Bundled app (chứa sẵn react/three/zustand — không phụ thuộc CDN) -->
|
| 91 |
+
<script src="/static/app.bundle.js"></script>
|
| 92 |
</body>
|
| 93 |
</html>
|