Spaces:
Sleeping
Sleeping
| <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>World Conquest MMO - Pixel Engine</title> | |
| <style> | |
| /* CSS Reset & Base */ | |
| * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; -webkit-tap-highlight-color: transparent; outline: none; } | |
| body { background-color: #050a14; color: #e0e0e0; overflow: hidden; touch-action: none; width: 100vw; height: 100vh; } | |
| /* تأثير الضباب المتحرك */ | |
| .fog-overlay { | |
| position: fixed; top: -50%; left: -50%; width: 200%; height: 200%; | |
| background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.01" numOctaves="3" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.08"/></svg>'); | |
| animation: fogAnim 40s linear infinite; pointer-events: none; z-index: 1; | |
| } | |
| @keyframes fogAnim { 0% { transform: translate(0, 0); } 100% { transform: translate(10%, 10%); } } | |
| /* شاشة التحميل */ | |
| #loading-screen { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: radial-gradient(circle at center, #1a1a2e 0%, #0f0f1a 100%); | |
| display: flex; flex-direction: column; justify-content: center; align-items: center; | |
| z-index: 9999; transition: opacity 0.5s ease; | |
| } | |
| .loader-ring { | |
| width: 60px; height: 60px; border: 4px solid rgba(0, 255, 136, 0.1); | |
| border-top: 4px solid #00ff88; border-radius: 50%; animation: spin 1s linear infinite; | |
| box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); | |
| } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| .loading-text { margin-top: 20px; font-size: 16px; color: #00ff88; letter-spacing: 1px; text-shadow: 0 0 10px rgba(0,255,136,0.5); } | |
| /* شاشة اختيار الخوادم */ | |
| #server-selection { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: radial-gradient(circle at top, #16222a 0%, #3a6073 100%); | |
| z-index: 1000; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; | |
| } | |
| .glass-panel { | |
| background: rgba(20, 20, 25, 0.7); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 20px; padding: 25px; width: 100%; max-width: 400px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); | |
| display: flex; flex-direction: column; align-items: center; z-index: 2; | |
| } | |
| .title { color: #fff; font-size: 24px; margin-bottom: 20px; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } | |
| .input-name { | |
| width: 100%; padding: 15px; border-radius: 12px; border: 1px solid #444; background: rgba(0,0,0,0.5); | |
| color: #fff; text-align: center; font-size: 16px; margin-bottom: 20px; outline: none; transition: 0.3s; | |
| } | |
| .input-name:focus { border-color: #00ff88; box-shadow: 0 0 10px rgba(0,255,136,0.3); } | |
| .servers-container { width: 100%; max-height: 40vh; overflow-y: auto; margin-bottom: 20px; padding-right: 5px; } | |
| .servers-container::-webkit-scrollbar { width: 5px; } | |
| .servers-container::-webkit-scrollbar-thumb { background: #00ff88; border-radius: 5px; } | |
| .server-btn { | |
| width: 100%; background: linear-gradient(145deg, #2a2a35, #1f1f28); border: 1px solid #333; | |
| border-radius: 12px; padding: 15px 20px; margin-bottom: 10px; display: flex; justify-content: space-between; | |
| align-items: center; color: #fff; font-size: 16px; transition: all 0.2s ease; cursor: pointer; | |
| } | |
| .server-btn:active { transform: scale(0.96); border-color: #00ff88; } | |
| .server-btn.full { opacity: 0.6; border-color: #ff4444; pointer-events: none; filter: grayscale(100%); } | |
| .server-info { display: flex; align-items: center; gap: 10px; } | |
| .icon { width: 20px; height: 20px; fill: currentColor; } | |
| .action-btn { | |
| display: flex; align-items: center; justify-content: center; gap: 8px; | |
| width: 100%; padding: 15px; border-radius: 12px; background: linear-gradient(45deg, #0077ff, #00bfff); | |
| color: #fff; border: none; font-size: 16px; font-weight: bold; box-shadow: 0 4px 15px rgba(0,119,255,0.4); | |
| transition: 0.2s; cursor: pointer; | |
| } | |
| .action-btn:active { transform: scale(0.96); } | |
| /* حاوية الخريطة البكسلية */ | |
| #map-container { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; background: #050a14; overflow: hidden; } | |
| /* إعدادات الـ Canvas لضمان أداء عالي وعدم تشويش البكسلات عند التكبير */ | |
| #world-canvas { | |
| transform-origin: 0 0; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| image-rendering: pixelated; /* مهم جداً للحفاظ على حدة البكسلات */ | |
| will-change: transform; | |
| } | |
| /* الإشعارات الذكية */ | |
| #notification { | |
| position: fixed; top: 30px; left: 50%; transform: translateX(-50%) translateY(-20px); | |
| background: rgba(255, 68, 68, 0.9); backdrop-filter: blur(5px); padding: 12px 25px; | |
| border-radius: 25px; z-index: 10000; color: white; font-weight: bold; font-size: 14px; | |
| box-shadow: 0 5px 15px rgba(255,68,68,0.4); opacity: 0; transition: all 0.3s ease; pointer-events: none; | |
| } | |
| #notification.show { transform: translateX(-50%) translateY(0); opacity: 1; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="fog-overlay"></div> | |
| <div id="loading-screen"> | |
| <div class="loader-ring"></div> | |
| <p class="loading-text" id="loading-msg">جاري الاتصال بالخادم الآمن...</p> | |
| </div> | |
| <div id="notification"></div> | |
| <div id="server-selection" style="display: none;"> | |
| <div class="glass-panel"> | |
| <h2 class="title">اختر العالم</h2> | |
| <input type="text" id="player-name" class="input-name" placeholder="أدخل اسمك (20 حرف كحد أقصى)" maxlength="20"> | |
| <div class="servers-container" id="servers-list"></div> | |
| <button id="refresh-btn" class="action-btn"> | |
| <svg class="icon" viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> | |
| تحديث القائمة | |
| </button> | |
| </div> | |
| </div> | |
| <div id="map-container"> | |
| <!-- تم استبدال SVG بـ Canvas --> | |
| <canvas id="world-canvas"></canvas> | |
| </div> | |
| <script type="module"> | |
| import { Packr } from 'https://esm.sh/msgpackr@1.10.1'; | |
| const packr = new Packr(); | |
| let ws; | |
| let myColor = ''; | |
| let myName = ''; | |
| let currentScale = 1; | |
| let currentX = 0; | |
| let currentY = 0; | |
| let lastRefreshTime = 0; | |
| // إعدادات الخريطة البكسلية | |
| let mapWidth = 0; | |
| let mapHeight = 0; | |
| let mapGrid = null; | |
| const TILE_SIZE = 10; // حجم كل بكسل/مربع على الشاشة | |
| const DOM = { | |
| loading: document.getElementById('loading-screen'), | |
| loadingMsg: document.getElementById('loading-msg'), | |
| serverSel: document.getElementById('server-selection'), | |
| serverList: document.getElementById('servers-list'), | |
| playerName: document.getElementById('player-name'), | |
| refreshBtn: document.getElementById('refresh-btn'), | |
| mapContainer: document.getElementById('map-container'), | |
| worldCanvas: document.getElementById('world-canvas'), | |
| notif: document.getElementById('notification') | |
| }; | |
| function connect() { | |
| DOM.loadingMsg.innerText = "جاري الاتصال بالخادم الآمن..."; | |
| const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(`${protocol}//${location.host}/`); | |
| ws.binaryType = 'arraybuffer'; | |
| const timeout = setTimeout(() => { | |
| if (ws.readyState !== WebSocket.OPEN) { | |
| showNotification("فشل الاتصال، يرجى تحديث الصفحة"); | |
| DOM.loadingMsg.innerText = "فشل الاتصال بالخادم"; | |
| } | |
| }, 5000); | |
| ws.onopen = () => { | |
| clearTimeout(timeout); | |
| DOM.loading.style.opacity = '0'; | |
| setTimeout(() => { | |
| DOM.loading.style.display = 'none'; | |
| DOM.serverSel.style.display = 'flex'; | |
| }, 500); | |
| }; | |
| ws.onmessage = async (event) => { | |
| try { | |
| const data = packr.unpack(new Uint8Array(event.data)); | |
| handleServerMessage(data); | |
| } catch (e) { | |
| console.error("Data unpack error", e); | |
| } | |
| }; | |
| ws.onclose = () => { | |
| showNotification("انقطع الاتصال، جاري إعادة المحاولة..."); | |
| DOM.loading.style.display = 'flex'; | |
| DOM.loading.style.opacity = '1'; | |
| DOM.serverSel.style.display = 'none'; | |
| setTimeout(connect, 3000); | |
| }; | |
| } | |
| function handleServerMessage(data) { | |
| switch (data.type) { | |
| case 'SERVER_LIST': | |
| renderServers(data.data); | |
| break; | |
| case 'JOIN_SUCCESS': | |
| myColor = data.color; | |
| myName = data.name; | |
| DOM.serverSel.style.display = 'none'; | |
| DOM.loading.style.display = 'flex'; | |
| DOM.loading.style.opacity = '1'; | |
| DOM.loadingMsg.innerText = "جاري تحميل خريطة العالم..."; | |
| break; | |
| case 'MAP_DATA': | |
| // استلام الخريطة البكسلية من الخادم | |
| mapWidth = data.width; | |
| mapHeight = data.height; | |
| mapGrid = data.grid; | |
| renderCanvasMap(); | |
| setupTouchControls(); | |
| DOM.loading.style.opacity = '0'; | |
| setTimeout(() => DOM.loading.style.display = 'none', 500); | |
| break; | |
| case 'ERROR': | |
| showNotification(data.msg); | |
| DOM.loading.style.display = 'none'; | |
| break; | |
| } | |
| } | |
| function renderServers(servers) { | |
| DOM.serverList.innerHTML = ''; | |
| servers.forEach(s => { | |
| const isFull = s.players >= 30; | |
| const btn = document.createElement('div'); | |
| btn.className = `server-btn ${isFull ? 'full' : ''}`; | |
| btn.innerHTML = ` | |
| <div class="server-info"> | |
| <svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg> | |
| <span>عالم ${s.id}</span> | |
| </div> | |
| <span>${s.players}/30</span> | |
| `; | |
| btn.onclick = () => { | |
| if (isFull) showNotification("هذا الخادم ممتلئ، اختر خادماً آخر"); | |
| else joinWorld(s.id); | |
| }; | |
| DOM.serverList.appendChild(btn); | |
| }); | |
| } | |
| function joinWorld(id) { | |
| const name = DOM.playerName.value.trim() || 'قائد مجهول'; | |
| ws.send(packr.pack({ type: 'JOIN_WORLD', worldId: id, playerName: name })); | |
| DOM.loadingMsg.innerText = "جاري تجهيز العالم..."; | |
| DOM.loading.style.display = 'flex'; | |
| DOM.loading.style.opacity = '1'; | |
| } | |
| // دالة رسم الخريطة البكسلية على الـ Canvas | |
| function renderCanvasMap() { | |
| const canvas = DOM.worldCanvas; | |
| const ctx = canvas.getContext('2d', { alpha: false }); // تحسين الأداء | |
| canvas.width = mapWidth * TILE_SIZE; | |
| canvas.height = mapHeight * TILE_SIZE; | |
| // رسم المحيط (الماء) | |
| ctx.fillStyle = '#0a192f'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // رسم القارات (اليابسة) | |
| ctx.fillStyle = '#1e1e28'; | |
| for (let y = 0; y < mapHeight; y++) { | |
| for (let x = 0; x < mapWidth; x++) { | |
| if (mapGrid[y * mapWidth + x] === 1) { | |
| ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); | |
| } | |
| } | |
| } | |
| } | |
| function setupTouchControls() { | |
| let startDist = 0, startScale = 1; | |
| let startX = 0, startY = 0; | |
| let isPanning = false; | |
| let focalX = 0, focalY = 0; | |
| // توسيط الخريطة مبدئياً | |
| function centerMapInitially() { | |
| const winWidth = window.innerWidth; | |
| const winHeight = window.innerHeight; | |
| const baseMapWidth = DOM.worldCanvas.width; | |
| const baseMapHeight = DOM.worldCanvas.height; | |
| const minScaleX = winWidth / baseMapWidth; | |
| const minScaleY = winHeight / baseMapHeight; | |
| currentScale = Math.max(minScaleX, minScaleY); | |
| const scaledWidth = baseMapWidth * currentScale; | |
| const scaledHeight = baseMapHeight * currentScale; | |
| currentX = (winWidth - scaledWidth) / 2; | |
| currentY = (winHeight - scaledHeight) / 2; | |
| clampBounds(); | |
| updateTransform(); | |
| } | |
| centerMapInitially(); | |
| DOM.mapContainer.addEventListener('touchstart', (e) => { | |
| if (e.touches.length === 2) { | |
| isPanning = false; | |
| startDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); | |
| startScale = currentScale; | |
| focalX = (e.touches[0].clientX + e.touches[1].clientX) / 2; | |
| focalY = (e.touches[0].clientY + e.touches[1].clientY) / 2; | |
| } else if (e.touches.length === 1) { | |
| isPanning = true; | |
| startX = e.touches[0].clientX - currentX; | |
| startY = e.touches[0].clientY - currentY; | |
| } | |
| }, {passive: true}); | |
| DOM.mapContainer.addEventListener('touchmove', (e) => { | |
| if (e.touches.length === 2) { | |
| const currentDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); | |
| let newScale = startScale * (currentDist / startDist); | |
| newScale = Math.min(Math.max(0.1, newScale), 20); | |
| const scaleRatio = newScale / currentScale; | |
| currentX = focalX - (focalX - currentX) * scaleRatio; | |
| currentY = focalY - (focalY - currentY) * scaleRatio; | |
| currentScale = newScale; | |
| clampBounds(); | |
| updateTransform(); | |
| } else if (e.touches.length === 1 && isPanning) { | |
| currentX = e.touches[0].clientX - startX; | |
| currentY = e.touches[0].clientY - startY; | |
| clampBounds(); | |
| updateTransform(); | |
| } | |
| }, {passive: true}); | |
| DOM.mapContainer.addEventListener('touchend', () => isPanning = false); | |
| function clampBounds() { | |
| const winWidth = window.innerWidth; | |
| const winHeight = window.innerHeight; | |
| const baseMapWidth = DOM.worldCanvas.width; | |
| const baseMapHeight = DOM.worldCanvas.height; | |
| const minScaleX = winWidth / baseMapWidth; | |
| const minScaleY = winHeight / baseMapHeight; | |
| const minScale = Math.max(minScaleX, minScaleY); | |
| if (currentScale < minScale) currentScale = minScale; | |
| const scaledWidth = baseMapWidth * currentScale; | |
| const scaledHeight = baseMapHeight * currentScale; | |
| if (currentX > 0) currentX = 0; | |
| else if (currentX < winWidth - scaledWidth) currentX = winWidth - scaledWidth; | |
| if (currentY > 0) currentY = 0; | |
| else if (currentY < winHeight - scaledHeight) currentY = winHeight - scaledHeight; | |
| } | |
| function updateTransform() { | |
| DOM.worldCanvas.style.transform = `translate(${currentX}px, ${currentY}px) scale(${currentScale})`; | |
| } | |
| } | |
| function showNotification(msg) { | |
| DOM.notif.innerText = msg; | |
| DOM.notif.classList.add('show'); | |
| setTimeout(() => DOM.notif.classList.remove('show'), 3000); | |
| } | |
| DOM.refreshBtn.addEventListener('click', () => { | |
| const now = Date.now(); | |
| if (now - lastRefreshTime < 5000) { | |
| showNotification("يرجى الانتظار 5 ثواني قبل التحديث"); | |
| return; | |
| } | |
| lastRefreshTime = now; | |
| DOM.refreshBtn.disabled = true; | |
| DOM.refreshBtn.innerHTML = '<div class="loader-ring" style="width:20px;height:20px;border-width:2px;"></div> جاري التحديث...'; | |
| if(ws.readyState === WebSocket.OPEN) { | |
| ws.send(packr.pack({ type: 'REFRESH_SERVERS' })); | |
| } | |
| setTimeout(() => { | |
| DOM.refreshBtn.disabled = false; | |
| DOM.refreshBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg> تحديث القائمة'; | |
| }, 5000); | |
| }); | |
| connect(); | |
| </script> | |
| </body> | |
| </html> |