hansaka01 commited on
Commit
00eae7f
·
verified ·
1 Parent(s): 05e5410

Upload 38 files

Browse files
api.js CHANGED
@@ -202,6 +202,81 @@ app.post('/claim', auth, async (req, res) => {
202
  }
203
  });
204
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  // ── Compat: GET /daily/status ──────────────────────────────────────────────
206
  app.get('/daily/status', auth, async (req, res) => {
207
  req.query.type = 'daily';
 
202
  }
203
  });
204
 
205
+ // ── GET /leaderboard?type=bank|wallet|xp|kills|robs&period=daily|weekly|monthly|all ──
206
+ app.get('/leaderboard', auth, async (req, res) => {
207
+ try {
208
+ const { type = 'wallet', period = 'all' } = req.query;
209
+
210
+ // Build sort field
211
+ const sortFields = {
212
+ bank: { bank: -1 },
213
+ wallet: { wallet: -1 },
214
+ xp: { xp: -1 },
215
+ kills: { kills: -1 },
216
+ robs: { robs: -1 },
217
+ };
218
+ const sort = sortFields[type] || { wallet: -1 };
219
+
220
+ // Period filter (for daily/weekly/monthly we filter by lastDaily or recent activity)
221
+ let dateFilter = {};
222
+ const now = new Date();
223
+ if (period === 'daily') {
224
+ const dayStart = new Date(now.toISOString().split('T')[0]);
225
+ dateFilter = { lastDaily: { $gte: dayStart } };
226
+ } else if (period === 'weekly') {
227
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
228
+ dateFilter = { lastDaily: { $gte: weekAgo } };
229
+ } else if (period === 'monthly') {
230
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
231
+ dateFilter = { lastDaily: { $gte: monthAgo } };
232
+ }
233
+ // 'all' = no date filter
234
+
235
+ const users = await User.find(dateFilter)
236
+ .sort(sort)
237
+ .limit(50)
238
+ .select('userId username wallet bank xp level kills robs totalEarned')
239
+ .lean();
240
+
241
+ // Resolve usernames via TG client for users missing username
242
+ if (global._tgClient) {
243
+ for (const u of users) {
244
+ if (!u.username) {
245
+ try {
246
+ const ent = await global._tgClient.getEntity(u.userId);
247
+ u.username = ent.username || ent.firstName || `User${u.userId.slice(-4)}`;
248
+ } catch { u.username = `User${u.userId.slice(-4)}`; }
249
+ }
250
+ }
251
+ } else {
252
+ for (const u of users) {
253
+ if (!u.username) u.username = `User${u.userId.slice(-4)}`;
254
+ }
255
+ }
256
+
257
+ res.json({ ok: true, leaderboard: users, type, period });
258
+ } catch (err) {
259
+ console.error('[API] /leaderboard error:', err);
260
+ res.status(500).json({ error: 'Server error' });
261
+ }
262
+ });
263
+
264
+ // ── GET /me — get current user stats ──
265
+ app.get('/me', auth, async (req, res) => {
266
+ try {
267
+ const userId = req.tgUser.id.toString();
268
+ let user = await User.findOne({ userId }).lean();
269
+ if (!user) {
270
+ user = await User.create({ userId, username: req.tgUser.username });
271
+ user = user.toObject();
272
+ }
273
+ res.json({ ok: true, user });
274
+ } catch (err) {
275
+ console.error('[API] /me error:', err);
276
+ res.status(500).json({ error: 'Server error' });
277
+ }
278
+ });
279
+
280
  // ── Compat: GET /daily/status ──────────────────────────────────────────────
281
  app.get('/daily/status', auth, async (req, res) => {
282
  req.query.type = 'daily';
bot.js CHANGED
@@ -7,12 +7,9 @@ const connectDB = require('./db/mongo');
7
  const User = require('./models/User');
8
  const { startApi } = require('./api');
9
 
10
- // Env Check
11
  const requiredEnv = ['API_ID', 'API_HASH', 'BOT_TOKEN', 'MONGO_URI'];
12
  const missing = requiredEnv.filter(k => !process.env[k]);
13
- if (missing.length > 0) {
14
- console.warn(`⚠️ WARNING: Missing environment variables: ${missing.join(", ")}`);
15
- }
16
 
17
  // Handlers
18
  const economy = require('./handlers/economy');
@@ -25,6 +22,7 @@ const boardGames = require('./handlers/boardGames');
25
  const boardHandler = require('./handlers/boardHandler');
26
  const blackjack = require('./handlers/blackjack');
27
  const cardGame = require('./handlers/cardGameHandler');
 
28
 
29
  const apiId = parseInt(process.env.API_ID || 0);
30
  const apiHash = process.env.API_HASH || "";
@@ -33,12 +31,10 @@ const stringSession = new StringSession("");
33
 
34
  (async () => {
35
  await connectDB();
36
- startApi(); // Start REST API for Mini App
37
  const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: 5 });
38
  await client.start({ botAuthToken: botToken });
39
  console.log("Bot is running...");
40
-
41
- // Expose client globally so api.js can send messages via MTProto
42
  global._tgClient = client;
43
 
44
  const isAlive = async (event) => {
@@ -46,10 +42,7 @@ const stringSession = new StringSession("");
46
  const user = await User.findOne({ userId });
47
  if (user && user.isDead) {
48
  if (Date.now() - (user.lastDeath || 0) > 4 * 60 * 60 * 1000) {
49
- user.isDead = false;
50
- user.health = 100;
51
- await user.save();
52
- return true;
53
  }
54
  await event.message.respond({ message: "💀 You are dead! Use /revive or wait 4 hours." });
55
  return false;
@@ -57,25 +50,46 @@ const stringSession = new StringSession("");
57
  return true;
58
  };
59
 
60
- // ─── Handle normal text messages ──────────────────────────────────────────
61
  client.addEventHandler(async (event) => {
62
  const text = event.message.message || '';
63
  if (!text) return;
64
 
65
  if (text === "/start") {
66
- await event.message.respond({ message: "🎮 **Alexagame Bot**\n\n💰 /daily - Claim $3000 daily reward (watch ad in mini app)\n💰 /wallet - Check level, wallet, bank & health\n⚔️ /kill (reply) - Kill user for cash & XP\n⚔️ /rob (reply) - Rob user (risky)\n⚔️ /revive - Revive for $1000\n\n🕹️ /cards <bet> - Multiplayer card flip game\n🕹️ /hack <bet> <len> - Multiplayer PIN hack\n🕹️ /xox <bet> - Tic-Tac-Toe\n🕹️ /c4 <bet> - Connect 4\n🕹️ /dice <bet> - Dice duel\n\n🃏 /bj <bet> - Blackjack\n🃏 /slots - Slot machine\n🃏 /mines - Avoid mines\n🃏 /coinflip - Coin flip\n🃏 /roulette - Russian roulette\n🃏 /hl - Higher or Lower\n\nUse /daily or /missions to open the reward mini app!" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
 
69
  if (text === "/wallet") {
70
  const userId = event.message.senderId.toString();
71
  let user = await User.findOne({ userId });
72
- if (!user) user = await User.create({ userId, username: event.message.sender ? event.message.sender.username : undefined });
73
- await event.message.respond({ message: `👤 **Level:** ${user.level} | **XP:** ${user.xp}\n💰 **Wallet:** $${user.wallet}\n💳 **Bank:** $${user.bank}\n❤️ **Health:** ${user.health}%` });
74
  }
75
 
76
  if (text === "/daily") await economy.daily(client, event);
77
  if (text === "/missions") await economy.missions(client, event);
78
-
79
  if (text.startsWith("/dp")) await economy.deposit(client, event);
80
  if (text.startsWith("/wd")) await economy.withdraw(client, event);
81
 
@@ -83,19 +97,29 @@ const stringSession = new StringSession("");
83
  if (text.startsWith("/rob")) { if (await isAlive(event)) await combat.rob(client, event); }
84
  if (text === "/revive") await combat.revive(client, event);
85
 
 
 
 
 
 
 
 
86
  if (text.startsWith("/cards")) { if (await isAlive(event)) await cardGame.initCardGame(client, event); }
87
  if (text.startsWith("/hack")) { if (await isAlive(event)) await multiHack.initHack(client, event); }
88
  if (text === "/join") {
89
  await multiHack.joinHack(client, event);
90
  await cardGame.joinCardGame(client, event);
 
91
  }
92
  if (text.startsWith("/guess")) await multiHack.processGuess(client, event);
93
  if (text.startsWith("/flip")) await cardGame.processFlip(client, event);
94
 
 
95
  if (text.startsWith("/xox")) { if (await isAlive(event)) await boardGames.startXOX(client, event); }
96
  if (text.startsWith("/c4")) { if (await isAlive(event)) await boardGames.startC4(client, event); }
97
  if (text.startsWith("/dice")) { if (await isAlive(event)) await games.startDice(client, event); }
98
 
 
99
  if (text.startsWith("/bj")) { if (await isAlive(event)) await blackjack.startBJ(client, event); }
100
  if (text === "/slots") await inlineGames.startSlots(client, event);
101
  if (text === "/mines") await inlineGames.startMines(client, event);
@@ -105,18 +129,16 @@ const stringSession = new StringSession("");
105
 
106
  }, new NewMessage({}));
107
 
108
- // ─── Handle raw updates: Callback queries ────────────────────────────────
109
  client.addEventHandler(async (update) => {
110
  if (update instanceof Api.UpdateBotCallbackQuery) {
111
- try { await games.handleCallback(client, update); } catch (e) { console.error('[CB] games.handleCallback:', e.message); }
112
- try { await games.handleDiceCallback(client, update); } catch (e) { console.error('[CB] games.handleDiceCallback:', e.message); }
113
- try { await inlineHandler.handleInlineCallback(client, update); } catch (e) { console.error('[CB] inlineHandler:', e.message); }
114
- try { await boardHandler.handleBoardCallback(client, update); } catch (e) { console.error('[CB] boardHandler:', e.message); }
115
- try { await blackjack.handleBJCallback(client, update); } catch (e) { console.error('[CB] blackjack:', e.message); }
116
-
117
- try {
118
- await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, cacheTime: 1 }));
119
- } catch (e) { }
120
  }
121
  });
122
  })();
 
7
  const User = require('./models/User');
8
  const { startApi } = require('./api');
9
 
 
10
  const requiredEnv = ['API_ID', 'API_HASH', 'BOT_TOKEN', 'MONGO_URI'];
11
  const missing = requiredEnv.filter(k => !process.env[k]);
12
+ if (missing.length > 0) console.warn(`⚠️ Missing env: ${missing.join(", ")}`);
 
 
13
 
14
  // Handlers
15
  const economy = require('./handlers/economy');
 
22
  const boardHandler = require('./handlers/boardHandler');
23
  const blackjack = require('./handlers/blackjack');
24
  const cardGame = require('./handlers/cardGameHandler');
25
+ const strategy = require('./handlers/strategyGames');
26
 
27
  const apiId = parseInt(process.env.API_ID || 0);
28
  const apiHash = process.env.API_HASH || "";
 
31
 
32
  (async () => {
33
  await connectDB();
34
+ startApi();
35
  const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: 5 });
36
  await client.start({ botAuthToken: botToken });
37
  console.log("Bot is running...");
 
 
38
  global._tgClient = client;
39
 
40
  const isAlive = async (event) => {
 
42
  const user = await User.findOne({ userId });
43
  if (user && user.isDead) {
44
  if (Date.now() - (user.lastDeath || 0) > 4 * 60 * 60 * 1000) {
45
+ user.isDead = false; user.health = 100; await user.save(); return true;
 
 
 
46
  }
47
  await event.message.respond({ message: "💀 You are dead! Use /revive or wait 4 hours." });
48
  return false;
 
50
  return true;
51
  };
52
 
 
53
  client.addEventHandler(async (event) => {
54
  const text = event.message.message || '';
55
  if (!text) return;
56
 
57
  if (text === "/start") {
58
+ await event.message.respond({ message:
59
+ "🎮 **Alexagame Bot**\n\n" +
60
+ "💰 /daily - Claim $3000 daily reward\n" +
61
+ "💰 /wallet - Balance & stats\n" +
62
+ "⚔️ /kill (reply) - Kill for cash & XP\n" +
63
+ "⚔️ /rob (reply) - Rob user (risky)\n" +
64
+ "⚔️ /revive - Revive for $1000\n\n" +
65
+ "♔ /chess <bet> - Chess (2P)\n" +
66
+ "⛀ /checkers <bet> - Checkers/Draughts (2P)\n" +
67
+ "🕹️ /xox <bet> - Tic-Tac-Toe\n" +
68
+ "🕹️ /c4 <bet> - Connect 4\n" +
69
+ "🕹️ /hack <bet> <len> - PIN hack\n" +
70
+ "🕹️ /cards <bet> - Card flip game\n" +
71
+ "🕹️ /dice <bet> - Dice duel\n\n" +
72
+ "🃏 /bj <bet> - Blackjack\n" +
73
+ "🃏 /slots - Slot machine\n" +
74
+ "🃏 /mines - Avoid mines\n" +
75
+ "🃏 /coinflip - Coin flip\n" +
76
+ "🃏 /roulette - Russian roulette\n" +
77
+ "🃏 /hl - Higher or Lower\n\n" +
78
+ "📋 Moves: `/m e2 e4` (chess/checkers)\n" +
79
+ "🏳️ /surrender - Forfeit game\n" +
80
+ "Use /daily or /missions to open the reward mini app!"
81
+ });
82
  }
83
 
84
  if (text === "/wallet") {
85
  const userId = event.message.senderId.toString();
86
  let user = await User.findOne({ userId });
87
+ if (!user) user = await User.create({ userId, username: event.message.sender?.username });
88
+ await event.message.respond({ message: `👤 **Lv.${user.level}** | XP: ${user.xp}\n💰 Wallet: $${user.wallet}\n💳 Bank: $${user.bank}\n❤️ HP: ${user.health}%\n💀 Kills: ${user.kills||0} | 💸 Robs: ${user.robs||0}` });
89
  }
90
 
91
  if (text === "/daily") await economy.daily(client, event);
92
  if (text === "/missions") await economy.missions(client, event);
 
93
  if (text.startsWith("/dp")) await economy.deposit(client, event);
94
  if (text.startsWith("/wd")) await economy.withdraw(client, event);
95
 
 
97
  if (text.startsWith("/rob")) { if (await isAlive(event)) await combat.rob(client, event); }
98
  if (text === "/revive") await combat.revive(client, event);
99
 
100
+ // Strategy games (text-based moves)
101
+ if (text.startsWith("/chess")) { if (await isAlive(event)) await strategy.startChess(client, event); }
102
+ if (text.startsWith("/checkers")) { if (await isAlive(event)) await strategy.startCheckers(client, event); }
103
+ if (text.startsWith("/m ")) await strategy.processMove(client, event);
104
+ if (text === "/surrender") await strategy.surrender(client, event);
105
+
106
+ // Multiplayer lobby games
107
  if (text.startsWith("/cards")) { if (await isAlive(event)) await cardGame.initCardGame(client, event); }
108
  if (text.startsWith("/hack")) { if (await isAlive(event)) await multiHack.initHack(client, event); }
109
  if (text === "/join") {
110
  await multiHack.joinHack(client, event);
111
  await cardGame.joinCardGame(client, event);
112
+ await strategy.joinStrategy(client, event);
113
  }
114
  if (text.startsWith("/guess")) await multiHack.processGuess(client, event);
115
  if (text.startsWith("/flip")) await cardGame.processFlip(client, event);
116
 
117
+ // Board games (inline buttons)
118
  if (text.startsWith("/xox")) { if (await isAlive(event)) await boardGames.startXOX(client, event); }
119
  if (text.startsWith("/c4")) { if (await isAlive(event)) await boardGames.startC4(client, event); }
120
  if (text.startsWith("/dice")) { if (await isAlive(event)) await games.startDice(client, event); }
121
 
122
+ // Casino
123
  if (text.startsWith("/bj")) { if (await isAlive(event)) await blackjack.startBJ(client, event); }
124
  if (text === "/slots") await inlineGames.startSlots(client, event);
125
  if (text === "/mines") await inlineGames.startMines(client, event);
 
129
 
130
  }, new NewMessage({}));
131
 
132
+ // Callback queries
133
  client.addEventHandler(async (update) => {
134
  if (update instanceof Api.UpdateBotCallbackQuery) {
135
+ try { await games.handleCallback(client, update); } catch (e) { console.error('[CB] games:', e.message); }
136
+ try { await games.handleDiceCallback(client, update); } catch (e) { console.error('[CB] dice:', e.message); }
137
+ try { await inlineHandler.handleInlineCallback(client, update); } catch (e) { console.error('[CB] inline:', e.message); }
138
+ try { await boardHandler.handleBoardCallback(client, update); } catch (e) { console.error('[CB] board:', e.message); }
139
+ try { await blackjack.handleBJCallback(client, update); } catch (e) { console.error('[CB] bj:', e.message); }
140
+
141
+ try { await client.invoke(new Api.messages.SetBotCallbackAnswer({ queryId: update.queryId, cacheTime: 1 })); } catch (e) {}
 
 
142
  }
143
  });
144
  })();
games/Checkers.js ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Checkers (Draughts) — Flying Kings variant
3
+ * 8x8 board, pieces only on dark squares.
4
+ * White: ⛀ (man) ⛁ (king) | Black: ⛂ (man) ⛃ (king)
5
+ * White moves up (row 7→0), Black moves down (row 0→7)
6
+ * Simplified: text-command based moves like /m a3 b4
7
+ */
8
+ class Checkers {
9
+ constructor(player1, bet) {
10
+ this.players = [player1]; // [white, black]
11
+ this.bet = bet;
12
+ this.board = this.initBoard();
13
+ this.turn = 0; // 0=white, 1=black
14
+ this.status = 'lobby';
15
+ }
16
+
17
+ initBoard() {
18
+ const b = Array(8).fill(null).map(() => Array(8).fill(null));
19
+ // Black pieces (top, rows 0-2)
20
+ for (let r = 0; r < 3; r++)
21
+ for (let c = 0; c < 8; c++)
22
+ if ((r + c) % 2 === 1) b[r][c] = '⛂';
23
+ // White pieces (bottom, rows 5-7)
24
+ for (let r = 5; r < 8; r++)
25
+ for (let c = 0; c < 8; c++)
26
+ if ((r + c) % 2 === 1) b[r][c] = '⛀';
27
+ return b;
28
+ }
29
+
30
+ addPlayer(userId) {
31
+ if (this.players.length < 2 && !this.players.includes(userId)) {
32
+ this.players.push(userId);
33
+ this.status = 'playing';
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ // Parse "a3" -> {r:5, c:0}
40
+ parsePos(s) {
41
+ if (!s || s.length < 2) return null;
42
+ const c = s.charCodeAt(0) - 97; // a=0
43
+ const r = 8 - parseInt(s[1]); // 1=row7, 8=row0
44
+ if (c < 0 || c > 7 || r < 0 || r > 7) return null;
45
+ return { r, c };
46
+ }
47
+
48
+ posStr(r, c) {
49
+ return String.fromCharCode(97 + c) + (8 - r);
50
+ }
51
+
52
+ isOwn(r, c) {
53
+ const piece = this.board[r][c];
54
+ if (!piece) return false;
55
+ if (this.turn === 0) return piece === '⛀' || piece === '⛁'; // white
56
+ return piece === '⛂' || piece === '⛃'; // black
57
+ }
58
+
59
+ isEnemy(r, c) {
60
+ const piece = this.board[r][c];
61
+ if (!piece) return false;
62
+ if (this.turn === 0) return piece === '⛂' || piece === '⛃';
63
+ return piece === '⛀' || piece === '⛁';
64
+ }
65
+
66
+ isKing(r, c) {
67
+ return this.board[r][c] === '⛁' || this.board[r][c] === '⛃';
68
+ }
69
+
70
+ makeMove(userId, fromStr, toStr) {
71
+ if (this.status !== 'playing') return { error: "Game not active" };
72
+ if (this.players[this.turn] !== userId) return { error: "Not your turn!" };
73
+
74
+ const from = this.parsePos(fromStr);
75
+ const to = this.parsePos(toStr);
76
+ if (!from || !to) return { error: "Invalid position. Use like: a3 b4" };
77
+ if (!this.isOwn(from.r, from.c)) return { error: "No piece of yours there!" };
78
+ if (this.board[to.r][to.c] !== null) return { error: "Target square occupied!" };
79
+ if ((to.r + to.c) % 2 === 0) return { error: "Can only move to dark squares!" };
80
+
81
+ const dr = to.r - from.r;
82
+ const dc = to.c - from.c;
83
+ const king = this.isKing(from.r, from.c);
84
+
85
+ // Normal move (1 diagonal)
86
+ if (Math.abs(dr) === 1 && Math.abs(dc) === 1) {
87
+ if (!king && this.turn === 0 && dr > 0) return { error: "White men move up only!" };
88
+ if (!king && this.turn === 1 && dr < 0) return { error: "Black men move down only!" };
89
+ this.board[to.r][to.c] = this.board[from.r][from.c];
90
+ this.board[from.r][from.c] = null;
91
+ this.promote(to.r, to.c);
92
+ this.turn = 1 - this.turn;
93
+ return { ok: true, capture: false };
94
+ }
95
+
96
+ // Jump (capture, 2 diagonal)
97
+ if (Math.abs(dr) === 2 && Math.abs(dc) === 2) {
98
+ const midR = from.r + dr / 2;
99
+ const midC = from.c + dc / 2;
100
+ if (!this.isEnemy(midR, midC)) return { error: "Must jump over enemy piece!" };
101
+ if (!king && this.turn === 0 && dr > 0) return { error: "White men jump up only!" };
102
+ if (!king && this.turn === 1 && dr < 0) return { error: "Black men jump down only!" };
103
+ this.board[to.r][to.c] = this.board[from.r][from.c];
104
+ this.board[from.r][from.c] = null;
105
+ this.board[midR][midC] = null; // captured
106
+ this.promote(to.r, to.c);
107
+ this.turn = 1 - this.turn;
108
+ return { ok: true, capture: true };
109
+ }
110
+
111
+ // Flying king: can move/jump multiple squares diagonally
112
+ if (king && Math.abs(dr) === Math.abs(dc)) {
113
+ const stepR = dr > 0 ? 1 : -1;
114
+ const stepC = dc > 0 ? 1 : -1;
115
+ let captured = null;
116
+ let cr = from.r + stepR, cc = from.c + stepC;
117
+ while (cr !== to.r || cc !== to.c) {
118
+ if (this.isOwn(cr, cc)) return { error: "Own piece in the way!" };
119
+ if (this.isEnemy(cr, cc)) {
120
+ if (captured) return { error: "Can't jump over two pieces!" };
121
+ captured = { r: cr, c: cc };
122
+ }
123
+ cr += stepR; cc += stepC;
124
+ }
125
+ this.board[to.r][to.c] = this.board[from.r][from.c];
126
+ this.board[from.r][from.c] = null;
127
+ if (captured) this.board[captured.r][captured.c] = null;
128
+ this.turn = 1 - this.turn;
129
+ return { ok: true, capture: !!captured };
130
+ }
131
+
132
+ return { error: "Invalid move!" };
133
+ }
134
+
135
+ promote(r, c) {
136
+ if (this.board[r][c] === '⛀' && r === 0) this.board[r][c] = '⛁'; // white king
137
+ if (this.board[r][c] === '⛂' && r === 7) this.board[r][c] = '⛃'; // black king
138
+ }
139
+
140
+ checkWinner() {
141
+ let white = 0, black = 0;
142
+ for (let r = 0; r < 8; r++)
143
+ for (let c = 0; c < 8; c++) {
144
+ if (this.board[r][c] === '⛀' || this.board[r][c] === '⛁') white++;
145
+ if (this.board[r][c] === '⛂' || this.board[r][c] === '⛃') black++;
146
+ }
147
+ if (white === 0) return this.players[1]; // black wins
148
+ if (black === 0) return this.players[0]; // white wins
149
+ return null;
150
+ }
151
+
152
+ renderBoard() {
153
+ let s = " a b c d e f g h\n";
154
+ for (let r = 0; r < 8; r++) {
155
+ s += (8 - r) + " ";
156
+ for (let c = 0; c < 8; c++) {
157
+ const p = this.board[r][c];
158
+ if (p) s += " " + p;
159
+ else s += ((r + c) % 2 === 1 ? " ·" : " ▪");
160
+ }
161
+ s += "\n";
162
+ }
163
+ return s;
164
+ }
165
+ }
166
+
167
+ module.exports = Checkers;
games/Chess.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Chess — Simplified for Telegram text-based play
3
+ * White: ♔♕♖♗♘♙ | Black: ♚♛♜♝♞♟
4
+ * Moves via text: /m e2 e4
5
+ * No castling, en passant, or promotion choice (auto-queen) for simplicity.
6
+ */
7
+ class Chess {
8
+ constructor(player1, bet) {
9
+ this.players = [player1]; // [white, black]
10
+ this.bet = bet;
11
+ this.board = this.initBoard();
12
+ this.turn = 0;
13
+ this.status = 'lobby';
14
+ this.moveCount = 0;
15
+ }
16
+
17
+ initBoard() {
18
+ return [
19
+ ['♜','♞','♝','♛','♚','♝','♞','♜'],
20
+ ['♟','♟','♟','♟','♟','♟','♟','♟'],
21
+ [null,null,null,null,null,null,null,null],
22
+ [null,null,null,null,null,null,null,null],
23
+ [null,null,null,null,null,null,null,null],
24
+ [null,null,null,null,null,null,null,null],
25
+ ['♙','♙','♙','♙','♙','♙','♙','♙'],
26
+ ['♖','♘','♗','♕','♔','♗','♘','♖'],
27
+ ];
28
+ }
29
+
30
+ addPlayer(userId) {
31
+ if (this.players.length < 2 && !this.players.includes(userId)) {
32
+ this.players.push(userId);
33
+ this.status = 'playing';
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ parsePos(s) {
40
+ if (!s || s.length < 2) return null;
41
+ const c = s.charCodeAt(0) - 97;
42
+ const r = 8 - parseInt(s[1]);
43
+ if (c < 0 || c > 7 || r < 0 || r > 7) return null;
44
+ return { r, c };
45
+ }
46
+
47
+ isWhite(p) { return '♔♕♖♗♘♙'.includes(p); }
48
+ isBlack(p) { return '♚♛♜♝♞♟'.includes(p); }
49
+ isOwn(r, c) {
50
+ const p = this.board[r][c];
51
+ if (!p) return false;
52
+ return this.turn === 0 ? this.isWhite(p) : this.isBlack(p);
53
+ }
54
+ isEnemy(r, c) {
55
+ const p = this.board[r][c];
56
+ if (!p) return false;
57
+ return this.turn === 0 ? this.isBlack(p) : this.isWhite(p);
58
+ }
59
+
60
+ makeMove(userId, fromStr, toStr) {
61
+ if (this.status !== 'playing') return { error: "Game not active" };
62
+ if (this.players[this.turn] !== userId) return { error: "Not your turn!" };
63
+
64
+ const from = this.parsePos(fromStr);
65
+ const to = this.parsePos(toStr);
66
+ if (!from || !to) return { error: "Invalid position. Use like: e2 e4" };
67
+ if (!this.isOwn(from.r, from.c)) return { error: "No piece of yours there!" };
68
+ if (this.isOwn(to.r, to.c)) return { error: "Can't capture own piece!" };
69
+
70
+ const piece = this.board[from.r][from.c];
71
+ const valid = this.validateMove(piece, from, to);
72
+ if (!valid) return { error: "Illegal move for that piece!" };
73
+
74
+ const captured = this.board[to.r][to.c];
75
+ this.board[to.r][to.c] = piece;
76
+ this.board[from.r][from.c] = null;
77
+
78
+ // Auto-promote pawns
79
+ if (piece === '♙' && to.r === 0) this.board[to.r][to.c] = '♕';
80
+ if (piece === '♟' && to.r === 7) this.board[to.r][to.c] = '♛';
81
+
82
+ this.turn = 1 - this.turn;
83
+ this.moveCount++;
84
+ return { ok: true, captured };
85
+ }
86
+
87
+ validateMove(piece, from, to) {
88
+ const dr = to.r - from.r, dc = to.c - from.c;
89
+ const ar = Math.abs(dr), ac = Math.abs(dc);
90
+
91
+ // Pawn
92
+ if (piece === '♙') {
93
+ if (dc === 0 && dr === -1 && !this.board[to.r][to.c]) return true;
94
+ if (dc === 0 && dr === -2 && from.r === 6 && !this.board[to.r][to.c] && !this.board[from.r-1][from.c]) return true;
95
+ if (ac === 1 && dr === -1 && this.isEnemy(to.r, to.c)) return true;
96
+ return false;
97
+ }
98
+ if (piece === '♟') {
99
+ if (dc === 0 && dr === 1 && !this.board[to.r][to.c]) return true;
100
+ if (dc === 0 && dr === 2 && from.r === 1 && !this.board[to.r][to.c] && !this.board[from.r+1][from.c]) return true;
101
+ if (ac === 1 && dr === 1 && this.isEnemy(to.r, to.c)) return true;
102
+ return false;
103
+ }
104
+ // Knight
105
+ if (piece === '♘' || piece === '♞') {
106
+ return (ar === 2 && ac === 1) || (ar === 1 && ac === 2);
107
+ }
108
+ // Bishop
109
+ if (piece === '♗' || piece === '♝') {
110
+ return ar === ac && ar > 0 && this.clearDiagonal(from, to);
111
+ }
112
+ // Rook
113
+ if (piece === '♖' || piece === '♜') {
114
+ return (ar === 0 || ac === 0) && (ar + ac > 0) && this.clearStraight(from, to);
115
+ }
116
+ // Queen
117
+ if (piece === '♕' || piece === '♛') {
118
+ if (ar === ac && ar > 0) return this.clearDiagonal(from, to);
119
+ if ((ar === 0 || ac === 0) && ar + ac > 0) return this.clearStraight(from, to);
120
+ return false;
121
+ }
122
+ // King
123
+ if (piece === '♔' || piece === '♚') {
124
+ return ar <= 1 && ac <= 1 && (ar + ac > 0);
125
+ }
126
+ return false;
127
+ }
128
+
129
+ clearStraight(from, to) {
130
+ const sr = Math.sign(to.r - from.r), sc = Math.sign(to.c - from.c);
131
+ let r = from.r + sr, c = from.c + sc;
132
+ while (r !== to.r || c !== to.c) {
133
+ if (this.board[r][c]) return false;
134
+ r += sr; c += sc;
135
+ }
136
+ return true;
137
+ }
138
+
139
+ clearDiagonal(from, to) {
140
+ const sr = Math.sign(to.r - from.r), sc = Math.sign(to.c - from.c);
141
+ let r = from.r + sr, c = from.c + sc;
142
+ while (r !== to.r || c !== to.c) {
143
+ if (this.board[r][c]) return false;
144
+ r += sr; c += sc;
145
+ }
146
+ return true;
147
+ }
148
+
149
+ checkWinner() {
150
+ let whiteKing = false, blackKing = false;
151
+ for (let r = 0; r < 8; r++)
152
+ for (let c = 0; c < 8; c++) {
153
+ if (this.board[r][c] === '♔') whiteKing = true;
154
+ if (this.board[r][c] === '♚') blackKing = true;
155
+ }
156
+ if (!whiteKing) return this.players[1];
157
+ if (!blackKing) return this.players[0];
158
+ if (this.moveCount >= 200) return 'draw'; // force draw after 200 moves
159
+ return null;
160
+ }
161
+
162
+ renderBoard() {
163
+ let s = " a b c d e f g h\n";
164
+ for (let r = 0; r < 8; r++) {
165
+ s += (8 - r) + " ";
166
+ for (let c = 0; c < 8; c++) {
167
+ const p = this.board[r][c];
168
+ if (p) s += " " + p + " ";
169
+ else s += ((r + c) % 2 === 0 ? " · " : " ▪ ");
170
+ }
171
+ s += " " + (8 - r) + "\n";
172
+ }
173
+ s += " a b c d e f g h";
174
+ return s;
175
+ }
176
+ }
177
+
178
+ module.exports = Chess;
handlers/blackjack.js CHANGED
@@ -3,62 +3,49 @@ const User = require('../models/User');
3
  const Blackjack = require('../games/Blackjack');
4
  const sessions = require('../games/sessions');
5
  const leveling = require('../utils/leveling');
 
6
 
7
  const startBJ = async (client, event) => {
8
  const bet = parseInt(event.message.message.split(" ")[1]) || 100;
9
  const userId = event.message.senderId.toString();
10
- let user = await User.findOne({ userId });
11
- if (!user) user = await User.create({ userId });
12
-
13
  if (user.wallet < bet) return event.message.respond({ message: "Insufficient funds!" });
14
 
15
  const gameId = `bj:${userId}:${Date.now()}`;
16
  const game = new Blackjack(userId, bet);
17
  sessions.set(gameId, game);
18
-
19
- await updateBJBoard(client, event.chatId, null, game, gameId);
20
  };
21
 
22
- function buildBJButtons(gameId) {
23
  return [[
24
  new Api.KeyboardButtonCallback({ text: "Hit ➕", data: Buffer.from(`bjhit|${gameId}`) }),
25
  new Api.KeyboardButtonCallback({ text: "Stand ✋", data: Buffer.from(`bjstd|${gameId}`) })
26
  ]];
27
  }
28
 
29
- async function updateBJBoard(client, peer, msgId, game, gameId) {
30
- const dealerHand = game.status === 'playing'
31
- ? `${game.players['dealer'].hand[0].v}${game.players['dealer'].hand[0].s}`
32
- : game.players['dealer'].hand.map(c => `${c.v}${c.s}`).join(" ");
33
-
34
- const playerHand = game.players[game.creatorId].hand.map(c => `${c.v}${c.s}`).join(" ");
35
-
36
- let message = `🃏 **BLACKJACK**\nBet: $${game.bet}\n\n`;
37
- message += `🏦 **Dealer:** ${dealerHand} (Score: ${game.status === 'playing' ? '?' : game.players['dealer'].score})\n`;
38
- message += `👤 **You:** ${playerHand} (Score: ${game.players[game.creatorId].score})`;
39
-
40
- let buttons = [];
41
- if (game.status === 'playing') {
42
- buttons = buildBJButtons(gameId);
43
- }
44
 
45
- if (msgId) {
46
- await client.editMessage(peer, { message: msgId, text: message, buttons: client.buildReplyMarkup(buttons) });
47
- } else {
48
- await client.sendMessage(peer, { message, buttons: client.buildReplyMarkup(buttons) });
49
- }
50
  }
51
 
52
  const handleBJCallback = async (client, update) => {
53
  const data = update.data.toString();
54
  if (!data.startsWith("bjhit|") && !data.startsWith("bjstd|")) return;
55
-
56
  const userId = update.userId.toString();
57
- const parts = data.split("|");
58
- const action = parts[0]; // bjhit or bjstd
59
- const gameId = parts[1];
60
  const game = sessions.get(gameId);
61
-
62
  if (!game || game.creatorId !== userId || game.status !== 'playing') return;
63
 
64
  if (action === 'bjhit') {
@@ -67,55 +54,31 @@ const handleBJCallback = async (client, update) => {
67
  game.status = 'bust';
68
  await finalizeBJ(client, update.peer, update.msgId, game, gameId, 'bust');
69
  } else {
70
- await updateBJBoard(client, update.peer, update.msgId, game, gameId);
71
  }
72
  } else if (action === 'bjstd') {
73
- // Dealer plays
74
- while (game.players['dealer'].score < 17) {
75
- game.deal('dealer');
76
- }
77
-
78
- let result = '';
79
- const pScore = game.players[userId].score;
80
- const dScore = game.players['dealer'].score;
81
-
82
- if (dScore > 21 || pScore > dScore) result = 'win';
83
- else if (dScore > pScore) result = 'lose';
84
- else result = 'push';
85
-
86
  game.status = 'finished';
87
  await finalizeBJ(client, update.peer, update.msgId, game, gameId, result);
88
  }
89
  };
90
 
91
  async function finalizeBJ(client, peer, msgId, game, gameId, result) {
92
- let user = await User.findOne({ userId: game.creatorId });
93
- if (!user) user = await User.create({ userId: game.creatorId });
94
- let finalMsg = "";
95
-
96
  if (result === 'win') {
97
  user.wallet += game.bet;
98
- const xpRes = await leveling.addXP(game.creatorId, 50);
99
- finalMsg = `🎉 **YOU WIN!** You gained $${game.bet} and 50 XP.`;
100
- if (xpRes.leveledUp) finalMsg += `\n🆙 Leveled up to ${xpRes.level}!`;
101
  } else if (result === 'lose' || result === 'bust') {
102
  user.wallet -= game.bet;
103
- finalMsg = result === 'bust' ? `💥 **BUST!** You exceeded 21. Lost $${game.bet}.` : `💀 **DEALER WINS!** Lost $${game.bet}.`;
104
- } else {
105
- finalMsg = "🤝 **PUSH!** It's a draw, money returned.";
106
- }
107
-
108
  await user.save();
109
-
110
- const dealerHand = game.players['dealer'].hand.map(c => `${c.v}${c.s}`).join(" ");
111
- const playerHand = game.players[game.creatorId].hand.map(c => `${c.v}${c.s}`).join(" ");
112
-
113
- let message = `🃏 **BLACKJACK - ${result.toUpperCase()}**\nBet: $${game.bet}\n\n`;
114
- message += `🏦 **Dealer:** ${dealerHand} (Score: ${game.players['dealer'].score})\n`;
115
- message += `👤 **You:** ${playerHand} (Score: ${game.players[game.creatorId].score})\n\n`;
116
- message += finalMsg;
117
-
118
- await client.editMessage(peer, { message: msgId, text: message, buttons: new Api.ReplyInlineMarkup({ rows: [] }) });
119
  sessions.delete(gameId);
120
  }
121
 
 
3
  const Blackjack = require('../games/Blackjack');
4
  const sessions = require('../games/sessions');
5
  const leveling = require('../utils/leveling');
6
+ const { editMsg } = require('../utils/editMsg');
7
 
8
  const startBJ = async (client, event) => {
9
  const bet = parseInt(event.message.message.split(" ")[1]) || 100;
10
  const userId = event.message.senderId.toString();
11
+ let user = await User.findOne({ userId }) || await User.create({ userId });
 
 
12
  if (user.wallet < bet) return event.message.respond({ message: "Insufficient funds!" });
13
 
14
  const gameId = `bj:${userId}:${Date.now()}`;
15
  const game = new Blackjack(userId, bet);
16
  sessions.set(gameId, game);
17
+ await sendBJBoard(client, event.chatId, game, gameId);
 
18
  };
19
 
20
+ function bjButtons(gameId) {
21
  return [[
22
  new Api.KeyboardButtonCallback({ text: "Hit ➕", data: Buffer.from(`bjhit|${gameId}`) }),
23
  new Api.KeyboardButtonCallback({ text: "Stand ✋", data: Buffer.from(`bjstd|${gameId}`) })
24
  ]];
25
  }
26
 
27
+ function bjText(game, showDealer) {
28
+ const dHand = showDealer
29
+ ? game.players['dealer'].hand.map(c => `${c.v}${c.s}`).join(" ")
30
+ : `${game.players['dealer'].hand[0].v}${game.players['dealer'].hand[0].s} ❓`;
31
+ const pHand = game.players[game.creatorId].hand.map(c => `${c.v}${c.s}`).join(" ");
32
+ return `🃏 **BLACKJACK** | Bet: $${game.bet}\n\n🏦 Dealer: ${dHand} (${showDealer ? game.players['dealer'].score : '?'})\n👤 You: ${pHand} (${game.players[game.creatorId].score})`;
33
+ }
 
 
 
 
 
 
 
 
34
 
35
+ async function sendBJBoard(client, peer, game, gameId) {
36
+ await client.sendMessage(peer, {
37
+ message: bjText(game, false),
38
+ buttons: client.buildReplyMarkup(bjButtons(gameId))
39
+ });
40
  }
41
 
42
  const handleBJCallback = async (client, update) => {
43
  const data = update.data.toString();
44
  if (!data.startsWith("bjhit|") && !data.startsWith("bjstd|")) return;
 
45
  const userId = update.userId.toString();
46
+ const action = data.split("|")[0];
47
+ const gameId = data.split("|")[1];
 
48
  const game = sessions.get(gameId);
 
49
  if (!game || game.creatorId !== userId || game.status !== 'playing') return;
50
 
51
  if (action === 'bjhit') {
 
54
  game.status = 'bust';
55
  await finalizeBJ(client, update.peer, update.msgId, game, gameId, 'bust');
56
  } else {
57
+ await editMsg(client, update.peer, update.msgId, bjText(game, false), bjButtons(gameId));
58
  }
59
  } else if (action === 'bjstd') {
60
+ while (game.players['dealer'].score < 17) game.deal('dealer');
61
+ const p = game.players[userId].score, d = game.players['dealer'].score;
62
+ const result = d > 21 || p > d ? 'win' : d > p ? 'lose' : 'push';
 
 
 
 
 
 
 
 
 
 
63
  game.status = 'finished';
64
  await finalizeBJ(client, update.peer, update.msgId, game, gameId, result);
65
  }
66
  };
67
 
68
  async function finalizeBJ(client, peer, msgId, game, gameId, result) {
69
+ let user = await User.findOne({ userId: game.creatorId }) || await User.create({ userId: game.creatorId });
70
+ let extra = "";
 
 
71
  if (result === 'win') {
72
  user.wallet += game.bet;
73
+ const xp = await leveling.addXP(game.creatorId, 50);
74
+ extra = `\n\n🎉 **YOU WIN!** +$${game.bet} +50XP`;
75
+ if (xp.leveledUp) extra += ` 🆙 Lv${xp.level}!`;
76
  } else if (result === 'lose' || result === 'bust') {
77
  user.wallet -= game.bet;
78
+ extra = result === 'bust' ? `\n\n💥 **BUST!** Lost $${game.bet}` : `\n\n💀 **DEALER WINS!** Lost $${game.bet}`;
79
+ } else { extra = "\n\n🤝 **PUSH!** Draw."; }
 
 
 
80
  await user.save();
81
+ await editMsg(client, peer, msgId, bjText(game, true) + extra, null);
 
 
 
 
 
 
 
 
 
82
  sessions.delete(gameId);
83
  }
84
 
handlers/boardHandler.js CHANGED
@@ -2,150 +2,92 @@ const { Api } = require("telegram");
2
  const User = require('../models/User');
3
  const sessions = require('../games/sessions');
4
  const leveling = require('../utils/leveling');
 
5
 
6
  const handleBoardCallback = async (client, update) => {
7
  const data = update.data.toString();
8
  const userId = update.userId.toString();
9
 
10
- // ── XOX Join: xjoin|gameId ──
11
  if (data.startsWith("xjoin|")) {
12
  const gameId = data.replace("xjoin|", "");
13
  const g = sessions.get(gameId);
14
  if (!g || g.status !== 'lobby' || g.players.includes(userId)) return;
15
-
16
- let user = await User.findOne({ userId });
17
- if (!user) user = await User.create({ userId });
18
  if (user.wallet < g.bet) return;
19
-
20
  g.addPlayer(userId);
21
  await updateBoard(client, update.peer, update.msgId, g, gameId, 'xox');
22
  return;
23
  }
24
-
25
- // ── C4 Join: cjoin|gameId ──
26
  if (data.startsWith("cjoin|")) {
27
  const gameId = data.replace("cjoin|", "");
28
  const g = sessions.get(gameId);
29
  if (!g || g.status !== 'lobby' || g.players.includes(userId)) return;
30
-
31
- let user = await User.findOne({ userId });
32
- if (!user) user = await User.create({ userId });
33
  if (user.wallet < g.bet) return;
34
-
35
  g.addPlayer(userId);
36
  await updateBoard(client, update.peer, update.msgId, g, gameId, 'c4');
37
  return;
38
  }
39
-
40
- // ── XOX Move: xmove|index|gameId ──
41
  if (data.startsWith("xmove|")) {
42
  const parts = data.split("|");
43
- const index = parseInt(parts[1]);
44
- const gameId = parts[2];
45
- const xoxGame = sessions.get(gameId);
46
-
47
- if (!xoxGame || xoxGame.status !== 'playing') return;
48
- if (!xoxGame.makeMove(userId, index)) return;
49
-
50
- const winner = xoxGame.checkWinner();
51
- if (winner) {
52
- await handleEndGame(client, update.peer, update.msgId, xoxGame, winner, gameId);
53
- } else {
54
- await updateBoard(client, update.peer, update.msgId, xoxGame, gameId, 'xox');
55
- }
56
  return;
57
  }
58
-
59
- // ── C4 Move: cmove|col|gameId ──
60
  if (data.startsWith("cmove|")) {
61
  const parts = data.split("|");
62
- const col = parseInt(parts[1]);
63
- const gameId = parts[2];
64
- const c4Game = sessions.get(gameId);
65
-
66
- if (!c4Game || c4Game.status !== 'playing') return;
67
- if (!c4Game.makeMove(userId, col)) return;
68
-
69
- const winner = c4Game.checkWinner();
70
- if (winner) {
71
- await handleEndGame(client, update.peer, update.msgId, c4Game, winner, gameId);
72
- } else {
73
- await updateBoard(client, update.peer, update.msgId, c4Game, gameId, 'c4');
74
- }
75
  return;
76
  }
77
  };
78
 
79
  async function updateBoard(client, peer, msgId, game, gameId, type) {
80
- let buttons = [];
81
- let message = "";
82
-
83
  if (type === 'xox') {
84
- message = `❌⭕ **TIC-TAC-TOE**\nBet: $${game.bet}\n\nTurn: [${game.players[game.turn]}](tg://user?id=${game.players[game.turn]})`;
85
  for (let i = 0; i < 3; i++) {
86
  let row = [];
87
  for (let j = 0; j < 3; j++) {
88
  const idx = i * 3 + j;
89
- row.push(new Api.KeyboardButtonCallback({
90
- text: game.board[idx] || '⬜',
91
- data: Buffer.from(`xmove|${idx}|${gameId}`)
92
- }));
93
  }
94
  buttons.push(row);
95
  }
96
  } else if (type === 'c4') {
97
- message = `🔴🟡 **CONNECT FOUR**\nBet: $${game.bet}\n\nTurn: [${game.players[game.turn]}](tg://user?id=${game.players[game.turn]})\n`;
98
- // Board display as text since buttons are limited
99
- let boardText = "";
100
- for (let r = 0; r < game.rows; r++) {
101
- boardText += game.board[r].map(cell => cell || '⚪').join("") + "\n";
102
- }
103
- message += "\n" + boardText;
104
-
105
  let row = [];
106
- for (let c = 0; c < game.cols; c++) {
107
- row.push(new Api.KeyboardButtonCallback({
108
- text: `${c + 1}`,
109
- data: Buffer.from(`cmove|${c}|${gameId}`)
110
- }));
111
- }
112
  buttons.push(row);
113
  }
114
-
115
- await client.editMessage(peer, {
116
- message: msgId,
117
- text: message,
118
- buttons: client.buildReplyMarkup(buttons)
119
- });
120
  }
121
 
122
  async function handleEndGame(client, peer, msgId, game, winner, gameId) {
123
  let msg = "";
124
- if (winner === 'draw') {
125
- msg = "🤝 **DRAW!** Money returned.";
126
- } else {
127
- let winUser = await User.findOne({ userId: winner });
128
- if (!winUser) winUser = await User.create({ userId: winner });
129
  const loserId = game.players.find(id => id !== winner);
130
- let loseUser = await User.findOne({ userId: loserId });
131
- if (!loseUser) loseUser = await User.create({ userId: loserId });
132
-
133
- winUser.wallet += game.bet;
134
- loseUser.wallet -= game.bet;
135
-
136
  const xpRes = await leveling.addXP(winner, 100);
137
- msg = `🏆 **WINNER!**\n\nPlayer: [${winner}](tg://user?id=${winner})\nPrize: $${game.bet}`;
138
- if (xpRes.leveledUp) msg += `\n🆙 Leveled up to ${xpRes.level}!`;
139
-
140
- await winUser.save();
141
- await loseUser.save();
142
  }
143
-
144
- await client.editMessage(peer, {
145
- message: msgId,
146
- text: msg,
147
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
148
- });
149
  sessions.delete(gameId);
150
  }
151
 
 
2
  const User = require('../models/User');
3
  const sessions = require('../games/sessions');
4
  const leveling = require('../utils/leveling');
5
+ const { editMsg } = require('../utils/editMsg');
6
 
7
  const handleBoardCallback = async (client, update) => {
8
  const data = update.data.toString();
9
  const userId = update.userId.toString();
10
 
 
11
  if (data.startsWith("xjoin|")) {
12
  const gameId = data.replace("xjoin|", "");
13
  const g = sessions.get(gameId);
14
  if (!g || g.status !== 'lobby' || g.players.includes(userId)) return;
15
+ let user = await User.findOne({ userId }) || await User.create({ userId });
 
 
16
  if (user.wallet < g.bet) return;
 
17
  g.addPlayer(userId);
18
  await updateBoard(client, update.peer, update.msgId, g, gameId, 'xox');
19
  return;
20
  }
 
 
21
  if (data.startsWith("cjoin|")) {
22
  const gameId = data.replace("cjoin|", "");
23
  const g = sessions.get(gameId);
24
  if (!g || g.status !== 'lobby' || g.players.includes(userId)) return;
25
+ let user = await User.findOne({ userId }) || await User.create({ userId });
 
 
26
  if (user.wallet < g.bet) return;
 
27
  g.addPlayer(userId);
28
  await updateBoard(client, update.peer, update.msgId, g, gameId, 'c4');
29
  return;
30
  }
 
 
31
  if (data.startsWith("xmove|")) {
32
  const parts = data.split("|");
33
+ const index = parseInt(parts[1]), gameId = parts[2];
34
+ const game = sessions.get(gameId);
35
+ if (!game || game.status !== 'playing' || !game.makeMove(userId, index)) return;
36
+ const winner = game.checkWinner();
37
+ if (winner) await handleEndGame(client, update.peer, update.msgId, game, winner, gameId);
38
+ else await updateBoard(client, update.peer, update.msgId, game, gameId, 'xox');
 
 
 
 
 
 
 
39
  return;
40
  }
 
 
41
  if (data.startsWith("cmove|")) {
42
  const parts = data.split("|");
43
+ const col = parseInt(parts[1]), gameId = parts[2];
44
+ const game = sessions.get(gameId);
45
+ if (!game || game.status !== 'playing' || !game.makeMove(userId, col)) return;
46
+ const winner = game.checkWinner();
47
+ if (winner) await handleEndGame(client, update.peer, update.msgId, game, winner, gameId);
48
+ else await updateBoard(client, update.peer, update.msgId, game, gameId, 'c4');
 
 
 
 
 
 
 
49
  return;
50
  }
51
  };
52
 
53
  async function updateBoard(client, peer, msgId, game, gameId, type) {
54
+ let buttons = [], message = "";
 
 
55
  if (type === 'xox') {
56
+ message = `❌⭕ **TIC-TAC-TOE**\nBet: $${game.bet}\nTurn: Player ${game.turn + 1}`;
57
  for (let i = 0; i < 3; i++) {
58
  let row = [];
59
  for (let j = 0; j < 3; j++) {
60
  const idx = i * 3 + j;
61
+ row.push(new Api.KeyboardButtonCallback({ text: game.board[idx] || '⬜', data: Buffer.from(`xmove|${idx}|${gameId}`) }));
 
 
 
62
  }
63
  buttons.push(row);
64
  }
65
  } else if (type === 'c4') {
66
+ message = `🔴🟡 **CONNECT FOUR**\nBet: $${game.bet}\nTurn: Player ${game.turn + 1}\n`;
67
+ for (let r = 0; r < game.rows; r++)
68
+ message += game.board[r].map(c => c || '⚪').join("") + "\n";
 
 
 
 
 
69
  let row = [];
70
+ for (let c = 0; c < game.cols; c++)
71
+ row.push(new Api.KeyboardButtonCallback({ text: `${c+1}`, data: Buffer.from(`cmove|${c}|${gameId}`) }));
 
 
 
 
72
  buttons.push(row);
73
  }
74
+ await editMsg(client, peer, msgId, message, buttons);
 
 
 
 
 
75
  }
76
 
77
  async function handleEndGame(client, peer, msgId, game, winner, gameId) {
78
  let msg = "";
79
+ if (winner === 'draw') { msg = "🤝 **DRAW!** Money returned."; }
80
+ else {
81
+ let winUser = await User.findOne({ userId: winner }) || await User.create({ userId: winner });
 
 
82
  const loserId = game.players.find(id => id !== winner);
83
+ let loseUser = await User.findOne({ userId: loserId }) || await User.create({ userId: loserId });
84
+ winUser.wallet += game.bet; loseUser.wallet -= game.bet;
 
 
 
 
85
  const xpRes = await leveling.addXP(winner, 100);
86
+ msg = `🏆 **WINNER!** Prize: $${game.bet}`;
87
+ if (xpRes.leveledUp) msg += `\n🆙 Level ${xpRes.level}!`;
88
+ await winUser.save(); await loseUser.save();
 
 
89
  }
90
+ await editMsg(client, peer, msgId, msg, null);
 
 
 
 
 
91
  sessions.delete(gameId);
92
  }
93
 
handlers/combat.js CHANGED
@@ -30,6 +30,8 @@ const kill = async (client, event) => {
30
  // Reward for killing
31
  const reward = Math.floor(Math.random() * 500) + 100;
32
  killer.wallet += reward;
 
 
33
  victim.isDead = true;
34
  victim.lastDeath = new Date();
35
  victim.health = 0;
@@ -84,6 +86,8 @@ const rob = async (client, event) => {
84
  if (success) {
85
  const stolen = Math.floor(Math.random() * (victim.wallet * 0.7)) + 1;
86
  robber.wallet += stolen;
 
 
87
  victim.wallet -= stolen;
88
 
89
  const xpRes = await leveling.addXP(robberId, 50);
 
30
  // Reward for killing
31
  const reward = Math.floor(Math.random() * 500) + 100;
32
  killer.wallet += reward;
33
+ killer.kills = (killer.kills || 0) + 1;
34
+ killer.totalEarned = (killer.totalEarned || 0) + reward;
35
  victim.isDead = true;
36
  victim.lastDeath = new Date();
37
  victim.health = 0;
 
86
  if (success) {
87
  const stolen = Math.floor(Math.random() * (victim.wallet * 0.7)) + 1;
88
  robber.wallet += stolen;
89
+ robber.robs = (robber.robs || 0) + 1;
90
+ robber.totalEarned = (robber.totalEarned || 0) + stolen;
91
  victim.wallet -= stolen;
92
 
93
  const xpRes = await leveling.addXP(robberId, 50);
handlers/games.js CHANGED
@@ -2,6 +2,7 @@ const { Api } = require("telegram");
2
  const User = require('../models/User');
3
  const HackGame = require('../games/HackGame');
4
  const sessions = require('../games/sessions');
 
5
 
6
  const startHack = async (client, event) => {
7
  const parts = event.message.message.split(" ");
@@ -19,39 +20,19 @@ const startHack = async (client, event) => {
19
  const game = new HackGame(length, bet);
20
  sessions.set(gameId, { type: 'hack', game, userId });
21
 
22
- const buttons = [];
23
- const digits = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
24
- let row = [];
25
- for (const d of digits) {
26
- row.push(new Api.KeyboardButtonCallback({ text: d, data: Buffer.from(`hinp|${gameId}|${d}`) }));
27
- if (row.length === 3) {
28
- buttons.push(row);
29
- row = [];
30
- }
31
- }
32
- if (row.length > 0) buttons.push(row);
33
- buttons.push([
34
- new Api.KeyboardButtonCallback({ text: "CLR", data: Buffer.from(`hclr|${gameId}`) }),
35
- new Api.KeyboardButtonCallback({ text: "ENTER", data: Buffer.from(`hent|${gameId}`) })
36
- ]);
37
-
38
  await client.sendMessage(event.chatId, {
39
  message: `🔍 **HACK INITIALIZED**\nBet: $${bet}\nLength: ${length}\nGuess the ${length}-digit PIN:\n\nInput: [ ${"_".repeat(length)} ]`,
40
- buttons: client.buildReplyMarkup(buttons)
41
  });
42
  };
43
 
44
- // Build hack keyboard for re-use in editMessage
45
- function buildHackKeyboard(gameId, game) {
46
  const buttons = [];
47
  const digits = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
48
  let row = [];
49
  for (const d of digits) {
50
  row.push(new Api.KeyboardButtonCallback({ text: d, data: Buffer.from(`hinp|${gameId}|${d}`) }));
51
- if (row.length === 3) {
52
- buttons.push(row);
53
- row = [];
54
- }
55
  }
56
  if (row.length > 0) buttons.push(row);
57
  buttons.push([
@@ -65,23 +46,19 @@ const handleCallback = async (client, update) => {
65
  const data = update.data.toString();
66
  const userId = update.userId.toString();
67
 
68
- // Callback data format: hinp|gameId|digit, hclr|gameId, hent|gameId
69
  if (!data.startsWith("hinp|") && !data.startsWith("hclr|") && !data.startsWith("hent|")) return;
70
 
71
  const parts = data.split("|");
72
- const action = parts[0]; // hinp, hclr, hent
73
  const gameId = parts[1];
74
- const val = parts[2]; // digit (only for hinp)
75
 
76
  const session = sessions.get(gameId);
77
  if (!session || session.userId !== userId) return;
78
-
79
  const { game } = session;
80
 
81
  if (action === "hinp") {
82
- if (game.currentGuess.length < game.length) {
83
- game.currentGuess += val;
84
- }
85
  } else if (action === "hclr") {
86
  game.currentGuess = "";
87
  } else if (action === "hent") {
@@ -91,49 +68,32 @@ const handleCallback = async (client, update) => {
91
  game.attempts++;
92
 
93
  if (result.bulls === game.length) {
94
- // WIN
95
  let user = await User.findOne({ userId });
96
  if (!user) user = await User.create({ userId });
97
  const prize = game.bet * game.length;
98
  user.wallet += prize;
99
  await user.save();
100
-
101
- await client.editMessage(update.peer, {
102
- message: update.msgId,
103
- text: `🎯 **ACCESS GRANTED**\n\nPIN: ${game.target}\nReward: $${prize}\nUser: ${userId}`,
104
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
105
- });
106
  sessions.delete(gameId);
107
  return;
108
  } else if (game.attempts >= game.maxAttempts) {
109
- // LOSE
110
  let user = await User.findOne({ userId });
111
  if (!user) user = await User.create({ userId });
112
  user.wallet -= game.bet;
113
  await user.save();
114
-
115
- await client.editMessage(update.peer, {
116
- message: update.msgId,
117
- text: `❌ **SYSTEM LOCKED**\n\nYou failed to crack the PIN: ${game.target}\nLost: $${game.bet}`,
118
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
119
- });
120
  sessions.delete(gameId);
121
  return;
122
  } else {
123
- // CONTINUE
124
- const history = `Guess: ${game.currentGuess} -> Bulls: ${result.bulls}, Cows: ${result.cows}`;
125
- game.lastResult = history;
126
  game.currentGuess = "";
127
  }
128
  }
129
 
130
  const displayGuess = game.currentGuess.padEnd(game.length, "_");
131
- const keyboard = buildHackKeyboard(gameId, game);
132
- await client.editMessage(update.peer, {
133
- message: update.msgId,
134
- text: `🔍 **HACKING...**\nBet: $${game.bet}\nAttempt: ${game.attempts}/${game.maxAttempts}\nLast: ${game.lastResult || "None"}\n\nInput: [ ${displayGuess} ]`,
135
- buttons: client.buildReplyMarkup(keyboard)
136
- });
137
  };
138
 
139
  const startDice = async (client, event) => {
@@ -150,7 +110,7 @@ const startDice = async (client, event) => {
150
  sessions.set(gameId, { type: 'dice', creator: userId, bet, status: 'pending' });
151
 
152
  await client.sendMessage(event.chatId, {
153
- message: `🎲 **DICE DUEL**\nCreator: ${userId}\nBet: $${bet}\n\nWaiting for an opponent...`,
154
  buttons: client.buildReplyMarkup([
155
  [new Api.KeyboardButtonCallback({ text: "Join Duel", data: Buffer.from(`djoin|${gameId}`) })]
156
  ])
@@ -160,12 +120,10 @@ const startDice = async (client, event) => {
160
  const handleDiceCallback = async (client, update) => {
161
  const data = update.data.toString();
162
  const userId = update.userId.toString();
163
-
164
  if (!data.startsWith("djoin|")) return;
165
 
166
  const gameId = data.replace("djoin|", "");
167
  const session = sessions.get(gameId);
168
-
169
  if (!session || session.status !== 'pending') return;
170
  if (session.creator === userId) return;
171
 
@@ -173,40 +131,25 @@ const handleDiceCallback = async (client, update) => {
173
  if (!opponent) opponent = await User.create({ userId });
174
  if (opponent.wallet < session.bet) return;
175
 
176
- session.opponent = userId;
177
- session.status = 'playing';
178
-
179
  const creatorRoll = Math.floor(Math.random() * 6) + 1;
180
  const opponentRoll = Math.floor(Math.random() * 6) + 1;
181
 
182
- let resultMsg = `🎲 **DICE DUEL RESULT**\n\n`;
183
- resultMsg += `Creator (${session.creator}): ${creatorRoll}\n`;
184
- resultMsg += `Opponent (${userId}): ${opponentRoll}\n\n`;
185
-
186
  let creator = await User.findOne({ userId: session.creator });
187
  if (!creator) creator = await User.create({ userId: session.creator });
188
 
 
189
  if (creatorRoll > opponentRoll) {
190
- creator.wallet += session.bet;
191
- opponent.wallet -= session.bet;
192
- resultMsg += `🏆 **Winner: Creator!** (+$${session.bet})`;
193
  } else if (opponentRoll > creatorRoll) {
194
- creator.wallet -= session.bet;
195
- opponent.wallet += session.bet;
196
- resultMsg += `🏆 **Winner: Opponent!** (+$${session.bet})`;
197
  } else {
198
- resultMsg += `🤝 **It's a Draw!** No money lost.`;
199
  }
200
-
201
- await creator.save();
202
- await opponent.save();
203
  sessions.delete(gameId);
204
-
205
- await client.editMessage(update.peer, {
206
- message: update.msgId,
207
- text: resultMsg,
208
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
209
- });
210
  };
211
 
212
  module.exports = { startHack, startDice, handleCallback, handleDiceCallback };
 
2
  const User = require('../models/User');
3
  const HackGame = require('../games/HackGame');
4
  const sessions = require('../games/sessions');
5
+ const { editMsg } = require('../utils/editMsg');
6
 
7
  const startHack = async (client, event) => {
8
  const parts = event.message.message.split(" ");
 
20
  const game = new HackGame(length, bet);
21
  sessions.set(gameId, { type: 'hack', game, userId });
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  await client.sendMessage(event.chatId, {
24
  message: `🔍 **HACK INITIALIZED**\nBet: $${bet}\nLength: ${length}\nGuess the ${length}-digit PIN:\n\nInput: [ ${"_".repeat(length)} ]`,
25
+ buttons: client.buildReplyMarkup(buildHackKeyboard(gameId))
26
  });
27
  };
28
 
29
+ function buildHackKeyboard(gameId) {
 
30
  const buttons = [];
31
  const digits = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
32
  let row = [];
33
  for (const d of digits) {
34
  row.push(new Api.KeyboardButtonCallback({ text: d, data: Buffer.from(`hinp|${gameId}|${d}`) }));
35
+ if (row.length === 3) { buttons.push(row); row = []; }
 
 
 
36
  }
37
  if (row.length > 0) buttons.push(row);
38
  buttons.push([
 
46
  const data = update.data.toString();
47
  const userId = update.userId.toString();
48
 
 
49
  if (!data.startsWith("hinp|") && !data.startsWith("hclr|") && !data.startsWith("hent|")) return;
50
 
51
  const parts = data.split("|");
52
+ const action = parts[0];
53
  const gameId = parts[1];
54
+ const val = parts[2];
55
 
56
  const session = sessions.get(gameId);
57
  if (!session || session.userId !== userId) return;
 
58
  const { game } = session;
59
 
60
  if (action === "hinp") {
61
+ if (game.currentGuess.length < game.length) game.currentGuess += val;
 
 
62
  } else if (action === "hclr") {
63
  game.currentGuess = "";
64
  } else if (action === "hent") {
 
68
  game.attempts++;
69
 
70
  if (result.bulls === game.length) {
 
71
  let user = await User.findOne({ userId });
72
  if (!user) user = await User.create({ userId });
73
  const prize = game.bet * game.length;
74
  user.wallet += prize;
75
  await user.save();
76
+ await editMsg(client, update.peer, update.msgId, `🎯 **ACCESS GRANTED**\n\nPIN: ${game.target}\nReward: $${prize}`, null);
 
 
 
 
 
77
  sessions.delete(gameId);
78
  return;
79
  } else if (game.attempts >= game.maxAttempts) {
 
80
  let user = await User.findOne({ userId });
81
  if (!user) user = await User.create({ userId });
82
  user.wallet -= game.bet;
83
  await user.save();
84
+ await editMsg(client, update.peer, update.msgId, `❌ **SYSTEM LOCKED**\n\nPIN was: ${game.target}\nLost: $${game.bet}`, null);
 
 
 
 
 
85
  sessions.delete(gameId);
86
  return;
87
  } else {
88
+ game.lastResult = `Guess: ${game.currentGuess} → Bulls: ${result.bulls}, Cows: ${result.cows}`;
 
 
89
  game.currentGuess = "";
90
  }
91
  }
92
 
93
  const displayGuess = game.currentGuess.padEnd(game.length, "_");
94
+ await editMsg(client, update.peer, update.msgId,
95
+ `🔍 **HACKING...**\nBet: $${game.bet}\nAttempt: ${game.attempts}/${game.maxAttempts}\nLast: ${game.lastResult || "None"}\n\nInput: [ ${displayGuess} ]`,
96
+ buildHackKeyboard(gameId));
 
 
 
97
  };
98
 
99
  const startDice = async (client, event) => {
 
110
  sessions.set(gameId, { type: 'dice', creator: userId, bet, status: 'pending' });
111
 
112
  await client.sendMessage(event.chatId, {
113
+ message: `🎲 **DICE DUEL**\nCreator: ${userId}\nBet: $${bet}\n\nWaiting for opponent...`,
114
  buttons: client.buildReplyMarkup([
115
  [new Api.KeyboardButtonCallback({ text: "Join Duel", data: Buffer.from(`djoin|${gameId}`) })]
116
  ])
 
120
  const handleDiceCallback = async (client, update) => {
121
  const data = update.data.toString();
122
  const userId = update.userId.toString();
 
123
  if (!data.startsWith("djoin|")) return;
124
 
125
  const gameId = data.replace("djoin|", "");
126
  const session = sessions.get(gameId);
 
127
  if (!session || session.status !== 'pending') return;
128
  if (session.creator === userId) return;
129
 
 
131
  if (!opponent) opponent = await User.create({ userId });
132
  if (opponent.wallet < session.bet) return;
133
 
 
 
 
134
  const creatorRoll = Math.floor(Math.random() * 6) + 1;
135
  const opponentRoll = Math.floor(Math.random() * 6) + 1;
136
 
 
 
 
 
137
  let creator = await User.findOne({ userId: session.creator });
138
  if (!creator) creator = await User.create({ userId: session.creator });
139
 
140
+ let resultMsg = `🎲 **DICE DUEL RESULT**\n\nCreator: 🎲 ${creatorRoll}\nOpponent: 🎲 ${opponentRoll}\n\n`;
141
  if (creatorRoll > opponentRoll) {
142
+ creator.wallet += session.bet; opponent.wallet -= session.bet;
143
+ resultMsg += `🏆 Creator wins +$${session.bet}`;
 
144
  } else if (opponentRoll > creatorRoll) {
145
+ creator.wallet -= session.bet; opponent.wallet += session.bet;
146
+ resultMsg += `🏆 Opponent wins +$${session.bet}`;
 
147
  } else {
148
+ resultMsg += `🤝 Draw! No money lost.`;
149
  }
150
+ await creator.save(); await opponent.save();
 
 
151
  sessions.delete(gameId);
152
+ await editMsg(client, update.peer, update.msgId, resultMsg, null);
 
 
 
 
 
153
  };
154
 
155
  module.exports = { startHack, startDice, handleCallback, handleDiceCallback };
handlers/inlineHandler.js CHANGED
@@ -2,238 +2,145 @@ const { Api } = require("telegram");
2
  const User = require('../models/User');
3
  const sessions = require('../games/sessions');
4
  const { buildMinesGrid } = require('./inlineGames');
 
 
5
 
6
  const handleInlineCallback = async (client, update) => {
7
  const data = update.data.toString();
8
  const userId = update.userId.toString();
9
-
10
- // All callback data uses "|" delimiter: type|gameId|value
11
  const parts = data.split("|");
12
- const gameType = parts[0]; // mines, roulette, hl, flip, rps
13
  const gameId = parts[1];
14
 
15
- // --- MINES LOGIC ---
16
- // Data format: mines|gameId|row|col
17
  if (gameType === 'mines') {
18
  const session = sessions.get(gameId);
19
- if (!session || session.type !== 'mines') return;
20
- if (session.userId !== userId) return;
21
-
22
- const r = parseInt(parts[2]);
23
- const c = parseInt(parts[3]);
24
-
25
- if (isNaN(r) || isNaN(c) || r < 0 || r > 2 || c < 0 || c > 2) return;
26
- if (session.grid[r][c] !== '⬜') return;
27
 
28
  const isMine = session.mines.some(m => m.r === r && m.c === c);
29
  if (isMine) {
30
  let user = await User.findOne({ userId });
31
  if (!user) user = await User.create({ userId });
32
- user.wallet -= session.bet;
33
- await user.save();
34
- await client.editMessage(update.peer, {
35
- message: update.msgId,
36
- text: `💥 **BOOM!** You hit a mine. Lost $${session.bet}.`,
37
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
38
- });
39
  sessions.delete(gameId);
40
  } else {
41
  session.grid[r][c] = '💎';
42
  session.revealed++;
43
- if (session.revealed === 7) { // All safe spots found (9 - 2 mines = 7)
44
  let user = await User.findOne({ userId });
45
  if (!user) user = await User.create({ userId });
46
- user.wallet += session.bet * 5;
47
- await user.save();
48
- await client.editMessage(update.peer, {
49
- message: update.msgId,
50
- text: `🏆 **CLEAR!** You found all safe spots. Won $${session.bet * 5}!`,
51
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
52
- });
53
  sessions.delete(gameId);
54
  } else {
55
- await client.editMessage(update.peer, {
56
- message: update.msgId,
57
- text: `💎 Safe! Revealed: ${session.revealed}/7`,
58
- buttons: client.buildReplyMarkup(buildMinesGrid(gameId, session.grid))
59
- });
60
  }
61
  }
62
  return;
63
  }
64
 
65
- // --- ROULETTE LOGIC ---
66
- // Data format: roulette|gameId
67
  if (gameType === 'roulette') {
68
  let user = await User.findOne({ userId });
69
  if (!user) user = await User.create({ userId });
70
  const bet = 200;
71
- const isDeadly = Math.random() < (1 / 6);
72
-
73
- if (isDeadly) {
74
- user.wallet -= bet;
75
- user.isDead = true;
76
- user.lastDeath = new Date();
77
- user.health = 0;
78
- await client.editMessage(update.peer, {
79
- message: update.msgId,
80
- text: `💥 **BANG!** You shot yourself. You are DEAD. Lost $${bet}.`,
81
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
82
- });
83
  } else {
84
- user.wallet += bet * 2;
85
- await client.editMessage(update.peer, {
86
- message: update.msgId,
87
- text: `🚩 **CLICK.** The chamber was empty. You won $${bet * 2}!`,
88
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
89
- });
90
  }
91
- await user.save();
92
  return;
93
  }
94
 
95
- // --- HIGHER OR LOWER LOGIC ---
96
- // Data format: hl|gameId|higher or hl|gameId|lower
97
  if (gameType === 'hl') {
98
  const session = sessions.get(gameId);
99
- if (!session || session.type !== 'hl') return;
100
- if (session.userId !== userId) return;
101
-
102
- const choice = parts[2]; // 'higher' or 'lower'
103
- if (choice !== 'higher' && choice !== 'lower') return;
104
-
105
  const nextNum = Math.floor(Math.random() * 13) + 1;
106
  let user = await User.findOne({ userId });
107
  if (!user) user = await User.create({ userId });
108
 
109
- const isHigher = nextNum > session.lastNum;
110
- const isWin = (choice === 'higher' && isHigher) || (choice === 'lower' && !isHigher && nextNum !== session.lastNum);
111
-
112
  if (nextNum === session.lastNum) {
113
- // Rebuild HL buttons with fresh callback data
114
- await client.editMessage(update.peer, {
115
- message: update.msgId,
116
- text: `🔄 The number was the same (**${nextNum}**)! Try again.`,
117
- buttons: client.buildReplyMarkup([[
118
- new Api.KeyboardButtonCallback({ text: "Higher ⬆️", data: Buffer.from(`hl|${gameId}|higher`) }),
119
- new Api.KeyboardButtonCallback({ text: "Lower ⬇️", data: Buffer.from(`hl|${gameId}|lower`) })
120
- ]])
121
- });
122
  return;
123
  }
124
-
125
  if (isWin) {
126
- session.streak++;
127
- session.lastNum = nextNum;
128
  if (session.streak >= 3) {
129
- user.wallet += session.bet * 3;
130
- await user.save();
131
- await client.editMessage(update.peer, {
132
- message: update.msgId,
133
- text: `🏆 **STREAK!** The number was **${nextNum}**. You won $${session.bet * 3}!`,
134
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
135
- });
136
  sessions.delete(gameId);
137
  } else {
138
- await client.editMessage(update.peer, {
139
- message: update.msgId,
140
- text: `✅ Correct! It was **${nextNum}**. Streak: ${session.streak}/3\nNext number Higher or Lower than **${nextNum}**?`,
141
- buttons: client.buildReplyMarkup([[
142
- new Api.KeyboardButtonCallback({ text: "Higher ⬆️", data: Buffer.from(`hl|${gameId}|higher`) }),
143
- new Api.KeyboardButtonCallback({ text: "Lower ⬇️", data: Buffer.from(`hl|${gameId}|lower`) })
144
- ]])
145
- });
146
  }
147
  } else {
148
- user.wallet -= session.bet;
149
- await user.save();
150
- await client.editMessage(update.peer, {
151
- message: update.msgId,
152
- text: `❌ Wrong! The number was **${nextNum}**. You lost $${session.bet}.`,
153
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
154
- });
155
  sessions.delete(gameId);
156
  }
157
  return;
158
  }
159
 
160
- // --- FLIP LOGIC ---
161
- // Data format: flip|gameId|heads or flip|gameId|tails
162
  if (gameType === 'flip') {
163
- const side = parts[2]; // 'heads' or 'tails'
164
- if (side !== 'heads' && side !== 'tails') return;
165
-
166
  const result = Math.random() > 0.5 ? 'heads' : 'tails';
167
  let user = await User.findOne({ userId });
168
  if (!user) user = await User.create({ userId });
169
  const bet = 100;
170
-
171
  if (side === result) {
172
- user.wallet += bet;
173
- await client.editMessage(update.peer, {
174
- message: update.msgId,
175
- text: `🪙 Result: **${result.toUpperCase()}**\n✅ You won $${bet}!`,
176
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
177
- });
178
  } else {
179
- user.wallet -= bet;
180
- await client.editMessage(update.peer, {
181
- message: update.msgId,
182
- text: `🪙 Result: **${result.toUpperCase()}**\n❌ You lost $${bet}!`,
183
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
184
- });
185
  }
186
- await user.save();
187
  return;
188
  }
189
 
190
- // --- RPS LOGIC ---
191
- // Data format: rps|gameId|rock/paper/scissors
192
  if (gameType === 'rps') {
193
  const session = sessions.get(gameId);
194
  if (!session || session.type !== 'rps') return;
195
-
196
  const move = parts[2];
197
- if (!['rock', 'paper', 'scissors'].includes(move)) return;
198
- if (session.moves[userId]) return; // Already moved
199
-
200
  session.moves[userId] = move;
201
  const players = Object.keys(session.moves);
202
-
203
  if (players.length === 2) {
204
- const p1 = players[0];
205
- const p2 = players[1];
206
- const m1 = session.moves[p1];
207
- const m2 = session.moves[p2];
208
-
209
  let winner = null;
210
  if (m1 === m2) winner = 'draw';
211
- else if ((m1 === 'rock' && m2 === 'scissors') ||
212
- (m1 === 'paper' && m2 === 'rock') ||
213
- (m1 === 'scissors' && m2 === 'paper')) winner = p1;
214
  else winner = p2;
215
 
216
- let msg = `✊✌️✋ **RPS RESULT**\n\nPlayer 1: ${m1}\nPlayer 2: ${m2}\n\n`;
217
- if (winner === 'draw') {
218
- msg += "🤝 It's a DRAW!";
219
- } else {
220
- let w = await User.findOne({ userId: winner });
221
- if (!w) w = await User.create({ userId: winner });
222
  const loser = winner === p1 ? p2 : p1;
223
- let l = await User.findOne({ userId: loser });
224
- if (!l) l = await User.create({ userId: loser });
225
- w.wallet += session.bet;
226
- l.wallet -= session.bet;
227
- await w.save();
228
- await l.save();
229
- msg += `🏆 Winner: [${winner}](tg://user?id=${winner})! Won $${session.bet}`;
230
  }
231
-
232
- await client.editMessage(update.peer, {
233
- message: update.msgId,
234
- text: msg,
235
- buttons: new Api.ReplyInlineMarkup({ rows: [] })
236
- });
237
  sessions.delete(gameId);
238
  }
239
  return;
 
2
  const User = require('../models/User');
3
  const sessions = require('../games/sessions');
4
  const { buildMinesGrid } = require('./inlineGames');
5
+ const { editMsg } = require('../utils/editMsg');
6
+ const leveling = require('../utils/leveling');
7
 
8
  const handleInlineCallback = async (client, update) => {
9
  const data = update.data.toString();
10
  const userId = update.userId.toString();
 
 
11
  const parts = data.split("|");
12
+ const gameType = parts[0];
13
  const gameId = parts[1];
14
 
15
+ // --- MINES ---
 
16
  if (gameType === 'mines') {
17
  const session = sessions.get(gameId);
18
+ if (!session || session.type !== 'mines' || session.userId !== userId) return;
19
+ const r = parseInt(parts[2]), c = parseInt(parts[3]);
20
+ if (isNaN(r) || isNaN(c) || session.grid[r][c] !== '⬜') return;
 
 
 
 
 
21
 
22
  const isMine = session.mines.some(m => m.r === r && m.c === c);
23
  if (isMine) {
24
  let user = await User.findOne({ userId });
25
  if (!user) user = await User.create({ userId });
26
+ user.wallet -= session.bet; await user.save();
27
+ await editMsg(client, update.peer, update.msgId, `💥 **BOOM!** You hit a mine. Lost $${session.bet}.`, null);
 
 
 
 
 
28
  sessions.delete(gameId);
29
  } else {
30
  session.grid[r][c] = '💎';
31
  session.revealed++;
32
+ if (session.revealed === 7) {
33
  let user = await User.findOne({ userId });
34
  if (!user) user = await User.create({ userId });
35
+ const prize = session.bet * 5;
36
+ user.wallet += prize; await user.save();
37
+ await editMsg(client, update.peer, update.msgId, `🏆 **CLEAR!** All safe spots found. Won $${prize}!`, null);
 
 
 
 
38
  sessions.delete(gameId);
39
  } else {
40
+ await editMsg(client, update.peer, update.msgId, `💎 Safe! Revealed: ${session.revealed}/7`, buildMinesGrid(gameId, session.grid));
 
 
 
 
41
  }
42
  }
43
  return;
44
  }
45
 
46
+ // --- ROULETTE ---
 
47
  if (gameType === 'roulette') {
48
  let user = await User.findOne({ userId });
49
  if (!user) user = await User.create({ userId });
50
  const bet = 200;
51
+ if (Math.random() < 1/6) {
52
+ user.wallet -= bet; user.isDead = true; user.lastDeath = new Date(); user.health = 0;
53
+ await user.save();
54
+ await editMsg(client, update.peer, update.msgId, `💥 **BANG!** You are DEAD. Lost $${bet}.`, null);
 
 
 
 
 
 
 
 
55
  } else {
56
+ user.wallet += bet * 2; await user.save();
57
+ await editMsg(client, update.peer, update.msgId, `🚩 **CLICK.** Empty chamber. Won $${bet * 2}!`, null);
 
 
 
 
58
  }
 
59
  return;
60
  }
61
 
62
+ // --- HIGHER OR LOWER ---
 
63
  if (gameType === 'hl') {
64
  const session = sessions.get(gameId);
65
+ if (!session || session.type !== 'hl' || session.userId !== userId) return;
66
+ const choice = parts[2];
 
 
 
 
67
  const nextNum = Math.floor(Math.random() * 13) + 1;
68
  let user = await User.findOne({ userId });
69
  if (!user) user = await User.create({ userId });
70
 
 
 
 
71
  if (nextNum === session.lastNum) {
72
+ await editMsg(client, update.peer, update.msgId, `🔄 Same number (**${nextNum}**)! Try again.`, [
73
+ [new Api.KeyboardButtonCallback({ text: "Higher ⬆️", data: Buffer.from(`hl|${gameId}|higher`) }),
74
+ new Api.KeyboardButtonCallback({ text: "Lower ⬇️", data: Buffer.from(`hl|${gameId}|lower`) })]
75
+ ]);
 
 
 
 
 
76
  return;
77
  }
78
+ const isWin = (choice === 'higher' && nextNum > session.lastNum) || (choice === 'lower' && nextNum < session.lastNum);
79
  if (isWin) {
80
+ session.streak++; session.lastNum = nextNum;
 
81
  if (session.streak >= 3) {
82
+ const prize = session.bet * 3;
83
+ user.wallet += prize; await user.save();
84
+ await editMsg(client, update.peer, update.msgId, `🏆 **STREAK x3!** Number was **${nextNum}**. Won $${prize}!`, null);
 
 
 
 
85
  sessions.delete(gameId);
86
  } else {
87
+ await editMsg(client, update.peer, update.msgId, `✅ Correct! Was **${nextNum}**. Streak: ${session.streak}/3\nHigher or Lower than **${nextNum}**?`, [
88
+ [new Api.KeyboardButtonCallback({ text: "Higher ⬆️", data: Buffer.from(`hl|${gameId}|higher`) }),
89
+ new Api.KeyboardButtonCallback({ text: "Lower ⬇️", data: Buffer.from(`hl|${gameId}|lower`) })]
90
+ ]);
 
 
 
 
91
  }
92
  } else {
93
+ user.wallet -= session.bet; await user.save();
94
+ await editMsg(client, update.peer, update.msgId, `❌ Wrong! Was **${nextNum}**. Lost $${session.bet}.`, null);
 
 
 
 
 
95
  sessions.delete(gameId);
96
  }
97
  return;
98
  }
99
 
100
+ // --- FLIP ---
 
101
  if (gameType === 'flip') {
102
+ const side = parts[2];
 
 
103
  const result = Math.random() > 0.5 ? 'heads' : 'tails';
104
  let user = await User.findOne({ userId });
105
  if (!user) user = await User.create({ userId });
106
  const bet = 100;
 
107
  if (side === result) {
108
+ user.wallet += bet; await user.save();
109
+ await editMsg(client, update.peer, update.msgId, `🪙 **${result.toUpperCase()}** ✅ You won $${bet}!`, null);
 
 
 
 
110
  } else {
111
+ user.wallet -= bet; await user.save();
112
+ await editMsg(client, update.peer, update.msgId, `🪙 **${result.toUpperCase()}** ❌ You lost $${bet}!`, null);
 
 
 
 
113
  }
 
114
  return;
115
  }
116
 
117
+ // --- RPS ---
 
118
  if (gameType === 'rps') {
119
  const session = sessions.get(gameId);
120
  if (!session || session.type !== 'rps') return;
 
121
  const move = parts[2];
122
+ if (session.moves[userId]) return;
 
 
123
  session.moves[userId] = move;
124
  const players = Object.keys(session.moves);
 
125
  if (players.length === 2) {
126
+ const [p1, p2] = players;
127
+ const m1 = session.moves[p1], m2 = session.moves[p2];
 
 
 
128
  let winner = null;
129
  if (m1 === m2) winner = 'draw';
130
+ else if ((m1==='rock'&&m2==='scissors')||(m1==='paper'&&m2==='rock')||(m1==='scissors'&&m2==='paper')) winner = p1;
 
 
131
  else winner = p2;
132
 
133
+ let msg = `✊✌️✋ **RPS RESULT**\n\nP1: ${m1} vs P2: ${m2}\n\n`;
134
+ if (winner === 'draw') { msg += "🤝 DRAW!"; }
135
+ else {
 
 
 
136
  const loser = winner === p1 ? p2 : p1;
137
+ let w = await User.findOne({ userId: winner }) || await User.create({ userId: winner });
138
+ let l = await User.findOne({ userId: loser }) || await User.create({ userId: loser });
139
+ w.wallet += session.bet; l.wallet -= session.bet;
140
+ await w.save(); await l.save();
141
+ msg += `🏆 Winner! +$${session.bet}`;
 
 
142
  }
143
+ await editMsg(client, update.peer, update.msgId, msg, null);
 
 
 
 
 
144
  sessions.delete(gameId);
145
  }
146
  return;
handlers/strategyGames.js ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const User = require('../models/User');
2
+ const Chess = require('../games/Chess');
3
+ const Checkers = require('../games/Checkers');
4
+ const sessions = require('../games/sessions');
5
+ const leveling = require('../utils/leveling');
6
+
7
+ // ── /chess <bet> ──
8
+ const startChess = async (client, event) => {
9
+ const bet = parseInt(event.message.message.split(" ")[1]) || 100;
10
+ const userId = event.message.senderId.toString();
11
+ let user = await User.findOne({ userId }) || await User.create({ userId });
12
+ if (user.wallet < bet) return event.message.respond({ message: "Insufficient funds!" });
13
+
14
+ const chatId = event.chatId.toString();
15
+ const key = `chess_${chatId}`;
16
+ if (sessions.get(key)) return event.message.respond({ message: "A chess game is already active here!" });
17
+
18
+ const game = new Chess(userId, bet);
19
+ sessions.set(key, game);
20
+
21
+ await event.message.respond({
22
+ message: `♔ **CHESS** ♚\nBet: $${bet}\nCreated by player. Use /join to play!\n\n♙ White: waiting...\n♟ Black: waiting...\n\nGame starts when opponent joins. Moves: \`/m e2 e4\``
23
+ });
24
+ };
25
+
26
+ // ── /checkers <bet> ──
27
+ const startCheckers = async (client, event) => {
28
+ const bet = parseInt(event.message.message.split(" ")[1]) || 100;
29
+ const userId = event.message.senderId.toString();
30
+ let user = await User.findOne({ userId }) || await User.create({ userId });
31
+ if (user.wallet < bet) return event.message.respond({ message: "Insufficient funds!" });
32
+
33
+ const chatId = event.chatId.toString();
34
+ const key = `checkers_${chatId}`;
35
+ if (sessions.get(key)) return event.message.respond({ message: "A checkers game is already active here!" });
36
+
37
+ const game = new Checkers(userId, bet);
38
+ sessions.set(key, game);
39
+
40
+ await event.message.respond({
41
+ message: `⛀ **CHECKERS (DRAUGHTS)** ⛂\nBet: $${bet}\nCreated by player. Use /join to play!\n\n⛀ White: waiting...\n⛂ Black: waiting...\n\nGame starts when opponent joins. Moves: \`/m a3 b4\`\nFlying Kings enabled! ⛁⛃`
42
+ });
43
+ };
44
+
45
+ // ── /join — also joins chess/checkers ──
46
+ const joinStrategy = async (client, event) => {
47
+ const chatId = event.chatId.toString();
48
+ const userId = event.message.senderId.toString();
49
+
50
+ for (const prefix of ['chess_', 'checkers_']) {
51
+ const key = prefix + chatId;
52
+ const game = sessions.get(key);
53
+ if (!game || game.status !== 'lobby') continue;
54
+ if (game.players.includes(userId)) continue;
55
+
56
+ let user = await User.findOne({ userId }) || await User.create({ userId });
57
+ if (user.wallet < game.bet) {
58
+ await event.message.respond({ message: `Not enough money! Need $${game.bet}` });
59
+ return;
60
+ }
61
+
62
+ game.addPlayer(userId);
63
+ const type = prefix === 'chess_' ? 'Chess' : 'Checkers';
64
+ const icons = prefix === 'chess_' ? ['♙','♟'] : ['⛀','⛂'];
65
+
66
+ await client.sendMessage(chatId, {
67
+ message: `🚀 **${type.toUpperCase()} STARTED!**\nBet: $${game.bet}\n\n${icons[0]} White: Player 1\n${icons[1]} Black: Player 2\n\n${game.renderBoard()}\n\n${icons[0]} White's turn! Use \`/m <from> <to>\`\nExample: \`/m e2 e4\``
68
+ });
69
+ return;
70
+ }
71
+ };
72
+
73
+ // ── /m <from> <to> — make a move ──
74
+ const processMove = async (client, event) => {
75
+ const text = event.message.message;
76
+ if (!text.startsWith("/m ")) return;
77
+
78
+ const chatId = event.chatId.toString();
79
+ const userId = event.message.senderId.toString();
80
+ const parts = text.split(" ");
81
+ if (parts.length < 3) return event.message.respond({ message: "Usage: `/m e2 e4`" });
82
+
83
+ const fromStr = parts[1].toLowerCase();
84
+ const toStr = parts[2].toLowerCase();
85
+
86
+ // Try chess first, then checkers
87
+ for (const prefix of ['chess_', 'checkers_']) {
88
+ const key = prefix + chatId;
89
+ const game = sessions.get(key);
90
+ if (!game || game.status !== 'playing') continue;
91
+
92
+ const result = game.makeMove(userId, fromStr, toStr);
93
+ if (result.error) {
94
+ return event.message.respond({ message: `❌ ${result.error}` });
95
+ }
96
+
97
+ const type = prefix === 'chess_' ? 'Chess' : 'Checkers';
98
+ const icons = prefix === 'chess_' ? ['♙','♟'] : ['⛀','⛂'];
99
+
100
+ // Check winner
101
+ const winner = game.checkWinner();
102
+ if (winner) {
103
+ let msg = "";
104
+ if (winner === 'draw') {
105
+ msg = `🤝 **${type.toUpperCase()} — DRAW!**\n\n${game.renderBoard()}\n\nMoney returned.`;
106
+ } else {
107
+ const loserId = game.players.find(id => id !== winner);
108
+ let w = await User.findOne({ userId: winner }) || await User.create({ userId: winner });
109
+ let l = await User.findOne({ userId: loserId }) || await User.create({ userId: loserId });
110
+ w.wallet += game.bet; l.wallet -= game.bet;
111
+ const xp = await leveling.addXP(winner, 200);
112
+ await w.save(); await l.save();
113
+
114
+ msg = `🏆 **${type.toUpperCase()} — GAME OVER!**\n\n${game.renderBoard()}\n\n🎉 Winner gets $${game.bet} + 200 XP!`;
115
+ if (xp.leveledUp) msg += `\n🆙 Level ${xp.level}!`;
116
+ }
117
+ await client.sendMessage(chatId, { message: msg });
118
+ sessions.delete(key);
119
+ return;
120
+ }
121
+
122
+ // Show board + next turn
123
+ const turnIdx = game.turn;
124
+ const captured = result.captured ? ` (captured ${result.captured})` : (result.capture ? " 💥 Capture!" : "");
125
+ await client.sendMessage(chatId, {
126
+ message: `${icons[game.turn === 0 ? 0 : 1]} **${type}** — Move: ${fromStr}→${toStr}${captured}\n\n${game.renderBoard()}\n\n${icons[turnIdx]} Player ${turnIdx + 1}'s turn! Use \`/m <from> <to>\``
127
+ });
128
+ return;
129
+ }
130
+ };
131
+
132
+ // ── /surrender — forfeit current game ──
133
+ const surrender = async (client, event) => {
134
+ const chatId = event.chatId.toString();
135
+ const userId = event.message.senderId.toString();
136
+
137
+ for (const prefix of ['chess_', 'checkers_']) {
138
+ const key = prefix + chatId;
139
+ const game = sessions.get(key);
140
+ if (!game || game.status !== 'playing') continue;
141
+ if (!game.players.includes(userId)) continue;
142
+
143
+ const winner = game.players.find(id => id !== userId);
144
+ let w = await User.findOne({ userId: winner }) || await User.create({ userId: winner });
145
+ let l = await User.findOne({ userId: userId }) || await User.create({ userId: userId });
146
+ w.wallet += game.bet; l.wallet -= game.bet;
147
+ await leveling.addXP(winner, 100);
148
+ await w.save(); await l.save();
149
+
150
+ const type = prefix === 'chess_' ? 'Chess' : 'Checkers';
151
+ await client.sendMessage(chatId, {
152
+ message: `🏳️ **${type.toUpperCase()} — SURRENDER!**\n\nA player surrendered. Winner gets $${game.bet}!`
153
+ });
154
+ sessions.delete(key);
155
+ return;
156
+ }
157
+ };
158
+
159
+ module.exports = { startChess, startCheckers, joinStrategy, processMove, surrender };
models/User.js CHANGED
@@ -17,6 +17,10 @@ const UserSchema = new mongoose.Schema({
17
  lastKill: { type: Date, default: null },
18
  xp: { type: Number, default: 0 },
19
  level: { type: Number, default: 1 },
 
 
 
 
20
  });
21
 
22
  module.exports = mongoose.model('User', UserSchema);
 
17
  lastKill: { type: Date, default: null },
18
  xp: { type: Number, default: 0 },
19
  level: { type: Number, default: 1 },
20
+ // Leaderboard stats
21
+ kills: { type: Number, default: 0 },
22
+ robs: { type: Number, default: 0 },
23
+ totalEarned: { type: Number, default: 0 },
24
  });
25
 
26
  module.exports = mongoose.model('User', UserSchema);
utils/editMsg.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Helper to edit a message and optionally clear inline buttons.
3
+ * Uses raw client.invoke to avoid GramJS wrapper issues with empty ReplyInlineMarkup.
4
+ */
5
+ const { Api } = require("telegram");
6
+
7
+ /**
8
+ * Edit message text and optionally set/clear buttons.
9
+ * @param {TelegramClient} client
10
+ * @param {*} peer - The peer (from update.peer or chatId)
11
+ * @param {number} msgId - Message ID to edit
12
+ * @param {string} text - New message text
13
+ * @param {Array|null} buttons - 2D array of buttons, or null to remove all buttons
14
+ */
15
+ async function editMsg(client, peer, msgId, text, buttons) {
16
+ try {
17
+ let replyMarkup;
18
+ if (buttons === null || buttons === undefined) {
19
+ // Remove all inline buttons
20
+ replyMarkup = new Api.ReplyInlineMarkup({ rows: [] });
21
+ } else if (Array.isArray(buttons) && buttons.length > 0) {
22
+ replyMarkup = client.buildReplyMarkup(buttons);
23
+ } else {
24
+ replyMarkup = new Api.ReplyInlineMarkup({ rows: [] });
25
+ }
26
+
27
+ const inputPeer = await client.getInputEntity(peer);
28
+ await client.invoke(new Api.messages.EditMessage({
29
+ peer: inputPeer,
30
+ id: msgId,
31
+ message: text,
32
+ replyMarkup: replyMarkup,
33
+ }));
34
+ } catch (e) {
35
+ // If REPLY_MARKUP_INVALID, retry without markup (just edit text, buttons stay)
36
+ if (e.message && e.message.includes('REPLY_MARKUP_INVALID')) {
37
+ try {
38
+ const inputPeer = await client.getInputEntity(peer);
39
+ await client.invoke(new Api.messages.EditMessage({
40
+ peer: inputPeer,
41
+ id: msgId,
42
+ message: text,
43
+ }));
44
+ } catch (e2) {
45
+ console.error('[editMsg] fallback failed:', e2.message);
46
+ }
47
+ } else {
48
+ console.error('[editMsg] error:', e.message);
49
+ }
50
+ }
51
+ }
52
+
53
+ module.exports = { editMsg };
webapp/index.html CHANGED
@@ -3,596 +3,618 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Rewards | Alexa Game</title>
7
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
8
-
9
- <!-- Unified OnClickA Loader -->
10
  <script async src="https://js.onclckmn.com/static/onclicka.js" data-admpid="443657"></script>
11
- <!-- OnClickA TMA SDK -->
12
  <script src="https://js.onclckvd.com/in-stream-ad-admanager/tma.js"></script>
13
-
14
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
15
  <style>
16
- :root {
17
- --bg-color: #0c0f1d;
18
- --card-bg: rgba(23, 28, 48, 0.85);
19
- --accent-glow: #2481cc;
20
- --accent-green: #31b545;
21
- --accent-orange: #ff8a00;
22
- --text-main: #ffffff;
23
- --text-dim: #94a3b8;
24
- --glass-border: rgba(255, 255, 255, 0.12);
25
- }
26
- body {
27
- font-family: 'Outfit', sans-serif;
28
- background: var(--bg-color);
29
- background-image: radial-gradient(circle at 50% -20%, var(--accent-glow), transparent 60%), radial-gradient(circle at 0% 100%, #1a1532, transparent 50%);
30
- color: var(--text-main);
31
- margin: 0;
32
- padding: 16px;
33
- display: flex;
34
- flex-direction: column;
35
- align-items: center;
36
- min-height: 100vh;
37
- overflow-x: hidden;
38
- }
39
- .header {
40
- text-align: center;
41
- margin-bottom: 20px;
42
- }
43
- .header h1 {
44
- font-size: 26px;
45
- margin: 0;
46
- background: linear-gradient(135deg, #fff 0%, var(--text-dim) 100%);
47
- -webkit-background-clip: text;
48
- background-clip: text;
49
- -webkit-text-fill-color: transparent;
50
- text-transform: uppercase;
51
- letter-spacing: 1.5px;
52
- }
53
- .header p {
54
- color: var(--text-dim);
55
- font-size: 13px;
56
- margin: 6px 0 0;
57
- }
58
- .hub {
59
- width: 100%;
60
- max-width: 420px;
61
- display: flex;
62
- flex-direction: column;
63
- gap: 16px;
64
- }
65
- .reward-card {
66
- background: var(--card-bg);
67
- backdrop-filter: blur(12px);
68
- border-radius: 20px;
69
- border: 1px solid var(--glass-border);
70
- padding: 20px;
71
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
72
- display: flex;
73
- flex-direction: column;
74
- align-items: center;
75
- text-align: center;
76
- }
77
- .reward-card h2 {
78
- margin: 0 0 8px;
79
- font-size: 20px;
80
- display: flex;
81
- align-items: center;
82
- gap: 8px;
83
- }
84
- .reward-card .amount {
85
- font-size: 28px;
86
- font-weight: 700;
87
- color: var(--accent-orange);
88
- margin: 8px 0;
89
- }
90
- .reward-card p {
91
- font-size: 14px;
92
- color: var(--text-dim);
93
- margin: 0 0 16px;
94
- line-height: 1.4;
95
- }
96
- .btn {
97
- width: 100%;
98
- padding: 14px 20px;
99
- border: none;
100
- border-radius: 14px;
101
- font-size: 15px;
102
- font-weight: 700;
103
- cursor: pointer;
104
- background: linear-gradient(135deg, var(--accent-glow) 0%, #1a6bb1 100%);
105
- color: white;
106
- transition: all 0.2s;
107
- text-transform: uppercase;
108
- letter-spacing: 0.5px;
109
- }
110
- .btn:hover:not(:disabled) {
111
- transform: translateY(-1px);
112
- box-shadow: 0 6px 16px rgba(36, 129, 204, 0.4);
113
- }
114
- .btn:disabled {
115
- opacity: 0.5;
116
- cursor: not-allowed;
117
- }
118
- .btn.mission {
119
- background: linear-gradient(135deg, #3a4cf0 0%, #202eb2 100%);
120
- }
121
- .btn.success {
122
- background: linear-gradient(135deg, #31b545 0%, #228b22 100%);
123
- }
124
- .flow-container {
125
- width: 100%;
126
- max-width: 400px;
127
- display: none;
128
- }
129
- .flow-card {
130
- background: var(--card-bg);
131
- backdrop-filter: blur(15px);
132
- border-radius: 20px;
133
- border: 1px solid var(--glass-border);
134
- padding: 24px;
135
- text-align: center;
136
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
137
- }
138
- .icon-badge {
139
- font-size: 48px;
140
- margin-bottom: 12px;
141
- display: block;
142
- }
143
- .timer-val {
144
- font-size: 42px;
145
- font-weight: 700;
146
- color: var(--accent-orange);
147
- }
148
- .progress-box {
149
- width: 100%;
150
- height: 8px;
151
- background: rgba(255,255,255,0.1);
152
- border-radius: 4px;
153
- overflow: hidden;
154
- margin: 12px 0;
155
- }
156
- .progress-bar {
157
- height: 100%;
158
- width: 0%;
159
- background: var(--accent-orange);
160
- transition: width 1s linear;
161
- }
162
- .back-btn {
163
- margin-top: 16px;
164
- background: transparent;
165
- border: 1px solid var(--glass-border);
166
- color: var(--text-dim);
167
- padding: 10px;
168
- font-size: 13px;
169
- }
170
- .status-text {
171
- font-size: 14px;
172
- color: var(--text-dim);
173
- margin-bottom: 12px;
174
- }
175
- @media (max-width: 480px) {
176
- .hub { gap: 12px; }
177
- }
 
 
 
 
 
 
 
 
 
 
 
 
178
  </style>
179
  </head>
180
  <body>
181
- <div class="header">
182
- <h1>🎁 Reward Center</h1>
183
- <p>Watch ads • Earn coins • Support the game</p>
184
- </div>
185
-
186
- <!-- Hub -->
187
- <div id="hub-view" class="hub">
188
- <div class="reward-card">
189
- <h2>💎 Daily Reward</h2>
190
- <div class="amount">$3,000</div>
191
- <p>Claim your daily bonus once every 24 hours.<br>Requires watching a short rewarded ad.</p>
192
- <button id="daily-btn" class="btn" onclick="selectDaily()">🚀 Claim Daily</button>
193
- <div id="daily-status" class="status-text" style="margin-top:8px; font-size:12px;"></div>
194
- </div>
195
 
196
- <div class="reward-card">
197
- <h2>🎯 Other Rewards</h2>
198
- <div class="amount">$200 × 50</div>
199
- <p>Complete missions by watching in-page ads.<br>Up to 50 claims per day.</p>
200
- <button id="mission-btn" class="btn mission" onclick="selectMission()">🎯 Start Missions</button>
201
- <div id="mission-status" class="status-text" style="margin-top:8px; font-size:12px;"></div>
 
 
 
 
 
 
 
 
 
 
202
  </div>
203
  </div>
204
 
205
- <!-- Daily Flow -->
206
- <div id="daily-flow" class="flow-container">
207
- <div class="flow-card">
208
- <div id="daily-setup">
209
- <span class="icon-badge">💎</span>
210
- <p id="daily-status-text">Watch a short ad to claim your $3000 bonus.</p>
211
- <button id="daily-start-btn" class="btn" onclick="startDailyFlow()">🚀 Watch &amp; Claim</button>
212
- </div>
213
- <div id="daily-timer-ui" style="display:none;">
214
- <div class="timer-val" id="daily-sec">30</div>
215
- <div class="progress-box">
216
- <div id="daily-progress" class="progress-bar"></div>
217
- </div>
218
- <p>Verification in progress...</p>
219
- </div>
220
- <button id="daily-claim-btn" class="btn success" style="display:none; margin-top: 10px;" onclick="handleDailyClaim()">🎁 Claim Your Reward $3,000</button>
221
-
222
- <div id="daily-result-view" style="display:none;">
223
- <span class="icon-badge">✅</span>
224
- <h2 style="margin:8px 0;">Success!</h2>
225
- <p>Your <span style="color:var(--accent-orange);font-weight:800;">$3000</span> reward added to wallet.</p>
226
- <p style="font-size:12px;">Closing in 3s...</p>
227
- </div>
228
- <div id="daily-error-view" style="display:none;">
229
- <span class="icon-badge">❌</span>
230
- <h2 style="margin:8px 0;">Error</h2>
231
- <p id="daily-error-msg" style="color:#ff6b6b;"></p>
232
- </div>
233
- <button class="btn back-btn" onclick="backToHub()">← Back to Rewards</button>
234
  </div>
235
  </div>
236
 
237
- <!-- Mission Flow -->
238
- <div id="mission-flow" class="flow-container">
239
- <div class="flow-card">
240
- <div id="mission-setup">
241
- <span class="icon-badge">🎯</span>
242
- <p id="mission-status-text">Watch in-page ads to earn $200 each (up to 50/day).</p>
243
- <div id="mission-progress-info" style="margin:12px 0; font-size:13px; color:var(--text-dim);"></div>
244
- <button id="mission-start-btn" class="btn mission" onclick="startMissionFlow()">🎯 Watch Ad &amp; Earn $200</button>
245
- </div>
246
- <div id="mission-timer-ui" style="display:none;">
247
- <div class="timer-val" id="mission-sec">30</div>
248
- <div class="progress-box">
249
- <div id="mission-progress" class="progress-bar"></div>
250
- </div>
251
- <p>Verification in progress...</p>
252
- </div>
253
- <button id="mission-claim-btn" class="btn mission" style="display:none; margin-top: 10px;" onclick="handleMissionClaim()">🎁 Claim $200 Mission Reward</button>
254
- <div id="mission-success-view" style="display:none; text-align:center;">
255
- <span class="icon-badge">🏆</span>
256
- <h2 style="margin-bottom:8px;">Mission Complete!</h2>
257
- <p>$200 added to your wallet.</p>
258
- <button class="btn" style="margin-top:12px;" onclick="resetMission()">Watch Another</button>
259
- <button class="btn back-btn" style="margin-top:8px;" onclick="backToHub()">Back to Hub</button>
260
- </div>
261
- <div id="mission-error-view" style="display:none;">
262
- <span class="icon-badge">❌</span>
263
- <p id="mission-error-msg" style="color:#ff6b6b;"></p>
264
- </div>
265
- <button class="btn back-btn" onclick="backToHub()">← Back to Rewards</button>
266
  </div>
267
  </div>
 
268
 
269
- <script>
270
- const webapp = window.Telegram.WebApp;
271
- webapp.expand();
272
- webapp.ready();
273
-
274
- // ── Auth check: block if no initData ──
275
- // KeyboardButtonWebView (inline button) provides full initData with user id.
276
- // Direct browser access won't have it.
277
- if (!webapp || !webapp.initData || !webapp.initDataUnsafe || !webapp.initDataUnsafe.user || !webapp.initDataUnsafe.user.id) {
278
- document.body.innerHTML = `
279
- <div style="padding:40px 20px; text-align:center; font-family: 'Outfit', sans-serif; background:#0c0f1d; color:#fff; min-height:100vh;">
280
- <h1 style="font-size:28px; margin-bottom:10px;">🔒 Access Blocked</h1>
281
- <p style="font-size:16px; max-width:320px; margin:0 auto 20px;">This reward mini-app page <strong>requires</strong> being opened from inside Telegram as a Mini App.</p>
282
- <p style="margin-bottom:20px;">Please go to <a href="https://t.me/Alexagamebot" style="color:#2481cc; text-decoration:underline;">@Alexagamebot</a> and send <strong>/daily</strong> or <strong>/missions</strong> to open it correctly.</p>
283
- <p style="font-size:12px; color:#94a3b8; border-top:1px solid rgba(255,255,255,0.1); padding-top:15px; margin-top:30px;">
284
- No Telegram user ID / initData detected.<br>
285
- Direct links or browser access are intentionally blocked for security and to enforce daily limits.
286
- </p>
287
- </div>
288
- `;
289
- throw new Error("Blocked: no TG initData");
290
- }
291
-
292
- const API_BASE = window.location.origin;
293
- const REWARD_SPOT = "6120581";
294
- const INPAGE_SPOT = "6120578";
295
- const MISSION_SPOT = "6120581";
296
-
297
- let triggerRewardAd = null;
298
- let triggerInPageAd = null;
299
- let triggerMissionAd = null;
300
- let dailyTimeLeft = 30;
301
- let missionAdsLeft = 50;
302
-
303
- // ── Helper: call backend API with initData auth ──
304
- async function apiCall(method, path, body) {
305
- const opts = {
306
- method,
307
- headers: {
308
- 'X-Init-Data': webapp.initData,
309
- 'Content-Type': 'application/json'
310
- }
311
- };
312
- if (body) opts.body = JSON.stringify(body);
313
- const res = await fetch(`${API_BASE}${path}`, opts);
314
- return res.json();
315
- }
316
-
317
- async function updateHubStatus() {
318
- if (!webapp.initData) return;
319
-
320
- try {
321
- const d = await apiCall('GET', '/status?type=daily');
322
- const dBtn = document.getElementById('daily-btn');
323
- const dStatus = document.getElementById('daily-status');
324
- if (!d.canClaim) {
325
- dBtn.disabled = true;
326
- dBtn.textContent = '⏳ Claimed';
327
- dStatus.innerHTML = 'Come back tomorrow after 00:00';
328
- } else {
329
- dBtn.disabled = false;
330
- dBtn.textContent = '🚀 Claim Daily';
331
- dStatus.innerHTML = 'Available now!';
332
- }
333
- } catch(e){}
334
-
335
- try {
336
- const m = await apiCall('GET', '/status?type=mission_inpage');
337
- const mBtn = document.getElementById('mission-btn');
338
- const mStatus = document.getElementById('mission-status');
339
- missionAdsLeft = m.adsLeft ?? 50;
340
- if (missionAdsLeft <= 0) {
341
- mBtn.disabled = true;
342
- mBtn.textContent = '✅ Limit Reached';
343
- mStatus.innerHTML = '50/50 today. Reset tomorrow.';
344
- } else {
345
- mBtn.disabled = false;
346
- mBtn.textContent = '🎯 Start Missions';
347
- mStatus.innerHTML = `${missionAdsLeft} ads left today`;
348
- }
349
- } catch(e){}
350
- }
351
-
352
- // Init ads
353
- window.initCdTma?.({ id: REWARD_SPOT })
354
- .then(show => { triggerRewardAd = show; console.log('Rewarded ready'); })
355
- .catch(e => console.error(e));
356
-
357
- window.initCdTma?.({ id: INPAGE_SPOT })
358
- .then(show => { triggerInPageAd = show; console.log('Inpage ready'); })
359
- .catch(e => console.error(e));
360
-
361
- window.initCdTma?.({ id: MISSION_SPOT })
362
- .then(show => { triggerMissionAd = show; console.log('Mission ad ready'); })
363
- .catch(e => console.error(e));
364
-
365
- // ── Navigation ──
366
- function selectDaily() {
367
- document.getElementById('hub-view').style.display = 'none';
368
- document.getElementById('daily-flow').style.display = 'block';
369
- document.getElementById('mission-flow').style.display = 'none';
370
- document.getElementById('daily-setup').style.display = 'block';
371
- document.getElementById('daily-timer-ui').style.display = 'none';
372
- document.getElementById('daily-claim-btn').style.display = 'none';
373
- document.getElementById('daily-result-view').style.display = 'none';
374
- document.getElementById('daily-error-view').style.display = 'none';
375
- document.getElementById('daily-start-btn').disabled = false;
376
- }
377
-
378
- function selectMission() {
379
- document.getElementById('hub-view').style.display = 'none';
380
- document.getElementById('daily-flow').style.display = 'none';
381
- document.getElementById('mission-flow').style.display = 'block';
382
- document.getElementById('mission-setup').style.display = 'block';
383
- document.getElementById('mission-timer-ui').style.display = 'none';
384
- document.getElementById('mission-claim-btn').style.display = 'none';
385
- document.getElementById('mission-success-view').style.display = 'none';
386
- document.getElementById('mission-error-view').style.display = 'none';
387
- updateMissionUI();
388
- }
389
-
390
- function backToHub() {
391
- document.getElementById('hub-view').style.display = 'flex';
392
- document.getElementById('daily-flow').style.display = 'none';
393
- document.getElementById('mission-flow').style.display = 'none';
394
- updateHubStatus();
395
- }
396
-
397
- // ── Daily status check ──
398
- async function checkDailyStatus() {
399
- try {
400
- const data = await apiCall('GET', '/status?type=daily');
401
- if (!data.canClaim) {
402
- document.getElementById('daily-status-text').innerHTML = `Already claimed today!<br><span style="font-size:12px">Come back tomorrow after 00:00!</span>`;
403
- document.getElementById('daily-start-btn').style.display = 'none';
404
- document.getElementById('daily-claim-btn').style.display = 'none';
405
- return false;
406
- }
407
- return true;
408
- } catch (e) {
409
- console.error('checkDailyStatus error', e);
410
- document.getElementById('daily-status-text').innerHTML = '⏳ Verification failed. Please reopen from /daily in bot.';
411
- document.getElementById('daily-start-btn').style.display = 'none';
412
- return false;
413
- }
414
- }
415
-
416
- function updateMissionUI() {
417
- const info = document.getElementById('mission-progress-info');
418
- info.textContent = `${50 - missionAdsLeft} / 50 ads watched today`;
419
- const btn = document.getElementById('mission-start-btn');
420
- if (missionAdsLeft <= 0) {
421
- btn.disabled = true;
422
- btn.textContent = '✅ Daily Limit Reached';
423
- } else {
424
- btn.disabled = !triggerMissionAd;
425
- btn.textContent = triggerMissionAd ? '🎯 Watch Ad & Earn $200' : '⌛ Loading...';
426
- }
427
- }
428
-
429
- // ── Daily flow ──
430
- async function startDailyFlow() {
431
- if (!triggerRewardAd) {
432
- alert("Ad loading, please try again.");
433
- return;
434
- }
435
- const can = await checkDailyStatus();
436
- if (!can) return;
437
-
438
- document.getElementById('daily-start-btn').disabled = true;
439
-
440
- triggerRewardAd()
441
- .then(() => {
442
- document.getElementById('daily-setup').style.display = 'none';
443
- document.getElementById('daily-timer-ui').style.display = 'block';
444
- dailyTimeLeft = 30;
445
- document.getElementById('daily-sec').innerText = dailyTimeLeft;
446
- document.getElementById('daily-progress').style.width = '0%';
447
-
448
- if (triggerInPageAd) {
449
- triggerInPageAd().catch(e => console.error("Inpage failed", e));
450
- }
451
-
452
- const countdown = setInterval(() => {
453
- dailyTimeLeft--;
454
- document.getElementById('daily-sec').innerText = dailyTimeLeft;
455
- document.getElementById('daily-progress').style.width = ((30 - dailyTimeLeft) / 30 * 100) + '%';
456
-
457
- if (dailyTimeLeft <= 0) {
458
- clearInterval(countdown);
459
- document.getElementById('daily-timer-ui').style.display = 'none';
460
- document.getElementById('daily-claim-btn').style.display = 'block';
461
- }
462
- }, 1000);
463
- })
464
- .catch(() => {
465
- document.getElementById('daily-start-btn').disabled = false;
466
- });
467
- }
468
-
469
- // ── Claim daily reward via HTTP POST /claim (not sendData!) ──
470
- async function handleDailyClaim() {
471
- document.getElementById('daily-claim-btn').disabled = true;
472
- document.getElementById('daily-claim-btn').textContent = '⏳ Claiming...';
473
-
474
- try {
475
- const result = await apiCall('POST', '/claim', { type: 'daily' });
476
- if (result.ok) {
477
- document.getElementById('daily-claim-btn').style.display = 'none';
478
- document.getElementById('daily-result-view').style.display = 'block';
479
- if (webapp.HapticFeedback) webapp.HapticFeedback.notificationOccurred('success');
480
- setTimeout(() => { webapp.close(); }, 3000);
481
- } else {
482
- document.getElementById('daily-claim-btn').style.display = 'none';
483
- document.getElementById('daily-error-view').style.display = 'block';
484
- document.getElementById('daily-error-msg').textContent = result.error || 'Claim failed';
485
- }
486
- } catch (e) {
487
- document.getElementById('daily-claim-btn').style.display = 'none';
488
- document.getElementById('daily-error-view').style.display = 'block';
489
- document.getElementById('daily-error-msg').textContent = 'Network error. Try again.';
490
- }
491
- }
492
-
493
- // ── Claim mission reward via HTTP POST /claim ──
494
- async function handleMissionClaim() {
495
- document.getElementById('mission-claim-btn').disabled = true;
496
- document.getElementById('mission-claim-btn').textContent = '⏳ Claiming...';
497
-
498
- try {
499
- const result = await apiCall('POST', '/claim', { type: 'mission' });
500
- if (result.ok) {
501
- document.getElementById('mission-timer-ui').style.display = 'none';
502
- document.getElementById('mission-claim-btn').style.display = 'none';
503
- document.getElementById('mission-success-view').style.display = 'block';
504
- if (webapp.HapticFeedback) webapp.HapticFeedback.notificationOccurred('success');
505
- missionAdsLeft = result.adsLeft ?? Math.max(0, missionAdsLeft - 1);
506
- setTimeout(() => { webapp.close(); }, 3000);
507
- } else {
508
- document.getElementById('mission-claim-btn').style.display = 'none';
509
- document.getElementById('mission-error-view').style.display = 'block';
510
- document.getElementById('mission-error-msg').textContent = result.error || 'Claim failed';
511
- }
512
- } catch (e) {
513
- document.getElementById('mission-claim-btn').style.display = 'none';
514
- document.getElementById('mission-error-view').style.display = 'block';
515
- document.getElementById('mission-error-msg').textContent = 'Network error. Try again.';
516
- }
517
- }
518
-
519
- // ── Mission flow ──
520
- async function startMissionFlow() {
521
- if (!triggerMissionAd) {
522
- alert("Ad loading, try again.");
523
- return;
524
- }
525
- try {
526
- const m = await apiCall('GET', '/status?type=mission_inpage');
527
- if (!m.canClaim || (m.adsLeft ?? 0) <= 0) {
528
- alert("⏳ You've reached the limit of 50 ads for today. Come back tomorrow after 00:00!");
529
- backToHub();
530
- return;
531
- }
532
- missionAdsLeft = m.adsLeft ?? 50;
533
- } catch (e) {
534
- console.error(e);
535
- alert("Status check failed. Try again.");
536
- return;
537
- }
538
-
539
- const btn = document.getElementById('mission-start-btn');
540
- btn.disabled = true;
541
- btn.textContent = "⌛ Watching...";
542
-
543
- triggerMissionAd()
544
- .then(() => {
545
- document.getElementById('mission-setup').style.display = 'none';
546
- document.getElementById('mission-timer-ui').style.display = 'block';
547
- let mTimeLeft = 30;
548
- document.getElementById('mission-sec').innerText = mTimeLeft;
549
- document.getElementById('mission-progress').style.width = '0%';
550
-
551
- const mCountdown = setInterval(() => {
552
- mTimeLeft--;
553
- document.getElementById('mission-sec').innerText = mTimeLeft;
554
- document.getElementById('mission-progress').style.width = ((30 - mTimeLeft) / 30 * 100) + '%';
555
-
556
- if (mTimeLeft <= 0) {
557
- clearInterval(mCountdown);
558
- document.getElementById('mission-timer-ui').style.display = 'none';
559
- document.getElementById('mission-claim-btn').style.display = 'block';
560
- document.getElementById('mission-claim-btn').disabled = false;
561
- document.getElementById('mission-claim-btn').textContent = '🎁 Claim $200 Mission Reward';
562
- }
563
- }, 1000);
564
- })
565
- .catch(() => {
566
- btn.disabled = false;
567
- btn.textContent = "🎯 Watch Ad & Earn $200";
568
- });
569
- }
570
-
571
- function resetMission() {
572
- document.getElementById('mission-success-view').style.display = 'none';
573
- document.getElementById('mission-timer-ui').style.display = 'none';
574
- document.getElementById('mission-claim-btn').style.display = 'none';
575
- document.getElementById('mission-error-view').style.display = 'none';
576
- document.getElementById('mission-setup').style.display = 'block';
577
- updateMissionUI();
578
- }
579
 
580
- // ── Init ──
581
- async function init() {
582
- await updateHubStatus();
 
 
 
 
583
 
584
- const params = new URLSearchParams(window.location.search);
585
- const type = params.get('type');
586
- if (type === 'daily') {
587
- setTimeout(() => selectDaily(), 300);
588
- } else if (type === 'mission') {
589
- setTimeout(() => selectMission(), 300);
590
- }
 
591
 
592
- setInterval(updateHubStatus, 30000);
593
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
- init();
596
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  </body>
598
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Alexa Game</title>
7
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
 
 
8
  <script async src="https://js.onclckmn.com/static/onclicka.js" data-admpid="443657"></script>
 
9
  <script src="https://js.onclckvd.com/in-stream-ad-admanager/tma.js"></script>
 
 
10
  <style>
11
+ *{margin:0;padding:0;box-sizing:border-box;}
12
+ :root{
13
+ --bg:#0a0e1a;--card:#141929;--card2:#1a2035;
14
+ --accent:#3b82f6;--green:#22c55e;--orange:#f59e0b;--red:#ef4444;
15
+ --text:#f1f5f9;--dim:#64748b;--border:rgba(255,255,255,.08);
16
+ --radius:16px;
17
+ }
18
+ body{
19
+ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
20
+ background:var(--bg);color:var(--text);
21
+ min-height:100vh;display:flex;flex-direction:column;
22
+ -webkit-user-select:none;user-select:none;
23
+ padding-bottom:72px;
24
+ }
25
+
26
+ /* ── Top Header ── */
27
+ .top-bar{
28
+ display:flex;align-items:center;justify-content:space-between;
29
+ padding:14px 16px 8px;
30
+ }
31
+ .top-bar .logo{font-size:18px;font-weight:800;letter-spacing:-.5px;}
32
+ .top-bar .logo span{color:var(--accent);}
33
+ .top-bar .coins{
34
+ background:var(--card);border:1px solid var(--border);
35
+ border-radius:20px;padding:6px 14px;font-size:13px;font-weight:700;
36
+ display:flex;align-items:center;gap:6px;
37
+ }
38
+
39
+ /* ── Bottom Tab Bar ── */
40
+ .tab-bar{
41
+ position:fixed;bottom:0;left:0;right:0;
42
+ background:var(--card);border-top:1px solid var(--border);
43
+ display:flex;z-index:100;
44
+ padding:6px 0 env(safe-area-inset-bottom,8px);
45
+ }
46
+ .tab-bar .tab{
47
+ flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;
48
+ padding:8px 0;cursor:pointer;opacity:.45;transition:.2s;font-size:10px;font-weight:600;
49
+ }
50
+ .tab-bar .tab.active{opacity:1;color:var(--accent);}
51
+ .tab-bar .tab .ico{font-size:20px;}
52
+
53
+ /* ── Pages ── */
54
+ .page{display:none;flex-direction:column;gap:14px;padding:8px 16px 16px;flex:1;}
55
+ .page.active{display:flex;}
56
+
57
+ /* ── Reward Cards ── */
58
+ .rw-card{
59
+ background:var(--card);border:1px solid var(--border);
60
+ border-radius:var(--radius);padding:18px;
61
+ display:flex;align-items:center;gap:14px;
62
+ }
63
+ .rw-card .rw-icon{font-size:36px;flex-shrink:0;}
64
+ .rw-card .rw-body{flex:1;min-width:0;}
65
+ .rw-card .rw-title{font-size:15px;font-weight:700;margin-bottom:2px;}
66
+ .rw-card .rw-sub{font-size:12px;color:var(--dim);}
67
+ .rw-card .rw-amount{font-size:20px;font-weight:800;color:var(--orange);margin:4px 0;}
68
+ .rw-btn{
69
+ width:100%;padding:11px;border:none;border-radius:12px;
70
+ font-size:13px;font-weight:700;cursor:pointer;color:#fff;
71
+ transition:.15s;margin-top:6px;
72
+ }
73
+ .rw-btn.blue{background:var(--accent);}
74
+ .rw-btn.green{background:var(--green);}
75
+ .rw-btn.gray{background:#334155;color:var(--dim);cursor:default;}
76
+ .rw-btn:disabled{opacity:.5;cursor:not-allowed;}
77
+
78
+ /* ── Section headers ── */
79
+ .section-hdr{
80
+ font-size:14px;font-weight:700;color:var(--dim);
81
+ text-transform:uppercase;letter-spacing:1px;margin-top:6px;
82
+ }
83
+
84
+ /* ── Leaderboard preview buttons ── */
85
+ .lb-btns{display:flex;flex-direction:column;gap:8px;}
86
+ .lb-link{
87
+ background:var(--card);border:1px solid var(--border);
88
+ border-radius:14px;padding:14px 16px;
89
+ display:flex;align-items:center;justify-content:space-between;
90
+ cursor:pointer;transition:.15s;
91
+ }
92
+ .lb-link:active{transform:scale(.98);background:var(--card2);}
93
+ .lb-link .ll-left{display:flex;align-items:center;gap:10px;}
94
+ .lb-link .ll-icon{font-size:22px;}
95
+ .lb-link .ll-text{font-size:14px;font-weight:600;}
96
+ .lb-link .ll-arrow{color:var(--dim);font-size:18px;}
97
+
98
+ /* ── Leaderboard Page ── */
99
+ .lb-tabs{
100
+ display:flex;gap:6px;padding:2px;background:var(--card);
101
+ border-radius:12px;border:1px solid var(--border);
102
+ }
103
+ .lb-tab{
104
+ flex:1;padding:9px 4px;border:none;background:none;
105
+ color:var(--dim);font-size:12px;font-weight:700;
106
+ border-radius:10px;cursor:pointer;transition:.15s;text-align:center;
107
+ }
108
+ .lb-tab.active{background:var(--accent);color:#fff;}
109
+
110
+ .lb-type-tabs{
111
+ display:flex;gap:6px;overflow-x:auto;padding-bottom:4px;
112
+ -webkit-overflow-scrolling:touch;
113
+ }
114
+ .lb-type-tabs::-webkit-scrollbar{display:none;}
115
+ .lb-type-btn{
116
+ padding:8px 14px;border:1px solid var(--border);background:var(--card);
117
+ color:var(--dim);font-size:12px;font-weight:600;
118
+ border-radius:20px;cursor:pointer;white-space:nowrap;transition:.15s;flex-shrink:0;
119
+ }
120
+ .lb-type-btn.active{background:var(--accent);color:#fff;border-color:var(--accent);}
121
+
122
+ .lb-list{display:flex;flex-direction:column;gap:4px;}
123
+ .lb-row{
124
+ display:flex;align-items:center;padding:12px 14px;
125
+ background:var(--card);border:1px solid var(--border);
126
+ border-radius:12px;gap:12px;
127
+ }
128
+ .lb-row.me{border-color:var(--accent);background:rgba(59,130,246,.08);}
129
+ .lb-rank{
130
+ width:28px;font-size:14px;font-weight:800;text-align:center;flex-shrink:0;
131
+ }
132
+ .lb-rank.gold{color:#fbbf24;} .lb-rank.silver{color:#94a3b8;} .lb-rank.bronze{color:#cd7f32;}
133
+ .lb-info{flex:1;min-width:0;}
134
+ .lb-name{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
135
+ .lb-stat{font-size:11px;color:var(--dim);}
136
+ .lb-val{font-size:15px;font-weight:800;color:var(--orange);flex-shrink:0;}
137
+ .lb-empty{text-align:center;color:var(--dim);padding:30px 0;font-size:13px;}
138
+
139
+ /* ── Wallet Page ── */
140
+ .wallet-hero{
141
+ background:linear-gradient(135deg,#1e3a5f 0%,#0f1b2d 100%);
142
+ border:1px solid var(--border);border-radius:var(--radius);
143
+ padding:24px 20px;text-align:center;
144
+ }
145
+ .wallet-hero .wh-label{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;}
146
+ .wallet-hero .wh-amount{font-size:36px;font-weight:800;margin:6px 0;}
147
+ .wallet-hero .wh-sub{font-size:13px;color:var(--dim);}
148
+ .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
149
+ .stat-box{
150
+ background:var(--card);border:1px solid var(--border);
151
+ border-radius:14px;padding:14px;text-align:center;
152
+ }
153
+ .stat-box .sb-val{font-size:20px;font-weight:800;}
154
+ .stat-box .sb-label{font-size:11px;color:var(--dim);margin-top:2px;}
155
+
156
+ /* ── Reward Flow Overlay ── */
157
+ .overlay{
158
+ display:none;position:fixed;inset:0;z-index:200;
159
+ background:rgba(0,0,0,.85);backdrop-filter:blur(6px);
160
+ flex-direction:column;align-items:center;justify-content:center;padding:20px;
161
+ }
162
+ .overlay.show{display:flex;}
163
+ .ov-card{
164
+ background:var(--card);border:1px solid var(--border);
165
+ border-radius:20px;padding:28px 24px;text-align:center;
166
+ width:100%;max-width:360px;
167
+ }
168
+ .ov-card .ov-icon{font-size:52px;margin-bottom:10px;}
169
+ .ov-card .ov-title{font-size:18px;font-weight:700;margin-bottom:6px;}
170
+ .ov-card .ov-sub{font-size:13px;color:var(--dim);margin-bottom:16px;line-height:1.4;}
171
+ .ov-timer{font-size:48px;font-weight:800;color:var(--orange);}
172
+ .ov-progress{
173
+ width:100%;height:6px;background:rgba(255,255,255,.08);
174
+ border-radius:3px;margin:12px 0 16px;overflow:hidden;
175
+ }
176
+ .ov-progress-bar{height:100%;width:0%;background:var(--orange);transition:width 1s linear;}
177
+ .ov-close{
178
+ margin-top:14px;background:none;border:1px solid var(--border);
179
+ color:var(--dim);padding:10px 20px;border-radius:10px;
180
+ font-size:12px;cursor:pointer;
181
+ }
182
+
183
+ @keyframes fadeIn{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
184
+ .fade-in{animation:fadeIn .3s ease;}
185
  </style>
186
  </head>
187
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ <!-- ── Top Bar ── -->
190
+ <div class="top-bar">
191
+ <div class="logo">Alexa<span>Game</span></div>
192
+ <div class="coins" id="hdr-coins">💰 $---</div>
193
+ </div>
194
+
195
+ <!-- ════════════ PAGE: REWARD ════════════ -->
196
+ <div class="page active" id="page-reward">
197
+ <!-- Daily Card -->
198
+ <div class="rw-card">
199
+ <div class="rw-icon">💎</div>
200
+ <div class="rw-body">
201
+ <div class="rw-title">Daily Reward</div>
202
+ <div class="rw-amount">$3,000 coins</div>
203
+ <button id="daily-btn" class="rw-btn blue" onclick="openDailyFlow()">🚀 Claim Now</button>
204
+ <div id="daily-status-text" style="font-size:11px;color:var(--dim);margin-top:4px;"></div>
205
  </div>
206
  </div>
207
 
208
+ <!-- Mission Card -->
209
+ <div class="rw-card">
210
+ <div class="rw-icon">🎯</div>
211
+ <div class="rw-body">
212
+ <div class="rw-title">Daily Ad Watch Rewards</div>
213
+ <div class="rw-amount">$200 × 50/day</div>
214
+ <button id="mission-btn" class="rw-btn blue" onclick="openMissionFlow()">🎯 Watch & Earn</button>
215
+ <div id="mission-status-text" style="font-size:11px;color:var(--dim);margin-top:4px;"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </div>
217
  </div>
218
 
219
+ <!-- Leaderboard Preview -->
220
+ <div class="section-hdr">🏆 Leaderboards</div>
221
+ <div class="lb-btns">
222
+ <div class="lb-link" onclick="goLeaderboard('bank')">
223
+ <div class="ll-left"><span class="ll-icon">🏦</span><span class="ll-text">Top Bank Account</span></div>
224
+ <span class="ll-arrow">��</span>
225
+ </div>
226
+ <div class="lb-link" onclick="goLeaderboard('wallet')">
227
+ <div class="ll-left"><span class="ll-icon">💰</span><span class="ll-text">Top Wallet</span></div>
228
+ <span class="ll-arrow">›</span>
229
+ </div>
230
+ <div class="lb-link" onclick="goLeaderboard('xp')">
231
+ <div class="ll-left"><span class="ll-icon">⚔️</span><span class="ll-text">Top Users (XP & Kills)</span></div>
232
+ <span class="ll-arrow">›</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  </div>
234
  </div>
235
+ </div>
236
 
237
+ <!-- ════════════ PAGE: LEADERBOARD ════════════ -->
238
+ <div class="page" id="page-lb">
239
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:2px;">
240
+ <span style="font-size:22px;cursor:pointer;" onclick="goPage('reward')">←</span>
241
+ <span style="font-size:16px;font-weight:700;">Leaderboards</span>
242
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
+ <!-- Period tabs -->
245
+ <div class="lb-tabs">
246
+ <button class="lb-tab active" data-period="daily" onclick="setPeriod('daily',this)">Daily</button>
247
+ <button class="lb-tab" data-period="weekly" onclick="setPeriod('weekly',this)">Weekly</button>
248
+ <button class="lb-tab" data-period="monthly" onclick="setPeriod('monthly',this)">Month</button>
249
+ <button class="lb-tab" data-period="all" onclick="setPeriod('all',this)">Overall</button>
250
+ </div>
251
 
252
+ <!-- Type tabs -->
253
+ <div class="lb-type-tabs">
254
+ <button class="lb-type-btn active" data-type="wallet" onclick="setLBType('wallet',this)">💰 Wallet</button>
255
+ <button class="lb-type-btn" data-type="bank" onclick="setLBType('bank',this)">🏦 Bank</button>
256
+ <button class="lb-type-btn" data-type="xp" onclick="setLBType('xp',this)">✨ XP</button>
257
+ <button class="lb-type-btn" data-type="kills" onclick="setLBType('kills',this)">💀 Kills</button>
258
+ <button class="lb-type-btn" data-type="robs" onclick="setLBType('robs',this)">💸 Robs</button>
259
+ </div>
260
 
261
+ <div class="lb-list" id="lb-list">
262
+ <div class="lb-empty">Loading...</div>
263
+ </div>
264
+ </div>
265
+
266
+ <!-- ════════════ PAGE: WALLET ════════════ -->
267
+ <div class="page" id="page-wallet">
268
+ <div class="wallet-hero">
269
+ <div class="wh-label">Total Balance</div>
270
+ <div class="wh-amount" id="w-total">$0</div>
271
+ <div class="wh-sub" id="w-sub">Wallet + Bank</div>
272
+ </div>
273
+ <div class="stat-grid">
274
+ <div class="stat-box"><div class="sb-val" id="w-wallet">$0</div><div class="sb-label">Wallet</div></div>
275
+ <div class="stat-box"><div class="sb-val" id="w-bank">$0</div><div class="sb-label">Bank</div></div>
276
+ <div class="stat-box"><div class="sb-val" id="w-level">1</div><div class="sb-label">Level</div></div>
277
+ <div class="stat-box"><div class="sb-val" id="w-xp">0</div><div class="sb-label">XP</div></div>
278
+ <div class="stat-box"><div class="sb-val" id="w-kills">0</div><div class="sb-label">Kills</div></div>
279
+ <div class="stat-box"><div class="sb-val" id="w-robs">0</div><div class="sb-label">Robs</div></div>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- ════════════ DAILY OVERLAY ════════════ -->
284
+ <div class="overlay" id="ov-daily">
285
+ <div class="ov-card">
286
+ <div id="dov-setup">
287
+ <div class="ov-icon">💎</div>
288
+ <div class="ov-title">Daily Reward</div>
289
+ <div class="ov-sub">Watch a short ad, wait 30s, then claim <strong>$3,000</strong></div>
290
+ <button class="rw-btn blue" id="dov-start-btn" onclick="startDaily()">🚀 Watch Ad & Claim</button>
291
+ </div>
292
+ <div id="dov-timer" style="display:none;">
293
+ <div class="ov-timer" id="dov-sec">30</div>
294
+ <div class="ov-progress"><div class="ov-progress-bar" id="dov-bar"></div></div>
295
+ <div class="ov-sub">Verification in progress...</div>
296
+ </div>
297
+ <div id="dov-claim" style="display:none;">
298
+ <button class="rw-btn green" onclick="claimDaily()">🎁 Claim $3,000 Now</button>
299
+ </div>
300
+ <div id="dov-done" style="display:none;">
301
+ <div class="ov-icon">✅</div>
302
+ <div class="ov-title">Success!</div>
303
+ <div class="ov-sub"><strong style="color:var(--orange);">$3,000</strong> added to wallet</div>
304
+ </div>
305
+ <div id="dov-err" style="display:none;">
306
+ <div class="ov-icon">❌</div>
307
+ <div class="ov-sub" id="dov-err-msg" style="color:var(--red);"></div>
308
+ </div>
309
+ <button class="ov-close" onclick="closeOverlay('ov-daily')">← Back</button>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- ════════════ MISSION OVERLAY ════════════ -->
314
+ <div class="overlay" id="ov-mission">
315
+ <div class="ov-card">
316
+ <div id="mov-setup">
317
+ <div class="ov-icon">🎯</div>
318
+ <div class="ov-title">Ad Mission</div>
319
+ <div class="ov-sub">Watch an ad to earn <strong>$200</strong></div>
320
+ <div id="mov-count" style="font-size:12px;color:var(--dim);margin-bottom:10px;"></div>
321
+ <button class="rw-btn blue" id="mov-start-btn" onclick="startMission()">🎯 Watch Ad</button>
322
+ </div>
323
+ <div id="mov-timer" style="display:none;">
324
+ <div class="ov-timer" id="mov-sec">30</div>
325
+ <div class="ov-progress"><div class="ov-progress-bar" id="mov-bar"></div></div>
326
+ <div class="ov-sub">Verification in progress...</div>
327
+ </div>
328
+ <div id="mov-claim" style="display:none;">
329
+ <button class="rw-btn green" onclick="claimMission()">🎁 Claim $200</button>
330
+ </div>
331
+ <div id="mov-done" style="display:none;">
332
+ <div class="ov-icon">🏆</div>
333
+ <div class="ov-title">Mission Complete!</div>
334
+ <div class="ov-sub"><strong>$200</strong> added to wallet</div>
335
+ <button class="rw-btn blue" style="margin-top:12px;" onclick="resetMission()">Watch Another</button>
336
+ </div>
337
+ <div id="mov-err" style="display:none;">
338
+ <div class="ov-icon">❌</div>
339
+ <div class="ov-sub" id="mov-err-msg" style="color:var(--red);"></div>
340
+ </div>
341
+ <button class="ov-close" onclick="closeOverlay('ov-mission')">← Back</button>
342
+ </div>
343
+ </div>
344
 
345
+ <!-- ── Bottom Tab Bar ── -->
346
+ <div class="tab-bar">
347
+ <div class="tab active" onclick="goPage('reward')">
348
+ <span class="ico">🎁</span>Reward
349
+ </div>
350
+ <div class="tab" onclick="goPage('lb')">
351
+ <span class="ico">🏆</span>Leaders
352
+ </div>
353
+ <div class="tab" onclick="goPage('wallet')">
354
+ <span class="ico">👤</span>Wallet
355
+ </div>
356
+ </div>
357
+
358
+ <script>
359
+ const webapp = window.Telegram.WebApp;
360
+ webapp.expand(); webapp.ready();
361
+
362
+ if(!webapp||!webapp.initData||!webapp.initDataUnsafe||!webapp.initDataUnsafe.user||!webapp.initDataUnsafe.user.id){
363
+ document.body.innerHTML=`
364
+ <div style="padding:50px 20px;text-align:center;color:#fff;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;">
365
+ <div style="font-size:48px;margin-bottom:16px;">🔒</div>
366
+ <h2 style="margin-bottom:8px;">Access Blocked</h2>
367
+ <p style="color:#64748b;max-width:280px;font-size:14px;line-height:1.5;">
368
+ Open this app from <a href="https://t.me/Alexagamebot" style="color:#3b82f6;">@Alexagamebot</a> using <strong>/daily</strong> or <strong>/missions</strong>
369
+ </p>
370
+ </div>`;
371
+ throw new Error("No TG");
372
+ }
373
+
374
+ const API=window.location.origin;
375
+ const myId=webapp.initDataUnsafe.user.id.toString();
376
+
377
+ // ── API helper ──
378
+ async function api(method,path,body){
379
+ const o={method,headers:{'X-Init-Data':webapp.initData,'Content-Type':'application/json'}};
380
+ if(body)o.body=JSON.stringify(body);
381
+ const r=await fetch(API+path,o);
382
+ return r.json();
383
+ }
384
+
385
+ // ── Ad SDK ──
386
+ const SPOT="6120581"; const INPAGE="6120578";
387
+ let triggerReward=null,triggerInpage=null,triggerMission=null;
388
+ let missionAdsLeft=50;
389
+
390
+ window.initCdTma?.({id:SPOT}).then(s=>{triggerReward=s;triggerMission=s;}).catch(()=>{});
391
+ window.initCdTma?.({id:INPAGE}).then(s=>{triggerInpage=s;}).catch(()=>{});
392
+
393
+ // ── Navigation ──
394
+ function goPage(id){
395
+ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
396
+ document.getElementById('page-'+id).classList.add('active');
397
+ document.querySelectorAll('.tab-bar .tab').forEach((t,i)=>{
398
+ t.classList.toggle('active',['reward','lb','wallet'][i]===id);
399
+ });
400
+ if(id==='lb')loadLeaderboard();
401
+ if(id==='wallet')loadWallet();
402
+ }
403
+
404
+ function goLeaderboard(type){
405
+ goPage('lb');
406
+ document.querySelectorAll('.lb-type-btn').forEach(b=>b.classList.toggle('active',b.dataset.type===type));
407
+ currentLBType=type;
408
+ loadLeaderboard();
409
+ }
410
+
411
+ // ── Hub status ──
412
+ async function refreshHub(){
413
+ try{
414
+ const d=await api('GET','/status?type=daily');
415
+ const btn=document.getElementById('daily-btn');
416
+ const st=document.getElementById('daily-status-text');
417
+ if(!d.canClaim){
418
+ btn.className='rw-btn gray';btn.textContent='✅ Already Claimed';btn.disabled=true;
419
+ st.textContent='Come back tomorrow after 00:00';
420
+ }else{
421
+ btn.className='rw-btn blue';btn.textContent='🚀 Claim Now';btn.disabled=false;
422
+ st.textContent='Available now!';
423
+ }
424
+ }catch(e){}
425
+ try{
426
+ const m=await api('GET','/status?type=mission_inpage');
427
+ missionAdsLeft=m.adsLeft??50;
428
+ const btn=document.getElementById('mission-btn');
429
+ const st=document.getElementById('mission-status-text');
430
+ if(missionAdsLeft<=0){
431
+ btn.className='rw-btn gray';btn.textContent='✅ Limit Reached';btn.disabled=true;
432
+ st.textContent='50/50 claimed today';
433
+ }else{
434
+ btn.className='rw-btn blue';btn.textContent='🎯 Watch & Earn';btn.disabled=false;
435
+ st.textContent=`${50-missionAdsLeft}/50 claimed · ${missionAdsLeft} left`;
436
+ }
437
+ }catch(e){}
438
+ try{
439
+ const me=await api('GET','/me');
440
+ if(me.ok){
441
+ document.getElementById('hdr-coins').textContent='💰 $'+me.user.wallet.toLocaleString();
442
+ }
443
+ }catch(e){}
444
+ }
445
+
446
+ // ── Wallet page ──
447
+ async function loadWallet(){
448
+ try{
449
+ const r=await api('GET','/me');
450
+ if(!r.ok)return;
451
+ const u=r.user;
452
+ document.getElementById('w-total').textContent='$'+(u.wallet+u.bank).toLocaleString();
453
+ document.getElementById('w-wallet').textContent='$'+u.wallet.toLocaleString();
454
+ document.getElementById('w-bank').textContent='$'+u.bank.toLocaleString();
455
+ document.getElementById('w-level').textContent=u.level||1;
456
+ document.getElementById('w-xp').textContent=(u.xp||0).toLocaleString();
457
+ document.getElementById('w-kills').textContent=u.kills||0;
458
+ document.getElementById('w-robs').textContent=u.robs||0;
459
+ }catch(e){}
460
+ }
461
+
462
+ // ── Leaderboard ──
463
+ let currentLBType='wallet',currentLBPeriod='daily';
464
+
465
+ function setPeriod(p,el){
466
+ currentLBPeriod=p;
467
+ document.querySelectorAll('.lb-tab').forEach(t=>t.classList.remove('active'));
468
+ el.classList.add('active');
469
+ loadLeaderboard();
470
+ }
471
+ function setLBType(t,el){
472
+ currentLBType=t;
473
+ document.querySelectorAll('.lb-type-btn').forEach(b=>b.classList.remove('active'));
474
+ el.classList.add('active');
475
+ loadLeaderboard();
476
+ }
477
+
478
+ const valLabels={wallet:'Wallet',bank:'Bank',xp:'XP',kills:'Kills',robs:'Robs'};
479
+ const valIcons={wallet:'💰',bank:'🏦',xp:'✨',kills:'💀',robs:'💸'};
480
+
481
+ async function loadLeaderboard(){
482
+ const list=document.getElementById('lb-list');
483
+ list.innerHTML='<div class="lb-empty">Loading...</div>';
484
+ try{
485
+ const r=await api('GET',`/leaderboard?type=${currentLBType}&period=${currentLBPeriod}`);
486
+ if(!r.ok||!r.leaderboard.length){
487
+ list.innerHTML='<div class="lb-empty">No data yet for this period</div>';return;
488
+ }
489
+ list.innerHTML='';
490
+ r.leaderboard.forEach((u,i)=>{
491
+ const rank=i+1;
492
+ const rc=rank===1?'gold':rank===2?'silver':rank===3?'bronze':'';
493
+ const isMe=u.userId===myId;
494
+ const val=u[currentLBType]||0;
495
+ const prefix=currentLBType==='wallet'||currentLBType==='bank'?'$':'';
496
+ const sub='Lv.'+u.level+' · '+valIcons[currentLBType]+' '+(prefix)+val.toLocaleString();
497
+ const div=document.createElement('div');
498
+ div.className='lb-row fade-in'+(isMe?' me':'');
499
+ div.style.animationDelay=(i*30)+'ms';
500
+ div.innerHTML=`
501
+ <div class="lb-rank ${rc}">${rank<=3?['🥇','🥈','🥉'][rank-1]:rank}</div>
502
+ <div class="lb-info">
503
+ <div class="lb-name">${isMe?'⭐ ':''} @${u.username||'unknown'}</div>
504
+ <div class="lb-stat">${sub}</div>
505
+ </div>
506
+ <div class="lb-val">${prefix}${val.toLocaleString()}</div>`;
507
+ list.appendChild(div);
508
+ });
509
+ }catch(e){
510
+ list.innerHTML='<div class="lb-empty">Error loading leaderboard</div>';
511
+ }
512
+ }
513
+
514
+ // ── Overlays ──
515
+ function closeOverlay(id){document.getElementById(id).classList.remove('show');refreshHub();}
516
+
517
+ // ── Daily Flow ──
518
+ function openDailyFlow(){
519
+ const ov=document.getElementById('ov-daily');
520
+ ov.classList.add('show');
521
+ _show('dov-setup');_hide('dov-timer','dov-claim','dov-done','dov-err');
522
+ document.getElementById('dov-start-btn').disabled=false;
523
+ }
524
+
525
+ async function startDaily(){
526
+ if(!triggerReward){alert("Ad loading, try again.");return;}
527
+ const st=await api('GET','/status?type=daily');
528
+ if(!st.canClaim){
529
+ _hide('dov-setup');_show('dov-err');
530
+ document.getElementById('dov-err-msg').textContent='Already claimed today!';return;
531
+ }
532
+ document.getElementById('dov-start-btn').disabled=true;
533
+ try{
534
+ await triggerReward();
535
+ }catch(e){document.getElementById('dov-start-btn').disabled=false;return;}
536
+ // No 30s wait — go straight to claim after watching ad
537
+ _hide('dov-setup');_show('dov-claim');
538
+ if(triggerInpage)triggerInpage().catch(()=>{});
539
+ }
540
+
541
+ async function claimDaily(){
542
+ _hide('dov-claim');
543
+ try{
544
+ const r=await api('POST','/claim',{type:'daily'});
545
+ if(r.ok){
546
+ _show('dov-done');
547
+ if(webapp.HapticFeedback)webapp.HapticFeedback.notificationOccurred('success');
548
+ setTimeout(()=>closeOverlay('ov-daily'),2500);
549
+ }else{
550
+ _show('dov-err');document.getElementById('dov-err-msg').textContent=r.error||'Failed';
551
+ }
552
+ }catch(e){_show('dov-err');document.getElementById('dov-err-msg').textContent='Network error';}
553
+ }
554
+
555
+ // ── Mission Flow ──
556
+ function openMissionFlow(){
557
+ const ov=document.getElementById('ov-mission');
558
+ ov.classList.add('show');
559
+ _show('mov-setup');_hide('mov-timer','mov-claim','mov-done','mov-err');
560
+ document.getElementById('mov-count').textContent=`${50-missionAdsLeft}/50 claimed today`;
561
+ document.getElementById('mov-start-btn').disabled=!triggerMission||missionAdsLeft<=0;
562
+ }
563
+
564
+ async function startMission(){
565
+ if(!triggerMission){alert("Ad loading.");return;}
566
+ const st=await api('GET','/status?type=mission_inpage');
567
+ if(!st.canClaim||st.adsLeft<=0){
568
+ _hide('mov-setup');_show('mov-err');
569
+ document.getElementById('mov-err-msg').textContent='Daily limit reached!';return;
570
+ }
571
+ missionAdsLeft=st.adsLeft;
572
+ document.getElementById('mov-start-btn').disabled=true;
573
+ try{await triggerMission();}catch(e){document.getElementById('mov-start-btn').disabled=false;return;}
574
+ _hide('mov-setup');_show('mov-timer');
575
+ let t=30;
576
+ document.getElementById('mov-sec').textContent=t;
577
+ document.getElementById('mov-bar').style.width='0%';
578
+ const iv=setInterval(()=>{
579
+ t--;document.getElementById('mov-sec').textContent=t;
580
+ document.getElementById('mov-bar').style.width=((30-t)/30*100)+'%';
581
+ if(t<=0){clearInterval(iv);_hide('mov-timer');_show('mov-claim');}
582
+ },1000);
583
+ }
584
+
585
+ async function claimMission(){
586
+ _hide('mov-claim');
587
+ try{
588
+ const r=await api('POST','/claim',{type:'mission'});
589
+ if(r.ok){
590
+ missionAdsLeft=r.adsLeft??Math.max(0,missionAdsLeft-1);
591
+ _show('mov-done');
592
+ if(webapp.HapticFeedback)webapp.HapticFeedback.notificationOccurred('success');
593
+ }else{
594
+ _show('mov-err');document.getElementById('mov-err-msg').textContent=r.error||'Failed';
595
+ }
596
+ }catch(e){_show('mov-err');document.getElementById('mov-err-msg').textContent='Network error';}
597
+ }
598
+
599
+ function resetMission(){
600
+ _show('mov-setup');_hide('mov-done','mov-err','mov-timer','mov-claim');
601
+ document.getElementById('mov-count').textContent=`${50-missionAdsLeft}/50 claimed today`;
602
+ document.getElementById('mov-start-btn').disabled=!triggerMission||missionAdsLeft<=0;
603
+ }
604
+
605
+ // ── Helpers ──
606
+ function _show(...ids){ids.forEach(id=>document.getElementById(id).style.display='block');}
607
+ function _hide(...ids){ids.forEach(id=>document.getElementById(id).style.display='none');}
608
+
609
+ // ── Init ──
610
+ async function init(){
611
+ await refreshHub();
612
+ const p=new URLSearchParams(location.search).get('type');
613
+ if(p==='daily')setTimeout(()=>openDailyFlow(),300);
614
+ else if(p==='mission')setTimeout(()=>openMissionFlow(),300);
615
+ setInterval(refreshHub,30000);
616
+ }
617
+ init();
618
+ </script>
619
  </body>
620
  </html>