/* patch_scene.mjs — v17: sân trước cửa hang + cờ MU + áo MU + bảng điểm danh
VẤN ĐỀ v16: cờ MU đặt z=66 nhưng sân ngoài (OutdoorGround) chỉ bắt đầu z~76 và
walkR guard chặn z<55 → vùng z 55..78 là khoảng trống giữa cửa hang và sân
ngoài: không đất nền, vật phẩm vô hình.
FIX v17 (chạy trên SOURCE GỐC cave3d.js — bản chưa patch):
(A) Forecourt — sân đất trước cửa hang (z 55..80) nối cửa với sân ngoài.
(B) Cờ MU/GVMU 2 hàng hai bên lối ra trên forecourt (z=62, z=74).
(C) Ghế gỗ nghỉ chân cạnh forecourt.
(D) Bảng điểm danh CHUYEN CAN THANG NAY dựng trái cửa (z=62).
(E) Giá treo áo MU #7 dựng phải cửa (z=62).
(F) Kéo dài OutdoorPath (11x86) nối liền forecourt.
LƯU Ý: OutdoorScene trong SOURCE GỐC chỉ gồm SkyDome/Ground/Path/Props/Light
(không có Flags/Bench — những cái đó chỉ tồn tại trong bản build v16).
Nếu pattern lệch -> exit(1) để build fail. */
import fs from 'node:fs'
function patchFile(path, reps) {
let s = fs.readFileSync(path, 'utf8')
const failures = []
for (const [oldStr, newStr] of reps) {
const count = s.split(oldStr).length - 1
if (count === 1) { s = s.replace(oldStr, newStr); continue }
failures.push(`pattern not unique/found (count=${count}): ${oldStr.slice(0, 80)}`)
}
if (failures.length) {
console.error(`PATCH SCENE FAILED (${path}):`)
failures.forEach((f) => console.error(' -', f))
process.exit(1)
}
fs.writeFileSync(path, s)
console.log('patched ok:', path)
}
/* ================= cave3d.js ================= */
patchFile('frontend/cave3d.js', [
/* ---- (A) Forecourt: san dat noi cua hang -> san ngoai ---- */
[
`function OutdoorGround() {`,
`function Forecourt() {
const geo = useMemo(() => {
const m = new THREE.PlaneGeometry(64, 26, 40, 18)
m.rotateX(-Math.PI / 2)
const p = m.attributes.position
const cols = new Float32Array(p.count * 3)
const cA = new THREE.Color('#7d7258'), cB = new THREE.Color('#5f5540'), moss = new THREE.Color('#6d6a3e'), tmp = new THREE.Color()
for (let i = 0; i < p.count; i++) {
const x = p.getX(i), z = p.getZ(i)
p.setY(i, fbm3(x * 0.09, z * 0.09, 0, 31) * 0.22 + 0.02)
tmp.copy(cA).lerp(cB, fbm3(x * 0.16, z * 0.16, 3, 32) * 0.5 + 0.25)
tmp.lerp(moss, Math.max(0, fbm3(x * 0.1, 9, z * 0.1, 33)) * 0.3)
cols[i * 3] = tmp.r; cols[i * 3 + 1] = tmp.g; cols[i * 3 + 2] = tmp.b
}
m.setAttribute('color', new THREE.BufferAttribute(cols, 3))
m.computeVertexNormals()
return m
}, [])
return (
)
}
function OutdoorGround() {`,
],
/* ---- (B2E) OutdoorScene mới: flags + bench + attendance + jersey ---- */
[
`function OutdoorScene() {
return (
)
}`,
`/* ---- Bang diem danh (chuyen can) truoc cua hang ---- */
function AttendanceForecourt() {
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('CHUYEN CAN THANG NAY', 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('v', 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 (
)
}
/* ---- Gia treo ao MU (jersey #7) canh cua ---- */
function JerseyStand() {
const [tex, setTex] = React.useState(null)
React.useEffect(() => {
let alive = true
const t = _jerseyTex()
if (alive && t) setTex(t)
return () => { alive = false }
}, [])
return (
)
}
/* ---- Co phuon MU hai ben loi ra (forecourt) ---- */
function makePennantTex(text) {
if (typeof document === 'undefined') return null
const c = document.createElement('canvas'); c.width = 128; c.height = 128
const g = c.getContext('2d')
g.fillStyle = '#c8102e'; g.fillRect(0, 0, 128, 128)
g.strokeStyle = '#ffd76a'; g.lineWidth = 6; g.strokeRect(4, 4, 120, 120)
g.fillStyle = '#ffffff'; g.font = '700 40px "Be Vietnam Pro", sans-serif'
g.textAlign = 'center'; g.textBaseline = 'middle'
g.fillText(text, 64, 66)
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace
return t
}
function OutdoorFlags() {
const texM = useMemo(() => makePennantTex('MU'), [])
const texG = useMemo(() => makePennantTex('GGMU'), [])
const flags = [
{ x: -7, z: 62, tex: texM }, { x: 7, z: 62, tex: texG },
{ x: -11, z: 74, tex: texG }, { x: 11, z: 74, tex: texM },
]
return (
{flags.map((f, i) => (
))}
)
}
/* ---- Ghe go nghi chan ---- */
function OutdoorBench() {
const bench = (x, z, rot) => (
)
return (
{bench(-6, 60, 0.12)}
{bench(6, 60, -0.1)}
)
}
function OutdoorScene() {
return (
)
}`,
],
/* ---- (F) Keo dai OutdoorPath len forecourt nhin lien mach ---- */
[
` `,
` `,
],
])
console.log('patch_scene.mjs OK — forecourt + MU flags + jersey stand + attendance board added')