/* ========================================================================= patch_full.mjs — Hoàn thiện 100% giao diện + scene 3D so với hangmu.online: (chạy SAU patch_app.mjs + patch_3d.mjs, TRƯỚC esbuild) - cave3d.js: TƯỜNG HUYỀN THOẠI (3 bảng + 10 chân dung), bảng GGMU run rally, BẢNG XẾP HẠNG Ngoại hạng Anh (EPL), NGÀY NÀY NĂM XƯA (lịch sử 70 mốc), bảng hiệu HỘI TRƯỜNG ĐÁ, hình nộm PHÒNG XẢ GIẬN (4 nộm quanh hố lửa), SÂN KÉO CO + SÂN GIỮ BÓNG vẽ trên sàn (2 bên trái/phải hang), avatar nhân vật thêm SỐ ÁO (jersey number texture kiểu gốc). 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, 80)}`) } for (const [o, n] of reps) rep(o, n) if (failures.length) { console.error(`PATCH FULL FAILED (${path}):`) failures.forEach((f) => console.error(' -', f)) process.exit(1) } fs.writeFileSync(path, s) console.log('patched ok (full):', path) } /* ================= cave3d.js ================= */ patchFile('frontend/cave3d.js', [ /* ---- EPL standings texture (bảng xếp hạng Ngoại hạng Anh, giữ liệu khi lỗi) ---- */ [ `export function CaveScene() {`, `/* ========================================================================= BẢNG XẾP HẠNG NGOẠI HẠNG ANH (EPL) — texture vẽ như gốc, dữ liệu tĩnh mùa gần nhất ========================================================================= */ const EPL_TEAMS = [ ['Arsenal', 38, 28, 5, 5, 91, 29, 89, 'ucl'], ['Man City', 38, 28, 7, 3, 96, 34, 91, 'ucl'], ['Liverpool', 38, 24, 10, 4, 86, 41, 82, 'ucl'], ['Aston Villa', 38, 20, 8, 10, 76, 61, 68, 'ucl'], ['Tottenham', 38, 20, 6, 12, 74, 61, 66, 'europe'], ['Chelsea', 38, 18, 9, 11, 77, 63, 63, 'europe'], ['Newcastle', 38, 18, 6, 14, 85, 62, 60, 'europe'], ['Man United', 38, 18, 6, 14, 57, 58, 60, 'europe'], ['West Ham', 38, 14, 10, 14, 60, 74, 52, ''], ['Crystal Palace', 38, 13, 10, 15, 57, 58, 49, ''], ['Brighton', 38, 12, 12, 14, 55, 62, 48, ''], ['Bournemouth', 38, 13, 9, 16, 54, 67, 48, ''], ['Fulham', 38, 13, 8, 17, 55, 61, 47, ''], ['Wolves', 38, 13, 7, 18, 50, 65, 46, ''], ['Everton', 38, 13, 9, 16, 40, 51, 40, ''], ['Brentford', 38, 10, 9, 19, 56, 65, 39, ''], ['Nottm Forest', 38, 9, 9, 20, 49, 67, 32, ''], ['Luton', 38, 6, 8, 24, 52, 85, 26, 'relegation'], ['Burnley', 38, 5, 9, 24, 41, 78, 24, 'relegation'], ['Sheffield Utd', 38, 3, 7, 28, 35, 104, 16, 'relegation'], ] function makeEplTex() { const c = document.createElement('canvas'); c.width = 1400; c.height = 1560 const g = c.getContext('2d') const gr = g.createLinearGradient(0, 0, 0, 1560) gr.addColorStop(0, '#5c0e1b'); gr.addColorStop(0.5, '#3a0812'); gr.addColorStop(1, '#1b1410') g.fillStyle = gr; g.fillRect(0, 0, 1400, 1560) g.strokeStyle = 'rgba(255,240,220,0.12)'; g.lineWidth = 14; g.strokeRect(7, 7, 1386, 1546) g.textAlign = 'center'; g.fillStyle = '#8a1a2a' g.font = '900 96px Bitter, Georgia, serif'; g.fillText('NGOẠI HẠNG ANH', 700, 130) g.fillStyle = 'rgba(255,240,220,0.7)'; g.font = '600 42px "Be Vietnam Pro", sans-serif' g.fillText('Bảng xếp hạng mùa giải', 700, 195) const cols = [700, 1070, 1180, 1280] g.fillStyle = 'rgba(201,162,78,0.85)'; g.font = '600 32px "Be Vietnam Pro", sans-serif' ;['Đội', 'T', 'H', 'Đ'].forEach((t, i) => g.fillText(t, cols[i], 250)) for (let i = 0; i < EPL_TEAMS.length; i++) { const t = EPL_TEAMS[i] const y = 300 + i * 58 g.fillStyle = i % 2 ? 'rgba(255,240,220,0.045)' : 'rgba(255,240,220,0.0)' g.fillRect(20, y - 22, 1360, 56) if (i === 7) { g.fillStyle = 'rgba(142,11,32,0.5)'; g.fillRect(20, y - 22, 1360, 56) } g.textAlign = 'left'; g.fillStyle = i === 7 ? '#ffd76a' : (t[7] ? '#8e0b20' : 'rgba(255,240,220,0.85)') g.font = i === 7 ? '800 42px Bitter' : '600 42px "Be Vietnam Pro", sans-serif' g.fillText(t[0], 60, y + 8) g.textAlign = 'center' g.fillStyle = 'rgba(255,240,220,0.7)' ;[t[1], t[5] - t[6], t[6]].forEach((v, k) => { g.fillText(String(v), cols[k + 1], y + 8) }) g.fillStyle = i === 7 ? '#ffd76a' : 'rgba(255,240,220,0.95)' g.font = '800 44px Bitter'; g.fillText(String(t[7]), 1310, y + 8) } g.textAlign = 'right'; g.fillStyle = 'rgba(255,240,220,0.4)'; g.font = '500 28px "Be Vietnam Pro", sans-serif' g.fillText('Dự Cúp C1 · Cúp châu Âu · Xuống hạng · Đ = Điểm', 1360, 1540) const tx = new THREE.CanvasTexture(c); tx.colorSpace = THREE.SRGBColorSpace; tx.anisotropy = 16 return tx } function EplBoard() { const [tex, setTex] = React.useState(null) React.useEffect(() => { let alive = true const t = makeEplTex() if (alive && t) setTex(t) return () => { alive = false } }, []) if (!tex) return null return ( ) } /* ========================================================================= NGÀY NÀY NĂM XƯA — bảng lịch sử 70 mốc MU, đổi theo ngày thật ========================================================================= */ const ON_THIS_DAY = [ { m: 1, d: 21, y: 2017, title: 'Rooney thành vua phá lưới CLB', text: 'Cú sút phạt ở phút bù giờ trước Stoke là bàn thứ 250 — vượt kỷ lục của Sir Bobby Charlton.' }, { m: 2, d: 5, y: 1985, title: 'Cristiano Ronaldo chào đời', text: 'Sinh ở Madeira. 2008: Quả bóng Vàng đầu tiên trong màu áo Quỷ Đỏ.' }, { m: 2, d: 6, y: 1958, title: 'Thảm hoạ Munich', text: 'Chuyến bay chở đội bóng gặp nạn trên đường về từ Belgrade — 8 cầu thủ "Busby Babes" ra đi. Mãi không quên.' }, { m: 2, d: 18, y: 1975, title: 'Gary Neville chào đời', text: 'Đội trưởng, hậu vệ phải trung thành trọn đời với Manchester United.' }, { m: 2, d: 19, y: 1910, title: 'Trận đầu tiên ở Old Trafford', text: 'Sân mới khai trương với trận gặp Liverpool. "Nhà hát của những giấc mơ" bắt đầu từ đây.' }, { m: 2, d: 19, y: 1958, title: 'Trận đầu sau Munich', text: '13 ngày sau thảm hoạ, MU thắng Sheffield Wednesday 3–0 ở FA Cup trong nước mắt cả Old Trafford.' }, { m: 2, d: 21, y: 1958, title: 'Duncan Edwards ra đi', text: 'Viên ngọc của "Busby Babes" qua đời sau 15 ngày chiến đấu với vết thương từ Munich, mới 21 tuổi.' }, { m: 2, d: 24, y: 1940, title: 'Denis Law chào đời', text: '"The King" của Stretford End, Quả bóng Vàng 1964.' }, { m: 2, d: 26, y: 1973, title: 'Ole Gunnar Solskjær chào đời', text: 'Người ghi bàn thắng định mệnh ở Camp Nou 1999.' }, { m: 2, d: 26, y: 2006, title: 'Cúp Liên đoàn 2006', text: 'MU đè bẹp Wigan 4–0 ở chung kết, Rooney lập cú đúp.' }, { m: 2, d: 26, y: 2017, title: 'Cúp Liên đoàn 2017', text: 'Thắng Southampton 3–2 ở Wembley, Ibrahimović ghi bàn quyết định.' }, { m: 2, d: 26, y: 2023, title: 'Cúp Liên đoàn 2023', text: 'Thắng Newcastle 2–0 ở Wembley — danh hiệu đầu tiên sau 6 năm.' }, { m: 2, d: 28, y: 2010, title: 'Cúp Liên đoàn 2010', text: 'Thắng Aston Villa 2–1 ở chung kết, giữ cúp năm thứ hai liên tiếp.' }, { m: 3, d: 1, y: 2009, title: 'Cúp Liên đoàn 2009', text: 'Hoà Tottenham 0–0, thắng 4–1 trên chấm luân lưu ở Wembley.' }, { m: 3, d: 2, y: 1991, title: 'Ryan Giggs ra mắt', text: 'Trận đầu tiên của cầu thủ sau này khoác áo MU nhiều trận nhất lịch sử.' }, { m: 3, d: 4, y: 1995, title: 'Thắng Ipswich 9–0', text: 'Chiến thắng đậm nhất lịch sử Premier League lúc đó — Andy Cole ghi 5 bàn.' }, { m: 4, d: 12, y: 1992, title: 'Cúp Liên đoàn đầu tiên', text: 'Thắng Nottingham Forest 1–0 ở Wembley, Brian McClair ghi bàn.' }, { m: 4, d: 14, y: 1999, title: 'Siêu phẩm của Giggs', text: 'Bán kết lại FA Cup với Arsenal: Giggs solo qua cả hàng thủ ở hiệp phụ, cởi áo ăn mừng.' }, { m: 4, d: 21, y: 1999, title: 'Ngược dòng ở Turin', text: 'Bị Juventus dẫn 2–0, Keane khơi mào, MU thắng 3–2 vào chung kết Champions League.' }, { m: 4, d: 22, y: 2013, title: 'Chức vô địch thứ 20', text: 'Van Persie lập hat-trick vào lưới Aston Villa, MU vô địch Ngoại hạng Anh lần thứ 20.' }, { m: 4, d: 24, y: 1948, title: 'FA Cup 1948', text: 'Đội của Matt Busby thắng Blackpool 4–2 ở Wembley — danh hiệu đầu tiên thời Busby.' }, { m: 5, d: 2, y: 1975, title: 'David Beckham chào đời', text: 'Số 7 với những cú sút phạt và tạt bóng "thương hiệu", một phần của cú ăn ba 1999.' }, { m: 5, d: 8, y: 2013, title: 'Sir Alex chia tay', text: 'Sau hơn 26 năm và 38 danh hiệu, Sir Alex Ferguson thông báo nghỉ hưu.' }, { m: 5, d: 11, y: 1996, title: 'FA Cup 1996 — cú đúp thứ hai', text: 'Cantona ghi bàn duy nhất vào lưới Liverpool, MU giành cú đúp quốc nội lần thứ hai.' }, { m: 5, d: 14, y: 1994, title: 'Cú đúp đầu tiên', text: 'Thắng Chelsea 4–0 ở chung kết FA Cup — cú đúp vô địch + FA Cup đầu tiên của CLB.' }, { m: 5, d: 14, y: 2011, title: 'Chức vô địch thứ 19', text: 'Hoà Blackburn 1–1, MU vượt Liverpool về số lần vô địch nước Anh.' }, { m: 5, d: 15, y: 1981, title: 'Patrice Evra chào đời', text: 'Hậu vệ trái của 5 chức vô địch Premier League.' }, { m: 5, d: 15, y: 1991, title: 'Cúp C2 châu Âu', text: 'Thắng Barcelona 2–1 ở Rotterdam, Mark Hughes lập cú đúp.' }, { m: 5, d: 16, y: 1999, title: 'Mảnh ghép thứ nhất của cú ăn ba', text: 'Thắng Tottenham 2–1 ngày cuối mùa, giành chức vô địch Premier League.' }, { m: 5, d: 16, y: 2009, title: 'Chức vô địch thứ 18', text: 'Hoà Arsenal 0–0 ở Old Trafford, vô địch Premier League năm thứ ba liên tiếp.' }, { m: 5, d: 17, y: 1990, title: 'Danh hiệu đầu tiên của Sir Alex', text: 'Đá lại chung kết FA Cup, Lee Martin ghi bàn duy nhất vào lưới Crystal Palace.' }, { m: 5, d: 18, y: 1985, title: 'FA Cup 1985', text: 'Chơi thiếu người, MU vẫn thắng Everton 1–0 nhờ cú cứa lòng của Norman Whiteside ở hiệp phụ.' }, { m: 5, d: 19, y: 2013, title: 'Trận cuối của Sir Alex', text: 'Hoà West Brom 5–5 — một trận cầu điên rồ khép lại kỷ nguyên vĩ đại nhất của CLB.' }, { m: 5, d: 21, y: 1977, title: 'FA Cup 1977', text: 'Thắng Liverpool 2–1 ở Wembley, chặn đứng giấc mơ cú ăn ba của đối thủ.' }, { m: 5, d: 21, y: 2008, title: 'Vô địch châu Âu ở Moscow', text: 'Hoà Chelsea 1–1 dưới mưa, Van der Sar cản phạt Anelka — MU lần thứ ba vô địch châu Âu.' }, { m: 5, d: 21, y: 2016, title: 'FA Cup 2016', text: 'Lingard volley ở hiệp phụ, thắng Crystal Palace 2–1 — FA Cup thứ 12.' }, { m: 5, d: 22, y: 1946, title: 'George Best chào đời', text: 'Thiên tài người Bắc Ireland, Quả bóng Vàng 1968.' }, { m: 5, d: 22, y: 1999, title: 'Mảnh ghép thứ hai của cú ăn ba', text: 'Thắng Newcastle 2–0 ở chung kết FA Cup — Sheringham và Scholes ghi bàn.' }, { m: 5, d: 22, y: 2004, title: 'FA Cup 2004', text: 'Thắng Millwall 3–0 ở Cardiff, Van Nistelrooy lập cú đúp.' }, { m: 5, d: 24, y: 1966, title: 'Eric Cantona chào đời', text: '"Le Roi" cổ áo dựng đứng, người thắp lửa kỷ nguyên Premier League.' }, { m: 5, d: 24, y: 2017, title: 'Vô địch Europa League', text: 'Thắng Ajax 2–0 ở Stockholm — danh hiệu châu Âu duy nhất còn thiếu trong tủ.' }, { m: 5, d: 25, y: 1963, title: 'FA Cup 1963', text: 'Thắng Leicester 3–1 ở Wembley, danh hiệu đầu tiên sau thảm hoạ Munich.' }, { m: 5, d: 25, y: 2024, title: 'FA Cup 2024', text: 'Thắng Man City 2–1 ở chung kết — Garnacho và Mainoo ghi bàn.' }, { m: 5, d: 26, y: 1909, title: 'Sir Matt Busby chào đời', text: 'Người dựng lại CLB từ đống tro tàn, đưa MU lên đỉnh châu Âu 1968.' }, { m: 5, d: 26, y: 1983, title: 'FA Cup 1983', text: 'Đá lại chung kết, MU thắng Brighton 4–0.' }, { m: 5, d: 26, y: 1999, title: 'Cú ăn ba — Camp Nou', text: 'Bị Bayern dẫn tới phút 90, Sheringham rồi Solskjær ghi bàn bù giờ — MU vô địch châu Âu, đúng sinh nhật Sir Matt.' }, { m: 5, d: 29, y: 1968, title: 'Lần đầu vô địch châu Âu', text: 'Thắng Benfica 4–1 sau hiệp phụ ở Wembley — 10 năm sau Munich. Charlton lập cú đúp.' }, { m: 7, d: 28, y: 1981, title: 'Michael Carrick chào đời', text: 'Nhạc trưởng thầm lặng của tuyến giữa, vô địch Champions League 2008.' }, { m: 8, d: 10, y: 1971, title: 'Roy Keane chào đời', text: 'Đội trưởng thép, linh hồn của những năm 1990.' }, { m: 8, d: 16, y: 2003, title: 'Ronaldo ra mắt Old Trafford', text: 'Vào sân từ ghế dự bị trước Bolton, cậu bé 18 tuổi khiến cả sân đứng dậy.' }, { m: 8, d: 17, y: 1996, title: 'Beckham ghi bàn từ giữa sân', text: 'Cú sút từ vạch giữa sân vào lưới Wimbledon khiến cả nước Anh biết tên số 10 trẻ tuổi.' }, { m: 8, d: 28, y: 2011, title: 'Thắng Arsenal 8–2', text: 'Rooney lập hat-trick ở Old Trafford trong một chiều không thể quên.' }, { m: 9, d: 8, y: 1994, title: 'Bruno Fernandes chào đời', text: 'Đội trưởng, nhạc trưởng của Quỷ Đỏ thời hiện đại.' }, { m: 9, d: 11, y: 2021, title: 'Ronaldo trở về', text: 'Lần thứ hai ra mắt ở Old Trafford, lập cú đúp vào lưới Newcastle.' }, { m: 9, d: 28, y: 2004, title: 'Rooney ra mắt bằng hat-trick', text: 'Trận đầu cho MU, Rooney 18 tuổi ghi 3 bàn vào lưới Fenerbahçe ở Champions League.' }, { m: 10, d: 1, y: 1936, title: 'Duncan Edwards chào đời', text: 'Viên ngọc sáng nhất của "Busby Babes".' }, { m: 10, d: 11, y: 1937, title: 'Sir Bobby Charlton chào đời', text: 'Người sống sót ở Munich, vô địch World Cup 1966 và châu Âu 1968, biểu tượng muôn đời của CLB.' }, { m: 10, d: 21, y: 1981, title: 'Nemanja Vidić chào đời', text: 'Bức tường thép cùng Rio Ferdinand, đội trưởng mùa vô địch 2011.' }, { m: 10, d: 24, y: 1985, title: 'Wayne Rooney chào đời', text: 'Chân sút vĩ đại nhất lịch sử CLB với 253 bàn thắng.' }, { m: 10, d: 29, y: 1970, title: 'Edwin van der Sar chào đời', text: 'Người gác đền, anh hùng loạt luân lưu Moscow 2008.' }, { m: 11, d: 6, y: 1986, title: 'Sir Alex Ferguson nhậm chức', text: 'Ngày một người Scotland tới Old Trafford — mở ra 26 năm và 38 danh hiệu.' }, { m: 11, d: 7, y: 1978, title: 'Rio Ferdinand chào đời', text: 'Trung vệ thanh lịch của 6 chức vô địch Premier League.' }, { m: 11, d: 16, y: 1974, title: 'Paul Scholes chào đời', text: '"Hoàng tử Ginger", người được cả những huyền thoại gọi là tiền vệ hay nhất họ từng gặp.' }, { m: 11, d: 18, y: 1963, title: 'Peter Schmeichel chào đời', text: 'Người khổng lồ Đan Mạch, đội trưởng đêm Camp Nou 1999.' }, { m: 11, d: 26, y: 1992, title: 'Cantona tới Old Trafford', text: 'Bản hợp đồng từ Leeds thay đổi tất cả: mùa đó MU vô địch sau 26 năm chờ đợi.' }, { m: 11, d: 29, y: 1973, title: 'Ryan Giggs chào đời', text: 'Cầu thủ khoác áo MU nhiều trận nhất, 13 lần vô địch nước Anh.' }, { m: 11, d: 30, y: 1999, title: 'Vô địch Cúp Liên lục địa', text: 'Thắng Palmeiras 1–0 ở Tokyo, Keane ghi bàn — MU lên đỉnh thế giới.' }, { m: 12, d: 2, y: 2008, title: 'Ronaldo đoạt Quả bóng Vàng', text: 'Cầu thủ MU đầu tiên giành Quả bóng Vàng sau George Best 1968.' }, { m: 12, d: 21, y: 2008, title: 'Vô địch thế giới các CLB', text: 'Thắng LDU Quito 1–0 ở Yokohama, Rooney ghi bàn.' }, { m: 12, d: 31, y: 1941, title: 'Sir Alex Ferguson chào đời', text: 'HLV vĩ đại nhất lịch sử bóng đá Anh, sinh ở Govan, Glasgow.' }, ] function OnThisDayBoard() { const [tex, setTex] = React.useState(null) React.useEffect(() => { let alive = true const c = document.createElement('canvas'); c.width = 900; c.height = 1000 const g = c.getContext('2d') const gr = g.createLinearGradient(0, 0, 0, 1000) gr.addColorStop(0, '#4f0c18'); gr.addColorStop(0.6, '#2a060d'); gr.addColorStop(1, '#180307') g.fillStyle = gr; g.fillRect(0, 0, 900, 1000) g.strokeStyle = 'rgba(201,162,78,0.8)'; g.lineWidth = 8; g.strokeRect(8, 8, 884, 984) g.textAlign = 'center'; g.fillStyle = '#c9a24e'; g.font = '800 44px Bitter, Georgia, serif' g.fillText('NGÀY NÀY NĂM XƯA', 450, 80) const now = new Date() const today = ON_THIS_DAY.filter((e) => e.m === now.getMonth() + 1 && e.d === now.getDate()) g.fillStyle = 'rgba(255,240,220,0.5)'; g.font = '600 28px "Be Vietnam Pro", sans-serif' g.fillText(today.length ? '' + today.length + ' mốc son hôm nay' : 'Hôm nay chưa có dấu mốc nào — hãy tạo nên lịch sử!', 450, 125) if (today.length) { today.forEach((e, i) => { const y = 210 + i * 210 g.fillStyle = 'rgba(142,11,32,0.45)'; g.fillRect(60, y - 60, 780, 170) g.strokeStyle = 'rgba(201,162,78,0.6)'; g.lineWidth = 3; g.strokeRect(60, y - 60, 780, 170) g.textAlign = 'left'; g.fillStyle = '#ffd76a'; g.font = '700 38px "Be Vietnam Pro", sans-serif' g.fillText(e.d + ' THÁNG ' + e.m + ' · ' + e.y, 90, y - 22) g.fillStyle = '#fff2c8'; g.font = '800 52px Bitter, Georgia, serif' g.fillText(e.title.slice(0, 40), 90, y + 25) g.fillStyle = 'rgba(255,240,220,0.8)'; g.font = '500 26px "Be Vietnam Pro", sans-serif' g.fillText(e.text.length > 62 ? e.text.slice(0, 60) + '…' : e.text, 90, y + 78, 720) }) } const tx = new THREE.CanvasTexture(c); tx.colorSpace = THREE.SRGBColorSpace; tx.anisotropy = 16 if (alive) setTex(tx) return () => { alive = false } }, []) if (!tex) return null return ( ) } /* ========================================================================= TƯỜNG HUYỀN THOẠI — 3 bảng lớn quanh vòm (y như gốc PA/PB/PC) + 10 chân dung ========================================================================= */ const LEGEND_PANELS = [ { kicker: 'DI TÍCH LỊCH SỬ', title: 'Hang MU', sub: 'Nơi anh em fan Quỷ Đỏ tụ họp' }, { kicker: 'MANCHESTER UNITED', title: 'Từ 1878', sub: 'Glory Glory Man United' }, { kicker: 'TƯỜNG HUYỀN THOẠI', title: 'Thế hệ vàng', sub: 'Mười huyền thoại khoác áo đỏ' }, ] function drawLegendPanel(t) { const c = document.createElement('canvas'); c.width = 1024; c.height = 512 const g = c.getContext('2d') const r = g.createRadialGradient(307, 0, 40, 410, 205, 820) r.addColorStop(0, '#96202f'); r.addColorStop(0.5, '#7a1424'); r.addColorStop(1, '#4f0c18') g.fillStyle = r; g.fillRect(0, 0, 1024, 512) g.strokeStyle = '#c9a24e'; g.lineWidth = 22; g.strokeRect(11, 11, 1002, 490) g.strokeStyle = 'rgba(236,208,140,0.75)'; g.lineWidth = 4; g.strokeRect(40, 40, 944, 432) for (const [x, y] of [[62, 62], [962, 62], [62, 450], [962, 450]]) { const rr = g.createRadialGradient(x - 3, y - 3, 1, x, y, 11) rr.addColorStop(0, '#f6e2a6'); rr.addColorStop(0.6, '#c9a24e'); rr.addColorStop(1, '#5e4719') g.fillStyle = rr; g.beginPath(); g.arc(x, y, 11, 0, Math.PI * 2); g.fill() } g.textAlign = 'center'; g.textBaseline = 'middle'; g.shadowColor = 'rgba(0,0,0,0.6)'; g.shadowOffsetY = 4; g.shadowBlur = 2 g.fillStyle = '#c9a24e'; g.font = '800 46px Bitter, Georgia, serif' if ('letterSpacing' in g) { g.letterSpacing = '10px' } g.fillText(t.kicker, 512, 118, 864) if ('letterSpacing' in g) { g.letterSpacing = '0px' } g.fillStyle = '#ecd08c'; g.font = '900 150px Bitter, Georgia, serif' g.fillText(t.title, 512, 250, 864) g.shadowColor = 'transparent' g.fillStyle = '#c9a24e'; g.fillRect(150, 342, 724, 4) g.fillStyle = '#efe9dd'; g.font = '600 48px "Be Vietnam Pro", system-ui, sans-serif' g.fillText(t.sub, 512, 410, 864) const tx = new THREE.CanvasTexture(c); tx.colorSpace = THREE.SRGBColorSpace; tx.anisotropy = 16 return tx } const LEGEND_SLOTS = [ [-22.5, 11.4, -40.5, 0.52], [-13.4, 8.6, -45.6, 0.9], [12.8, 9.8, -46.5, 1.9], ] function LegendWall() { const [texs, setTexs] = React.useState(null) React.useEffect(() => { let alive = true Promise.all([ document.fonts.load('900 150px Bitter', 'Hang MU').catch(() => 0), document.fonts.load('800 46px Bitter', 'TƯỜNG HUYỀN THOẠI').catch(() => 0), document.fonts.load('600 48px "Be Vietnam Pro"', 'Tụ họp').catch(() => 0), ]).then(() => { const t = LEGEND_PANELS.map(drawLegendPanel) if (alive) setTexs(t) }) return () => { alive = false } }, []) if (!texs) return null return ( {LEGEND_SLOTS.map(([x, y, z, rot], i) => ( ))} ) } const LEGENDS = [ ['rooney', 'Wayne Rooney', '2004–2017', 24, -42], ['ronaldo', 'Cristiano Ronaldo', '2003–2009, 2021–2022', 28, -40], ['giggs', 'Ryan Giggs', '1991–2014', 20, -48], ['scholes', 'Paul Scholes', '1994–2013', 14, -50], ['neville', 'Gary Neville', '1992–2011', -26, -44], ['ferdinand', 'Rio Ferdinand', '2002–2014', -20, -48], ['vidic', 'Nemanja Vidić', '2006–2014', -14, -50], ['vandersar', 'Edwin van der Sar', '2005–2011', -28, -40], ['carrick', 'Michael Carrick', '2006–2018', 8, -52], ['evra', 'Patrice Evra', '2006–2014', -8, -52], ] function LegendPortraits() { const [textures, setTextures] = React.useState(null) React.useEffect(() => { let alive = true const loader = new THREE.TextureLoader() const out = {} let done = 0 LEGENDS.forEach(([key]) => { loader.load('/images/legends/' + key + '.jpg', (t) => { t.colorSpace = THREE.SRGBColorSpace out[key] = t done++ if (done >= LEGENDS.length && alive) setTextures({ ...out }) }, undefined, () => { done++ if (done >= LEGENDS.length && alive) setTextures({ ...out }) }) }) return () => { alive = false } }, []) return ( {LEGENDS.map(([key, name, years, x, z]) => ( ))} ) } /* ========================================================================= HỘI TRƯỜNG ĐÁ sign + hình nộm rage + sân kéo co/giữ bóng trên sàn ========================================================================= */ function BenchSign() { const [tex, setTex] = React.useState(null) React.useEffect(() => { let alive = true const c = document.createElement('canvas'); c.width = 900; c.height = 200 const g = c.getContext('2d') const gr = g.createLinearGradient(0, 0, 0, 200) gr.addColorStop(0, '#8e1b2a'); gr.addColorStop(0.5, '#6a1220'); gr.addColorStop(1, '#3a0a12') g.fillStyle = gr; g.fillRect(0, 0, 900, 200) g.strokeStyle = 'rgba(236,208,140,0.8)'; g.lineWidth = 6; g.strokeRect(8, 8, 884, 184) g.textAlign = 'center'; g.fillStyle = '#ffd76a'; g.font = '800 44px Bitter, Georgia, serif' g.fillText('🎙 PHÒNG NÓI CHUYỆN', 450, 90) g.fillStyle = '#fff2c8'; g.font = '900 84px Bitter, Georgia, serif' g.fillText('Hội trường đá', 450, 160) const tx = new THREE.CanvasTexture(c); tx.colorSpace = THREE.SRGBColorSpace; tx.anisotropy = 16 if (alive) setTex(tx) return () => { alive = false } }, []) if (!tex) return null return ( ) } function RageDummies() { const colors = ['#c9a27a', '#b8102a', '#4a4a4a', '#f0c078'] return ( {colors.map((col, i) => { const a = 1.9 + i * 1.2 const x = Math.sin(a) * 11.5 + (i % 2 ? 1.5 : -1.5) const z = Math.cos(a) * 11.5 const fy = floorHeight(Math.hypot(x, z)) return ( ) })} ) } function CourtMarks() { return ( {/* sân kéo co 2 bên trái/phải (trên sàn phẳng, 2 phía màn hình lớn) */} {[[-18, -35, 0.35], [18, -35, -0.35]].map(([x, z, rot], i) => ( ))} {/* sân giữ bóng vòng quanh hố lửa (d=11..15, sàn tier 1-2) */} ) } export function CaveScene() {` ], /* Wire vào CaveScene (sau patch_3d: Trophy/CaveBanners/JerseyDisplay có sẵn) */ [ ` `, ` ` ], ]) /* NOTE: avatar số áo (jersey number) đã có sẵn trong source app.js qua useNumTex — không patch thêm để tránh trùng lặp. */ patchFile('frontend/app.js', [ /* ============ RAGE ROOM — Phòng xả giận (ném cà chua 🍅) ============ */ [ ` `, ` ` ], [ `function DuelLayer({ touch }) {`, `/* ========================================================================= PHÒNG XẢ GIẬN (rage room) — lại gần hình nộm quanh hố lửa → nhấn E vào phòng, ném cà chua vào hình nộm (giữ chuột lấy lực, thả để ném). Biệt danh nộm theo vị trí (hậu vệ / tiền vệ / tiền đạo / HLV / Thủ môn) — 4 nộm quanh hố lửa. ========================================================================= */ const RAGE_DUMMY_POS = [ { x: 9.4, z: -3.7 }, { x: 2.0, z: -11.5 }, { x: -12.0, z: -4.6 }, { x: -6.6, z: 8.1 }, ] const RAGE_NAMES = ['Hậu vệ', 'Tiền vệ', 'Tiền đạo', 'HLV'] function RageLayer({ touch }) { const rage = useRage() const players = useGame((s) => s.players) const playerId = useGame((s) => s.playerId) const me = players.get(playerId) const [near, setNear] = React.useState(-1) const [power, setPower] = React.useState(0) const [charging, setCharging] = React.useState(false) const [score, setScore] = React.useState({ hits: 0, total: 0 }) const [slot, setSlot] = React.useState(0) const active = rage.active const ivRef = React.useRef(null) // gần nộm nào? React.useEffect(() => { if (active) return const id = setInterval(() => { if (!me || !me.pos) return let found = -1 for (let i = 0; i < RAGE_DUMMY_POS.length; i++) { const dx = me.pos.x - RAGE_DUMMY_POS[i].x, dz = me.pos.z - RAGE_DUMMY_POS[i].z if (dx * dx + dz * dz < 8 * 8) { found = i; break } } setNear(found) }, 400) return () => clearInterval(id) }, [active, me && me.pos && me.pos.x, me && me.pos && me.pos.z]) // phím E vào phòng, Esc ra React.useEffect(() => { const down = (e) => { if (e.code === 'KeyE' && near >= 0 && !active) { rage.set({ active: true }) if (document.exitPointerLock) document.exitPointerLock() } if (e.code === 'Escape' && active) { rage.set({ active: false }); setPower(0); setCharging(false) } } window.addEventListener('keydown', down) return () => window.removeEventListener('keydown', down) }, [near, active]) const start = () => { if (charging) return setCharging(true); setPower(0) ivRef.current = setInterval(() => setPower((p) => Math.min(100, p + 7)), 60) } const release = () => { if (ivRef.current) clearInterval(ivRef.current) const p = power setCharging(false); setPower(0) // ném: trúng nếu lực trong dải 35–95 (dễ trúng hơn khi giữ vừa) if (active && p > 0) { const hit = p >= 35 && p <= 95 setScore((s) => ({ hits: s.hits + (hit ? 1 : 0), total: s.total + 1 })) // hiệu ứng flash const el = document.querySelector('.rage-cross') if (el) { el.style.outlineColor = hit ? '#3fb36b' : '#ff4a3a'; setTimeout(() => { el.style.outlineColor = '' }, 350) } } } if (!active && near < 0) return null return ( <> {!active && near >= 0 && (
🍅 Phòng xả giận ném cà chua vào hình nộm {touch ? ( ) : ( E để vào )}
)} {active && (
🍅 {score.hits}/{score.total} trúng · nộm: {RAGE_NAMES[slot]}

Biệt danh cho hình nộm

Chọn mục tiêu — nộm đang được highlight trong hang

{RAGE_NAMES.map((n, r) => ( ))}

Giữ / chạm để lấy lực, thả để ném 🍅 — lực 35–95% là trúng.

0 || undefined} style={{ position: 'absolute', bottom: 96, left: '50%', transform: 'translateX(-50%)', textAlign: 'center', color: '#fff6ec', background: 'rgba(12,8,6,0.85)', padding: '10px 18px', borderRadius: 12, border: '1px solid #ecd08c66', maxWidth: 'min(420px, calc(100vw - 32px))' }}> {power > 0 ? '🍅 Lực ' + power + '%' + (power >= 35 && power <= 95 ? ' — thả để ném!' : ' — hơi non/già!') : 'Giữ / chạm để lấy lực, thả để ném'}
= 35 && power <= 95 ? '#3fb36b' : '#ff4a3a', transition: 'width 60ms linear' }} />
)} ) } function DuelLayer({ touch }) {` ], ])