/* 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')