Poorpoor6976 commited on
Commit
faf3a50
·
verified ·
1 Parent(s): 816b38b

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +50 -318
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; // 60 ثانية فقط
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  class World {
23
  constructor(id) {
24
  this.id = id;
25
- this.players = new Map(); // RAM is the source of truth
26
- this.territories = new Map();
27
- this.isDirty = false; // لمعرفة هل نحتاج للحفظ في Turso
28
- this.tickRate = 10; // يبدأ بـ 10 TPS
29
- this.activeEvents = 0;
 
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
- territories: [],
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
- const worldState = [];
194
- for (let [tId, pId] of world.territories.entries()) {
195
- if (!pId.startsWith('CLAIMING_')) {
196
- const owner = world.players.get(pId);
197
- if (owner && owner.online) {
198
- worldState.push({ tId, color: owner.color, name: owner.name, power: owner.power });
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} (Hugging Face Ready)`);
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
  }