دوري الهوكي 2026 — Three.js
by GoldenGlider991066 lines42.1 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>دوري الهوكي 2026 — Three.js</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<script>
tailwind.config = { theme: { extend: { fontFamily: { sans: ['Cairo','sans-serif'] } } } };
</script>
<style>
html,body{height:100%;}
body{
font-family:'Cairo',sans-serif;
overscroll-behavior:none;
-webkit-tap-highlight-color:transparent;
}
#scene{ touch-action:none; display:block; }
.badge{
display:flex;align-items:center;justify-content:center;border-radius:9999px;
font-weight:900;color:#fff;text-shadow:0 1px 4px rgba(0,0,0,.7);
border:2px solid rgba(255,255,255,.28);
box-shadow:inset 0 -6px 12px rgba(0,0,0,.25), 0 4px 14px -4px rgba(0,0,0,.6);
flex:none;
}
.screen{ position:absolute; inset:0; overflow-y:auto; }
.glass{ background:rgba(15,23,42,.82); backdrop-filter:blur(14px); border:1px solid rgba(148,163,184,.18); }
.club-opt{ transition:all .18s ease; }
.club-opt:hover{ transform:translateY(-4px); border-color:rgba(251,191,36,.7); }
.club-opt.sel{ border-color:#fbbf24; box-shadow:0 0 0 2px rgba(251,191,36,.45), 0 12px 30px -12px rgba(251,191,36,.5); }
@keyframes popIn{ from{opacity:0; transform:scale(.86) translateY(12px);} to{opacity:1; transform:scale(1) translateY(0);} }
.pop{ animation:popIn .38s cubic-bezier(.34,1.56,.64,1) both; }
@keyframes flashRed{ 0%{opacity:.75} 100%{opacity:0} }
.flash{ animation:flashRed .7s ease-out forwards; }
.bracket-col{ min-width:190px; }
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:#334155;border-radius:8px}
.tabular-nums{font-variant-numeric:tabular-nums;}
</style>
</head>
<body class="bg-slate-950 text-slate-100 select-none">
<canvas id="scene" class="fixed inset-0 z-0"></canvas>
<!-- ======================= HUD ======================= -->
<div id="hud" class="fixed inset-0 z-20 pointer-events-none hidden">
<div class="absolute top-2 left-1/2 -translate-x-1/2 bg-slate-900/85 backdrop-blur-md border border-slate-700 rounded-2xl px-3 sm:px-5 py-2 flex items-center gap-3 sm:gap-5 shadow-2xl">
<div class="flex items-center gap-2">
<div id="hudYouBadge"></div>
<span id="hudYouName" class="text-xs sm:text-sm font-bold max-w-[80px] truncate"></span>
</div>
<div class="text-center px-1">
<div id="hudScore" class="text-xl sm:text-2xl font-black tabular-nums leading-none">0 - 0</div>
<div id="hudTimer" class="text-[11px] text-amber-400 font-bold tabular-nums mt-0.5">1:30</div>
</div>
<div class="flex items-center gap-2">
<span id="hudAiName" class="text-xs sm:text-sm font-bold max-w-[80px] truncate"></span>
<div id="hudAiBadge"></div>
</div>
</div>
<button onclick="quitMatch()" class="pointer-events-auto absolute top-2 left-2 w-10 h-10 rounded-xl bg-slate-900/85 border border-slate-700 text-slate-300 hover:text-white hover:border-rose-500 flex items-center justify-center transition">
<i class="fa-solid fa-xmark"></i>
</button>
<div id="centerWrap" class="absolute inset-0 flex items-center justify-center">
<div id="centerMsg" class="text-center"></div>
</div>
<div id="flashLayer" class="absolute inset-0 opacity-0 pointer-events-none" style="background:radial-gradient(circle,rgba(251,191,36,.6),transparent 70%)"></div>
</div>
<!-- ======================= SCREENS ======================= -->
<div id="screens" class="fixed inset-0 z-30">
<!-- START -->
<section id="screen-start" class="screen flex items-center justify-center p-4" style="background:radial-gradient(circle at 50% 0%,#1e293b,#020617 65%)">
<div class="w-full max-w-3xl text-center pop">
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/25 text-amber-400 text-xs font-bold mb-5">
<i class="fa-solid fa-hockey-puck"></i><span>الموسم الرياضي 2025 / 2026</span>
</div>
<h1 class="text-4xl sm:text-6xl font-black tracking-tight mb-3 bg-gradient-to-b from-white to-slate-400 bg-clip-text text-transparent">
دوري الهوكي الأوروبي
</h1>
<p class="text-slate-400 mb-8 max-w-xl mx-auto text-sm sm:text-base">
اختر الدوري، اسحب القرعة، والعب مباريات خروج المغلوب — 90 ثانية لكل مباراة!
</p>
<div class="glass rounded-3xl p-5 sm:p-7 mb-6">
<h3 class="text-sm font-bold text-slate-300 mb-3 text-right"><i class="fa-solid fa-gauge-high ml-2 text-amber-400"></i>مستوى الصعوبة</h3>
<div id="diffGrid" class="grid grid-cols-3 gap-3"></div>
</div>
<div class="glass rounded-3xl p-5 sm:p-7 mb-6">
<h3 class="text-sm font-bold text-slate-300 mb-3 text-right"><i class="fa-solid fa-trophy ml-2 text-amber-400"></i>اختر الدوري</h3>
<div id="leagueGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-3"></div>
</div>
<button id="btnGoClub" disabled
class="w-full sm:w-auto px-12 py-4 rounded-2xl font-black text-lg transition
bg-gradient-to-r from-amber-500 to-orange-600 text-slate-950 shadow-lg shadow-amber-900/40
disabled:opacity-30 disabled:cursor-not-allowed hover:brightness-110">
متابعة <i class="fa-solid fa-arrow-left mr-2"></i>
</button>
</div>
</section>
<!-- CLUB SELECT -->
<section id="screen-club" class="screen hidden items-center justify-center p-4" style="background:radial-gradient(circle at 50% 0%,#1e293b,#020617 65%)">
<div class="w-full max-w-4xl pop">
<div class="flex items-center justify-between mb-5">
<button onclick="showScreen('start')" class="px-4 py-2 rounded-xl bg-slate-800/80 border border-slate-700 text-sm hover:border-slate-500 transition">
<i class="fa-solid fa-arrow-right ml-1"></i> رجوع
</button>
<h2 id="clubTitle" class="text-xl sm:text-2xl font-black"></h2>
</div>
<p class="text-center text-slate-400 text-sm mb-5">اختر النادي الذي ستلعب به</p>
<div id="clubGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-7"></div>
<button id="btnGoDraw" disabled
class="w-full px-12 py-4 rounded-2xl font-black text-lg transition
bg-gradient-to-r from-emerald-500 to-teal-600 text-white shadow-lg shadow-emerald-900/40
disabled:opacity-30 disabled:cursor-not-allowed hover:brightness-110">
<i class="fa-solid fa-shuffle ml-2"></i> إجراء القرعة
</button>
</div>
</section>
<!-- DRAW / BRACKET -->
<section id="screen-draw" class="screen hidden items-center justify-center p-4" style="background:radial-gradient(circle at 50% 0%,#1e293b,#020617 65%)">
<div class="w-full max-w-6xl pop">
<div class="text-center mb-6">
<h2 class="text-2xl sm:text-3xl font-black mb-1">جدول البطولة</h2>
<p id="roundLabel" class="text-amber-400 font-bold text-sm"></p>
</div>
<div id="bracket" class="flex flex-row-reverse justify-center gap-4 overflow-x-auto pb-4 mb-6"></div>
<div class="text-center">
<button id="btnPlay" onclick="beginMatch()"
class="px-14 py-4 rounded-2xl font-black text-lg bg-gradient-to-r from-rose-500 to-red-600 text-white
shadow-lg shadow-rose-900/40 hover:brightness-110 transition">
<i class="fa-solid fa-play ml-2"></i> ابدأ المباراة
</button>
</div>
</div>
</section>
<!-- RESULT -->
<section id="screen-result" class="screen hidden items-center justify-center p-4" style="background:radial-gradient(circle at 50% 0%,#1e293b,#020617 65%)">
<div class="w-full max-w-lg text-center pop glass rounded-3xl p-8">
<div id="resultIcon" class="text-6xl mb-4"></div>
<h2 id="resultTitle" class="text-3xl font-black mb-2"></h2>
<p id="resultSub" class="text-slate-400 mb-6"></p>
<div id="resultScore" class="text-5xl font-black tabular-nums mb-8"></div>
<button id="btnResult" class="w-full px-8 py-4 rounded-2xl font-black text-lg bg-gradient-to-r from-amber-500 to-orange-600 text-slate-950 hover:brightness-110 transition"></button>
</div>
</section>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
/* ==================================================================
1) DATA
================================================================== */
const LEAGUES = [
{ id:'epl', name:'الإنجليزي', full:'الدوري الإنجليزي الممتاز', flag:'🏴', accent:'#8b5cf6', clubs:[
{ ar:'أرسنال', en:'Arsenal', c1:'#EF0107', c2:'#063672' },
{ ar:'مانشستر سيتي', en:'Man City', c1:'#6CABDD', c2:'#1C2C5B' },
{ ar:'ليفربول', en:'Liverpool', c1:'#C8102E', c2:'#00B2A9' },
{ ar:'مانشستر يونايتد', en:'Man United', c1:'#DA291C', c2:'#FBE122' },
{ ar:'تشيلسي', en:'Chelsea', c1:'#034694', c2:'#EEF0F4' },
{ ar:'توتنهام', en:'Tottenham', c1:'#132257', c2:'#FFFFFF' },
]},
{ id:'laliga', name:'الإسباني', full:'الدوري الإسباني', flag:'🇪🇸', accent:'#ef4444', clubs:[
{ ar:'ريال مدريد', en:'Real Madrid', c1:'#FEBE10', c2:'#00529F' },
{ ar:'برشلونة', en:'Barcelona', c1:'#A50044', c2:'#004D98' },
{ ar:'أتلتيكو مدريد', en:'Atletico', c1:'#CB3524', c2:'#262E62' },
{ ar:'أتلتيك بيلباو', en:'Athletic', c1:'#EE2523', c2:'#000000' },
{ ar:'ريال سوسيداد', en:'Sociedad', c1:'#0067B1', c2:'#FFFFFF' },
{ ar:'إشبيلية', en:'Sevilla', c1:'#D8042B', c2:'#FFFFFF' },
]},
{ id:'seriea', name:'الإيطالي', full:'الدوري الإيطالي', flag:'🇮🇹', accent:'#3b82f6', clubs:[
{ ar:'إنتر ميلان', en:'Inter', c1:'#0068A8', c2:'#000000' },
{ ar:'إيه سي ميلان', en:'Milan', c1:'#FB090B', c2:'#000000' },
{ ar:'يوفنتوس', en:'Juventus', c1:'#2b2b2b', c2:'#FFFFFF' },
{ ar:'نابولي', en:'Napoli', c1:'#12A0D7', c2:'#003C82' },
{ ar:'روما', en:'Roma', c1:'#8E1F2F', c2:'#F0BC42' },
{ ar:'أتالانتا', en:'Atalanta', c1:'#1D71B8', c2:'#000000' },
]},
{ id:'bundesliga', name:'الألماني', full:'الدوري الألماني', flag:'🇩🇪', accent:'#10b981', clubs:[
{ ar:'بايرن ميونخ', en:'Bayern', c1:'#DC052D', c2:'#0066B2' },
{ ar:'دورتموند', en:'Dortmund', c1:'#FDE100', c2:'#000000' },
{ ar:'ليفركوزن', en:'Leverkusen', c1:'#E32221', c2:'#000000' },
{ ar:'لايبزيغ', en:'Leipzig', c1:'#DD0741', c2:'#001F47' },
{ ar:'فرانكفورت', en:'Frankfurt', c1:'#E1000F', c2:'#000000' },
{ ar:'شتوتغارت', en:'Stuttgart', c1:'#E32219', c2:'#FFFFFF' },
]},
{ id:'ligue1', name:'الفرنسي', full:'الدوري الفرنسي', flag:'🇫🇷', accent:'#06b6d4', clubs:[
{ ar:'باريس سان جيرمان',en:'PSG', c1:'#004170', c2:'#DA291C' },
{ ar:'مارسيليا', en:'Marseille', c1:'#2FAEE0', c2:'#FFFFFF' },
{ ar:'موناكو', en:'Monaco', c1:'#E63946', c2:'#FFFFFF' },
{ ar:'ليون', en:'Lyon', c1:'#1B3282', c2:'#DA291C' },
{ ar:'ليل', en:'Lille', c1:'#E01E13', c2:'#0A2E6E' },
{ ar:'نيس', en:'Nice', c1:'#C8102E', c2:'#000000' },
]},
{ id:'superlig', name:'التركي', full:'الدوري التركي الممتاز', flag:'🇹🇷', accent:'#f43f5e', clubs:[
{ ar:'غلطة سراي', en:'Galatasaray', c1:'#FDB912', c2:'#A90432' },
{ ar:'فنربخشة', en:'Fenerbahce', c1:'#FFED00', c2:'#00296B' },
{ ar:'بشكتاش', en:'Besiktas', c1:'#2b2b2b', c2:'#FFFFFF' },
{ ar:'طرابزون سبور', en:'Trabzonspor', c1:'#7A1230', c2:'#5BB8E6' },
{ ar:'باشاكشهير', en:'Basaksehir', c1:'#F97316', c2:'#1E3A8A' },
{ ar:'سامسون سبور', en:'Samsunspor', c1:'#E30613', c2:'#FFFFFF' },
]},
];
const DIFFS = {
easy: { label:'بسيط', icon:'fa-face-smile', speed:7.5, react:0.34, error:2.6, color:'from-emerald-500 to-teal-600' },
medium: { label:'متوسط', icon:'fa-face-meh', speed:12.5, react:0.17, error:1.2, color:'from-amber-500 to-orange-600' },
hard: { label:'صعب', icon:'fa-face-grimace',speed:19.0, react:0.07, error:0.45,color:'from-rose-500 to-red-600' },
};
/* ==================================================================
2) STATE
================================================================== */
const ui = {
difficulty: 'medium',
leagueId: null,
club: null,
};
let tour = null; // { rounds:[], roundIndex:0 }
let matchCtx = null; // { you, opp }
function shuffle(a){ a=a.slice(); for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; }
const rnd = (a,b)=> a + Math.random()*(b-a);
function badge(club, size){
return `<div class="badge" style="width:${size}px;height:${size}px;font-size:${Math.round(size*0.4)}px;background:linear-gradient(135deg,${club.c1},${club.c2})">${club.en[0]}</div>`;
}
/* ==================================================================
3) UI BUILDERS
================================================================== */
const diffGrid = document.getElementById('diffGrid');
Object.entries(DIFFS).forEach(([k,d])=>{
const b = document.createElement('button');
b.dataset.diff = k;
b.className = 'py-4 rounded-2xl border border-slate-700 bg-slate-800/60 hover:border-slate-500 transition flex flex-col items-center gap-1';
b.innerHTML = `<i class="fa-solid ${d.icon} text-2xl mb-1"></i><span class="font-bold text-sm">${d.label}</span>`;
b.onclick = ()=>{
ui.difficulty = k;
[...diffGrid.children].forEach(c=>{
const on = c.dataset.diff === k;
c.classList.toggle('border-amber-400', on);
c.classList.toggle('bg-amber-500/10', on);
c.classList.toggle('text-amber-300', on);
c.classList.toggle('border-slate-700', !on);
c.classList.toggle('bg-slate-800/60', !on);
});
};
diffGrid.appendChild(b);
});
diffGrid.children[1].click();
const leagueGrid = document.getElementById('leagueGrid');
LEAGUES.forEach(l=>{
const b = document.createElement('button');
b.dataset.lid = l.id;
b.className = 'py-4 rounded-2xl border border-slate-700 bg-slate-800/60 hover:border-slate-500 transition flex flex-col items-center gap-1';
b.innerHTML = `<span class="text-2xl">${l.flag}</span><span class="font-bold text-sm">${l.name}</span>`;
b.onclick = ()=>{
ui.leagueId = l.id;
[...leagueGrid.children].forEach(c=>{
const on = c.dataset.lid === l.id;
c.classList.toggle('border-amber-400', on);
c.classList.toggle('bg-amber-500/10', on);
c.classList.toggle('border-slate-700', !on);
c.classList.toggle('bg-slate-800/60', !on);
});
document.getElementById('btnGoClub').disabled = false;
};
leagueGrid.appendChild(b);
});
document.getElementById('btnGoClub').onclick = ()=>{
buildClubScreen();
showScreen('club');
};
function buildClubScreen(){
const league = LEAGUES.find(l=>l.id===ui.leagueId);
document.getElementById('clubTitle').innerHTML = `${league.flag} ${league.full}`;
const grid = document.getElementById('clubGrid');
grid.innerHTML = '';
league.clubs.forEach(c=>{
const b = document.createElement('button');
b.className = 'club-opt rounded-2xl border-2 border-slate-700 bg-slate-900/70 p-4 flex flex-col items-center gap-2';
b.innerHTML = `${badge(c,56)}<span class="font-bold text-sm mt-1">${c.ar}</span><span class="text-[11px] text-slate-500">${c.en}</span>`;
b.onclick = ()=>{
ui.club = c;
[...grid.children].forEach(x=>x.classList.remove('sel'));
b.classList.add('sel');
document.getElementById('btnGoDraw').disabled = false;
};
grid.appendChild(b);
});
document.getElementById('btnGoDraw').disabled = true;
}
/* ==================================================================
4) TOURNAMENT / DRAW
================================================================== */
function makeTeam(c){
return { ...c, strength: rnd(1,5) };
}
document.getElementById('btnGoDraw').onclick = ()=>{
const league = LEAGUES.find(l=>l.id===ui.leagueId);
const you = makeTeam(ui.club);
// 6 clubs of the league + 2 wildcards from other leagues = 8
const others = league.clubs.filter(c=>c.en !== ui.club.en).map(makeTeam);
const pool = LEAGUES.filter(l=>l.id!==league.id).flatMap(l=>l.clubs.map(makeTeam));
const wildcards = shuffle(pool).slice(0,2);
const eight = shuffle([you, ...others, ...wildcards]);
tour = {
roundIndex: 0,
you,
rounds: [
{ name:'ربع النهائي', matches: [
{a:eight[0],b:eight[1],sa:null,sb:null,winner:null},
{a:eight[2],b:eight[3],sa:null,sb:null,winner:null},
{a:eight[4],b:eight[5],sa:null,sb:null,winner:null},
{a:eight[6],b:eight[7],sa:null,sb:null,winner:null},
]},
{ name:'نصف النهائي', matches: [] },
{ name:'النهائي', matches: [] },
]
};
renderBracket();
showScreen('draw');
};
function yourMatch(){
const r = tour.rounds[tour.roundIndex];
return r.matches.find(m => m.a.en === tour.you.en || m.b.en === tour.you.en);
}
function renderBracket(){
const wrap = document.getElementById('bracket');
wrap.innerHTML = '';
tour.rounds.forEach((round, ri)=>{
const col = document.createElement('div');
col.className = 'bracket-col flex-1 flex flex-col gap-3';
col.innerHTML = `<div class="text-center text-xs font-bold mb-1 ${ri===tour.roundIndex?'text-amber-400':'text-slate-500'}">${round.name}</div>`;
if (round.matches.length === 0){
col.innerHTML += `<div class="flex-1 flex items-center justify-center text-slate-700 text-3xl"><i class="fa-solid fa-hourglass-half"></i></div>`;
}
round.matches.forEach((m, mi)=>{
const isYours = (m.a.en === tour.you.en || m.b.en === tour.you.en);
const card = document.createElement('div');
card.className = `rounded-xl border p-2 text-xs ${isYours && !m.winner ? 'border-amber-400/70 bg-amber-500/10' : 'border-slate-700 bg-slate-900/70'} pop`;
card.style.animationDelay = (mi*45)+'ms';
const row = (t, s) => {
const win = m.winner && m.winner.en === t.en;
const lose = m.winner && m.winner.en !== t.en;
return `<div class="flex items-center gap-2 py-1 ${lose?'opacity-40':''}">
${badge(t, 22)}
<span class="flex-1 truncate font-bold ${win?'text-emerald-400':''}">${t.ar}</span>
<span class="tabular-nums font-black ${win?'text-emerald-400':'text-slate-500'}">${s===null?'-':s}</span>
</div>`;
};
card.innerHTML = row(m.a, m.sa) + `<div class="h-px bg-slate-700/70 my-0.5"></div>` + row(m.b, m.sb);
col.appendChild(card);
});
wrap.appendChild(col);
});
const r = tour.rounds[tour.roundIndex];
document.getElementById('roundLabel').textContent =
(r.matches.some(m=>m.a.en===tour.you.en||m.b.en===tour.you.en) || r.matches.length===0)
? `دور: ${r.name}` : r.name;
}
/* ==================================================================
5) THREE.JS SCENE
================================================================== */
const RINK = { w:20, l:30, hw:10, hl:15, goal:3 };
const PUCK_R = 0.5, PAD_R = 1.0, PUCK_MAX = 44;
let scene, camera, renderer, clock;
let puckMesh, youMesh, aiMesh, netMeshes = [];
let puck = { x:0, z:0, vx:0, vz:0 };
let you = { x:0, z:11, vx:0, vz:0, tx:0, tz:11 };
let ai = { x:0, z:-11, vx:0, vz:0, px:0, pz:-11, tx:0, tz:-11, timer:0 };
const game = {
active:false, phase:'idle', timeLeft:90, countdown:0, goalTimer:0,
playerScore:0, aiScore:0, overtime:false, pendingEnd:false,
};
const pointer = { x:0, y:0, has:false };
const raycaster = new THREE.Raycaster();
const groundPlane = new THREE.Plane(new THREE.Vector3(0,1,0), -0.35);
const ndc = new THREE.Vector2();
const hitPoint = new THREE.Vector3();
function makeIceTexture(){
const W=800, H=1200;
const c = document.createElement('canvas'); c.width=W; c.height=H;
const g = c.getContext('2d');
g.fillStyle = '#f3faff'; g.fillRect(0,0,W,H);
const grd = g.createLinearGradient(0,0,0,H);
grd.addColorStop(0,'rgba(186,230,253,.7)');
grd.addColorStop(.5,'rgba(255,255,255,0)');
grd.addColorStop(1,'rgba(186,230,253,.7)');
g.fillStyle = grd; g.fillRect(0,0,W,H);
const toY = z => (z + 15) / 30 * H;
const toX = x => (x + 10) / 20 * W;
g.fillStyle = '#3b82f6';
g.fillRect(0, toY(-5)-5, W, 10);
g.fillRect(0, toY(5)-5, W, 10);
g.fillStyle = '#ef4444';
g.fillRect(0, toY(0)-6, W, 12);
g.strokeStyle = '#ef4444'; g.lineWidth = 5;
g.beginPath(); g.arc(toX(0), toY(0), 130, 0, Math.PI*2); g.stroke();
[[-6,-8],[6,-8],[-6,8],[6,8]].forEach(([x,z])=>{
g.beginPath(); g.arc(toX(x), toY(z), 88, 0, Math.PI*2); g.stroke();
g.fillStyle = '#ef4444'; g.beginPath(); g.arc(toX(x), toY(z), 9, 0, Math.PI*2); g.fill();
});
g.strokeStyle = '#60a5fa'; g.lineWidth = 5;
g.beginPath(); g.arc(toX(0), toY(-15), 110, 0, Math.PI); g.stroke();
g.beginPath(); g.arc(toX(0), toY(15), 110, Math.PI, Math.PI*2); g.stroke();
const tex = new THREE.CanvasTexture(c);
tex.anisotropy = 4;
return tex;
}
function initThree(){
const canvas = document.getElementById('scene');
renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050b18);
scene.fog = new THREE.Fog(0x050b18, 55, 110);
camera = new THREE.PerspectiveCamera(62, window.innerWidth/window.innerHeight, 0.1, 300);
camera.position.set(0, 32, 19);
camera.lookAt(0, 0, 0);
clock = new THREE.Clock();
/* ---- lights ---- */
scene.add(new THREE.HemisphereLight(0xbfdbfe, 0x0b1220, 1.15));
const amb = new THREE.AmbientLight(0xffffff, 0.42); scene.add(amb);
const dir = new THREE.DirectionalLight(0xffffff, 1.35);
dir.position.set(10, 34, 14);
dir.castShadow = true;
dir.shadow.mapSize.set(1024,1024);
dir.shadow.camera.left = -24; dir.shadow.camera.right = 24;
dir.shadow.camera.top = 24; dir.shadow.camera.bottom = -24;
dir.shadow.camera.near = 1; dir.shadow.camera.far = 90;
dir.shadow.bias = -0.0008;
scene.add(dir);
const rim = new THREE.DirectionalLight(0x60a5fa, 0.6);
rim.position.set(-14, 16, -20);
scene.add(rim);
/* ---- ice ---- */
const iceGeo = new THREE.PlaneGeometry(RINK.w, RINK.l);
const iceMat = new THREE.MeshStandardMaterial({
map: makeIceTexture(), roughness: 0.28, metalness: 0.06, color: 0xffffff
});
const ice = new THREE.Mesh(iceGeo, iceMat);
ice.rotation.x = -Math.PI/2;
ice.receiveShadow = true;
scene.add(ice);
/* ---- outer floor for shadow catch ---- */
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(120,120),
new THREE.MeshStandardMaterial({ color:0x0a1424, roughness:1 })
);
floor.rotation.x = -Math.PI/2;
floor.position.y = -0.6;
scene.add(floor);
/* ---- boards ---- */
const boardMat = new THREE.MeshStandardMaterial({ color:0xe2e8f0, roughness:0.55, metalness:0.15 });
const trimMat = new THREE.MeshStandardMaterial({ color:0x1d4ed8, roughness:0.4, metalness:0.4, emissive:0x1e3a8a, emissiveIntensity:0.4 });
const BH = 1.1, BT = 0.55;
function board(x, z, w, d){
const b = new THREE.Mesh(new THREE.BoxGeometry(w, BH, d), boardMat);
b.position.set(x, BH/2, z);
b.castShadow = true; b.receiveShadow = true;
scene.add(b);
const t = new THREE.Mesh(new THREE.BoxGeometry(w, 0.12, d), trimMat);
t.position.set(x, BH, z);
scene.add(t);
}
board(-RINK.hw - BT/2, 0, BT, RINK.l + BT*2);
board( RINK.hw + BT/2, 0, BT, RINK.l + BT*2);
const segW = RINK.hw - RINK.goal;
const segX = RINK.goal + segW/2;
board( segX, -RINK.hl - BT/2, segW, BT);
board(-segX, -RINK.hl - BT/2, segW, BT);
board( segX, RINK.hl + BT/2, segW, BT);
board(-segX, RINK.hl + BT/2, segW, BT);
/* ---- goals ---- */
const postMat = new THREE.MeshStandardMaterial({ color:0xf8fafc, roughness:0.25, metalness:0.7 });
const netMat = new THREE.MeshBasicMaterial({ color:0x93c5fd, transparent:true, opacity:0.16, side:THREE.DoubleSide });
const edgeMat = new THREE.LineBasicMaterial({ color:0x60a5fa, transparent:true, opacity:0.75 });
[1,-1].forEach(sgn=>{
const gx = 0, gz = sgn * (RINK.hl + 0.65);
const grp = new THREE.Group();
const box = new THREE.BoxGeometry(RINK.goal, 1.9, 1.3);
const net = new THREE.Mesh(box, netMat);
net.position.set(gx, 0.95, gz);
grp.add(net);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(box), edgeMat);
edges.position.copy(net.position);
grp.add(edges);
[-1,1].forEach(s=>{
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.11,0.11,1.9,10), postMat);
post.position.set(s*RINK.goal, 0.95, sgn*RINK.hl);
post.castShadow = true;
grp.add(post);
});
scene.add(grp);
netMeshes.push(grp);
});
/* ---- puck ---- */
const puckGeo = new THREE.CylinderGeometry(PUCK_R, PUCK_R, 0.28, 28);
const puckMat = new THREE.MeshStandardMaterial({ color:0x0f172a, roughness:0.55, metalness:0.35 });
puckMesh = new THREE.Mesh(puckGeo, puckMat);
puckMesh.castShadow = true;
puckMesh.position.set(0, 0.25, 0);
scene.add(puckMesh);
const puckRing = new THREE.Mesh(
new THREE.TorusGeometry(PUCK_R*0.85, 0.05, 8, 24),
new THREE.MeshStandardMaterial({ color:0x64748b, roughness:0.3, metalness:0.8 })
);
puckRing.rotation.x = Math.PI/2;
puckRing.position.y = 0.15;
puckMesh.add(puckRing);
/* ---- paddles ---- */
youMesh = makePaddle(0xfbbf24);
aiMesh = makePaddle(0xef4444);
scene.add(youMesh, aiMesh);
window.addEventListener('resize', onResize);
onResize();
window.addEventListener('pointermove', e=>{
pointer.x = e.clientX; pointer.y = e.clientY; pointer.has = true;
}, { passive:true });
animate();
}
function makePaddle(color){
const g = new THREE.Group();
const base = new THREE.Mesh(
new THREE.CylinderGeometry(PAD_R, PAD_R*0.92, 0.36, 36),
new THREE.MeshStandardMaterial({ color, roughness:0.35, metalness:0.55, emissive:color, emissiveIntensity:0.12 })
);
base.position.y = 0.2;
base.castShadow = true;
const knob = new THREE.Mesh(
new THREE.CylinderGeometry(PAD_R*0.52, PAD_R*0.68, 0.34, 30),
new THREE.MeshStandardMaterial({ color:0xf8fafc, roughness:0.4, metalness:0.4 })
);
knob.position.y = 0.53;
knob.castShadow = true;
const ring = new THREE.Mesh(
new THREE.TorusGeometry(PAD_R*0.98, 0.07, 8, 40),
new THREE.MeshStandardMaterial({ color:0xffffff, roughness:0.2, metalness:0.9, emissive:0xffffff, emissiveIntensity:0.3 })
);
ring.rotation.x = Math.PI/2;
ring.position.y = 0.02;
g.add(base, knob, ring);
return g;
}
function onResize(){
const w = window.innerWidth, h = window.innerHeight;
const aspect = w/h;
renderer.setSize(w, h);
camera.aspect = aspect;
let fov = 62;
if (aspect < 1.6){
const need = Math.atan(Math.tan(THREE.MathUtils.degToRad(62)/2) * (1.6/aspect));
fov = Math.min(96, THREE.MathUtils.radToDeg(need*2));
}
camera.fov = fov;
camera.updateProjectionMatrix();
}
/* ==================================================================
6) SOUND (tiny synth)
================================================================== */
let actx = null;
function beep(freq, dur, type='sine', vol=0.055){
try{
if(!actx) actx = new (window.AudioContext||window.webkitAudioContext)();
if (actx.state === 'suspended') actx.resume();
const o = actx.createOscillator(), g = actx.createGain();
o.type = type; o.frequency.setValueAtTime(freq, actx.currentTime);
g.gain.setValueAtTime(vol, actx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime + dur);
o.connect(g); g.connect(actx.destination);
o.start(); o.stop(actx.currentTime + dur + 0.02);
}catch(e){}
}
/* ==================================================================
7) MATCH FLOW
================================================================== */
function beginMatch(){
const m = yourMatch();
if(!m) return;
const opp = (m.a.en === tour.you.en) ? m.b : m.a;
matchCtx = { you: tour.you, opp };
game.active = true;
game.phase = 'countdown';
game.countdown = 3.0;
game.timeLeft = 90;
game.playerScore = 0;
game.aiScore = 0;
game.overtime = false;
game.pendingEnd = false;
resetPuck(0);
you.x = 0; you.z = 11; you.vx = 0; you.vz = 0; you.tx = 0; you.tz = 11;
ai.x = 0; ai.z = -11; ai.vx = 0; ai.vz = 0; ai.tx = 0; ai.tz = -11; ai.timer = 0;
// colours
youMesh.children[0].material.color.set(matchCtx.you.c1);
youMesh.children[0].material.emissive.set(matchCtx.you.c1);
aiMesh.children[0].material.color.set(matchCtx.opp.c1);
aiMesh.children[0].material.emissive.set(matchCtx.opp.c1);
// HUD
document.getElementById('hudYouBadge').innerHTML = badge(matchCtx.you, 28);
document.getElementById('hudAiBadge').innerHTML = badge(matchCtx.opp, 28);
document.getElementById('hudYouName').textContent = matchCtx.you.ar;
document.getElementById('hudAiName').textContent = matchCtx.opp.ar;
updateHUD();
document.getElementById('screens').style.display = 'none';
document.getElementById('hud').classList.remove('hidden');
setCenterMsg('3');
}
function resetPuck(){
puck.x = 0; puck.z = 0; puck.vx = 0; puck.vz = 0;
puckMesh.position.set(0, 0.25, 0);
puckMesh.visible = true;
// spread paddles back
you.x = 0; you.z = 11; you.tx = 0; you.tz = 11;
ai.x = 0; ai.z = -11; ai.tx = 0; ai.tz = -11;
}
function updateHUD(){
document.getElementById('hudScore').textContent = `${game.playerScore} - ${game.aiScore}`;
const t = Math.max(0, Math.ceil(game.timeLeft));
document.getElementById('hudTimer').textContent = game.overtime
? 'وقت إضافي ⏱'
: `${Math.floor(t/60)}:${String(t%60).padStart(2,'0')}`;
}
function setCenterMsg(html, sub){
const el = document.getElementById('centerMsg');
el.innerHTML = html
? `<div class="text-6xl sm:text-8xl font-black text-white drop-shadow-[0_8px_30px_rgba(0,0,0,.9)]">${html}</div>
${sub?`<div class="mt-3 text-lg font-bold text-amber-300">${sub}</div>`:''}`
: '';
}
/* ---------- pointer → rink ---------- */
function pointerToRink(){
if(!pointer.has) return null;
ndc.x = (pointer.x / window.innerWidth) * 2 - 1;
ndc.y = -(pointer.y / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(ndc, camera);
if (raycaster.ray.intersectPlane(groundPlane, hitPoint)) return hitPoint;
return null;
}
/* ---------- paddles ---------- */
function updateYouPaddle(dt){
const p = pointerToRink();
let tx = you.x, tz = you.z;
if (p){
tx = THREE.MathUtils.clamp(p.x, -RINK.hw + PAD_R, RINK.hw - PAD_R);
tz = THREE.MathUtils.clamp(p.z, 0.9, RINK.hl - PAD_R - 0.3);
}
const dx = tx - you.x, dz = tz - you.z;
const d = Math.hypot(dx, dz);
const maxStep = 85 * dt;
const px = you.x, pz = you.z;
if (d > 0.0005){
const step = Math.min(d, maxStep);
you.x += (dx/d) * step;
you.z += (dz/d) * step;
}
you.vx = (you.x - px) / Math.max(dt, 0.0001);
you.vz = (you.z - pz) / Math.max(dt, 0.0001);
youMesh.position.set(you.x, 0, you.z);
}
function updateAIPaddle(dt){
const diff = DIFFS[ui.difficulty];
ai.timer -= dt;
if (ai.timer <= 0){
ai.timer = diff.react;
let tx, tz;
if (puck.z < 1.2){
// aim point behind the puck, opposite to the player's goal
const gx = 0 - puck.x;
const gz = RINK.hl - puck.z;
const gl = Math.hypot(gx, gz) || 1;
tx = puck.x - (gx/gl) * (PAD_R + PUCK_R + 0.35);
tz = puck.z - (gz/gl) * (PAD_R + PUCK_R + 0.35);
tz = Math.min(tz, -0.95);
tx += (Math.random()-0.5) * diff.error * 2;
tz += (Math.random()-0.5) * diff.error;
} else {
tx = puck.x * 0.55 + (Math.random()-0.5) * diff.error * 1.4;
tz = -RINK.hl + 2.6 + (Math.random()-0.5) * diff.error;
}
ai.tx = THREE.MathUtils.clamp(tx, -RINK.hw + PAD_R, RINK.hw - PAD_R);
ai.tz = THREE.MathUtils.clamp(tz, -RINK.hl + PAD_R + 0.3, -0.95);
}
const dx = ai.tx - ai.x, dz = ai.tz - ai.z;
const d = Math.hypot(dx, dz);
const px = ai.x, pz = ai.z;
if (d > 0.02){
const step = Math.min(d, diff.speed * dt);
ai.x += (dx/d) * step;
ai.z += (dz/d) * step;
}
ai.vx = (ai.x - px) / Math.max(dt, 0.0001);
ai.vz = (ai.z - pz) / Math.max(dt, 0.0001);
aiMesh.position.set(ai.x, 0, ai.z);
}
/* ---------- puck physics ---------- */
function collidePaddle(pad, pvx, pvz){
const dx = puck.x - pad.x;
const dz = puck.z - pad.z;
const dist = Math.hypot(dx, dz);
const minD = PUCK_R + PAD_R;
if (dist >= minD || dist < 0.0001) return;
const nx = dx / dist, nz = dz / dist;
puck.x = pad.x + nx * minD;
puck.z = pad.z + nz * minD;
const rvx = puck.vx - pvx;
const rvz = puck.vz - pvz;
const rel = rvx*nx + rvz*nz;
if (rel < 0){
const e = 1.06;
puck.vx -= (1+e) * rel * nx;
puck.vz -= (1+e) * rel * nz;
}
// paddle momentum push
puck.vx += pvx * 0.55;
puck.vz += pvz * 0.55;
// ensure a minimum kick away
const out = puck.vx*nx + puck.vz*nz;
if (out < 2.5){
puck.vx += nx * (2.5 - out);
puck.vz += nz * (2.5 - out);
}
const sp = Math.hypot(puck.vx, puck.vz);
if (sp > PUCK_MAX){ puck.vx *= PUCK_MAX/sp; puck.vz *= PUCK_MAX/sp; }
beep(160 + Math.min(240, sp*8), 0.06, 'square', 0.035);
}
function updatePuck(dt){
const damp = Math.pow(0.9885, dt*60);
puck.vx *= damp; puck.vz *= damp;
if (Math.abs(puck.vx) < 0.04) puck.vx = 0;
if (Math.abs(puck.vz) < 0.04) puck.vz = 0;
puck.x += puck.vx * dt;
puck.z += puck.vz * dt;
const inMouth = Math.abs(puck.x) < RINK.goal;
// side walls
if (puck.x > RINK.hw - PUCK_R){ puck.x = RINK.hw - PUCK_R; puck.vx = -Math.abs(puck.vx)*0.94; beep(110,0.05,'sine',0.03); }
if (puck.x < -RINK.hw + PUCK_R){ puck.x = -RINK.hw + PUCK_R; puck.vx = Math.abs(puck.vx)*0.94; beep(110,0.05,'sine',0.03); }
// end walls (only outside goal mouth)
if (!inMouth){
if (puck.z > RINK.hl - PUCK_R){ puck.z = RINK.hl - PUCK_R; puck.vz = -Math.abs(puck.vz)*0.94; beep(110,0.05,'sine',0.03); }
if (puck.z < -RINK.hl + PUCK_R){ puck.z = -RINK.hl + PUCK_R; puck.vz = Math.abs(puck.vz)*0.94; beep(110,0.05,'sine',0.03); }
}
// paddles
collidePaddle(you, you.vx, you.vz);
collidePaddle(ai, ai.vx, ai.vz);
// mesh
puckMesh.position.set(puck.x, 0.25, puck.z);
// GOAL?
if (Math.abs(puck.x) < RINK.goal){
if (puck.z > RINK.hl + 0.4) return scoreGoal('ai');
if (puck.z < -RINK.hl - 0.4) return scoreGoal('player');
}
}
function scoreGoal(side){
if (game.phase !== 'playing') return;
if (side === 'player') game.playerScore++; else game.aiScore++;
game.phase = 'goal';
game.goalTimer = 1.35;
game.pendingEnd = game.overtime;
puck.vx = 0; puck.vz = 0; puck.x = 0; puck.z = 0;
puckMesh.visible = false;
setCenterMsg('هدف!', side === 'player' ? `${matchCtx.you.ar} يسجل` : `${matchCtx.opp.ar} يسجل`);
const fl = document.getElementById('flashLayer');
fl.style.background = side === 'player'
? 'radial-gradient(circle,rgba(16,185,129,.65),transparent 70%)'
: 'radial-gradient(circle,rgba(244,63,94,.65),transparent 70%)';
fl.classList.remove('flash');
void fl.offsetWidth;
fl.classList.add('flash');
beep(side==='player'?880:220, 0.22, 'triangle', 0.07);
setTimeout(()=>beep(side==='player'?1320:170, 0.28, 'sine', 0.05), 110);
updateHUD();
}
/* ---------- game loop ---------- */
function updateGame(dt){
if (!game.active) return;
if (game.phase === 'countdown'){
game.countdown -= dt;
if (game.countdown > 0.6){
setCenterMsg(String(Math.ceil(game.countdown - 0.6)));
} else if (game.countdown > 0){
setCenterMsg('انطلق!', `${matchCtx.you.ar} ضد ${matchCtx.opp.ar}`);
} else {
setCenterMsg('');
game.phase = 'playing';
}
updateYouPaddle(dt);
updateAIPaddle(dt);
return;
}
if (game.phase === 'goal'){
game.goalTimer -= dt;
if (game.goalTimer <= 0){
setCenterMsg('');
if (game.pendingEnd){ endMatch(); return; }
resetPuck();
game.phase = 'playing';
}
updateYouPaddle(dt);
updateAIPaddle(dt);
return;
}
if (game.phase !== 'playing') return;
// timer
if (game.timeLeft > 0){
game.timeLeft -= dt;
if (game.timeLeft <= 0){
game.timeLeft = 0;
if (game.playerScore !== game.aiScore){ endMatch(); return; }
game.overtime = true;
setCenterMsg('وقت إضافي', 'الهدف الذهبي يحسم المباراة');
setTimeout(()=>{ if(game.phase==='playing') setCenterMsg(''); }, 1800);
}
}
updateYouPaddle(dt);
updateAIPaddle(dt);
updatePuck(dt);
updateHUD();
}
function endMatch(){
game.phase = 'ended';
game.active = false;
const won = game.playerScore > game.aiScore;
const ps = game.playerScore, as = game.aiScore;
// record result in the bracket
const round = tour.rounds[tour.roundIndex];
const m = yourMatch();
const opp = (m.a.en === tour.you.en) ? m.b : m.a;
if (m.a.en === tour.you.en){ m.sa = ps; m.sb = as; }
else { m.sb = ps; m.sa = as; }
m.winner = won ? tour.you : opp;
// simulate the remaining matches in this round
round.matches.forEach(o=>{ if (o !== m && !o.winner) simulateMatch(o); });
document.getElementById('hud').classList.add('hidden');
const screens = document.getElementById('screens');
screens.style.display = 'block';
if (!won){
showResult(false, ps, as, `خروج من ${round.name}`);
return;
}
// build the next round
const winners = round.matches.map(x=>x.winner);
if (tour.roundIndex < 2){
const next = tour.rounds[tour.roundIndex + 1];
next.matches = [];
for (let i = 0; i < winners.length; i += 2){
next.matches.push({ a: winners[i], b: winners[i+1], sa:null, sb:null, winner:null });
}
tour.roundIndex++;
showResult(true, ps, as, `تأهلت إلى ${tour.rounds[tour.roundIndex].name}`);
} else {
showResult(true, ps, as, '🏆 بطل البطولة!');
}
}
function simulateMatch(m){
const pa = m.a.strength / (m.a.strength + m.b.strength);
const aWins = Math.random() < pa;
const wScore = 3 + Math.floor(Math.random()*4);
const lScore = Math.floor(Math.random()*Math.min(wScore, 3));
if (aWins){ m.sa = wScore; m.sb = lScore; m.winner = m.a; }
else { m.sb = wScore; m.sa = lScore; m.winner = m.b; }
}
/* ==================================================================
8) RESULT SCREEN
================================================================== */
let resultAction = null;
function showResult(won, ps, as, sub){
const isChampion = won && tour.roundIndex === 2;
const icon = document.getElementById('resultIcon');
const title = document.getElementById('resultTitle');
const subEl = document.getElementById('resultSub');
const score = document.getElementById('resultScore');
const btn = document.getElementById('btnResult');
if (isChampion){
icon.textContent = '🏆';
title.textContent = 'أنت البطل!';
title.className = 'text-3xl font-black mb-2 text-amber-400';
btn.textContent = 'بطولة جديدة';
btn.className = 'w-full px-8 py-4 rounded-2xl font-black text-lg bg-gradient-to-r from-amber-400 to-yellow-500 text-slate-950 hover:brightness-110 transition';
resultAction = () => location.reload();
} else if (won){
icon.textContent = '✅';
title.textContent = 'فوز!';
title.className = 'text-3xl font-black mb-2 text-emerald-400';
btn.textContent = 'متابعة البطولة';
btn.className = 'w-full px-8 py-4 rounded-2xl font-black text-lg bg-gradient-to-r from-emerald-500 to-teal-600 text-white hover:brightness-110 transition';
resultAction = () => { renderBracket(); showScreen('draw'); };
} else {
icon.textContent = '❌';
title.textContent = 'خسارة';
title.className = 'text-3xl font-black mb-2 text-rose-400';
btn.textContent = 'بطولة جديدة';
btn.className = 'w-full px-8 py-4 rounded-2xl font-black text-lg bg-gradient-to-r from-rose-500 to-red-600 text-white hover:brightness-110 transition';
resultAction = () => location.reload();
}
subEl.textContent = `${matchCtx.you.ar} ضد ${matchCtx.opp.ar} — ${sub}`;
score.innerHTML = `<span class="${won?'text-emerald-400':'text-rose-400'}">${ps}</span>
<span class="text-slate-600">-</span>
<span class="text-slate-300">${as}</span>`;
showScreen('result');
beep(won?740:180, 0.3, 'triangle', 0.06);
}
document.getElementById('btnResult').onclick = ()=> resultAction && resultAction();
/* ==================================================================
9) SCREEN SWITCHER + LOOP
================================================================== */
function showScreen(id){
document.querySelectorAll('.screen').forEach(s=>{
s.classList.add('hidden');
s.classList.remove('flex');
});
const el = document.getElementById('screen-' + id);
el.classList.remove('hidden');
el.classList.add('flex');
}
function quitMatch(){
if (!confirm('هل تريد الخروج من المباراة؟ سيتم اعتبارها خسارة.')) return;
game.active = false;
game.phase = 'ended';
document.getElementById('hud').classList.add('hidden');
document.getElementById('screens').style.display = 'block';
showResult(false, game.playerScore, game.aiScore, 'انسحاب');
}
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.045);
updateGame(dt);
renderer.render(scene, camera);
}
/* ---------- boot ---------- */
initThree();
showScreen('start');
</script>
</body>
</html>Game Source: دوري الهوكي 2026 — Three.js
Creator: GoldenGlider99
Libraries: three
Complexity: complex (1066 lines, 42.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 2026-three-js-goldenglider99" to link back to the original. Then publish at arcadelab.ai/publish.