hansaka01 commited on
Commit
b7d623e
·
verified ·
1 Parent(s): baedc75

Upload 59 files

Browse files
api.js CHANGED
@@ -2,6 +2,7 @@ const express = require('express');
2
  const crypto = require('crypto');
3
  const path = require('path');
4
  const User = require('./models/User');
 
5
 
6
  const app = express();
7
  app.use(express.json());
@@ -141,6 +142,15 @@ app.get('/me', auth, async (req, res) => {
141
  } catch (err) { res.status(500).json({ error: 'Error' }); }
142
  });
143
 
 
 
 
 
 
 
 
 
 
144
  // ── Carrom API ──
145
  app.post('/carrom/mode', auth, (req, res) => {
146
  try {
@@ -223,11 +233,7 @@ app.post('/carrom/shoot', auth, async (req, res) => {
223
  await leveling.addXP(w.userId, 200);
224
  await u.save();
225
  }
226
- if (global._tgClient && game.chatId) {
227
- try {
228
- await global._tgClient.sendMessage(game.chatId, { message: `🏆 <b>CARROM OVER!</b> ${winners.map(w => w.name).join(' & ')} win <b>$${pot}</b>!` });
229
- } catch {}
230
- }
231
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
232
  }
233
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
@@ -259,11 +265,7 @@ app.post('/carrom/forfeit', auth, async (req, res) => {
259
  await leveling.addXP(w.userId, 100);
260
  await u.save();
261
  }
262
- if (global._tgClient && game.chatId) {
263
- try {
264
- await global._tgClient.sendMessage(game.chatId, { message: `🏳️ <b>${quitter}</b> gave up! ${winners.map(w => w.name).join(' & ')} win <b>$${pot}</b>!` });
265
- } catch {}
266
- }
267
  }
268
  setTimeout(() => sessions.delete(gameId), 5000);
269
  res.json({ ok: true, state: game.getState(userId) });
@@ -310,7 +312,7 @@ app.post('/wcards/play', auth, async (req, res) => {
310
  const pot = game.bet * game.players.length;
311
  const share = Math.floor(pot / winners.length);
312
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,150); await u.save(); }
313
- if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🃏 <b>CARDS OVER!</b> ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
314
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
315
  }
316
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
@@ -343,7 +345,7 @@ app.post('/wcards/forfeit', auth, async (req, res) => {
343
  const pot = game.bet * game.players.length;
344
  const share = winners.length ? Math.floor(pot / winners.length) : 0;
345
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,75); await u.save(); }
346
- if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🏳️ <b>${quitter}</b> gave up! ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
347
  }
348
  setTimeout(() => sessions.delete(req.body.gameId), 4000);
349
  res.json({ ok: true, state: game.getState(userId) });
@@ -398,7 +400,7 @@ app.post('/omi/play', auth, async (req, res) => {
398
  const pot = game.bet * game.players.length;
399
  const share = Math.floor(pot / winners.length);
400
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,150); await u.save(); }
401
- if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🃏 <b>OMI OVER!</b> ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
402
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
403
  }
404
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
@@ -421,7 +423,7 @@ app.post('/omi/forfeit', auth, async (req, res) => {
421
  const pot = game.bet * game.players.length;
422
  const share = winners.length ? Math.floor(pot / winners.length) : 0;
423
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,75); await u.save(); }
424
- if (global._tgClient && game.chatId) try { await global._tgClient.sendMessage(game.chatId, { message: `🏳️ <b>${quitter}</b> gave up! ${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!` }); } catch {}
425
  }
426
  setTimeout(() => sessions.delete(req.body.gameId), 4000);
427
  res.json({ ok: true, state: game.getState(userId) });
 
2
  const crypto = require('crypto');
3
  const path = require('path');
4
  const User = require('./models/User');
5
+ const { editMsg } = require('./utils/editMsg');
6
 
7
  const app = express();
8
  app.use(express.json());
 
142
  } catch (err) { res.status(500).json({ error: 'Error' }); }
143
  });
144
 
145
+ async function editGameChatResult(game, text) {
146
+ if (!global._tgClient || !game?.chatId || !game?.chatMsgId) return;
147
+ try {
148
+ await editMsg(global._tgClient, game.chatId, game.chatMsgId, text, null);
149
+ } catch (e) {
150
+ console.error('[API] editGameChatResult:', e.message);
151
+ }
152
+ }
153
+
154
  // ── Carrom API ──
155
  app.post('/carrom/mode', auth, (req, res) => {
156
  try {
 
233
  await leveling.addXP(w.userId, 200);
234
  await u.save();
235
  }
236
+ await editGameChatResult(game, `🏆 <b>CARROM OVER!</b>\n${winners.map(w => w.name).join(' & ')} win <b>$${pot}</b>!`);
 
 
 
 
237
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
238
  }
239
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
 
265
  await leveling.addXP(w.userId, 100);
266
  await u.save();
267
  }
268
+ await editGameChatResult(game, `🏳️ <b>${quitter}</b> gave up!\n${winners.map(w => w.name).join(' & ')} win <b>$${pot}</b>!`);
 
 
 
 
269
  }
270
  setTimeout(() => sessions.delete(gameId), 5000);
271
  res.json({ ok: true, state: game.getState(userId) });
 
312
  const pot = game.bet * game.players.length;
313
  const share = Math.floor(pot / winners.length);
314
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,150); await u.save(); }
315
+ await editGameChatResult(game, `🃏 <b>CARDS OVER!</b>\n${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!`);
316
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
317
  }
318
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
 
345
  const pot = game.bet * game.players.length;
346
  const share = winners.length ? Math.floor(pot / winners.length) : 0;
347
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,75); await u.save(); }
348
+ await editGameChatResult(game, `🏳️ <b>${quitter}</b> gave up!\n${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!`);
349
  }
350
  setTimeout(() => sessions.delete(req.body.gameId), 4000);
351
  res.json({ ok: true, state: game.getState(userId) });
 
400
  const pot = game.bet * game.players.length;
401
  const share = Math.floor(pot / winners.length);
402
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,150); await u.save(); }
403
+ await editGameChatResult(game, `🃏 <b>OMI OVER!</b>\n${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!`);
404
  setTimeout(() => sessions.delete(req.body.gameId), 5000);
405
  }
406
  res.json({ ok: true, state: game.getState(req.tgUser.id.toString()) });
 
423
  const pot = game.bet * game.players.length;
424
  const share = winners.length ? Math.floor(pot / winners.length) : 0;
425
  for (const w of winners) { let u = await User.findOne({userId:w.userId})||await User.create({userId:w.userId}); u.wallet+=share; await leveling.addXP(w.userId,75); await u.save(); }
426
+ await editGameChatResult(game, `🏳️ <b>${quitter}</b> gave up!\n${winners.map(w=>w.name).join(' & ')} win <b>$${pot}</b>!`);
427
  }
428
  setTimeout(() => sessions.delete(req.body.gameId), 4000);
429
  res.json({ ok: true, state: game.getState(userId) });
handlers/carrom.js CHANGED
@@ -105,15 +105,16 @@ const startCarrom = async (client, event) => {
105
 
106
  const gameId = `crm:${event.chatId}:${crypto.randomBytes(4).toString('hex')}`;
107
  const name = await getName(client, userId);
108
- const game = new CarromGame(bet, 4);
109
- game.addPlayer(userId, name);
110
- game.chatId = event.chatId.toString();
111
- sessions.set(gameId, game);
112
-
113
- await client.sendMessage(event.chatId, {
114
- message: lobbyMsg(game),
115
- buttons: lobbyButtons(game, gameId),
116
- });
 
117
 
118
  game._timer = setTimeout(async () => {
119
  const g = sessions.get(gameId);
 
105
 
106
  const gameId = `crm:${event.chatId}:${crypto.randomBytes(4).toString('hex')}`;
107
  const name = await getName(client, userId);
108
+ const game = new CarromGame(bet, 4);
109
+ game.addPlayer(userId, name);
110
+ game.chatId = event.chatId.toString();
111
+ sessions.set(gameId, game);
112
+
113
+ const sent = await client.sendMessage(event.chatId, {
114
+ message: lobbyMsg(game),
115
+ buttons: lobbyButtons(game, gameId),
116
+ });
117
+ game.chatMsgId = sent.id;
118
 
119
  game._timer = setTimeout(async () => {
120
  const g = sessions.get(gameId);
handlers/omi.js CHANGED
@@ -23,7 +23,7 @@ const startOmi = async (client, event) => {
23
  game.chatId = event.chatId.toString();
24
  sessions.set(gameId, game);
25
 
26
- await client.sendMessage(event.chatId, {
27
  message: `🃏 <b>OMI</b>\nBet: $${bet} each | 2-4 players\n\n👤 ${name} (host)\n⏳ Waiting...`,
28
  buttons: new Api.ReplyInlineMarkup({ rows: [
29
  new Api.KeyboardButtonRow({ buttons: [
@@ -34,6 +34,7 @@ const startOmi = async (client, event) => {
34
  ] })
35
  ] })
36
  });
 
37
 
38
  game._timer = setTimeout(async () => {
39
  const g = sessions.get(gameId);
 
23
  game.chatId = event.chatId.toString();
24
  sessions.set(gameId, game);
25
 
26
+ const sent = await client.sendMessage(event.chatId, {
27
  message: `🃏 <b>OMI</b>\nBet: $${bet} each | 2-4 players\n\n👤 ${name} (host)\n⏳ Waiting...`,
28
  buttons: new Api.ReplyInlineMarkup({ rows: [
29
  new Api.KeyboardButtonRow({ buttons: [
 
34
  ] })
35
  ] })
36
  });
37
+ game.chatMsgId = sent.id;
38
 
39
  game._timer = setTimeout(async () => {
40
  const g = sessions.get(gameId);
handlers/webCards.js CHANGED
@@ -23,7 +23,7 @@ const startWebCards = async (client, event) => {
23
  game.chatId = event.chatId.toString();
24
  sessions.set(gameId, game);
25
 
26
- await client.sendMessage(event.chatId, {
27
  message: `🃏 <b>CARD GAME</b>\nBet: $${bet} each\n\n👤 ${name} (host)\n⏳ Waiting for players (2-6)...\n\n<i>Open the Mini App to see your cards!</i>`,
28
  buttons: new Api.ReplyInlineMarkup({ rows: [
29
  new Api.KeyboardButtonRow({ buttons: [
@@ -34,6 +34,7 @@ const startWebCards = async (client, event) => {
34
  ] })
35
  ] })
36
  });
 
37
 
38
  game._timer = setTimeout(async () => {
39
  const g = sessions.get(gameId);
 
23
  game.chatId = event.chatId.toString();
24
  sessions.set(gameId, game);
25
 
26
+ const sent = await client.sendMessage(event.chatId, {
27
  message: `🃏 <b>CARD GAME</b>\nBet: $${bet} each\n\n👤 ${name} (host)\n⏳ Waiting for players (2-6)...\n\n<i>Open the Mini App to see your cards!</i>`,
28
  buttons: new Api.ReplyInlineMarkup({ rows: [
29
  new Api.KeyboardButtonRow({ buttons: [
 
34
  ] })
35
  ] })
36
  });
37
+ game.chatMsgId = sent.id;
38
 
39
  game._timer = setTimeout(async () => {
40
  const g = sessions.get(gameId);
webapp/cards.html CHANGED
@@ -27,14 +27,14 @@ body{background:#0a1628;color:#fff;font-family:-apple-system,sans-serif;min-heig
27
  .tcard{width:65px;height:95px;border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:700;position:relative;transition:transform .3s,box-shadow .3s;}
28
  .tcard.facedown{background:linear-gradient(135deg,#1e40af,#1e3a8a);border:2px solid #3b82f6;box-shadow:0 4px 12px rgba(59,130,246,.2);}
29
  .tcard.facedown::after{content:'🂠';font-size:32px;}
30
- .tcard.faceup{background:#fff;color:#1a1a1a;border:2px solid #ddd;box-shadow:0 4px 15px rgba(0,0,0,.3);animation:flipIn .4s ease;}
31
  .tcard.faceup .tsuit{font-size:24px;}
32
  .tcard.faceup .tlabel{font-size:18px;font-weight:800;}
33
  .tcard.faceup.red{color:#dc2626;}
34
  .tcard.faceup.winner{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,.4);}
35
  .tcard-name{font-size:9px;color:#888;text-align:center;margin-top:2px;max-width:65px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
36
  /* CardMeister card wrappers (table) */
37
- .tcard-wrap{display:inline-block;border-radius:8px;box-shadow:0 4px 15px rgba(0,0,0,.3);transition:transform .3s,box-shadow .3s;animation:flipIn .4s ease;line-height:0;overflow:hidden;}
38
  .tcard-wrap svg,.tcard-wrap playing-card,.tcard-wrap playing-card img{display:block;border-radius:8px;}
39
  .tcard-wrap playing-card img{width:100%;height:100%;}
40
  .tcard-wrap.winner{box-shadow:0 0 20px rgba(245,158,11,.5);}
@@ -71,6 +71,11 @@ body{background:#0a1628;color:#fff;font-family:-apple-system,sans-serif;min-heig
71
  .lplayers{margin:16px 0;font-size:14px;line-height:1.8;}
72
  .lstbtn{padding:12px 36px;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer;color:#fff;background:linear-gradient(135deg,#22c55e,#16a34a);margin:6px;}
73
  .lstbtn:disabled{opacity:.3;cursor:not-allowed;}
 
 
 
 
 
74
  </style>
75
  </head>
76
  <body>
@@ -84,6 +89,7 @@ body{background:#0a1628;color:#fff;font-family:-apple-system,sans-serif;min-heig
84
  <div id="actions"></div>
85
  <div id="hand-area"><div id="hand-label">Your Cards</div><div id="hand"></div></div>
86
  </div>
 
87
  <script>
88
  const tg=window.Telegram?.WebApp;if(tg){tg.expand();tg.ready();}
89
  if(!tg?.initData||!tg.initDataUnsafe?.user?.id){document.getElementById('blocked').style.display='flex';throw new Error('no auth');}
@@ -93,7 +99,9 @@ const sp=new URLSearchParams(location.search).get('tgWebAppStartParam')||tg.init
93
  const GID=sp.replace('wcards_','').replace(/_0_/g,':');
94
  async function rpc(m,p,b){try{const o={method:m,headers:{'X-Init-Data':tg.initData,'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);const r=await fetch(API+p,o);return JSON.parse(await r.text());}catch{return{ok:false,error:'Network error'};}}
95
 
96
- let S=null,selectedCard=null,pollId=null;
 
 
97
 
98
  // Give Up: confirm popup then forfeit
99
  GiveUp.bind({
@@ -225,10 +233,20 @@ async function hostStart(){
225
  else{document.getElementById('lwait').textContent=r.error;document.getElementById('lstbtn').disabled=false;}
226
  }
227
 
 
 
 
 
 
 
 
 
228
  function render(){
229
  if(!S)return;
230
  if(S.status==='lobby')renderLobby();
231
  else renderGame();
 
 
232
  }
233
 
234
  let pollFails=0;
 
27
  .tcard{width:65px;height:95px;border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:700;position:relative;transition:transform .3s,box-shadow .3s;}
28
  .tcard.facedown{background:linear-gradient(135deg,#1e40af,#1e3a8a);border:2px solid #3b82f6;box-shadow:0 4px 12px rgba(59,130,246,.2);}
29
  .tcard.facedown::after{content:'🂠';font-size:32px;}
30
+ .tcard.faceup{background:#fff;color:#1a1a1a;border:2px solid #ddd;box-shadow:0 4px 15px rgba(0,0,0,.3);}
31
  .tcard.faceup .tsuit{font-size:24px;}
32
  .tcard.faceup .tlabel{font-size:18px;font-weight:800;}
33
  .tcard.faceup.red{color:#dc2626;}
34
  .tcard.faceup.winner{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,.4);}
35
  .tcard-name{font-size:9px;color:#888;text-align:center;margin-top:2px;max-width:65px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
36
  /* CardMeister card wrappers (table) */
37
+ .tcard-wrap{display:inline-block;border-radius:8px;box-shadow:0 4px 15px rgba(0,0,0,.3);transition:transform .3s,box-shadow .3s;line-height:0;overflow:hidden;}
38
  .tcard-wrap svg,.tcard-wrap playing-card,.tcard-wrap playing-card img{display:block;border-radius:8px;}
39
  .tcard-wrap playing-card img{width:100%;height:100%;}
40
  .tcard-wrap.winner{box-shadow:0 0 20px rgba(245,158,11,.5);}
 
71
  .lplayers{margin:16px 0;font-size:14px;line-height:1.8;}
72
  .lstbtn{padding:12px 36px;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer;color:#fff;background:linear-gradient(135deg,#22c55e,#16a34a);margin:6px;}
73
  .lstbtn:disabled{opacity:.3;cursor:not-allowed;}
74
+ #endModal{position:fixed;inset:0;background:rgba(0,0,0,.72);display:none;align-items:center;justify-content:center;padding:20px;z-index:50;}
75
+ #endModal.show{display:flex;}
76
+ #endCard{background:#111827;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:22px;max-width:360px;width:100%;text-align:center;}
77
+ #endCard h2{margin-bottom:10px;color:#f59e0b;}
78
+ #endBody{font-size:14px;line-height:1.5;margin-bottom:12px;}
79
  </style>
80
  </head>
81
  <body>
 
89
  <div id="actions"></div>
90
  <div id="hand-area"><div id="hand-label">Your Cards</div><div id="hand"></div></div>
91
  </div>
92
+ <div id="endModal"><div id="endCard"><h2 id="endTitle">Game Over</h2><div id="endBody"></div><button class="abtn green" id="endOkBtn">OK</button></div></div>
93
  <script>
94
  const tg=window.Telegram?.WebApp;if(tg){tg.expand();tg.ready();}
95
  if(!tg?.initData||!tg.initDataUnsafe?.user?.id){document.getElementById('blocked').style.display='flex';throw new Error('no auth');}
 
99
  const GID=sp.replace('wcards_','').replace(/_0_/g,':');
100
  async function rpc(m,p,b){try{const o={method:m,headers:{'X-Init-Data':tg.initData,'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);const r=await fetch(API+p,o);return JSON.parse(await r.text());}catch{return{ok:false,error:'Network error'};}}
101
 
102
+ let S=null,selectedCard=null,pollId=null,endShown=false;
103
+ function closeMiniApp(){try{tg.close();}catch(e){try{window.close();}catch(_){}}}
104
+ document.getElementById('endOkBtn').onclick=closeMiniApp;
105
 
106
  // Give Up: confirm popup then forfeit
107
  GiveUp.bind({
 
233
  else{document.getElementById('lwait').textContent=r.error;document.getElementById('lstbtn').disabled=false;}
234
  }
235
 
236
+ function showEndModal(){
237
+ if(!S||S.status!=='ended')return;
238
+ const gu=S.forfeitedBy?`<b>${S.forfeitedBy}</b> gave up.<br><br>`:'';
239
+ document.getElementById('endTitle').textContent='🏆 Winners';
240
+ document.getElementById('endBody').innerHTML=`${gu}<b>${(S.winner||[]).join(' & ')}</b> win!`;
241
+ document.getElementById('endModal').classList.add('show');
242
+ }
243
+
244
  function render(){
245
  if(!S)return;
246
  if(S.status==='lobby')renderLobby();
247
  else renderGame();
248
+ if(S.status==='ended'&&!endShown){endShown=true;showEndModal();}
249
+ if(S.status!=='ended')endShown=false;
250
  }
251
 
252
  let pollFails=0;
webapp/carrom.html CHANGED
@@ -253,6 +253,7 @@
253
  'use strict';
254
  const tg = window.Telegram?.WebApp;
255
  if (tg) { try { tg.expand(); tg.ready(); } catch {} }
 
256
 
257
  const startParam = new URLSearchParams(location.search).get('tgWebAppStartParam') || tg?.initDataUnsafe?.start_param || '';
258
  const GAME_ID = startParam.replace('carrom_', '').replace(/_0_/g, ':');
@@ -674,7 +675,7 @@
674
  $('rulesBtnTop').onclick = () => rulesModal.classList.add('show');
675
  $('closeRules').onclick = () => rulesModal.classList.remove('show');
676
  $('closeBoard').onclick = () => boardModal.classList.remove('show');
677
- $('closeMatch').onclick = () => matchModal.classList.remove('show');
678
 
679
  document.querySelectorAll('#modeModal button[data-mode]').forEach(btn => {
680
  btn.onclick = async () => {
 
253
  'use strict';
254
  const tg = window.Telegram?.WebApp;
255
  if (tg) { try { tg.expand(); tg.ready(); } catch {} }
256
+ function closeMiniApp(){ try{ tg?.close(); } catch(e){ try{ window.close(); } catch(_){} } }
257
 
258
  const startParam = new URLSearchParams(location.search).get('tgWebAppStartParam') || tg?.initDataUnsafe?.start_param || '';
259
  const GAME_ID = startParam.replace('carrom_', '').replace(/_0_/g, ':');
 
675
  $('rulesBtnTop').onclick = () => rulesModal.classList.add('show');
676
  $('closeRules').onclick = () => rulesModal.classList.remove('show');
677
  $('closeBoard').onclick = () => boardModal.classList.remove('show');
678
+ $('closeMatch').onclick = () => closeMiniApp();
679
 
680
  document.querySelectorAll('#modeModal button[data-mode]').forEach(btn => {
681
  btn.onclick = async () => {
webapp/omi.html CHANGED
@@ -45,6 +45,11 @@ body{background:#0d1b2a;color:#fff;font-family:-apple-system,sans-serif;min-heig
45
  #lobby{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:30px;}
46
  .lbtn{padding:12px 30px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;color:#fff;background:linear-gradient(135deg,#22c55e,#16a34a);margin:6px;}
47
  .lbtn:disabled{opacity:.3;}
 
 
 
 
 
48
  </style>
49
  </head>
50
  <body>
@@ -59,6 +64,7 @@ body{background:#0d1b2a;color:#fff;font-family:-apple-system,sans-serif;min-heig
59
  <div id="actions"></div>
60
  <div id="hand-area"><div id="hand-label">Your Cards</div><div id="hand"></div></div>
61
  </div>
 
62
  <script>
63
  const tg=window.Telegram?.WebApp;if(tg){tg.expand();tg.ready();}
64
  if(!tg?.initData||!tg.initDataUnsafe?.user?.id){document.getElementById('blocked').style.display='flex';throw new Error('no auth');}
@@ -68,7 +74,9 @@ const sp=new URLSearchParams(location.search).get('tgWebAppStartParam')||tg.init
68
  const GID=sp.replace('omi_','').replace(/_0_/g,':');
69
  async function rpc(m,p,b){try{const o={method:m,headers:{'X-Init-Data':tg.initData,'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);const r=await fetch(API+p,o);return JSON.parse(await r.text());}catch{return{ok:false,error:'Network'};}}
70
 
71
- let S=null,sel=null,pollId=null;
 
 
72
 
73
  // Give Up: confirm popup then forfeit
74
  GiveUp.bind({
@@ -203,11 +211,21 @@ async function hostStart(){
203
  else{document.getElementById('lwait').textContent=r.error;document.getElementById('lbtn').disabled=false;}
204
  }
205
 
 
 
 
 
 
 
 
 
206
  function render(){
207
  if(!S)return;
208
  if(S.status==='lobby')renderLobby();
209
  else if(S.status==='chooseTrump')renderTrumpChoice();
210
  else renderGame();
 
 
211
  }
212
 
213
  let pollFails=0;
 
45
  #lobby{display:none;flex:1;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:30px;}
46
  .lbtn{padding:12px 30px;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;color:#fff;background:linear-gradient(135deg,#22c55e,#16a34a);margin:6px;}
47
  .lbtn:disabled{opacity:.3;}
48
+ #endModal{position:fixed;inset:0;background:rgba(0,0,0,.72);display:none;align-items:center;justify-content:center;padding:20px;z-index:50;}
49
+ #endModal.show{display:flex;}
50
+ #endCard{background:#111827;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:22px;max-width:360px;width:100%;text-align:center;}
51
+ #endCard h2{margin-bottom:10px;color:#f59e0b;}
52
+ #endBody{font-size:14px;line-height:1.5;margin-bottom:12px;}
53
  </style>
54
  </head>
55
  <body>
 
64
  <div id="actions"></div>
65
  <div id="hand-area"><div id="hand-label">Your Cards</div><div id="hand"></div></div>
66
  </div>
67
+ <div id="endModal"><div id="endCard"><h2 id="endTitle">Game Over</h2><div id="endBody"></div><button class="abtn g" id="endOkBtn">OK</button></div></div>
68
  <script>
69
  const tg=window.Telegram?.WebApp;if(tg){tg.expand();tg.ready();}
70
  if(!tg?.initData||!tg.initDataUnsafe?.user?.id){document.getElementById('blocked').style.display='flex';throw new Error('no auth');}
 
74
  const GID=sp.replace('omi_','').replace(/_0_/g,':');
75
  async function rpc(m,p,b){try{const o={method:m,headers:{'X-Init-Data':tg.initData,'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);const r=await fetch(API+p,o);return JSON.parse(await r.text());}catch{return{ok:false,error:'Network'};}}
76
 
77
+ let S=null,sel=null,pollId=null,endShown=false;
78
+ function closeMiniApp(){try{tg.close();}catch(e){try{window.close();}catch(_){}}}
79
+ document.getElementById('endOkBtn').onclick=closeMiniApp;
80
 
81
  // Give Up: confirm popup then forfeit
82
  GiveUp.bind({
 
211
  else{document.getElementById('lwait').textContent=r.error;document.getElementById('lbtn').disabled=false;}
212
  }
213
 
214
+ function showEndModal(){
215
+ if(!S||S.status!=='ended')return;
216
+ const gu=S.forfeitedBy?`<b>${S.forfeitedBy}</b> gave up.<br><br>`:'';
217
+ document.getElementById('endTitle').textContent='🏆 Winners';
218
+ document.getElementById('endBody').innerHTML=`${gu}<b>${(S.winner||[]).join(' & ')}</b> win!`;
219
+ document.getElementById('endModal').classList.add('show');
220
+ }
221
+
222
  function render(){
223
  if(!S)return;
224
  if(S.status==='lobby')renderLobby();
225
  else if(S.status==='chooseTrump')renderTrumpChoice();
226
  else renderGame();
227
+ if(S.status==='ended'&&!endShown){endShown=true;showEndModal();}
228
+ if(S.status!=='ended')endShown=false;
229
  }
230
 
231
  let pollFails=0;