Spaces:
Running
Running
Download patch_app.mjs from bep40/hangmu-docker: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/spaces/bep40/hangmu-docker/resolve/refs%2Fpr%2F2/patch_app.mjs
- Command line
-
hf download hf://spaces/bep40/hangmu-docker@refs/pr/2/patch_app.mjs
-
curl -L -o patch_app.mjs https://huggingface.co/spaces/bep40/hangmu-docker/resolve/refs%2Fpr%2F2/patch_app.mjs
15.5 kB
| /* Patch app.js + cave3d.js tại build-time. | |
| app.js: thêm nút Kéo co + Giữ bóng (challenge sheet), | |
| ErrorBoundary + window.onerror/unhandledrejection → POST /api/log, | |
| FIX UpdateNotice "Có bản mới": lần check đầu = baseline version đang chạy; | |
| chỉ báo "Có bản mới" khi version.json thực sự đổi — hết tự reload liên tục. | |
| cave3d.js: OutdoorScene (sân ngoài cửa hang) + LỐI VÀO sáng rõ + ÁO ĐỎ MU #7 trưng bày. | |
| Chạy TRƯỚC esbuild trong Dockerfile build stage. | |
| Nếu pattern không khớp -> exit(1) để build fail (không ship bản thiếu). */ | |
| 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 FAILED (${path}):`) | |
| failures.forEach((f) => console.error(' -', f)) | |
| process.exit(1) | |
| } | |
| fs.writeFileSync(path, s) | |
| console.log('patched ok:', path) | |
| } | |
| /* ================= app.js ================= */ | |
| patchFile('frontend/app.js', [ | |
| [ "const INVITE_ICON = { talk: '💬', duel: '🥊', penalty: '⚽' }", | |
| "const INVITE_ICON = { talk: '💬', duel: '🥊', penalty: '⚽', tug: '💪', ball: '👑' }" ], | |
| [ "const INVITE_ASKING = { talk: 'giao lưu mồm', duel: 'giao lưu võ thuật', penalty: 'sút luân lưu' }", | |
| "const INVITE_ASKING = { talk: 'giao lưu mồm', duel: 'giao lưu võ thuật', penalty: 'sút luân lưu', tug: 'kéo co', ball: 'giữ bóng' }" ], | |
| [ ` <button type="button" className="challenge-game is-last" onClick={() => setConfirm('penalty')}> | |
| <span className="challenge-icon">⚽</span> Sút luân lưu | |
| <small>5 lượt mỗi bên</small> | |
| </button>`, | |
| ` <button type="button" className="challenge-game" onClick={() => setConfirm('penalty')}> | |
| <span className="challenge-icon">⚽</span> Sút luân lưu | |
| <small>5 lượt mỗi bên</small> | |
| </button> | |
| <button type="button" className="challenge-game" onClick={() => setConfirm('tug')}> | |
| <span className="challenge-icon">💪</span> Kéo co | |
| <small>1v1 giữ nút để kéo</small> | |
| </button> | |
| <button type="button" className="challenge-game is-last" onClick={() => setConfirm('ball')}> | |
| <span className="challenge-icon">👑</span> Giữ bóng | |
| <small>1v1 cướp bóng giữ hết giờ</small> | |
| </button>` ], | |
| [ `createRoot(document.getElementById('root')).render( | |
| <React.StrictMode> | |
| <App /> | |
| </React.StrictMode> | |
| )`, | |
| `/* ============ ErrorBoundary + client error log (chống màn hình đen) ============ */ | |
| class CaveErrorBoundary extends React.Component { | |
| constructor(props) { super(props); this.state = { err: null } } | |
| static getDerivedStateFromError(err) { return { err } } | |
| componentDidCatch(err, info) { | |
| try { | |
| fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ kind: 'react', msg: String((err && err.message) || err).slice(0, 600), | |
| stack: String((err && err.stack) || '').slice(0, 2500), | |
| info: String((info && info.componentStack) || '').slice(0, 2500), href: location.href }) }).catch(() => {}) | |
| } catch {} | |
| } | |
| render() { | |
| if (this.state.err) { | |
| return ( | |
| <div style={{ position: 'fixed', inset: 0, zIndex: 99999, display: 'grid', placeItems: 'center', | |
| background: '#120b08', color: '#f3e7d8', fontFamily: 'system-ui, sans-serif', padding: 24, textAlign: 'center' }}> | |
| <div> | |
| <p style={{ fontSize: 28, fontWeight: 800, margin: '0 0 8px' }}>Có lỗi xảy ra 🛠️</p> | |
| <p style={{ opacity: 0.85, wordBreak: 'break-word', maxWidth: 640 }}> | |
| {String((this.state.err && this.state.err.message) || this.state.err)} | |
| </p> | |
| <button onClick={() => location.reload()} style={{ marginTop: 14, padding: '10px 24px', | |
| background: '#c8102e', color: '#fff', border: 0, borderRadius: 8, fontSize: 16, cursor: 'pointer' }}> | |
| Tải lại | |
| </button> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| return this.props.children | |
| } | |
| } | |
| window.addEventListener('error', (e) => { | |
| try { | |
| fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ kind: 'window', msg: String(e.message || '').slice(0, 600), | |
| stack: String((e.error && e.error.stack) || '').slice(0, 2500), href: location.href }) }).catch(() => {}) | |
| } catch {} | |
| }) | |
| window.addEventListener('unhandledrejection', (e) => { | |
| try { | |
| fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ kind: 'promise', msg: String((e.reason && e.reason.message) || e.reason || '').slice(0, 600), href: location.href }) }).catch(() => {}) | |
| } catch {} | |
| }) | |
| createRoot(document.getElementById('root')).render( | |
| <React.StrictMode> | |
| <CaveErrorBoundary> | |
| <App /> | |
| </CaveErrorBoundary> | |
| </React.StrictMode> | |
| )` ], | |
| /* ---- FIX "Có bản mới" tự tải lại liên tục ---- | |
| Cũ: `window.__hangmuVersion = null` mỗi lần mount → `id !== null` | |
| LUÔN đúng → cứ 15s reload, kể cả khi không có bản mới. | |
| Mới: lần check đầu = baseline (version đang chạy), chỉ báo "Có bản mới" | |
| khi version.json thay đổi THẬT SỰ sau đó. */ | |
| [ ` if (alive && id && id !== window.__hangmuVersion) setState({ available: true, seconds: 15, busy: false })`, | |
| ` if (!alive || !id) return | |
| if (baseline === null) { baseline = id; return } | |
| if (id !== baseline) setState({ available: true, seconds: 15, busy: false })` ], | |
| [ ` window.__hangmuVersion = null`, | |
| ` let baseline = null` ], | |
| ]) | |
| /* ================= cave3d.js ================= */ | |
| patchFile('frontend/cave3d.js', [ | |
| /* CaveScene: thêm JerseyDisplay + OutdoorScene */ | |
| [ ` <Entrance /> | |
| <hemisphereLight args={[0xe8c090, 0x7a6152, 1.5]} />`, | |
| ` <JerseyDisplay /> | |
| <Entrance /> | |
| <OutdoorScene /> | |
| <hemisphereLight args={[0xe8c090, 0x7a6152, 1.5]} />` ], | |
| /* LỐI VÀO HANG: thay tấm chặn mờ bằng vành đá sáng + vòm đá quanh cửa */ | |
| [ ` <mesh position={[0, 3, 17.5]}> | |
| <planeGeometry args={[14, 10]} /> | |
| <meshBasicMaterial color="#f4e7cf" side={THREE.DoubleSide} fog={false} /> | |
| </mesh>`, | |
| ` <mesh position={[0, 3.2, 17.6]}> | |
| <ringGeometry args={[6.3, 7.5, 30]} /> | |
| <meshLambertMaterial color="#6b5540" side={THREE.DoubleSide} fog={false} /> | |
| </mesh> | |
| <mesh position={[0, 3.3, 17.3]}> | |
| <torusGeometry args={[6.9, 0.72, 8, 26, Math.PI]} /> | |
| <meshLambertMaterial color="#8a6f55" flatShading side={THREE.DoubleSide} fog={false} /> | |
| </mesh> | |
| <pointLight position={[0, 10, 15.5]} color="#ffe9c8" intensity={45} distance={30} decay={1.2} fog={false} />` ], | |
| /* ÁO ĐỎ MU TRƯNG BÀY — jersey #7 trong khung gỗ cạnh màn hình lớn */ | |
| [ `export function CaveScene() {`, | |
| `/* ========================================================================= | |
| ÁO ĐỎ MU TRƯNG BÀY — jersey #7 trong khung gỗ cạnh màn hình lớn | |
| ========================================================================= */ | |
| function _jerseyTex() { | |
| if (typeof document === 'undefined') return null | |
| const c = document.createElement('canvas'); c.width = 512; c.height = 640 | |
| const g = c.getContext('2d') | |
| g.fillStyle = '#17120d'; g.fillRect(0, 0, 512, 640) | |
| g.fillStyle = '#241b12'; g.fillRect(26, 26, 460, 588) | |
| g.fillStyle = '#c8102e' | |
| g.beginPath(); g.moveTo(138, 168); g.lineTo(374, 168); g.lineTo(374, 468); g.lineTo(138, 468); g.closePath(); g.fill() | |
| g.beginPath(); g.moveTo(138, 168); g.lineTo(50, 118); g.lineTo(86, 332); g.lineTo(138, 298); g.closePath(); g.fill() | |
| g.beginPath(); g.moveTo(374, 168); g.lineTo(462, 118); g.lineTo(426, 332); g.lineTo(374, 298); g.closePath(); g.fill() | |
| g.fillStyle = '#7c0d20' | |
| g.beginPath(); g.arc(256, 172, 36, 0, Math.PI * 2); g.fill() | |
| g.fillStyle = '#ffffff' | |
| g.font = '800 200px Georgia, serif'; g.textAlign = 'center'; g.textBaseline = 'middle' | |
| g.fillText('7', 256, 368) | |
| g.fillStyle = '#ffe9c0'; g.font = '700 40px Georgia, serif' | |
| g.fillText('MANCHESTER UNITED', 256, 548) | |
| const t = new THREE.CanvasTexture(c) | |
| t.colorSpace = THREE.SRGBColorSpace | |
| t.anisotropy = 16 | |
| return t | |
| } | |
| function JerseyDisplay() { | |
| const [tex, setTex] = React.useState(null) | |
| React.useEffect(() => { | |
| let alive = true | |
| const t = _jerseyTex() | |
| if (alive && t) setTex(t) | |
| return () => { alive = false } | |
| }, []) | |
| return ( | |
| <group position={[15.5, 0, -41]} rotation={[0, -0.42, 0]}> | |
| <mesh position={[0, 0.425, -0.12]}> | |
| <boxGeometry args={[0.55, 0.85, 0.55]} /> | |
| <meshLambertMaterial color="#3d2a1a" flatShading /> | |
| </mesh> | |
| <mesh position={[0, 2.15, -0.1]}> | |
| <boxGeometry args={[2.7, 3.55, 0.16]} /> | |
| <meshLambertMaterial color="#5a3b22" flatShading /> | |
| </mesh> | |
| {tex && ( | |
| <mesh position={[0, 2.18, 0.04]}> | |
| <planeGeometry args={[2.35, 2.94]} /> | |
| <meshBasicMaterial map={tex} transparent fog={false} toneMapped={false} /> | |
| </mesh> | |
| )} | |
| </group> | |
| ) | |
| } | |
| export function CaveScene() {` ], | |
| /* OutdoorScene — sân ngoài cửa hang */ | |
| [ `export { floorHeight, domeHeight }`, | |
| `export { floorHeight, domeHeight } | |
| /* ========================================================================= | |
| OUTDOOR SCENE — sân ngoài cửa hang (+z), nhìn qua lối vào | |
| ========================================================================= */ | |
| function _makeSkyTex() { | |
| if (typeof document === 'undefined') return null | |
| const c = document.createElement('canvas'); c.width = 64; c.height = 256 | |
| const g = c.getContext('2d') | |
| const gr = g.createLinearGradient(0, 0, 0, 256) | |
| gr.addColorStop(0, '#7fb2e0'); gr.addColorStop(0.55, '#cfe3f2'); gr.addColorStop(0.8, '#e8d9bd'); gr.addColorStop(1, '#d8c2a0') | |
| g.fillStyle = gr; g.fillRect(0, 0, 64, 256) | |
| const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace | |
| return t | |
| } | |
| const _skyTexC = _makeSkyTex() | |
| function SkyDome() { | |
| const tex = useMemo(() => _skyTexC, []) | |
| return ( | |
| <mesh position={[0, 20, 178]} rotation={[0, Math.PI, 0]} fog={false}> | |
| <planeGeometry args={[170, 96]} /> | |
| <meshBasicMaterial map={tex} fog={false} toneMapped={false} depthWrite={false} /> | |
| </mesh> | |
| ) | |
| } | |
| function OutdoorGround() { | |
| const geo = useMemo(() => { | |
| const m = new THREE.PlaneGeometry(170, 100, 70, 40) | |
| m.rotateX(-Math.PI / 2) | |
| const p = m.attributes.position | |
| const cols = new Float32Array(p.count * 3) | |
| const cA = new THREE.Color('#8a7a5c'), cB = new THREE.Color('#6d5c42'), moss = new THREE.Color('#7d7a45'), 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.06, z * 0.06, 0, 21) * 1.1 - 0.3) | |
| tmp.copy(cA).lerp(cB, fbm3(x * 0.12, z * 0.12, 3, 22) * 0.5 + 0.25) | |
| tmp.lerp(moss, Math.max(0, fbm3(x * 0.08, 9, z * 0.08, 23)) * 0.35) | |
| 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 ( | |
| <mesh geometry={geo} position={[0, 0, 126]} receiveShadow fog={false}> | |
| <meshLambertMaterial vertexColors fog={false} /> | |
| </mesh> | |
| ) | |
| } | |
| function OutdoorPath() { | |
| return ( | |
| <mesh position={[0, 0.04, 112]} rotation={[-Math.PI / 2, 0, 0]} fog={false}> | |
| <planeGeometry args={[11, 66]} /> | |
| <meshBasicMaterial color="#a89474" fog={false} transparent opacity={0.85} /> | |
| </mesh> | |
| ) | |
| } | |
| function OutdoorProps() { | |
| const items = useMemo(() => { | |
| const out = [] | |
| const boulder = (x, z, s, seed) => { | |
| const geo = displaceAndColor(new THREE.IcosahedronGeometry(1, 1), | |
| { amp: 0.3, freq: 1.1, seed, base: '#9a8a6c', shade: '#77664c', dark: '#4c3f2e', warm: '#b3a284' }) | |
| out.push({ kind: 'boulder', geo, x, z, s }) | |
| } | |
| const tree = (x, z, s, seed) => out.push({ kind: 'tree', x, z, s, seed }) | |
| boulder(-8, 80, 1.6, 101); boulder(9, 82, 2.1, 102); boulder(-14, 86, 2.8, 103) | |
| boulder(15, 88, 2.4, 104); boulder(-6, 94, 1.2, 105); boulder(11, 96, 1.4, 106) | |
| boulder(-18, 100, 3.2, 107); boulder(19, 102, 3.0, 108) | |
| tree(-12, 88, 1.5, 201); tree(12, 90, 1.8, 202); tree(-16, 96, 2.1, 203); tree(16, 98, 2.0, 204) | |
| tree(-8, 104, 1.4, 205); tree(8, 108, 1.6, 206) | |
| return out | |
| }, []) | |
| return ( | |
| <group> | |
| {items.map((it, i) => it.kind === 'boulder' ? ( | |
| <mesh key={i} geometry={it.geo} position={[it.x, it.s * 0.35, it.z]} scale={[it.s, it.s * 0.85, it.s]} fog={false}> | |
| <meshLambertMaterial vertexColors flatShading fog={false} /> | |
| </mesh> | |
| ) : ( | |
| <group key={i} position={[it.x, 0, it.z]} scale={[it.s, it.s, it.s]} fog={false}> | |
| <mesh position={[0, 1.1, 0]} fog={false}> | |
| <cylinderGeometry args={[0.22, 0.3, 2.2, 6]} /> | |
| <meshLambertMaterial color="#6b4a2c" flatShading fog={false} /> | |
| </mesh> | |
| {[0, 1, 2].map((k) => ( | |
| <mesh key={k} position={[Math.sin(k * 2.1) * 0.7, 2.6 + k * 0.55, Math.cos(k * 2.1) * 0.7]} fog={false}> | |
| <coneGeometry args={[1.35 - k * 0.25, 1.6, 7]} /> | |
| <meshLambertMaterial color={k === 0 ? '#3f6b2e' : k === 1 ? '#4c7a38' : '#5a8a42'} flatShading fog={false} /> | |
| </mesh> | |
| ))} | |
| </group> | |
| ))} | |
| </group> | |
| ) | |
| } | |
| function OutdoorLight() { | |
| const tex = useMemo(() => { | |
| const c = document.createElement('canvas'); c.width = c.height = 128 | |
| const g = c.getContext('2d') | |
| const gr = g.createRadialGradient(64, 64, 2, 64, 64, 62) | |
| gr.addColorStop(0, 'rgba(255,240,200,0.9)'); gr.addColorStop(0.5, 'rgba(255,214,140,0.35)'); gr.addColorStop(1, 'rgba(255,214,140,0)') | |
| g.fillStyle = gr; g.fillRect(0, 0, 128, 128) | |
| const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace | |
| return t | |
| }, []) | |
| return ( | |
| <> | |
| <directionalLight position={[15, 30, 140]} intensity={1.6} color="#fff1d6" fog={false} /> | |
| <pointLight position={[0, 16, 118]} color="#ffe6c0" intensity={5} distance={70} decay={0.5} fog={false} /> | |
| <sprite position={[0, 34, 132]} scale={[34, 34, 1]} fog={false}> | |
| <spriteMaterial map={tex} transparent opacity={0.9} fog={false} depthWrite={false} blending={THREE.AdditiveBlending} /> | |
| </sprite> | |
| </> | |
| ) | |
| } | |
| function OutdoorScene() { | |
| return ( | |
| <group> | |
| <SkyDome /> | |
| <OutdoorGround /> | |
| <OutdoorPath /> | |
| <OutdoorProps /> | |
| <OutdoorLight /> | |
| </group> | |
| ) | |
| }` ], | |
| ]) |