Spaces:
Sleeping
Sleeping
Update server.js
Browse files
server.js
CHANGED
|
@@ -9,72 +9,81 @@ dotenv.config();
|
|
| 9 |
// إعداد ضغط البيانات (MessagePack)
|
| 10 |
const packr = new Packr();
|
| 11 |
|
| 12 |
-
// إعداد قاعدة بيانات Turso (
|
| 13 |
const db = createClient({
|
| 14 |
url: process.env.TURSO_URL || 'file:local.db',
|
| 15 |
authToken: process.env.TURSO_AUTH_TOKEN || ''
|
| 16 |
});
|
| 17 |
|
| 18 |
-
// إعدادات العوالم
|
| 19 |
const MAX_PLAYERS_PER_WORLD = 30;
|
| 20 |
-
const DISCONNECT_GRACE_PERIOD = 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
class World {
|
| 23 |
constructor(id) {
|
| 24 |
this.id = id;
|
| 25 |
-
this.players = new Map();
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
this.
|
| 29 |
-
this.
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
get playerCount() {
|
| 33 |
-
// حساب اللاعبين المتصلين فقط
|
| 34 |
let count = 0;
|
| 35 |
this.players.forEach(p => { if (p.online) count++; });
|
| 36 |
return count;
|
| 37 |
}
|
| 38 |
|
| 39 |
-
// Tick Rate ديناميكي
|
| 40 |
-
updateTickRate() {
|
| 41 |
-
if (this.activeEvents > 10) this.tickRate = 60;
|
| 42 |
-
else if (this.activeEvents > 0) this.tickRate = 30;
|
| 43 |
-
else this.tickRate = 10;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
addPlayer(ws, playerData) {
|
| 47 |
this.players.set(ws.id, playerData);
|
| 48 |
-
this.isDirty = true;
|
| 49 |
}
|
| 50 |
|
| 51 |
removePlayer(wsId) {
|
| 52 |
this.players.delete(wsId);
|
| 53 |
-
this.isDirty = true;
|
| 54 |
}
|
| 55 |
}
|
| 56 |
|
| 57 |
// إنشاء 5 عوالم
|
| 58 |
const worlds = [1, 2, 3, 4, 5].map(id => new World(id));
|
| 59 |
|
| 60 |
-
// نظام الحفظ الاقتصادي (كل 5 دقائق إذا حدث تغيير)
|
| 61 |
-
setInterval(() => {
|
| 62 |
-
worlds.forEach(world => {
|
| 63 |
-
if (world.isDirty) {
|
| 64 |
-
console.log(`Saving World ${world.id} to Turso...`);
|
| 65 |
-
// هنا كود الحفظ في Turso
|
| 66 |
-
world.isDirty = false;
|
| 67 |
-
}
|
| 68 |
-
});
|
| 69 |
-
}, 5 * 60 * 1000);
|
| 70 |
-
|
| 71 |
const app = uWS.App().ws('/*', {
|
| 72 |
compression: uWS.SHARED_COMPRESSOR,
|
| 73 |
maxPayloadLength: 16 * 1024 * 1024,
|
| 74 |
-
idleTimeout: 60,
|
| 75 |
|
| 76 |
upgrade: (res, req, context) => {
|
| 77 |
-
// التحقق من الأمان (IP, Headers) لمنع الـ Spam
|
| 78 |
const ip = res.getRemoteAddressAsText();
|
| 79 |
res.upgrade(
|
| 80 |
{ id: Math.random().toString(36).substr(2, 9), ip },
|
|
@@ -87,31 +96,19 @@ const app = uWS.App().ws('/*', {
|
|
| 87 |
|
| 88 |
open: (ws) => {
|
| 89 |
ws.isAlive = true;
|
| 90 |
-
// إرسال حالة العوالم فور الاتصال
|
| 91 |
const worldStats = worlds.map(w => ({ id: w.id, players: w.playerCount }));
|
| 92 |
ws.send(packr.pack({ type: 'SERVER_LIST', data: worldStats }), true);
|
| 93 |
},
|
| 94 |
|
| 95 |
message: (ws, message, isBinary) => {
|
| 96 |
try {
|
| 97 |
-
// فك التشفير (نقبل فقط Binary MessagePack)
|
| 98 |
if (!isBinary) return;
|
| 99 |
const data = packr.unpack(message);
|
| 100 |
|
| 101 |
-
// الخادم لا يثق بالعميل (Authoritative)
|
| 102 |
switch (data.type) {
|
| 103 |
case 'JOIN_WORLD':
|
| 104 |
handleJoinWorld(ws, data.worldId, data.playerName);
|
| 105 |
break;
|
| 106 |
-
case 'CLAIM_TERRITORY':
|
| 107 |
-
handleClaimTerritory(ws, data.territoryId);
|
| 108 |
-
break;
|
| 109 |
-
case 'BUY_ITEM':
|
| 110 |
-
handleBuyItem(ws, data.itemId, data.amount);
|
| 111 |
-
break;
|
| 112 |
-
case 'LAUNCH_ATTACK':
|
| 113 |
-
handleAttack(ws, data.targetId, data.weaponType, data.amount);
|
| 114 |
-
break;
|
| 115 |
case 'PING':
|
| 116 |
ws.send(packr.pack({ type: 'PONG' }), true);
|
| 117 |
break;
|
|
@@ -129,25 +126,14 @@ const app = uWS.App().ws('/*', {
|
|
| 129 |
if (ws.worldId) {
|
| 130 |
const world = worlds.find(w => w.id === ws.worldId);
|
| 131 |
if (world && world.players.has(ws.id)) {
|
| 132 |
-
// تعيين اللاعب كـ Offline فوراً حتى لا يظهر في العدد
|
| 133 |
const player = world.players.get(ws.id);
|
| 134 |
player.online = false;
|
| 135 |
-
world.isDirty = true;
|
| 136 |
|
| 137 |
-
// حفظ مؤقت لمدة 60 ثانية قبل الحذف النهائي
|
| 138 |
setTimeout(() => {
|
| 139 |
const checkPlayer = world.players.get(ws.id);
|
| 140 |
if (checkPlayer && !checkPlayer.online) {
|
| 141 |
-
// اللاعب لم يعد، تفكيك دولته وحذف بياناته
|
| 142 |
world.removePlayer(ws.id);
|
| 143 |
-
|
| 144 |
-
// إزالة ألوانه من المقاطعات التي احتلها
|
| 145 |
-
for (let [tId, pId] of world.territories.entries()) {
|
| 146 |
-
if (pId === ws.id) {
|
| 147 |
-
world.territories.delete(tId);
|
| 148 |
-
broadcast(world, { type: 'TERRITORY_CLAIMED', tId: tId, color: '#1e1e28' }); // إعادة اللون الافتراضي
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
}
|
| 152 |
}, DISCONNECT_GRACE_PERIOD);
|
| 153 |
}
|
|
@@ -164,290 +150,36 @@ function handleJoinWorld(ws, worldId, playerName) {
|
|
| 164 |
return;
|
| 165 |
}
|
| 166 |
|
| 167 |
-
// تنظيف الاسم (أمان)
|
| 168 |
const safeName = String(playerName).substring(0, 20).replace(/[^a-zA-Z0-9أ-ي]/g, '');
|
| 169 |
-
|
| 170 |
ws.worldId = worldId;
|
| 171 |
ws.subscribe(`world-${worldId}`);
|
| 172 |
-
// إصلاح مشكلة الألوان الناقصة التي تجعل الدولة تبدو فارغة
|
| 173 |
const color = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`;
|
| 174 |
|
| 175 |
-
// إضافة بيانات الاقتصاد والجيش للاعب (تبدأ بـ 0 حتى يحتل أول منطقة)
|
| 176 |
world.addPlayer(ws, {
|
| 177 |
id: ws.id,
|
| 178 |
name: safeName,
|
| 179 |
color: color,
|
| 180 |
-
|
| 181 |
-
online: true,
|
| 182 |
-
money: 0,
|
| 183 |
-
power: 0,
|
| 184 |
-
income: 0, // يبدأ بـ 0
|
| 185 |
-
armyIncome: 0, // يبدأ بـ 0
|
| 186 |
-
assets: {}, // الممتلكات
|
| 187 |
-
cooldowns: {} // لتتبع التهدئة (Spam Prevention)
|
| 188 |
});
|
| 189 |
|
| 190 |
ws.send(packr.pack({ type: 'JOIN_SUCCESS', worldId, color, name: safeName }), true);
|
| 191 |
|
| 192 |
-
// إرسال
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
}
|
| 202 |
-
ws.send(packr.pack({ type: 'INIT_STATE', territories: worldState }), true);
|
| 203 |
|
| 204 |
broadcast(world, { type: 'PLAYER_JOINED', id: ws.id, name: safeName });
|
| 205 |
}
|
| 206 |
|
| 207 |
-
// بيانات المتجر في السيرفر (شراء مرة واحدة فقط لكل عنصر لمنع الاحتيال)
|
| 208 |
-
const SHOP_DATA = {
|
| 209 |
-
// الاقتصاد
|
| 210 |
-
'farm': { cost: 0, income: 0.1, type: 'eco', max: 1, cd: 0 },
|
| 211 |
-
'mine': { cost: 5, income: 0.4, type: 'eco', max: 1, cd: 0 },
|
| 212 |
-
'shop1': { cost: 20, income: 2, type: 'eco', max: 1, cd: 0 },
|
| 213 |
-
'shop2': { cost: 100, income: 10, type: 'eco', max: 1, cd: 0 },
|
| 214 |
-
'iron': { cost: 300, income: 30, type: 'eco', max: 1, cd: 0 },
|
| 215 |
-
'glass': { cost: 600, income: 50, type: 'eco', max: 1, cd: 0 },
|
| 216 |
-
'steel': { cost: 1000, income: 70, type: 'eco', max: 1, cd: 0 },
|
| 217 |
-
'gold': { cost: 1500, income: 150, type: 'eco', max: 1, cd: 0 },
|
| 218 |
-
'car': { cost: 5000, income: 400, type: 'eco', max: 1, cd: 0 },
|
| 219 |
-
'plane': { cost: 20000, income: 1000, type: 'eco', max: 1, cd: 0 },
|
| 220 |
-
'gpu': { cost: 100000, income: 10000, type: 'eco', max: 1, cd: 0 },
|
| 221 |
-
|
| 222 |
-
// العسكري
|
| 223 |
-
'soldier': { cost: 0.1, power: 1, type: 'mil', max: 1, cd: 0 },
|
| 224 |
-
'armor': { cost: 1, power: 10, type: 'mil', max: 1, cd: 0 },
|
| 225 |
-
'tank': { cost: 20, power: 250, type: 'mil', max: 1, cd: 0 },
|
| 226 |
-
'mlrs': { cost: 50, power: 700, type: 'mil', max: 1, cd: 0 },
|
| 227 |
-
'sam': { cost: 100, power: 1500, type: 'mil', max: 1, cd: 0 },
|
| 228 |
-
'aaa': { cost: 150, power: 2000, type: 'mil', max: 1, cd: 0 },
|
| 229 |
-
'fighter': { cost: 350, power: 5000, type: 'mil', max: 1, cd: 0 },
|
| 230 |
-
'attack_plane': { cost: 1000, power: 15000, type: 'mil', max: 1, cd: 0 },
|
| 231 |
-
'stealth': { cost: 2500, power: 40000, type: 'mil', max: 1, cd: 0 },
|
| 232 |
-
'bomber': { cost: 10000, power: 150000, type: 'mil', max: 1, cd: 0 },
|
| 233 |
-
'bm1': { cost: 1000, power: 20000, type: 'mil', max: 1, cd: 0 },
|
| 234 |
-
'bm2': { cost: 5000, power: 100000, type: 'mil', max: 1, cd: 0 },
|
| 235 |
-
'bm3': { cost: 15000, power: 300000, type: 'mil', max: 1, cd: 0 }
|
| 236 |
-
};
|
| 237 |
-
function handleBuyItem(ws, itemId, amount) {
|
| 238 |
-
const world = worlds.find(w => w.id === ws.worldId);
|
| 239 |
-
if (!world) return;
|
| 240 |
-
const player = world.players.get(ws.id);
|
| 241 |
-
if (!player) return;
|
| 242 |
-
|
| 243 |
-
// لا يمكن الشراء إذا لم يكن لديه دولة
|
| 244 |
-
if (player.territories.length === 0) {
|
| 245 |
-
ws.send(packr.pack({ type: 'ERROR', msg: 'يجب أن تحتل منطقة أولاً لتأسيس دولتك!' }), true);
|
| 246 |
-
return;
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
const item = SHOP_DATA[itemId];
|
| 250 |
-
if (!item) return;
|
| 251 |
-
|
| 252 |
-
// التحقق من الحد الأقصى للشراء في الدفعة الواحدة
|
| 253 |
-
if (item.maxBuy && amount > item.maxBuy) amount = item.maxBuy;
|
| 254 |
-
|
| 255 |
-
// التحقق من الحد الأقصى للمتلاك (مثل المزرعة 1 فقط)
|
| 256 |
-
if (item.max && (player.assets[itemId] || 0) + amount > item.max) {
|
| 257 |
-
ws.send(packr.pack({ type: 'ERROR', msg: `لا يمكنك امتلاك أكثر من ${item.max} من هذا العنصر` }), true);
|
| 258 |
-
return;
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
// التحقق من التهدئة (Cooldown) لمنع الـ Spam
|
| 262 |
-
const now = Date.now();
|
| 263 |
-
if (item.cd > 0) {
|
| 264 |
-
const lastBought = player.cooldowns[itemId] || 0;
|
| 265 |
-
if (now - lastBought < item.cd) {
|
| 266 |
-
const timeLeft = Math.ceil((item.cd - (now - lastBought)) / 60000);
|
| 267 |
-
ws.send(packr.pack({ type: 'ERROR', msg: `يجب الانتظار ${timeLeft} دقيقة لشراء هذا العنصر مجدداً` }), true);
|
| 268 |
-
return;
|
| 269 |
-
}
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
const totalCost = item.cost * amount;
|
| 273 |
-
|
| 274 |
-
// التحقق من المال
|
| 275 |
-
if (player.money >= totalCost) {
|
| 276 |
-
player.money -= totalCost;
|
| 277 |
-
|
| 278 |
-
if (!player.assets[itemId]) player.assets[itemId] = 0;
|
| 279 |
-
player.assets[itemId] += amount;
|
| 280 |
-
|
| 281 |
-
// تسجيل وقت الشراء للتهدئة
|
| 282 |
-
if (item.cd > 0) player.cooldowns[itemId] = now;
|
| 283 |
-
|
| 284 |
-
if (item.type === 'eco') {
|
| 285 |
-
player.income += (item.income * amount);
|
| 286 |
-
} else if (item.type === 'mil') {
|
| 287 |
-
player.power += (item.power * amount);
|
| 288 |
-
broadcast(world, { type: 'UPDATE_POWER', pId: player.id, power: player.power });
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
world.isDirty = true;
|
| 292 |
-
ws.send(packr.pack({
|
| 293 |
-
type: 'SYNC_STATS',
|
| 294 |
-
money: player.money,
|
| 295 |
-
power: player.power,
|
| 296 |
-
income: player.income,
|
| 297 |
-
armyIncome: player.armyIncome,
|
| 298 |
-
assets: player.assets
|
| 299 |
-
}), true);
|
| 300 |
-
} else {
|
| 301 |
-
ws.send(packr.pack({ type: 'ERROR', msg: 'لا تملك المال الكافي' }), true);
|
| 302 |
-
}
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
// نظام الاقتصاد (يتم التحديث كل 30 ثانية لمنع الـ Spam والسرعة المبالغ فيها)
|
| 306 |
-
setInterval(() => {
|
| 307 |
-
worlds.forEach(world => {
|
| 308 |
-
world.players.forEach(player => {
|
| 309 |
-
if (player.online && player.territories.length > 0) {
|
| 310 |
-
// نضرب الدخل في 30 لأنه يحسب بالثانية
|
| 311 |
-
player.money += (player.income * 30);
|
| 312 |
-
player.power += (player.armyIncome * 30);
|
| 313 |
-
world.isDirty = true;
|
| 314 |
-
}
|
| 315 |
-
});
|
| 316 |
-
});
|
| 317 |
-
}, 30000);
|
| 318 |
-
|
| 319 |
-
// مزامنة البيانات مع العملاء كل 5 ثواني (لتخفيف الضغط، العميل يتنبأ بالباقي)
|
| 320 |
-
setInterval(() => {
|
| 321 |
-
worlds.forEach(world => {
|
| 322 |
-
world.players.forEach((player, wsId) => {
|
| 323 |
-
if (player.online) {
|
| 324 |
-
// يجب إيجاد كائن ws الخاص باللاعب لإرسال الرسالة
|
| 325 |
-
// في uWebSockets نحتاج لطريقة لحفظ ws، للتبسيط سنفترض أننا نرسل للجميع
|
| 326 |
-
broadcast(world, { type: 'SYNC_STATS_SILENT', pId: player.id, money: player.money, power: player.power });
|
| 327 |
-
}
|
| 328 |
-
});
|
| 329 |
-
});
|
| 330 |
-
}, 5000);
|
| 331 |
-
|
| 332 |
-
function handleClaimTerritory(ws, territoryId) {
|
| 333 |
-
const world = worlds.find(w => w.id === ws.worldId);
|
| 334 |
-
if (!world) return;
|
| 335 |
-
const player = world.players.get(ws.id);
|
| 336 |
-
|
| 337 |
-
if (world.territories.has(territoryId)) {
|
| 338 |
-
ws.send(packr.pack({ type: 'ERROR', msg: 'المنطقة غير متاحة' }), true);
|
| 339 |
-
return;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
// المنطقة الأولى مجانية لتأسيس الدولة، التوسع اللاحق رخيص ليتناسب مع الدخل
|
| 343 |
-
const isFirstClaim = player.territories.length === 0;
|
| 344 |
-
const CLAIM_COST_MONEY = isFirstClaim ? 0 : 0.05;
|
| 345 |
-
const CLAIM_COST_POWER = isFirstClaim ? 0 : 10;
|
| 346 |
-
|
| 347 |
-
if (player.money < CLAIM_COST_MONEY || player.power < CLAIM_COST_POWER) {
|
| 348 |
-
ws.send(packr.pack({ type: 'ERROR', msg: `لا تملك موارد كافية للتوسع (مطلوب ${CLAIM_COST_MONEY}$ و ${CLAIM_COST_POWER} قوة)` }), true);
|
| 349 |
-
return;
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
// خصم الموارد
|
| 353 |
-
player.money -= CLAIM_COST_MONEY;
|
| 354 |
-
player.power -= CLAIM_COST_POWER;
|
| 355 |
-
|
| 356 |
-
// منع ثغرة الاحتلال المتعدد في نفس الوقت
|
| 357 |
-
if (player.isClaiming) {
|
| 358 |
-
ws.send(packr.pack({ type: 'ERROR', msg: 'أنت تقوم بعملية احتلال حالياً، انتظر!' }), true);
|
| 359 |
-
return;
|
| 360 |
-
}
|
| 361 |
-
player.isClaiming = true;
|
| 362 |
-
|
| 363 |
-
world.territories.set(territoryId, 'CLAIMING_' + player.id);
|
| 364 |
-
|
| 365 |
-
// إذا كان أول احتلال، يتم فوراً (0 ثانية)
|
| 366 |
-
const claimDuration = isFirstClaim ? 0 : 60000;
|
| 367 |
-
|
| 368 |
-
broadcast(world, { type: 'CLAIM_STARTED', tId: territoryId, pId: player.id, color: player.color, duration: claimDuration / 1000 });
|
| 369 |
-
|
| 370 |
-
setTimeout(() => {
|
| 371 |
-
player.isClaiming = false;
|
| 372 |
-
// التأكد أن اللاعب لم يخرج من اللعبة
|
| 373 |
-
if (world.players.has(ws.id) && world.territories.get(territoryId) === 'CLAIMING_' + player.id) {
|
| 374 |
-
world.territories.set(territoryId, player.id);
|
| 375 |
-
player.territories.push(territoryId);
|
| 376 |
-
|
| 377 |
-
// تفعيل الاقتصاد إذا كانت هذه أول منطقة له
|
| 378 |
-
if (player.territories.length === 1) {
|
| 379 |
-
player.income = 0.001;
|
| 380 |
-
player.armyIncome = 1;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
world.isDirty = true;
|
| 384 |
-
|
| 385 |
-
broadcast(world, {
|
| 386 |
-
type: 'TERRITORY_CLAIMED',
|
| 387 |
-
tId: territoryId,
|
| 388 |
-
color: player.color,
|
| 389 |
-
pId: player.id,
|
| 390 |
-
name: player.name,
|
| 391 |
-
power: player.power
|
| 392 |
-
});
|
| 393 |
-
} else {
|
| 394 |
-
// إذا خرج اللاعب، نلغي الاحتلال
|
| 395 |
-
world.territories.delete(territoryId);
|
| 396 |
-
broadcast(world, { type: 'TERRITORY_CLAIMED', tId: territoryId, color: '#1e1e28' });
|
| 397 |
-
}
|
| 398 |
-
}, claimDuration); // 60 ثانية أو 0 ثانية
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
// نظام الهجوم المبسط (سيتم تطويره للنووي لاحقاً)
|
| 402 |
-
function handleAttack(ws, targetId, weaponType, amount) {
|
| 403 |
-
const world = worlds.find(w => w.id === ws.worldId);
|
| 404 |
-
if (!world) return;
|
| 405 |
-
const attacker = world.players.get(ws.id);
|
| 406 |
-
|
| 407 |
-
// التحقق من امتلاك السلاح
|
| 408 |
-
if (!attacker.assets[weaponType] || attacker.assets[weaponType] < amount) {
|
| 409 |
-
ws.send(packr.pack({ type: 'ERROR', msg: 'لا تملك هذا العدد من السلاح' }), true);
|
| 410 |
-
return;
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
// خصم السلاح من المهاجم
|
| 414 |
-
attacker.assets[weaponType] -= amount;
|
| 415 |
-
|
| 416 |
-
// حساب قوة الهجوم (مثال: الدبابة قوتها 250)
|
| 417 |
-
const weaponPower = SHOP_DATA[weaponType].power * amount;
|
| 418 |
-
|
| 419 |
-
// إرسال إشعار للجميع ببدء الهجوم (يستغرق وقت حسب المسافة، للتبسيط هنا 60ث)
|
| 420 |
-
broadcast(world, { type: 'ATTACK_LAUNCHED', from: attacker.name, target: targetId, time: 60 });
|
| 421 |
-
|
| 422 |
-
setTimeout(() => {
|
| 423 |
-
const defenderId = world.territories.get(targetId);
|
| 424 |
-
if (defenderId && world.players.has(defenderId)) {
|
| 425 |
-
const defender = world.players.get(defenderId);
|
| 426 |
-
// خصم قوة المدافع
|
| 427 |
-
defender.power = Math.max(0, defender.power - weaponPower);
|
| 428 |
-
broadcast(world, { type: 'UPDATE_POWER', pId: defender.id, power: defender.power });
|
| 429 |
-
|
| 430 |
-
// إذا وصلت قوة المدافع لـ 0، تسقط المنطقة (يمكن برمجتها لاحقاً)
|
| 431 |
-
}
|
| 432 |
-
}, 60000);
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
function broadcast(world, messageObj) {
|
| 436 |
const buffer = packr.pack(messageObj);
|
| 437 |
-
// إرسال البيانات لجميع اللاعبين المتصلين بنفس العالم بسرعة خارقة
|
| 438 |
app.publish(`world-${world.id}`, buffer, true);
|
| 439 |
}
|
| 440 |
|
| 441 |
-
// تقديم ملف الخريطة (world.svg)
|
| 442 |
-
app.get('/world.svg', (res, req) => {
|
| 443 |
-
try {
|
| 444 |
-
const svg = fs.readFileSync('world.svg', 'utf8');
|
| 445 |
-
res.writeHeader('Content-Type', 'image/svg+xml; charset=utf-8').end(svg);
|
| 446 |
-
} catch (e) {
|
| 447 |
-
res.writeStatus('404 Not Found').end('Map not found');
|
| 448 |
-
}
|
| 449 |
-
});
|
| 450 |
-
|
| 451 |
// تقديم ملف الواجهة (index.html)
|
| 452 |
app.get('/*', (res, req) => {
|
| 453 |
try {
|
|
@@ -461,7 +193,7 @@ app.get('/*', (res, req) => {
|
|
| 461 |
const PORT = process.env.PORT || 7860;
|
| 462 |
app.listen(PORT, (token) => {
|
| 463 |
if (token) {
|
| 464 |
-
console.log(`Server running on port ${PORT} (
|
| 465 |
} else {
|
| 466 |
console.log(`Failed to listen to port ${PORT}`);
|
| 467 |
}
|
|
|
|
| 9 |
// إعداد ضغط البيانات (MessagePack)
|
| 10 |
const packr = new Packr();
|
| 11 |
|
| 12 |
+
// إعداد قاعدة بيانات Turso (احتياطياً للمستقبل)
|
| 13 |
const db = createClient({
|
| 14 |
url: process.env.TURSO_URL || 'file:local.db',
|
| 15 |
authToken: process.env.TURSO_AUTH_TOKEN || ''
|
| 16 |
});
|
| 17 |
|
|
|
|
| 18 |
const MAX_PLAYERS_PER_WORLD = 30;
|
| 19 |
+
const DISCONNECT_GRACE_PERIOD = 60 * 1000;
|
| 20 |
+
|
| 21 |
+
// خوارزمية توليد قارات بكسلية (Cellular Automata)
|
| 22 |
+
function generatePixelMap(width, height) {
|
| 23 |
+
const grid = new Uint8Array(width * height);
|
| 24 |
+
// عشوائية أولية
|
| 25 |
+
for (let i = 0; i < grid.length; i++) {
|
| 26 |
+
grid[i] = Math.random() > 0.55 ? 1 : 0; // 1 يابسة، 0 ماء
|
| 27 |
+
}
|
| 28 |
+
// تنعيم لإنشاء قارات
|
| 29 |
+
for (let iter = 0; iter < 4; iter++) {
|
| 30 |
+
const newGrid = new Uint8Array(width * height);
|
| 31 |
+
for (let y = 0; y < height; y++) {
|
| 32 |
+
for (let x = 0; x < width; x++) {
|
| 33 |
+
let neighbors = 0;
|
| 34 |
+
for (let dy = -1; dy <= 1; dy++) {
|
| 35 |
+
for (let dx = -1; dx <= 1; dx++) {
|
| 36 |
+
if (dx === 0 && dy === 0) continue;
|
| 37 |
+
let nx = x + dx, ny = y + dy;
|
| 38 |
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
| 39 |
+
neighbors += grid[ny * width + nx];
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
// قواعد الحياة للقارات
|
| 44 |
+
newGrid[y * width + x] = neighbors > 4 || (neighbors === 4 && grid[y * width + x]) ? 1 : 0;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
for (let i = 0; i < grid.length; i++) grid[i] = newGrid[i];
|
| 48 |
+
}
|
| 49 |
+
return grid;
|
| 50 |
+
}
|
| 51 |
|
| 52 |
class World {
|
| 53 |
constructor(id) {
|
| 54 |
this.id = id;
|
| 55 |
+
this.players = new Map();
|
| 56 |
+
|
| 57 |
+
// إعداد الخريطة البكسلية (مثلاً 250x250 بكسل/مربع)
|
| 58 |
+
this.mapWidth = 250;
|
| 59 |
+
this.mapHeight = 250;
|
| 60 |
+
this.mapGrid = generatePixelMap(this.mapWidth, this.mapHeight);
|
| 61 |
}
|
| 62 |
|
| 63 |
get playerCount() {
|
|
|
|
| 64 |
let count = 0;
|
| 65 |
this.players.forEach(p => { if (p.online) count++; });
|
| 66 |
return count;
|
| 67 |
}
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
addPlayer(ws, playerData) {
|
| 70 |
this.players.set(ws.id, playerData);
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
removePlayer(wsId) {
|
| 74 |
this.players.delete(wsId);
|
|
|
|
| 75 |
}
|
| 76 |
}
|
| 77 |
|
| 78 |
// إنشاء 5 عوالم
|
| 79 |
const worlds = [1, 2, 3, 4, 5].map(id => new World(id));
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
const app = uWS.App().ws('/*', {
|
| 82 |
compression: uWS.SHARED_COMPRESSOR,
|
| 83 |
maxPayloadLength: 16 * 1024 * 1024,
|
| 84 |
+
idleTimeout: 60,
|
| 85 |
|
| 86 |
upgrade: (res, req, context) => {
|
|
|
|
| 87 |
const ip = res.getRemoteAddressAsText();
|
| 88 |
res.upgrade(
|
| 89 |
{ id: Math.random().toString(36).substr(2, 9), ip },
|
|
|
|
| 96 |
|
| 97 |
open: (ws) => {
|
| 98 |
ws.isAlive = true;
|
|
|
|
| 99 |
const worldStats = worlds.map(w => ({ id: w.id, players: w.playerCount }));
|
| 100 |
ws.send(packr.pack({ type: 'SERVER_LIST', data: worldStats }), true);
|
| 101 |
},
|
| 102 |
|
| 103 |
message: (ws, message, isBinary) => {
|
| 104 |
try {
|
|
|
|
| 105 |
if (!isBinary) return;
|
| 106 |
const data = packr.unpack(message);
|
| 107 |
|
|
|
|
| 108 |
switch (data.type) {
|
| 109 |
case 'JOIN_WORLD':
|
| 110 |
handleJoinWorld(ws, data.worldId, data.playerName);
|
| 111 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
case 'PING':
|
| 113 |
ws.send(packr.pack({ type: 'PONG' }), true);
|
| 114 |
break;
|
|
|
|
| 126 |
if (ws.worldId) {
|
| 127 |
const world = worlds.find(w => w.id === ws.worldId);
|
| 128 |
if (world && world.players.has(ws.id)) {
|
|
|
|
| 129 |
const player = world.players.get(ws.id);
|
| 130 |
player.online = false;
|
|
|
|
| 131 |
|
|
|
|
| 132 |
setTimeout(() => {
|
| 133 |
const checkPlayer = world.players.get(ws.id);
|
| 134 |
if (checkPlayer && !checkPlayer.online) {
|
|
|
|
| 135 |
world.removePlayer(ws.id);
|
| 136 |
+
broadcast(world, { type: 'PLAYER_REMOVED', id: ws.id });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
}, DISCONNECT_GRACE_PERIOD);
|
| 139 |
}
|
|
|
|
| 150 |
return;
|
| 151 |
}
|
| 152 |
|
|
|
|
| 153 |
const safeName = String(playerName).substring(0, 20).replace(/[^a-zA-Z0-9أ-ي]/g, '');
|
|
|
|
| 154 |
ws.worldId = worldId;
|
| 155 |
ws.subscribe(`world-${worldId}`);
|
|
|
|
| 156 |
const color = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`;
|
| 157 |
|
|
|
|
| 158 |
world.addPlayer(ws, {
|
| 159 |
id: ws.id,
|
| 160 |
name: safeName,
|
| 161 |
color: color,
|
| 162 |
+
online: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
});
|
| 164 |
|
| 165 |
ws.send(packr.pack({ type: 'JOIN_SUCCESS', worldId, color, name: safeName }), true);
|
| 166 |
|
| 167 |
+
// إرسال الخريطة البكسلية للعميل (عرض، طول، والمصفوفة)
|
| 168 |
+
ws.send(packr.pack({
|
| 169 |
+
type: 'MAP_DATA',
|
| 170 |
+
width: world.mapWidth,
|
| 171 |
+
height: world.mapHeight,
|
| 172 |
+
grid: world.mapGrid
|
| 173 |
+
}), true);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
broadcast(world, { type: 'PLAYER_JOINED', id: ws.id, name: safeName });
|
| 176 |
}
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
function broadcast(world, messageObj) {
|
| 179 |
const buffer = packr.pack(messageObj);
|
|
|
|
| 180 |
app.publish(`world-${world.id}`, buffer, true);
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
// تقديم ملف الواجهة (index.html)
|
| 184 |
app.get('/*', (res, req) => {
|
| 185 |
try {
|
|
|
|
| 193 |
const PORT = process.env.PORT || 7860;
|
| 194 |
app.listen(PORT, (token) => {
|
| 195 |
if (token) {
|
| 196 |
+
console.log(`Server running on port ${PORT} (Pixel Grid Engine Ready)`);
|
| 197 |
} else {
|
| 198 |
console.log(`Failed to listen to port ${PORT}`);
|
| 199 |
}
|