hangmu-docker / patch_3d.mjs
bep40's picture
Fix crash vao hang (R3F Gold) + day leo + bang chuyen can dung; cache-bust v8
dbadb8f
Raw History Blame Contribute Delete
19.8 kB
/* patch_3d.mjs — Bổ sung vật thể/scene/nhân vật 3D còn thiếu so với hangmu.online.
Chạy SAU patch_app.mjs (pattern trỏ vào source ĐÃ patch).
- app.js: avatar mặc định áo đỏ MU #c8102e (ưu tiên kit nếu server gửi),
thêm vai + giày, va chạm ghế đá hội trường (5×10).
- home3d.js: thiên nhiên quanh cửa hang (cỏ/bụi/lá/đá instancedMesh),
bảng chuyên cần attendance trên bia đá.
- cave3d.js: cúp vàng trung tâm + banner MU trên vòm.
Nếu pattern không khớp -> exit(1). */
import fs from 'node:fs'
function patchFile(path, reps) {
let s = fs.readFileSync(path, 'utf8')
const failures = []
function toRe(str) {
const esc = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return esc.replace(/\s+/g, '\\s+')
}
function rep(oldStr, newStr) {
const exact = s.split(oldStr).length - 1
if (exact === 1) { s = s.replace(oldStr, newStr); return }
const re = new RegExp(toRe(oldStr), 'g')
const m = s.match(re)
if (m && m.length === 1) { s = s.replace(re, newStr); return }
failures.push(`pattern not unique/found (exact=${exact}, re=${m ? m.length : 0}): ${oldStr.slice(0, 70)}`)
}
for (const [o, n] of reps) rep(o, n)
if (failures.length) {
console.error(`PATCH 3D FAILED (${path}):`)
failures.forEach((f) => console.error(' -', f))
process.exit(1)
}
fs.writeFileSync(path, s)
console.log('patched ok (3d):', path)
}
/* ================= app.js ================= */
patchFile('frontend/app.js', [
/* Palette: thêm đỏ MU da291c */
[ "const COLORS = ['#c8102e', '#fdbd10', '#ffffff', '#4ade80', '#60a5fa', '#f472b6', '#a78bfa', '#fb923c']",
"const COLORS = ['#c8102e', '#da291c', '#fdbd10', '#4ade80', '#60a5fa', '#f472b6', '#a78bfa', '#fb923c']" ],
/* Avatar: mặc định áo đỏ MU nếu server không gửi kit */
[ `function PlayerAvatar({ id, p, isLocal }) {
const color = COLORS[parseInt(id.slice(-1), 16) % COLORS.length]`,
`function PlayerAvatar({ id, p, isLocal }) {
const color = (p.kit != null && COLORS[p.kit % COLORS.length]) || '#c8102e'` ],
/* Va chạm ghế đá hội trường (5 hàng × 10 ghế, y hệt hall.js gốc) */
[ "const KEYMAP = { ArrowUp: 'up', KeyW: 'up', ArrowDown: 'down', KeyS: 'down', ArrowLeft: 'left', KeyA: 'left', ArrowRight: 'right', KeyD: 'right' }",
`const KEYMAP = { ArrowUp: 'up', KeyW: 'up', ArrowDown: 'down', KeyS: 'down', ArrowLeft: 'left', KeyA: 'left', ArrowRight: 'right', KeyD: 'right' }
/* Va chạm ghế đá (hội trường trước màn hình lớn) — 5 hàng × 10 ghế, y hệt hall.js gốc */
const BENCH_COLLIDE = (() => {
const out = []
for (let row = 0; row < 5; row++) {
const z = -37 + row * 2.6
for (let col = 0; col < 10; col++) {
const x = -10.8 + col * 2.4
out.push({ x, z, r: 1.05 })
}
}
return out
})()
function benchBlocked(nx, nz) {
for (let i = 0; i < BENCH_COLLIDE.length; i++) {
const b = BENCH_COLLIDE[i]
const dx = nx - b.x, dz = nz - b.z
if (dx * dx + dz * dz < b.r * b.r) return true
}
return false
}` ],
/* Chặn đi xuyên ghế */
[ ` const fireSafe = 8.2
const d0 = Math.hypot(me.pos.x, me.pos.z)
if (d0 >= fireSafe && Math.hypot(nx, nz) < fireSafe) {
const s = fireSafe / Math.max(Math.hypot(nx, nz), 1e-6)
nx *= s; nz *= s
}
me.pos.x = nx; me.pos.z = nz`,
` const fireSafe = 8.2
const d0 = Math.hypot(me.pos.x, me.pos.z)
if (d0 >= fireSafe && Math.hypot(nx, nz) < fireSafe) {
const s = fireSafe / Math.max(Math.hypot(nx, nz), 1e-6)
nx *= s; nz *= s
}
// va chạm ghế đá hội trường: không cho đi xuyên qua
if (benchBlocked(nx, nz)) {
if (!benchBlocked(nx, me.pos.z)) nz = me.pos.z
else if (!benchBlocked(me.pos.x, nz)) nx = me.pos.x
else { nx = me.pos.x; nz = me.pos.z }
}
me.pos.x = nx; me.pos.z = nz` ],
/* Vai + giày cho avatar */
[ ` <group position={[0.22, 1.0 + yOff, 0]}>
<mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}>
<capsuleGeometry args={[0.18, 0.95, 4, 8]} />
<meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} />
</mesh>
</group>`,
` <group position={[0.22, 1.0 + yOff, 0]}>
<mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}>
<capsuleGeometry args={[0.18, 0.95, 4, 8]} />
<meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} />
</mesh>
</group>
{/* vai — cầu vai rộng kiểu áo bóng đá */}
<mesh position={[-0.62, 1.9 + yOff, 0]} rotation-z={0.2}>
<sphereGeometry args={[0.16, 8, 8]} />
<meshStandardMaterial color={color} roughness={0.7} />
</mesh>
<mesh position={[0.62, 1.9 + yOff, 0]} rotation-z={-0.2}>
<sphereGeometry args={[0.16, 8, 8]} />
<meshStandardMaterial color={color} roughness={0.7} />
</mesh>
{/* giày — đế dưới chân */}
<mesh position={[-0.09, 0.22 + yOff, 0.16]} rotation-x={0.25}>
<boxGeometry args={[0.18, 0.09, 0.3]} />
<meshStandardMaterial color={0x1a1a1a} roughness={0.5} metalness={0.15} />
</mesh>
<mesh position={[0.09, 0.22 + yOff, 0.16]} rotation-x={0.25}>
<boxGeometry args={[0.18, 0.09, 0.3]} />
<meshStandardMaterial color={0x1a1a1a} roughness={0.5} metalness={0.15} />
</mesh>` ],
])
/* ================= home3d.js ================= */
patchFile('frontend/home3d.js', [
/* Thiên nhiên quanh cửa hang */
[ `const FULLR = Math.PI * 2`,
`const FULLR = Math.PI * 2
/* =========================================================================
THIÊN NHIÊN QUANH CỬA HANG — cỏ / bụi / lá / đá (instancedMesh, y hệt gốc)
========================================================================= */
function _instancedNature() {
const rand = mulberry(31)
const grass = [], bushes = [], leaves = [], stones = []
const push = (arr, geo, px, py, pz, sx, sy, sz, ry) => arr.push({ geo, px, py, pz, sx, sy, sz, ry })
const gGeo = new THREE.ConeGeometry(0.05, 0.42, 3)
for (let i = 0; i < 220; i++) {
const a = rand() * Math.PI * 2, r = 4 + rand() * 13
push(grass, gGeo, Math.sin(a) * r * 0.9, 0.14 + rand() * 0.2, Math.cos(a) * r * 0.8 + 1.5, 0.8 + rand() * 0.9, 0.8 + rand() * 0.9, 0.8 + rand() * 0.9, rand() * Math.PI)
}
const bGeo = new THREE.IcosahedronGeometry(0.5, 1)
for (let i = 0; i < 26; i++) {
const a = rand() * Math.PI * 2, r = 6 + rand() * 15
push(bushes, bGeo, Math.sin(a) * r * 0.9, 0.25 + rand() * 0.25, Math.cos(a) * r * 0.8 + 1.5, 0.5 + rand() * 0.7, 0.5 + rand() * 0.5, 0.5 + rand() * 0.7, rand() * Math.PI)
}
const lGeo = new THREE.PlaneGeometry(0.09, 0.05)
for (let i = 0; i < 40; i++) {
const a = rand() * Math.PI * 2, r = 3 + rand() * 16
push(leaves, lGeo, Math.sin(a) * r * 0.9, 0.6 + rand() * 1.8, Math.cos(a) * r * 0.8 + 1.5, 1, 1, 1, rand() * Math.PI)
}
const sGeo = new THREE.IcosahedronGeometry(0.28, 1)
for (let i = 0; i < 14; i++) {
const a = rand() * Math.PI * 2, r = 4.5 + rand() * 14
push(stones, sGeo, Math.sin(a) * r * 0.9, 0.12 + rand() * 0.18, Math.cos(a) * r * 0.8 + 1.5, 0.5 + rand() * 1.2, 0.45 + rand() * 0.9, 0.5 + rand() * 1.2, rand() * Math.PI)
}
return { grass, bushes, leaves, stones }
}
const _nature = _instancedNature()
function NatureGround() {
const gRef = useRef(null), bRef = useRef(null), lRef = useRef(null), sRef = useRef(null)
const green = new THREE.Color('#6d7d38'), green2 = new THREE.Color('#56662d'), leaf = new THREE.Color('#7f9a4a')
const stone = new THREE.Color('#9a8a6c'), stone2 = new THREE.Color('#77664c')
useMemo(() => {
const dummy = new THREE.Object3D()
const fill = (ref, arr, cA, cB) => {
if (!ref.current) return
arr.forEach((it, i) => {
dummy.position.set(it.px, it.py, it.pz); dummy.scale.set(it.sx, it.sy, it.sz); dummy.rotation.y = it.ry
dummy.updateMatrix(); ref.current.setMatrixAt(i, dummy.matrix)
ref.current.setColorAt(i, i % 2 ? cB : cA)
})
ref.current.instanceMatrix.needsUpdate = true
ref.current.instanceColor.needsUpdate = true
}
fill(gRef, _nature.grass, green, green2)
fill(bRef, _nature.bushes, green, green2)
fill(lRef, _nature.leaves, leaf, leaf)
fill(sRef, _nature.stones, stone, stone2)
}, [])
return (
<>
<instancedMesh ref={gRef} args={[undefined, undefined, _nature.grass.length]}>
<coneGeometry args={[0.05, 0.42, 3]} />
<meshLambertMaterial vertexColors flatShading />
</instancedMesh>
<instancedMesh ref={bRef} args={[undefined, undefined, _nature.bushes.length]}>
<icosahedronGeometry args={[0.5, 1]} />
<meshLambertMaterial vertexColors flatShading />
</instancedMesh>
<instancedMesh ref={lRef} args={[undefined, undefined, _nature.leaves.length]}>
<planeGeometry args={[0.09, 0.05]} />
<meshLambertMaterial vertexColors flatShading side={THREE.DoubleSide} transparent opacity={0.92} />
</instancedMesh>
<instancedMesh ref={sRef} args={[undefined, undefined, _nature.stones.length]}>
<icosahedronGeometry args={[0.28, 1]} />
<meshLambertMaterial vertexColors flatShading />
</instancedMesh>
</>
)
}` ],
/* Dây leo quanh vòm cửa + bảng chuyên cần đứng — vines/board như gốc */
[ `function GroundGeom() {`,
`function Vines() {
const strands = useMemo(() => {
const out = []
const rand = mulberry(13)
for (let i = 0; i < 26; i++) {
const a = Math.PI + (i - 13) * 0.12
const r = 4.9
const x = Math.sin(a) * r * 1.15 + 1.2, z = Math.cos(a) * r - 1.5
const len = 1.6 + rand() * 2.2
out.push({ x, z, len, sway: rand() * Math.PI * 2, leaf: rand() * 5 })
}
return out
}, [])
return (
<group>
{strands.map((v, i) => (
<group key={i} position={[v.x, 3.4 - v.len / 2, v.z]}>
<mesh>
<cylinderGeometry args={[0.035, 0.05, v.len, 4]} />
<meshLambertMaterial color="#3d5a22" flatShading />
</mesh>
<mesh position={[0, -v.len / 2 + 0.1, 0]} rotation-x={0.5}>
<sphereGeometry args={[0.1, 5, 5]} />
<meshLambertMaterial color="#4c7a2e" flatShading />
</mesh>
</group>
))}
</group>
)
}
function AttendanceBoard() {
const [rows, setRows] = React.useState(null)
React.useEffect(() => {
let alive = true
fetch('/api/attendance').then(r => r.ok ? r.json() : Promise.reject()).then(d => {
if (alive && d && d.rows) setRows(d.rows)
}).catch(() => {
if (!alive) return
const r = []
for (let i = 0; i < 5; i++) {
const row = []
for (let j = 0; j < 10; j++) row.push(Math.random() > 0.35 ? { name: '', number: 0 } : null)
r.push(row)
}
setRows(r)
})
return () => { alive = false }
}, [])
const tex = React.useMemo(() => {
const c = document.createElement('canvas'); c.width = 640; c.height = 320
const g = c.getContext('2d')
g.fillStyle = '#7a5f42'; g.fillRect(0, 0, 640, 320)
g.strokeStyle = '#4a3522'; g.lineWidth = 4; g.strokeRect(4, 4, 632, 312)
g.fillStyle = '#3a2a18'; g.font = '700 26px "Be Vietnam Pro", sans-serif'; g.textAlign = 'center'
g.fillText('CHUYÊN CẦN THÁNG NÀY', 320, 36)
for (let r = 0; r < 5; r++) for (let c = 0; c < 10; c++) {
const x = 16 + c * 61, y = 50 + r * 50
g.fillStyle = '#5a442c'; g.fillRect(x, y, 56, 42)
if (rows && rows[r] && rows[r][c]) {
g.fillStyle = '#c8102e'; g.fillRect(x + 3, y + 3, 50, 36)
g.fillStyle = '#fff'; g.font = '600 18px "Be Vietnam Pro", sans-serif'
g.fillText('✓', x + 28, y + 27)
} else { g.fillStyle = '#8a6f4e'; g.fillRect(x + 3, y + 3, 50, 36) }
}
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace
return t
}, [rows])
if (!rows) return null
return (
<mesh position={[0, 1.45, 0.02]} castShadow>
<planeGeometry args={[2.9, 1.45]} />
<meshStandardMaterial map={tex} roughness={0.85} />
</mesh>
)
}
function GroundGeom() {` ],
/* Wire AttendanceBoard vào PostAndPlaque (trụ bảng) */
[ ` <meshStandardMaterial map={tex} alphaTest={0.5} side={THREE.DoubleSide} roughness={0.85} />
</mesh>
)}`,
` <meshStandardMaterial map={tex} alphaTest={0.5} side={THREE.DoubleSide} roughness={0.85} />
</mesh>
)}
<AttendanceBoard />` ],
/* Wire NatureGround + Vines vào HomeScene */
[ ` <CaveExterior />
<group position={layout.stele} rotation-y={layout.steleRotY}>`,
` <CaveExterior />
<NatureGround />
<Vines />
<group position={layout.stele} rotation-y={layout.steleRotY}>` ],
/* Bảng chuyên cần attendance trên bia đá */
[ `/* ---- Camera rig + layout (MA gốc) ---- */`,
`/* =========================================================================
BẢNG CHUYÊN CẦN (attendance) — tái hiện Mn() hangmu.online:
5×10 ô trên bia đá, mỗi ô = người có mặt hôm nay
========================================================================= */
function SteleTop() {
const [rows, setRows] = React.useState(null)
React.useEffect(() => {
let alive = true
fetch('/api/attendance').then(r => r.ok ? r.json() : Promise.reject()).then(d => {
if (alive && d && d.rows) setRows(d.rows)
}).catch(() => {
if (!alive) return
const r = []
for (let i = 0; i < 5; i++) {
const row = []
for (let j = 0; j < 10; j++) row.push(Math.random() > 0.35 ? { name: '', number: 0 } : null)
r.push(row)
}
setRows(r)
})
return () => { alive = false }
}, [])
const boardTex = React.useMemo(() => {
const c = document.createElement('canvas')
c.width = 640; c.height = 320
const g = c.getContext('2d')
g.fillStyle = '#8a6f4e'; g.fillRect(0, 0, 640, 320)
g.fillStyle = '#7a5f42'; g.fillRect(8, 8, 624, 304)
g.strokeStyle = '#4a3522'; g.lineWidth = 4; g.strokeRect(8, 8, 624, 304)
g.fillStyle = '#3a2a18'; g.font = '700 26px "Be Vietnam Pro", sans-serif'
g.textAlign = 'center'
g.fillText('CHUYÊN CẦN THÁNG NÀY', 320, 38)
if (rows) {
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 10; c++) {
const x = 18 + c * 61, y = 52 + r * 50
g.fillStyle = '#5a442c'; g.fillRect(x, y, 56, 42)
if (rows[r] && rows[r][c]) {
g.fillStyle = '#c8102e'; g.fillRect(x + 3, y + 3, 50, 36)
g.fillStyle = '#fff'; g.font = '600 18px "Be Vietnam Pro", sans-serif'
g.fillText('✓', x + 28, y + 26)
} else {
g.fillStyle = '#8a6f4e'; g.fillRect(x + 3, y + 3, 50, 36)
}
}
}
}
const t = new THREE.CanvasTexture(c)
t.colorSpace = THREE.SRGBColorSpace
return t
}, [rows])
if (!rows) return null
return (
<mesh position={[0, 2.4, 0.16]} castShadow>
<planeGeometry args={[2.6, 1.3]} />
<meshStandardMaterial map={boardTex} roughness={0.85} />
</mesh>
)
}
/* ---- Camera rig + layout (MA gốc) ---- */` ],
/* Wire SteleTop vào StelePlate */
[ ` <meshStandardMaterial attach="material-1" color="#d6ccba" roughness={0.9} />
</mesh>
</group>
</group>
)
}`,
` <meshStandardMaterial attach="material-1" color="#d6ccba" roughness={0.9} />
</mesh>
</group>
<SteleTop />
</group>
)
}` ],
])
/* ================= cave3d.js ================= */
patchFile('frontend/cave3d.js', [
/* Cúp vàng + banner MU trên vòm */
[ `export function CaveScene() {`,
`/* =========================================================================
CÚP VÀNG TRUNG TÂM (trophy) — trước vòng huyền thoại, kỷ vật MU
========================================================================= */
function Trophy() {
return (
<group position={[0, 0, 0]} rotation-y={0.5}>
<mesh position={[0, 0.12, 0]} castShadow>
<cylinderGeometry args={[0.85, 1.0, 0.24, 8]} />
<meshStandardMaterial color="#5a4a2c" metalness={0.6} roughness={0.4} flatShading />
</mesh>
<mesh position={[0, 0.34, 0]} castShadow>
<cylinderGeometry args={[0.55, 0.7, 0.2, 8]} />
<meshStandardMaterial color="#8a6a2c" metalness={0.7} roughness={0.35} flatShading />
</mesh>
<mesh position={[0, 0.85, 0]} castShadow>
<cylinderGeometry args={[0.42, 0.5, 0.85, 10]} />
<meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading />
</mesh>
<mesh position={[0, 1.32, 0]} castShadow>
<torusGeometry args={[0.5, 0.09, 8, 18]} />
<meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading />
</mesh>
<mesh position={[-0.42, 0.85, 0]} rotation-z={0.3}>
<torusGeometry args={[0.26, 0.07, 8, 14, Math.PI]} />
<meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading />
</mesh>
<mesh position={[0.42, 0.85, 0]} rotation-z={-0.3}>
<torusGeometry args={[0.26, 0.07, 8, 14, Math.PI]} />
<meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading />
</mesh>
<mesh position={[0, 1.62, 0]} castShadow>
<sphereGeometry args={[0.2, 14, 14]} />
<meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading />
</mesh>
<pointLight position={[0, 2.6, 0]} color="#ffd76a" intensity={18} distance={9} decay={1.6} />
</group>
)
}
/* =========================================================================
BANNER MU TRÊN VÒM — không khí fan quanh hang
========================================================================= */
function makeBannerTex(text) {
const c = document.createElement('canvas')
c.width = 512; c.height = 128
const g = c.getContext('2d')
g.fillStyle = '#c8102e'; g.fillRect(0, 0, 512, 128)
g.fillStyle = '#8f0f22'; g.fillRect(6, 6, 500, 116)
g.strokeStyle = '#ffd76a'; g.lineWidth = 4; g.strokeRect(6, 6, 500, 116)
g.fillStyle = '#ffffff'; g.font = '900 52px "Be Vietnam Pro", sans-serif'
g.textAlign = 'center'; g.textBaseline = 'middle'
g.fillText(text, 256, 66)
const t = new THREE.CanvasTexture(c)
t.colorSpace = THREE.SRGBColorSpace
return t
}
function CaveBanners() {
const texA = useMemo(() => makeBannerTex('GLORY GLORY'), [])
const texB = useMemo(() => makeBannerTex('MAN UNITED'), [])
const texC = useMemo(() => makeBannerTex('MU FAN'), [])
const conf = [
{ tex: texA, pos: [-14, 12.5, -24], rot: [0, 0.5, 0] },
{ tex: texB, pos: [16, 12.8, -30], rot: [0, -0.45, 0] },
{ tex: texC, pos: [0, 13.2, -8], rot: [0, 0, 0] },
]
return (
<group>
{conf.map((c, i) => (
<group key={i} position={c.pos} rotation={c.rot}>
<mesh>
<planeGeometry args={[6, 1.5]} />
<meshBasicMaterial map={c.tex} transparent toneMapped={false} fog={false} />
</mesh>
</group>
))}
</group>
)
}
export function CaveScene() {` ],
/* Wire vào CaveScene (pattern sau patch_app: <JerseyDisplay /> có sẵn) */
[ ` <JerseyDisplay />
<Entrance />`,
` <Trophy />
<CaveBanners />
<JerseyDisplay />
<Entrance />` ],
])