reaper-arcade-hub 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/app.js ADDED
@@ -0,0 +1,1758 @@
1
+ // REAPER HUB - Core Engine (3 Roles: Member/Chat Mod/Owner, Key Limits, Cyber Font, Mod Tab)
2
+
3
+ class ReaperHubApp {
4
+ constructor() {
5
+ this.games = window.REAPER_GAMES || [];
6
+ this.filteredGames = [...this.games];
7
+ this.favorites = JSON.parse(localStorage.getItem('reaper_favs') || '[]');
8
+ this.searchHistory = JSON.parse(localStorage.getItem('reaper_search_history') || '[]');
9
+ this.currentUser = {
10
+ username: localStorage.getItem('reaper_username') || '',
11
+ rank: localStorage.getItem('reaper_rank') || 'member',
12
+ isBanned: localStorage.getItem('reaper_is_banned') === 'true'
13
+ };
14
+ this.isAdmin = this.currentUser.rank === 'owner';
15
+ this.bannedUsers = JSON.parse(localStorage.getItem('reaper_banned_users') || '[]');
16
+ this.siteLockdown = localStorage.getItem('reaper_site_lockdown') === 'true';
17
+ this.isChatLocked = localStorage.getItem('reaper_chat_locked') === 'true';
18
+ this.aiBypass = localStorage.getItem('reaper_ai_bypass') === 'true';
19
+ this.activeTab = 'home';
20
+ this.activeCategory = 'all';
21
+ this.searchQuery = '';
22
+ this.currentGame = null;
23
+ this.chatMessages = JSON.parse(localStorage.getItem('reaper_local_chat') || '[]');
24
+
25
+ // Playtime Tracker State
26
+ this.playtimeSeconds = parseInt(localStorage.getItem('reaper_playtime_' + (this.currentUser.username || 'anon')) || '0', 10);
27
+ this.isPlayingGame = false;
28
+ this.lastActivityTime = Date.now();
29
+ this.isPausedForInactivity = false;
30
+ this.leaderboardData = [];
31
+ }
32
+
33
+ init() {
34
+ // 1. Run 10-Second Splash Screen
35
+ this.runSplashLoader();
36
+
37
+ // 2. Check Ban State
38
+ if (this.currentUser.isBanned || this.bannedUsers.includes(this.currentUser.username)) {
39
+ this.renderBanScreen();
40
+ return;
41
+ }
42
+
43
+ // 3. Process URL Master Token from Gateway if present
44
+ this.processUrlToken();
45
+
46
+ // 4. Validate Gateway Key Verification Session with HWID Check
47
+ const isAuthed = this.checkGatewayAuthentication();
48
+ const gateModal = document.getElementById('key-gate-modal');
49
+ if (!isAuthed) {
50
+ if (gateModal) gateModal.style.display = 'flex';
51
+ } else {
52
+ if (gateModal) gateModal.style.display = 'none';
53
+ }
54
+
55
+ // 5. Random identity if empty
56
+ if (!this.currentUser.username) {
57
+ const newName = typeof getRandomUsername === 'function' ? getRandomUsername() : "Reaper_" + Math.floor(Math.random() * 9999);
58
+ this.currentUser.username = newName;
59
+ localStorage.setItem('reaper_username', newName);
60
+ this.playtimeSeconds = parseInt(localStorage.getItem('reaper_playtime_' + newName) || '0', 10);
61
+ }
62
+
63
+ // 6. Role check
64
+ if (this.currentUser.rank === 'owner') {
65
+ this.isAdmin = true;
66
+ }
67
+
68
+ // 7. Site lockdown
69
+ if (this.siteLockdown && this.currentUser.rank !== 'owner') {
70
+ this.renderLockdownScreen();
71
+ return;
72
+ }
73
+
74
+ // 8. Connect Firebase
75
+ if (window.reaperFirebase) {
76
+ window.reaperFirebase.init();
77
+ window.reaperFirebase.listenChatMessages((msgs) => {
78
+ if (msgs && msgs.length > 0) {
79
+ this.chatMessages = msgs;
80
+ localStorage.setItem('reaper_local_chat', JSON.stringify(msgs));
81
+ }
82
+ this.renderChatMessages();
83
+ this.renderAdminChatLogs();
84
+ this.renderModChatLogs();
85
+ });
86
+ window.reaperFirebase.listenChatLock((isLocked) => {
87
+ this.isChatLocked = isLocked;
88
+ this.updateChatStateUI();
89
+ });
90
+ this.listenLeaderboard();
91
+ this.listenUserRanks();
92
+ window.reaperFirebase.listenGeneratedKeys((keys) => {
93
+ this.renderAdminKeyList(keys);
94
+ });
95
+ window.reaperFirebase.listenBannedUsers((bannedList) => {
96
+ this.bannedUsers = bannedList || [];
97
+ this.renderBannedUsersList();
98
+ if (this.currentUser.rank !== 'owner' && this.bannedUsers.includes(this.currentUser.username)) {
99
+ this.currentUser.isBanned = true;
100
+ localStorage.setItem('reaper_is_banned', 'true');
101
+ localStorage.removeItem('reaper_verified_token');
102
+ localStorage.removeItem('reaper_token_expires');
103
+ localStorage.removeItem('reaper_gateway_verified');
104
+ localStorage.removeItem('reaper_playtime_' + this.currentUser.username);
105
+ this.renderBanScreen("Your account has been permanently blacklisted and all recorded progress has been purged.");
106
+ }
107
+ });
108
+ window.reaperFirebase.listenShutdownMode((isShutdown) => {
109
+ this.siteShutdownMode = isShutdown;
110
+ const toggle = document.getElementById('admin-shutdown-toggle');
111
+ if (toggle) toggle.checked = isShutdown;
112
+ if (isShutdown && this.currentUser.rank !== 'owner') {
113
+ this.renderPermanentShutdownScreen();
114
+ }
115
+ });
116
+ window.reaperFirebase.listenSiteLockdown((isLocked) => {
117
+ this.siteLockdown = isLocked;
118
+ const toggle = document.getElementById('admin-lockdown-toggle');
119
+ if (toggle) toggle.checked = isLocked;
120
+ if (isLocked && this.currentUser.rank !== 'owner') {
121
+ this.renderLockdownScreen();
122
+ }
123
+ });
124
+ window.reaperFirebase.listenVerifyLockdown((isLocked) => {
125
+ this.verifyLockdown = isLocked;
126
+ const toggle = document.getElementById('admin-verify-lockdown-toggle');
127
+ if (toggle) toggle.checked = isLocked;
128
+ });
129
+ window.reaperFirebase.listenKeysLocked((isLocked) => {
130
+ this.keysLocked = isLocked;
131
+ const toggle = document.getElementById('admin-keys-locked-toggle');
132
+ if (toggle) toggle.checked = isLocked;
133
+ });
134
+ window.reaperFirebase.listenRedirectGoogle((isRedirect) => {
135
+ this.redirectGoogle = isRedirect;
136
+ const toggle = document.getElementById('admin-redirect-google-toggle');
137
+ if (toggle) toggle.checked = isRedirect;
138
+ if (isRedirect && this.currentUser.rank !== 'owner') {
139
+ window.location.replace("https://www.google.com");
140
+ }
141
+ });
142
+ }
143
+
144
+ // 9. Render UI, Games, Leaderboard
145
+ this.updateUserUI();
146
+ this.updateStats();
147
+ this.renderChatMessages();
148
+ this.renderGamesGrid();
149
+ this.setupEventListeners();
150
+ this.setupAdminShortcuts();
151
+ this.startTimeoutTimerLoop();
152
+ this.startPlaytimeTrackerLoop();
153
+
154
+ this.syncPlaytimeToDB();
155
+
156
+ console.log(`[REAPER HUB] System Online. Role: ${this.currentUser.rank.toUpperCase()}`);
157
+ }
158
+
159
+ // ==========================================
160
+ // DETERMINISTIC CROSS-DOMAIN HWID & GATEKEEPER
161
+ // ==========================================
162
+ getDeviceHWID() {
163
+ const rawString = [
164
+ navigator.userAgent,
165
+ screen.width || 1920,
166
+ screen.height || 1080,
167
+ screen.colorDepth || 24,
168
+ navigator.hardwareConcurrency || 4,
169
+ navigator.language || 'en'
170
+ ].join('###');
171
+
172
+ let hash = 0;
173
+ for (let i = 0; i < rawString.length; i++) {
174
+ hash = ((hash << 5) - hash) + rawString.charCodeAt(i);
175
+ hash |= 0;
176
+ }
177
+ return 'HWID_' + Math.abs(hash).toString(16);
178
+ }
179
+
180
+ redirectToVerify() {
181
+ if (window.location.hostname.includes('unpkg.com') || window.location.hostname.includes('jsdelivr.net')) {
182
+ const parts = window.location.pathname.split('/').filter(Boolean);
183
+ // Example: /packageName/main/index.html -> /packageName/verify/index.html
184
+ const pkgName = parts[0] || '';
185
+ window.location.href = `${window.location.origin}/${pkgName}/verify/index.html`;
186
+ } else if (window.location.hostname.includes('neocities.org')) {
187
+ window.location.href = 'https://verifyreaper.neocities.org/';
188
+ } else {
189
+ window.location.href = '../verify/index.html';
190
+ }
191
+ }
192
+
193
+ async processUrlToken() {
194
+ const params = new URLSearchParams(window.location.search);
195
+ const token = params.get('token') || params.get('key');
196
+ if (token) {
197
+ const cleanToken = token.trim();
198
+ const tokenUpper = cleanToken.toUpperCase();
199
+
200
+ // Check Master Owner Key
201
+ if (tokenUpper === '679-102' || tokenUpper === '679102') {
202
+ const infiniteExp = Date.now() + (999999 * 3600 * 1000);
203
+ localStorage.setItem('reaper_verified_token', 'REAPER_MASTER_OWNER_KEY_679_102');
204
+ localStorage.setItem('reaper_token_expires', infiniteExp.toString());
205
+ localStorage.setItem('reaper_gateway_verified', 'true');
206
+ localStorage.setItem('reaper_admin_auth', 'true');
207
+ localStorage.setItem('reaper_rank', 'owner');
208
+ this.currentUser.rank = 'owner';
209
+ this.isAdmin = true;
210
+ window.history.replaceState({}, document.title, window.location.pathname);
211
+ this.showToast("TOTAL MASTER OWNER AUTHENTICATED");
212
+ return;
213
+ }
214
+
215
+ // Check if it's a Base64 Master Token
216
+ try {
217
+ const payloadBase64 = cleanToken.split('.')[0];
218
+ const decoded = atob(payloadBase64);
219
+ if (decoded.startsWith('REAPER_SECURE_TOKEN::')) {
220
+ const parts = decoded.split('::');
221
+ const rank = parts[2] || 'member';
222
+ const expiresAt = parseInt(parts[4], 10);
223
+
224
+ if (expiresAt > Date.now()) {
225
+ localStorage.setItem('reaper_verified_token', cleanToken);
226
+ localStorage.setItem('reaper_token_expires', expiresAt.toString());
227
+ localStorage.setItem('reaper_gateway_verified', 'true');
228
+ localStorage.setItem('reaper_rank', rank);
229
+ this.currentUser.rank = rank;
230
+ if (rank === 'owner') {
231
+ this.isAdmin = true;
232
+ localStorage.setItem('reaper_admin_auth', 'true');
233
+ }
234
+ window.history.replaceState({}, document.title, window.location.pathname);
235
+ this.showToast(`Role Activated: ${rank.toUpperCase()}`);
236
+ return;
237
+ }
238
+ }
239
+ } catch (e) {}
240
+
241
+ // Check if it's a plain Access Key
242
+ let keyInfo = null;
243
+ if (window.reaperFirebase) {
244
+ try {
245
+ keyInfo = await window.reaperFirebase.fetchKeyInfo(tokenUpper);
246
+ } catch (e) {}
247
+ }
248
+ if (!keyInfo) {
249
+ const localKeys = JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
250
+ keyInfo = localKeys[tokenUpper] || localKeys['REAPER-' + tokenUpper];
251
+ }
252
+
253
+ if (keyInfo) {
254
+ const hours = keyInfo.hours || 24;
255
+ const rank = keyInfo.rank || 'member';
256
+ const expiresAt = Date.now() + (hours * 3600 * 1000);
257
+
258
+ if (window.reaperFirebase) {
259
+ window.reaperFirebase.incrementKeyUsage(tokenUpper);
260
+ }
261
+
262
+ localStorage.setItem('reaper_verified_token', `KEY_AUTH_${tokenUpper}`);
263
+ localStorage.setItem('reaper_token_expires', expiresAt.toString());
264
+ localStorage.setItem('reaper_gateway_verified', 'true');
265
+ localStorage.setItem('reaper_rank', rank);
266
+ this.currentUser.rank = rank;
267
+ if (rank === 'owner') {
268
+ this.isAdmin = true;
269
+ localStorage.setItem('reaper_admin_auth', 'true');
270
+ }
271
+ window.history.replaceState({}, document.title, window.location.pathname);
272
+ this.showToast(`Access Key Verified: ${rank.toUpperCase()}`);
273
+ return;
274
+ }
275
+ }
276
+ }
277
+
278
+ checkGatewayAuthentication() {
279
+ const isVerified = localStorage.getItem('reaper_gateway_verified') === 'true';
280
+ const expiresAt = parseInt(localStorage.getItem('reaper_token_expires') || '0', 10);
281
+ const token = localStorage.getItem('reaper_verified_token');
282
+ const rank = localStorage.getItem('reaper_rank');
283
+
284
+ if (rank === 'owner' || (token && token.includes('OWNER'))) {
285
+ return true;
286
+ }
287
+
288
+ if (!isVerified || !token || expiresAt <= Date.now()) {
289
+ return false;
290
+ }
291
+ return true;
292
+ }
293
+
294
+ renderKeyGateModal() {
295
+ const gateModal = document.getElementById('key-gate-modal');
296
+ if (gateModal) {
297
+ gateModal.style.display = 'flex';
298
+ }
299
+ }
300
+
301
+ async submitGateToken() {
302
+ const input = document.getElementById('gate-token-input');
303
+ if (!input || !input.value.trim()) {
304
+ this.showToast("Please enter an access key or Master Token.");
305
+ return;
306
+ }
307
+
308
+ const token = input.value.trim();
309
+ const tokenUpper = token.toUpperCase();
310
+
311
+ // 1. Direct Master Owner Key: 679-102
312
+ if (tokenUpper === '679-102' || tokenUpper === '679102') {
313
+ const infiniteExp = Date.now() + (999999 * 3600 * 1000);
314
+ localStorage.setItem('reaper_verified_token', 'REAPER_MASTER_OWNER_KEY_679_102');
315
+ localStorage.setItem('reaper_token_expires', infiniteExp.toString());
316
+ localStorage.setItem('reaper_gateway_verified', 'true');
317
+ localStorage.setItem('reaper_admin_auth', 'true');
318
+ localStorage.setItem('reaper_rank', 'owner');
319
+ this.currentUser.rank = 'owner';
320
+ this.isAdmin = true;
321
+
322
+ const gateModal = document.getElementById('key-gate-modal');
323
+ if (gateModal) gateModal.style.display = 'none';
324
+ this.updateUserUI();
325
+ this.showToast("TOTAL MASTER OWNER AUTHENTICATED");
326
+ return;
327
+ }
328
+
329
+ // 2. Direct Access Key verification (e.g. REAPER-123 or whatever key)
330
+ let keyInfo = null;
331
+ if (window.reaperFirebase) {
332
+ try {
333
+ keyInfo = await window.reaperFirebase.fetchKeyInfo(tokenUpper);
334
+ } catch (e) {}
335
+ }
336
+ if (!keyInfo) {
337
+ const localKeys = JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
338
+ keyInfo = localKeys[tokenUpper] || localKeys['REAPER-' + tokenUpper];
339
+ }
340
+
341
+ if (keyInfo) {
342
+ const hours = keyInfo.hours || 24;
343
+ const rank = keyInfo.rank || 'member';
344
+ const expiresAt = Date.now() + (hours * 3600 * 1000);
345
+
346
+ // Increment usage
347
+ if (window.reaperFirebase) {
348
+ window.reaperFirebase.incrementKeyUsage(tokenUpper);
349
+ }
350
+
351
+ localStorage.setItem('reaper_verified_token', `KEY_AUTH_${tokenUpper}`);
352
+ localStorage.setItem('reaper_token_expires', expiresAt.toString());
353
+ localStorage.setItem('reaper_gateway_verified', 'true');
354
+ localStorage.setItem('reaper_rank', rank);
355
+ this.currentUser.rank = rank;
356
+ if (rank === 'owner') {
357
+ this.isAdmin = true;
358
+ localStorage.setItem('reaper_admin_auth', 'true');
359
+ }
360
+
361
+ const gateModal = document.getElementById('key-gate-modal');
362
+ if (gateModal) gateModal.style.display = 'none';
363
+ this.updateUserUI();
364
+ this.showToast(`Access Key Verified. Role: ${rank.toUpperCase()}`);
365
+ return;
366
+ }
367
+
368
+ // 3. Base64 HWID-Locked Master Token
369
+ try {
370
+ const payloadBase64 = token.split('.')[0];
371
+ const decoded = atob(payloadBase64);
372
+ if (decoded.startsWith('REAPER_SECURE_TOKEN::')) {
373
+ const parts = decoded.split('::');
374
+ const rank = parts[2] || 'member';
375
+ const expiresAt = parseInt(parts[4], 10);
376
+ const tokenHWID = parts[5];
377
+ const localHWID = this.getDeviceHWID();
378
+
379
+ if (rank !== 'owner' && tokenHWID && localHWID && tokenHWID !== localHWID) {
380
+ alert("[HWID LOCK ERROR]\nThis Master Token is locked to another hardware device. Please verify on this device.");
381
+ return;
382
+ }
383
+
384
+ if (expiresAt > Date.now()) {
385
+ localStorage.setItem('reaper_verified_token', token);
386
+ localStorage.setItem('reaper_token_expires', expiresAt.toString());
387
+ localStorage.setItem('reaper_gateway_verified', 'true');
388
+ localStorage.setItem('reaper_rank', rank);
389
+ this.currentUser.rank = rank;
390
+ if (rank === 'owner') {
391
+ this.isAdmin = true;
392
+ localStorage.setItem('reaper_admin_auth', 'true');
393
+ }
394
+ const gateModal = document.getElementById('key-gate-modal');
395
+ if (gateModal) gateModal.style.display = 'none';
396
+ this.updateUserUI();
397
+ this.showToast(`Access Granted: ${rank.toUpperCase()}`);
398
+ return;
399
+ }
400
+ }
401
+ } catch (e) {}
402
+
403
+ alert("[ACCESS DENIED]\nInvalid, exhausted, or expired Key/Token. Please obtain a valid key from the Owner.");
404
+ }
405
+
406
+ // ==========================================
407
+ // CUSTOM KEY CREATION WITH MAX USES & DELETION
408
+ // ==========================================
409
+ createCustomKey() {
410
+ const input = document.getElementById('admin-custom-key-input');
411
+ const durSelect = document.getElementById('admin-custom-key-duration');
412
+ const usesSelect = document.getElementById('admin-custom-key-maxuses');
413
+ const rankSelect = document.getElementById('admin-custom-key-rank');
414
+
415
+ let rawName = input && input.value.trim() ? input.value.trim().toUpperCase().replace(/\s+/g, '-') : '';
416
+ let keyName = '';
417
+
418
+ if (!rawName) {
419
+ // Auto-generate REAPER-$$$
420
+ keyName = 'REAPER-' + Math.floor(100 + Math.random() * 900);
421
+ } else {
422
+ keyName = rawName.startsWith('REAPER-') ? rawName : 'REAPER-' + rawName;
423
+ }
424
+
425
+ const hours = parseInt(durSelect ? durSelect.value : '24', 10);
426
+ const maxUses = parseInt(usesSelect ? usesSelect.value : '1', 10);
427
+ const rank = rankSelect ? rankSelect.value : 'member';
428
+
429
+ this.saveKeyToStore(keyName, hours, maxUses, rank);
430
+ if (input) input.value = '';
431
+ }
432
+
433
+ async saveKeyToStore(keyName, hours, maxUses, rank) {
434
+ const keyInfo = {
435
+ hours: hours,
436
+ maxUses: maxUses,
437
+ usedCount: 0,
438
+ rank: rank,
439
+ createdAt: Date.now()
440
+ };
441
+
442
+ if (window.reaperFirebase) {
443
+ await window.reaperFirebase.saveKey(keyName, keyInfo);
444
+ } else {
445
+ const existingKeys = JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
446
+ existingKeys[keyName] = keyInfo;
447
+ localStorage.setItem('reaper_generated_keys', JSON.stringify(existingKeys));
448
+ }
449
+
450
+ this.renderAdminKeyList();
451
+ const durLabel = hours === 72 ? '3 Days' : hours === 120 ? '5 Days' : hours >= 87000 ? 'Lifetime' : `${hours}h`;
452
+ this.showToast(`Created Key: ${keyName} (${maxUses >= 1000 ? 'Unlimited' : maxUses + ' Uses'} | ${durLabel})`);
453
+ }
454
+
455
+ async deleteKey(keyName) {
456
+ if (window.reaperFirebase) {
457
+ await window.reaperFirebase.deleteKey(keyName);
458
+ } else {
459
+ const existingKeys = JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
460
+ delete existingKeys[keyName];
461
+ localStorage.setItem('reaper_generated_keys', JSON.stringify(existingKeys));
462
+ }
463
+ this.renderAdminKeyList();
464
+ this.showToast(`Deleted Key: ${keyName}`);
465
+ }
466
+
467
+ async clearAllKeys() {
468
+ if (confirm("Are you sure you want to delete all generated access keys?")) {
469
+ const existingKeys = JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
470
+ for (const k of Object.keys(existingKeys)) {
471
+ if (window.reaperFirebase) await window.reaperFirebase.deleteKey(k);
472
+ }
473
+ localStorage.removeItem('reaper_generated_keys');
474
+ this.renderAdminKeyList();
475
+ this.showToast("All custom keys deleted.");
476
+ }
477
+ }
478
+
479
+ renderAdminKeyList(keysObj) {
480
+ const container = document.getElementById('admin-keys-list');
481
+ if (!container) return;
482
+
483
+ const existingKeys = keysObj || JSON.parse(localStorage.getItem('reaper_generated_keys') || '{}');
484
+ const entries = Object.entries(existingKeys);
485
+
486
+ if (entries.length === 0) {
487
+ container.innerHTML = `<div style="color: #88aacc; font-size: 0.8rem;">No custom keys active. Create one above.</div>`;
488
+ return;
489
+ }
490
+
491
+ container.innerHTML = entries.map(([key, info]) => {
492
+ const usesStr = info.maxUses >= 1000 ? 'Unlimited Uses' : `${info.usedCount || 0}/${info.maxUses} Uses`;
493
+ let durStr = `${info.hours}h`;
494
+ if (info.hours === 72) durStr = '3 Days';
495
+ else if (info.hours === 120) durStr = '5 Days';
496
+ else if (info.hours === 168) durStr = '7 Days';
497
+ else if (info.hours === 720) durStr = '30 Days';
498
+ else if (info.hours >= 87000) durStr = 'Lifetime';
499
+
500
+ return `
501
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; border: 1px solid var(--blue-border); padding: 0.45rem 0.75rem; border-radius: 6px; margin-bottom: 0.4rem; font-size: 0.8rem;">
502
+ <div>
503
+ <strong style="color: #00d2ff; font-family: var(--font-mono);">${key}</strong>
504
+ <span style="color: #aaa; margin-left: 0.4rem;">(${usesStr} | ${durStr} | ${(info.rank || 'member').toUpperCase()})</span>
505
+ </div>
506
+ <div style="display: flex; gap: 0.35rem;">
507
+ <button onclick="navigator.clipboard.writeText('${key}'); app.showToast('Key copied');" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.72rem;">COPY</button>
508
+ <button onclick="app.deleteKey('${key}')" class="btn-stealth" style="color: #ff5555; padding: 0.2rem 0.5rem; font-size: 0.72rem;">DEL</button>
509
+ </div>
510
+ </div>
511
+ `;
512
+ }).join('');
513
+ }
514
+
515
+ // ==========================================
516
+ // USER PROMOTION & DE-RANKING (OWNER ONLY)
517
+ // ==========================================
518
+ setUserRank(username, newRank) {
519
+ if (this.currentUser.rank !== 'owner') return;
520
+ if (username === this.currentUser.username && newRank !== 'owner') {
521
+ this.showToast("Cannot demote Owner account.");
522
+ return;
523
+ }
524
+
525
+ if (window.reaperFirebase && window.reaperFirebase.db) {
526
+ window.reaperFirebase.db.ref(`reaperUserRanks/${username}`).set(newRank);
527
+ }
528
+
529
+ // Update local leaderboard data copy
530
+ const userObj = this.leaderboardData.find(u => u.username === username);
531
+ if (userObj) userObj.rank = newRank;
532
+
533
+ this.renderAdminUsersDirectory();
534
+ this.showToast(`Updated ${username} to ${newRank.toUpperCase()}`);
535
+ }
536
+
537
+ listenUserRanks() {
538
+ if (window.reaperFirebase && window.reaperFirebase.db) {
539
+ window.reaperFirebase.db.ref("reaperUserRanks").on("value", (snap) => {
540
+ const ranks = snap.val();
541
+ if (ranks && ranks[this.currentUser.username]) {
542
+ const assignedRank = ranks[this.currentUser.username];
543
+ if (assignedRank !== this.currentUser.rank) {
544
+ this.currentUser.rank = assignedRank;
545
+ localStorage.setItem('reaper_rank', assignedRank);
546
+ this.updateUserUI();
547
+ this.showToast(`Your rank was updated to: ${assignedRank.toUpperCase()}`);
548
+ }
549
+ }
550
+ });
551
+ }
552
+ }
553
+
554
+ // ==========================================
555
+ // TAB NAVIGATION & MOD / ADMIN SECTIONS
556
+ // ==========================================
557
+ switchTab(tabName) {
558
+ if (tabName === 'admin' && this.currentUser.rank !== 'owner') {
559
+ this.promptAdminLogin();
560
+ return;
561
+ }
562
+
563
+ if (tabName === 'mod' && this.currentUser.rank !== 'chat_mod' && this.currentUser.rank !== 'mod' && this.currentUser.rank !== 'owner') {
564
+ this.showToast("Access restricted to Chat Mods.");
565
+ return;
566
+ }
567
+
568
+ this.activeTab = tabName;
569
+ document.querySelectorAll('.nav-tab-btn').forEach(btn => {
570
+ btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
571
+ });
572
+
573
+ document.querySelectorAll('.tab-section').forEach(sec => {
574
+ sec.style.display = 'none';
575
+ });
576
+
577
+ const activeSec = document.getElementById(`section-${tabName}`);
578
+ if (activeSec) {
579
+ activeSec.style.display = 'block';
580
+ }
581
+
582
+ if (tabName === 'mod') {
583
+ this.renderModChatLogs();
584
+ } else if (tabName === 'admin') {
585
+ this.renderAdminUsersDirectory();
586
+ this.renderAdminKeyList();
587
+ this.renderAdminChatLogs();
588
+ this.renderBannedUsersList();
589
+ const redirectToggle = document.getElementById('admin-redirect-google-toggle');
590
+ if (redirectToggle) redirectToggle.checked = Boolean(this.redirectGoogle);
591
+ const keysLockedToggle = document.getElementById('admin-keys-locked-toggle');
592
+ if (keysLockedToggle) keysLockedToggle.checked = Boolean(this.keysLocked);
593
+ const verifyLockToggle = document.getElementById('admin-verify-lockdown-toggle');
594
+ if (verifyLockToggle) verifyLockToggle.checked = Boolean(this.verifyLockdown);
595
+ const shutdownToggle = document.getElementById('admin-shutdown-toggle');
596
+ if (shutdownToggle) shutdownToggle.checked = Boolean(this.siteShutdownMode);
597
+ const lockToggle = document.getElementById('admin-lockdown-toggle');
598
+ if (lockToggle) lockToggle.checked = Boolean(this.siteLockdown);
599
+ const chatLockToggle = document.getElementById('admin-chatlock-toggle');
600
+ if (chatLockToggle) chatLockToggle.checked = Boolean(this.isChatLocked);
601
+ const bypassToggle = document.getElementById('admin-bypass-toggle');
602
+ if (bypassToggle) bypassToggle.checked = Boolean(this.aiBypass);
603
+ } else if (tabName === 'shield') {
604
+ this.renderShieldUI();
605
+ }
606
+ }
607
+
608
+ // ==========================================
609
+ // BROWSER EXTENSION SHIELD & HOOK DEFENDER
610
+ // ==========================================
611
+ renderShieldUI() {
612
+ const list = document.getElementById('shield-extensions-list');
613
+ const autoToggle = document.getElementById('auto-shield-toggle');
614
+ if (autoToggle && window.reaperShield) {
615
+ autoToggle.checked = window.reaperShield.autoShield;
616
+ }
617
+ if (!list) return;
618
+
619
+ const extensions = window.reaperShield ? window.reaperShield.scanExtensions() : [];
620
+
621
+ if (extensions.length === 0) {
622
+ list.innerHTML = `<div style="color: #00ffaa; padding: 1.5rem; text-align: center; font-weight: 700; background: #040e1e; border: 1px solid var(--blue-border); border-radius: 8px;">[✓] NO ACTIVE EXTENSION HOOKS DETECTED. BROWSER IS FULLY ISOLATED.</div>`;
623
+ return;
624
+ }
625
+
626
+ list.innerHTML = extensions.map(ext => {
627
+ const isBlocked = ext.status === 'ACTIVE_BLOCKED';
628
+ const riskColor = ext.risk === 'CRITICAL' ? '#ff3333' : ext.risk === 'HIGH' ? '#ff8800' : '#ffd700';
629
+
630
+ return `
631
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; border: 1px solid ${isBlocked ? '#ff3333' : 'var(--blue-border)'}; border-radius: 8px; padding: 0.85rem 1.15rem; flex-wrap: wrap; gap: 0.75rem;">
632
+ <div style="display: flex; flex-direction: column; gap: 0.25rem;">
633
+ <div style="display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;">
634
+ <strong style="color: #fff; font-size: 0.95rem;">${ext.name}</strong>
635
+ <span style="font-size: 0.68rem; color: ${riskColor}; border: 1px solid ${riskColor}; padding: 0.15rem 0.45rem; border-radius: 4px; font-weight: 800;">${ext.risk} RISK</span>
636
+ <span style="font-size: 0.68rem; color: #88aacc; background: #020814; padding: 0.15rem 0.45rem; border-radius: 4px;">${ext.category}</span>
637
+ </div>
638
+ <div style="font-size: 0.78rem; color: #88aacc;">
639
+ ${ext.details}
640
+ </div>
641
+ </div>
642
+
643
+ <div style="display: flex; align-items: center; gap: 0.65rem;">
644
+ <span style="color: #00ffaa; font-weight: 800; font-size: 0.78rem; background: rgba(0, 255, 170, 0.1); border: 1px solid #00ffaa; padding: 0.3rem 0.65rem; border-radius: 4px;">
645
+ 🛡️ ISOLATED &amp; BLOCKED
646
+ </span>
647
+ <button onclick="app.isolateSpecificExtension('${ext.id}')" class="btn-stealth" style="color: #ff5555; border-color: #771111; font-size: 0.75rem; padding: 0.35rem 0.75rem;">
648
+ CUT HOOK
649
+ </button>
650
+ </div>
651
+ </div>
652
+ `;
653
+ }).join('');
654
+ }
655
+
656
+ purgeAllExtensionHooks() {
657
+ if (window.reaperShield) {
658
+ const purged = window.reaperShield.purgeSpyInjections();
659
+ this.renderShieldUI();
660
+ this.showToast(`Purged and killed all active extension hooks (${purged} removed)`);
661
+ } else {
662
+ this.showToast("All extension hooks cut and neutralized.");
663
+ }
664
+ }
665
+
666
+ refreshExtensionScan() {
667
+ if (window.reaperShield) {
668
+ window.reaperShield.scanExtensions();
669
+ this.renderShieldUI();
670
+ this.showToast("Browser extension scan completed.");
671
+ }
672
+ }
673
+
674
+ isolateSpecificExtension(extId) {
675
+ if (window.reaperShield) {
676
+ window.reaperShield.purgeSpyInjections();
677
+ this.renderShieldUI();
678
+ this.showToast(`Isolated and killed hooks for: ${extId.toUpperCase()}`);
679
+ }
680
+ }
681
+
682
+ toggleAutoShield(enabled) {
683
+ if (window.reaperShield) {
684
+ window.reaperShield.setAutoPurge(enabled);
685
+ this.showToast(enabled ? "Auto-Shield active on every visit." : "Auto-Shield disabled.");
686
+ }
687
+ }
688
+
689
+ renderAdminUsersDirectory() {
690
+ const container = document.getElementById('admin-users-directory');
691
+ if (!container) return;
692
+
693
+ if (this.leaderboardData.length === 0) {
694
+ this.leaderboardData = [
695
+ { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
696
+ ];
697
+ }
698
+
699
+ container.innerHTML = this.leaderboardData.map(u => {
700
+ const isSelf = u.username === this.currentUser.username;
701
+ const currentRank = (u.rank || 'member').toLowerCase();
702
+ const timeStr = this.formatPlaytime(u.seconds || 0);
703
+
704
+ return `
705
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #041022; padding: 0.45rem 0.75rem; border-radius: 6px; border: 1px solid var(--blue-border); font-size: 0.8rem;">
706
+ <div>
707
+ <strong style="color: #fff;" class="${currentRank === 'owner' ? 'owner-animated-name' : ''}">${this.escapeHtml(u.username || 'User')}</strong>
708
+ <span class="rank-pill rank-${currentRank}">${currentRank.toUpperCase()}</span>
709
+ <span style="color: #88ccff; margin-left: 0.5rem;">Playtime: ${timeStr}</span>
710
+ </div>
711
+ <div style="display: flex; gap: 0.35rem;">
712
+ ${!isSelf ? `
713
+ ${currentRank === 'member' ? `
714
+ <button onclick="app.setUserRank('${u.username}', 'chat_mod')" class="btn-stealth" style="color: #00ffaa; padding: 0.2rem 0.5rem; font-size: 0.72rem;">+ PROMOTE MOD</button>
715
+ ` : currentRank === 'chat_mod' || currentRank === 'mod' ? `
716
+ <button onclick="app.setUserRank('${u.username}', 'member')" class="btn-stealth" style="color: #ffaa00; padding: 0.2rem 0.5rem; font-size: 0.72rem;">- DE-RANK MEMBER</button>
717
+ ` : ''}
718
+ <button onclick="app.banUser('${u.username}')" class="btn-stealth" style="color: #ff5555; padding: 0.2rem 0.5rem; font-size: 0.72rem;">BAN</button>
719
+ ` : '<span style="color: #ffd700; font-weight: 800;">YOU</span>'}
720
+ </div>
721
+ </div>
722
+ `;
723
+ }).join('');
724
+ }
725
+
726
+ // ==========================================
727
+ // CHAT MOD DESK ACTIONS
728
+ // ==========================================
729
+ modApplyTimeout(minutes) {
730
+ const input = document.getElementById('mod-timeout-username');
731
+ if (!input || !input.value.trim()) {
732
+ this.showToast("Enter a username to timeout.");
733
+ return;
734
+ }
735
+ const targetUser = input.value.trim();
736
+ if (targetUser === 'owner' || targetUser === this.currentUser.username) {
737
+ this.showToast("Cannot timeout this account.");
738
+ return;
739
+ }
740
+
741
+ if (window.reaperFirebase && window.reaperFirebase.db) {
742
+ window.reaperFirebase.db.ref(`reaperTimeouts/${targetUser}`).set(Date.now() + (minutes * 60 * 1000));
743
+ }
744
+ this.showToast(`Applied ${minutes}m timeout to ${targetUser}`);
745
+ input.value = '';
746
+ }
747
+
748
+ renderModChatLogs() {
749
+ const container = document.getElementById('mod-chat-logs');
750
+ if (!container) return;
751
+
752
+ if (this.chatMessages.length === 0) {
753
+ container.innerHTML = `<div style="color: #88aacc; padding: 0.75rem;">No active chat messages.</div>`;
754
+ return;
755
+ }
756
+
757
+ container.innerHTML = this.chatMessages.map(msg => `
758
+ <div style="display: flex; justify-content: space-between; align-items: center; padding: 0.4rem 0.65rem; border-bottom: 1px solid #041022; font-size: 0.8rem;">
759
+ <div>
760
+ <strong style="color: ${msg.rank === 'owner' ? '#ffd700' : '#00d2ff'};">${this.escapeHtml(msg.username || 'Anon')}:</strong>
761
+ <span style="color: #d0e4ff; margin-left: 0.35rem;">${this.escapeHtml(msg.message)}</span>
762
+ </div>
763
+ <div style="display: flex; gap: 0.35rem;">
764
+ <button onclick="app.deleteMessage('${msg.id}', '${msg.username}')" style="background: #2b0808; border: 1px solid #ff4444; color: #ff8888; border-radius: 4px; padding: 0.15rem 0.45rem; cursor: pointer; font-size: 0.7rem;">DEL</button>
765
+ <button onclick="document.getElementById('mod-timeout-username').value='${msg.username}'; app.modApplyTimeout(5);" style="background: #2b1f08; border: 1px solid #ffaa00; color: #ffcc66; border-radius: 4px; padding: 0.15rem 0.45rem; cursor: pointer; font-size: 0.7rem;">MUTE</button>
766
+ </div>
767
+ </div>
768
+ `).join('');
769
+ }
770
+
771
+ // ==========================================
772
+ // 10-SECOND INITIAL SPLASH LOADER
773
+ // ==========================================
774
+ runSplashLoader() {
775
+ const splash = document.getElementById('app-splash-screen');
776
+ const bar = document.getElementById('splash-progress-bar');
777
+ const status = document.getElementById('splash-status-text');
778
+ const quoteEl = document.getElementById('splash-quote-text');
779
+ if (!splash || !bar) return;
780
+
781
+ const quotes = [
782
+ "Bypassing school firewall protocols...",
783
+ "Calibrating 788 reasons to not do math homework...",
784
+ "Rerouting through quantum Fastly CDN tunnels...",
785
+ "Overclocking 60 FPS HTML5 game engines...",
786
+ "Loading stealth cloak matrices...",
787
+ "Decompiling high-speed arcade manifests...",
788
+ "Synchronizing real-time playtime leaderboards...",
789
+ "Initializing unblocked dopamine...",
790
+ "Warming up Friday Night Funkin rhythm tracks...",
791
+ "Establishing encrypted starfield link...",
792
+ "Optimizing potato PC performance mode...",
793
+ "Preparing the ultimate browser arcade...",
794
+ "Decrypting high-speed game ROMs...",
795
+ "Connecting to Firebase Realtime neural network...",
796
+ "Checking for homework: 0% found...",
797
+ "All systems nominal. Get ready to play."
798
+ ];
799
+
800
+ let quoteIndex = 0;
801
+ if (quoteEl) {
802
+ quoteEl.textContent = `"${quotes[0]}"`;
803
+ }
804
+
805
+ const quoteInterval = setInterval(() => {
806
+ quoteIndex = (quoteIndex + 1) % quotes.length;
807
+ if (quoteEl) {
808
+ quoteEl.style.opacity = '0';
809
+ quoteEl.style.transform = 'translateY(-3px)';
810
+ setTimeout(() => {
811
+ quoteEl.textContent = `"${quotes[quoteIndex]}"`;
812
+ quoteEl.style.opacity = '1';
813
+ quoteEl.style.transform = 'translateY(0)';
814
+ }, 200);
815
+ }
816
+ }, 1400);
817
+
818
+ let progress = 0;
819
+ const interval = 100;
820
+
821
+ const timer = setInterval(() => {
822
+ progress += 1;
823
+ bar.style.width = `${progress}%`;
824
+
825
+ if (progress < 25) {
826
+ if (status) status.textContent = `[${progress}%] INITIALIZING REAPER CORE...`;
827
+ } else if (progress < 50) {
828
+ if (status) status.textContent = `[${progress}%] CONNECTING REALTIME FIREBASE DB...`;
829
+ } else if (progress < 75) {
830
+ if (status) status.textContent = `[${progress}%] UNBLOCKING FASTLY CDN ASSETS...`;
831
+ } else if (progress < 95) {
832
+ if (status) status.textContent = `[${progress}%] DECRYPTING 788 GAME MANIFESTS...`;
833
+ } else if (progress < 100) {
834
+ if (status) status.textContent = `[${progress}%] PREPARING STARFIELD ENGINE...`;
835
+ } else {
836
+ if (status) status.textContent = `[100%] ALL SYSTEMS ONLINE`;
837
+ clearInterval(timer);
838
+ clearInterval(quoteInterval);
839
+ setTimeout(() => {
840
+ splash.style.opacity = '0';
841
+ setTimeout(() => { splash.style.display = 'none'; }, 600);
842
+ }, 400);
843
+ }
844
+ }, interval);
845
+ }
846
+
847
+ // ==========================================
848
+ // PLAYTIME TRACKER & LEADERBOARD
849
+ // ==========================================
850
+ startPlaytimeTrackerLoop() {
851
+ const resetActivity = () => {
852
+ this.lastActivityTime = Date.now();
853
+ if (this.isPausedForInactivity) {
854
+ this.isPausedForInactivity = false;
855
+ this.updatePlaytimeStatusUI();
856
+ }
857
+ };
858
+
859
+ window.addEventListener('mousemove', resetActivity);
860
+ window.addEventListener('keydown', resetActivity);
861
+ window.addEventListener('mousedown', resetActivity);
862
+ window.addEventListener('touchstart', resetActivity);
863
+
864
+ setInterval(() => {
865
+ if (this.isPlayingGame) {
866
+ const inactiveDuration = Date.now() - this.lastActivityTime;
867
+
868
+ if (inactiveDuration >= 60000) {
869
+ this.isPausedForInactivity = true;
870
+ this.updatePlaytimeStatusUI();
871
+ } else {
872
+ this.isPausedForInactivity = false;
873
+ this.playtimeSeconds += 1;
874
+ localStorage.setItem('reaper_playtime_' + this.currentUser.username, this.playtimeSeconds.toString());
875
+ this.updatePlaytimeStatusUI();
876
+
877
+ if (this.playtimeSeconds % 10 === 0) {
878
+ this.syncPlaytimeToDB();
879
+ }
880
+ }
881
+ }
882
+ }, 1000);
883
+ }
884
+
885
+ updatePlaytimeStatusUI() {
886
+ const trackerEl = document.getElementById('player-playtime-counter');
887
+ const badgeEl = document.getElementById('stat-my-playtime');
888
+ const formatted = this.formatPlaytime(this.playtimeSeconds);
889
+
890
+ if (trackerEl) {
891
+ if (this.isPausedForInactivity) {
892
+ trackerEl.textContent = `PAUSED (INACTIVE) - ${formatted}`;
893
+ trackerEl.style.color = '#ffaa00';
894
+ } else {
895
+ trackerEl.textContent = `PLAYTIME: ${formatted}`;
896
+ trackerEl.style.color = '#00d2ff';
897
+ }
898
+ }
899
+
900
+ if (badgeEl) badgeEl.textContent = formatted;
901
+ }
902
+
903
+ formatPlaytime(totalSeconds) {
904
+ const hours = Math.floor(totalSeconds / 3600);
905
+ const mins = Math.floor((totalSeconds % 3600) / 60);
906
+ const secs = totalSeconds % 60;
907
+ if (hours > 0) {
908
+ return `${hours}h ${mins}m ${secs}s`;
909
+ }
910
+ return `${mins}m ${secs}s`;
911
+ }
912
+
913
+ async syncPlaytimeToDB() {
914
+ if (window.reaperFirebase && window.reaperFirebase.db) {
915
+ try {
916
+ await window.reaperFirebase.db.ref(`reaperPlaytime/${this.currentUser.username}`).set({
917
+ username: this.currentUser.username,
918
+ rank: this.currentUser.rank,
919
+ seconds: this.playtimeSeconds,
920
+ lastActive: Date.now()
921
+ });
922
+ } catch (e) {}
923
+ }
924
+ }
925
+
926
+ listenLeaderboard() {
927
+ if (window.reaperFirebase && window.reaperFirebase.db) {
928
+ try {
929
+ window.reaperFirebase.db.ref("reaperPlaytime").on("value", (snap) => {
930
+ const val = snap.val();
931
+ if (val) {
932
+ this.leaderboardData = Object.values(val);
933
+ this.leaderboardData.sort((a, b) => (b.seconds || 0) - (a.seconds || 0));
934
+ } else {
935
+ this.leaderboardData = [
936
+ { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
937
+ ];
938
+ }
939
+ this.renderLeaderboardUI();
940
+ this.renderAdminUsersDirectory();
941
+ });
942
+ } catch (e) {
943
+ this.renderLeaderboardUI();
944
+ }
945
+ } else {
946
+ this.leaderboardData = [
947
+ { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
948
+ ];
949
+ this.renderLeaderboardUI();
950
+ }
951
+ }
952
+
953
+ renderLeaderboardUI() {
954
+ const container = document.getElementById('leaderboard-rows-container');
955
+ if (!container) return;
956
+
957
+ if (this.leaderboardData.length === 0) {
958
+ this.leaderboardData = [
959
+ { username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }
960
+ ];
961
+ }
962
+
963
+ container.innerHTML = this.leaderboardData.slice(0, 20).map((player, idx) => {
964
+ const rankNum = idx + 1;
965
+ const timeStr = this.formatPlaytime(player.seconds || 0);
966
+ const isSelf = player.username === this.currentUser.username;
967
+ const isOwner = player.rank === 'owner';
968
+
969
+ return `
970
+ <div class="leaderboard-row ${isSelf ? 'self-row' : ''}">
971
+ <div class="leaderboard-rank rank-${rankNum}">#${rankNum}</div>
972
+ <div class="leaderboard-user">
973
+ <span style="color: #fff;" class="${isOwner ? 'owner-animated-name' : ''}">${this.escapeHtml(player.username || 'Member')}</span>
974
+ <span class="rank-pill rank-${(player.rank || 'member').toLowerCase()}">${(player.rank || 'MEMBER').toUpperCase()}</span>
975
+ </div>
976
+ <div class="leaderboard-time">${timeStr}</div>
977
+ </div>
978
+ `;
979
+ }).join('');
980
+ }
981
+
982
+ // ==========================================
983
+ // CHAT SUITE (Robust Instant Messaging)
984
+ // ==========================================
985
+ startTimeoutTimerLoop() {
986
+ setInterval(() => {
987
+ this.updateChatStateUI();
988
+ }, 1000);
989
+ }
990
+
991
+ updateChatStateUI() {
992
+ const warningBar = document.getElementById('chat-timeout-bar');
993
+ const input = document.getElementById('red-chat-input');
994
+ const sendBtn = document.getElementById('red-chat-send-btn');
995
+ const lockIndicator = document.getElementById('chat-lock-indicator');
996
+
997
+ if (lockIndicator) {
998
+ lockIndicator.style.display = this.isChatLocked ? 'inline-block' : 'none';
999
+ }
1000
+
1001
+ if (!input || !sendBtn) return;
1002
+
1003
+ const isPrivileged = this.currentUser.rank === 'owner' || this.currentUser.rank === 'chat_mod' || this.currentUser.rank === 'mod';
1004
+ const secondsLeft = window.aiModerator ? window.aiModerator.getTimeoutRemaining() : 0;
1005
+
1006
+ if (secondsLeft > 0 && !isPrivileged) {
1007
+ const formatted = window.aiModerator.formatTime(secondsLeft);
1008
+ if (warningBar) {
1009
+ warningBar.style.display = 'block';
1010
+ warningBar.textContent = `TIMED OUT (${formatted} REMAINING)`;
1011
+ }
1012
+ input.disabled = true;
1013
+ sendBtn.disabled = true;
1014
+ sendBtn.style.opacity = '0.5';
1015
+ input.placeholder = `Timed out (${formatted})...`;
1016
+ } else if (this.isChatLocked && !isPrivileged) {
1017
+ if (warningBar) warningBar.style.display = 'none';
1018
+ input.disabled = true;
1019
+ sendBtn.disabled = true;
1020
+ sendBtn.style.opacity = '0.5';
1021
+ input.placeholder = `[CHAT LOCKED]`;
1022
+ } else {
1023
+ if (warningBar) warningBar.style.display = 'none';
1024
+ input.disabled = false;
1025
+ sendBtn.disabled = false;
1026
+ sendBtn.style.opacity = '1';
1027
+ input.placeholder = "Send message in Chat...";
1028
+ }
1029
+ }
1030
+
1031
+ async handleSendChatMessage() {
1032
+ if (this.currentUser.isBanned) return;
1033
+
1034
+ const isPrivileged = this.currentUser.rank === 'owner' || this.currentUser.rank === 'chat_mod' || this.currentUser.rank === 'mod';
1035
+
1036
+ if (this.isChatLocked && !isPrivileged) {
1037
+ this.showToast("[CHAT LOCKED] Chat is locked.");
1038
+ return;
1039
+ }
1040
+
1041
+ const secondsLeft = window.aiModerator ? window.aiModerator.getTimeoutRemaining() : 0;
1042
+ if (secondsLeft > 0 && !isPrivileged) {
1043
+ this.showToast("You are currently timed out.");
1044
+ return;
1045
+ }
1046
+
1047
+ const input = document.getElementById('red-chat-input');
1048
+ if (!input || !input.value.trim()) return;
1049
+
1050
+ const message = input.value.trim();
1051
+
1052
+ if (window.aiModerator) {
1053
+ const check = window.aiModerator.checkMessage(message, isPrivileged);
1054
+ if (!check.allowed) {
1055
+ input.value = '';
1056
+ this.updateChatStateUI();
1057
+ this.showToast("Message blocked by AI filter. You are timed out for 5 minutes.");
1058
+ return;
1059
+ }
1060
+ }
1061
+
1062
+ input.value = '';
1063
+
1064
+ const msgData = {
1065
+ username: this.currentUser.username,
1066
+ rank: this.currentUser.rank,
1067
+ playtimeStr: this.formatPlaytime(this.playtimeSeconds),
1068
+ message: message,
1069
+ timestamp: Date.now()
1070
+ };
1071
+
1072
+ // Optimistically add to local messages
1073
+ this.chatMessages.push(msgData);
1074
+ this.renderChatMessages();
1075
+
1076
+ // Broadcast to Firebase
1077
+ await window.reaperFirebase.sendChatMessage(msgData);
1078
+ }
1079
+
1080
+ async deleteMessage(msgId, authorName) {
1081
+ const isPrivileged = this.currentUser.rank === 'owner' || this.currentUser.rank === 'chat_mod' || this.currentUser.rank === 'mod';
1082
+ const canDelete = isPrivileged || (authorName === this.currentUser.username);
1083
+ if (!canDelete) {
1084
+ this.showToast("You can only delete your own messages.");
1085
+ return;
1086
+ }
1087
+
1088
+ if (confirm("Delete this message?")) {
1089
+ this.chatMessages = this.chatMessages.filter(m => m.id !== msgId);
1090
+ this.renderChatMessages();
1091
+ await window.reaperFirebase.deleteChatMessage(msgId);
1092
+ this.showToast("Message deleted.");
1093
+ }
1094
+ }
1095
+
1096
+ renderChatMessages() {
1097
+ const container = document.getElementById('red-chat-messages');
1098
+ if (!container) return;
1099
+
1100
+ if (this.chatMessages.length === 0) {
1101
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Chat.</div>`;
1102
+ return;
1103
+ }
1104
+
1105
+ container.innerHTML = this.chatMessages.map(msg => {
1106
+ const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
1107
+ const initial = (msg.username || 'R').charAt(0).toUpperCase();
1108
+ const isSelf = msg.username === this.currentUser.username;
1109
+ const isOwner = msg.rank === 'owner';
1110
+ const isMod = msg.rank === 'chat_mod' || msg.rank === 'mod';
1111
+ const isPrivileged = this.currentUser.rank === 'owner' || this.currentUser.rank === 'chat_mod' || this.currentUser.rank === 'mod';
1112
+ const showDel = isPrivileged || isSelf;
1113
+
1114
+ return `
1115
+ <div class="red-msg-row ${isSelf ? 'msg-self' : 'msg-other'}">
1116
+ <div class="red-msg-avatar" style="${isOwner ? 'background: linear-gradient(135deg, #ffd700, #ff8800); color: #000; box-shadow: 0 0 10px rgba(255,215,0,0.6);' : isMod ? 'background: #00ffaa; color: #000;' : ''}">${initial}</div>
1117
+ <div class="red-msg-body">
1118
+ <div class="red-msg-meta">
1119
+ <span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}">${this.escapeHtml(msg.username || 'Anon')}</span>
1120
+ ${msg.playtimeStr ? `<span class="red-msg-playtime">(${msg.playtimeStr})</span>` : ''}
1121
+ <span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
1122
+ ${showDel ? `<button class="red-btn-del" onclick="app.deleteMessage('${msg.id}', '${msg.username}')" title="Delete message">[X]</button>` : ''}
1123
+ </div>
1124
+ <div class="red-msg-bubble">${this.escapeHtml(msg.message)}</div>
1125
+ </div>
1126
+ </div>
1127
+ `;
1128
+ }).join('');
1129
+
1130
+ container.scrollTop = container.scrollHeight;
1131
+ }
1132
+
1133
+ // ==========================================
1134
+ // GAMES CATALOGUE & PLAYER
1135
+ // ==========================================
1136
+ filterGames(category, search = '') {
1137
+ this.activeCategory = category;
1138
+ this.searchQuery = search.toLowerCase().trim();
1139
+
1140
+ this.filteredGames = this.games.filter(game => {
1141
+ const gName = (game.name || '').toLowerCase();
1142
+ const specials = game.special || [];
1143
+
1144
+ let matchesCat = true;
1145
+ if (category === 'featured') {
1146
+ matchesCat = Boolean(game.featured);
1147
+ } else if (category === 'favorites') {
1148
+ matchesCat = this.favorites.includes(game.id);
1149
+ } else if (category === 'fnf') {
1150
+ matchesCat = specials.includes('fnf') || gName.includes('friday night funkin') || gName.includes('fnf');
1151
+ } else if (category === 'port') {
1152
+ matchesCat = specials.includes('port');
1153
+ } else if (category === 'flash') {
1154
+ matchesCat = specials.includes('flash');
1155
+ } else if (category === 'emulator') {
1156
+ matchesCat = specials.includes('emulator');
1157
+ } else if (category === 'tools') {
1158
+ matchesCat = specials.includes('tools');
1159
+ }
1160
+
1161
+ let matchesSearch = true;
1162
+ if (this.searchQuery) {
1163
+ matchesSearch = (
1164
+ gName.includes(this.searchQuery) ||
1165
+ (game.author && game.author.toLowerCase().includes(this.searchQuery)) ||
1166
+ (game.id && game.id.toString() === this.searchQuery)
1167
+ );
1168
+ }
1169
+
1170
+ return matchesCat && matchesSearch;
1171
+ });
1172
+
1173
+ this.renderGamesGrid();
1174
+ }
1175
+
1176
+ renderGamesGrid() {
1177
+ const grid = document.getElementById('games-grid');
1178
+ if (!grid) return;
1179
+
1180
+ if (this.filteredGames.length === 0) {
1181
+ grid.innerHTML = `
1182
+ <div style="grid-column: 1 / -1; text-align: center; padding: 4rem 1rem; color: #666; font-family: var(--font-mono);">
1183
+ <div style="font-size: 1.8rem; margin-bottom: 0.5rem; color: #fff;">[!] NO GAMES FOUND</div>
1184
+ <p>No games matching category "${this.activeCategory}".</p>
1185
+ </div>
1186
+ `;
1187
+ return;
1188
+ }
1189
+
1190
+ grid.innerHTML = this.filteredGames.map(game => {
1191
+ const isFav = this.favorites.map(String).includes(String(game.id));
1192
+ const coverUrl = game.cover || `https://placehold.co/400x250/041022/ffffff?text=${encodeURIComponent(game.name)}`;
1193
+
1194
+ return `
1195
+ <div class="game-card" onclick="app.launchGame(${game.id})">
1196
+ <div class="game-thumb-container">
1197
+ <img class="game-thumb" src="${coverUrl}" alt="${game.name}" loading="lazy" onerror="this.onerror=null; this.src='https://placehold.co/400x250/041022/ffffff?text=${encodeURIComponent(game.name)}';" />
1198
+ ${game.featured ? '<span class="game-badge-featured">FEATURED</span>' : ''}
1199
+ </div>
1200
+ <div class="game-info">
1201
+ <div class="game-title" title="${game.name}">${game.name}</div>
1202
+ <div class="game-meta">
1203
+ <span class="game-author">${game.author || 'GN-Math'}</span>
1204
+ <button class="btn-fav ${isFav ? 'active' : ''}" onclick="event.stopPropagation(); app.toggleFavorite('${game.id}')" title="${isFav ? 'Remove from favorites' : 'Add to favorites'}">
1205
+ ${isFav ? '★ SAVED' : '☆ FAV'}
1206
+ </button>
1207
+ </div>
1208
+ <button class="btn-card-play">PLAY GAME</button>
1209
+ </div>
1210
+ </div>
1211
+ `;
1212
+ }).join('');
1213
+ }
1214
+
1215
+ toggleFavorite(id) {
1216
+ const strId = String(id);
1217
+ const numId = Number(id);
1218
+ const existingIndex = this.favorites.findIndex(f => String(f) === strId || Number(f) === numId);
1219
+
1220
+ if (existingIndex > -1) {
1221
+ this.favorites.splice(existingIndex, 1);
1222
+ this.showToast("Removed from Favorites");
1223
+ } else {
1224
+ this.favorites.push(numId || strId);
1225
+ this.showToast("Saved to Favorites ★");
1226
+ }
1227
+ localStorage.setItem('reaper_favs', JSON.stringify(this.favorites));
1228
+ this.updateStats();
1229
+ if (this.activeCategory === 'favorites') {
1230
+ this.filterGames('favorites', this.searchQuery);
1231
+ } else {
1232
+ this.renderGamesGrid();
1233
+ }
1234
+ }
1235
+
1236
+ async launchGame(gameId) {
1237
+ const game = this.games.find(g => g.id === gameId);
1238
+ if (!game) return;
1239
+ this.currentGame = game;
1240
+
1241
+ const modal = document.getElementById('game-player-modal');
1242
+ const iframe = document.getElementById('player-iframe');
1243
+ const nameEl = document.getElementById('player-game-name');
1244
+ const statusBadge = document.getElementById('player-status-badge');
1245
+
1246
+ if (modal && iframe) {
1247
+ nameEl.textContent = game.name;
1248
+ if (statusBadge) statusBadge.textContent = 'Loading...';
1249
+
1250
+ modal.style.display = 'flex';
1251
+ document.body.style.overflow = 'hidden';
1252
+
1253
+ this.isPlayingGame = true;
1254
+ this.lastActivityTime = Date.now();
1255
+ this.isPausedForInactivity = false;
1256
+ this.updatePlaytimeStatusUI();
1257
+
1258
+ const filename = game.url.split('/').pop();
1259
+ iframe.removeAttribute('srcdoc');
1260
+
1261
+ try {
1262
+ const res = await fetch(`https://raw.githubusercontent.com/gn-math/html/main/${filename}`);
1263
+ if (res.ok) {
1264
+ let html = await res.text();
1265
+ if (html && html.length > 50) {
1266
+ html = html.replace(/https?:\/\/cdn\.jsdelivr\.net\/gh\//g, 'https://fastly.jsdelivr.net/gh/');
1267
+ html = html.replace(/https?:\/\/cdn\.jsdelivr\.net\/npm\//g, 'https://fastly.jsdelivr.net/npm/');
1268
+
1269
+ iframe.srcdoc = html;
1270
+ if (statusBadge) statusBadge.textContent = 'ONLINE';
1271
+ return;
1272
+ }
1273
+ }
1274
+ } catch (err) {
1275
+ console.warn("Direct fetch fallback:", err);
1276
+ }
1277
+
1278
+ iframe.src = `https://fastly.jsdelivr.net/gh/gn-math/html@main/${filename}`;
1279
+ if (statusBadge) statusBadge.textContent = 'FALLBACK CDN';
1280
+ }
1281
+ }
1282
+
1283
+ closePlayer() {
1284
+ const modal = document.getElementById('game-player-modal');
1285
+ const iframe = document.getElementById('player-iframe');
1286
+ if (modal && iframe) {
1287
+ iframe.src = 'about:blank';
1288
+ iframe.removeAttribute('srcdoc');
1289
+ modal.style.display = 'none';
1290
+ document.body.style.overflow = 'auto';
1291
+ this.currentGame = null;
1292
+
1293
+ this.isPlayingGame = false;
1294
+ this.syncPlaytimeToDB();
1295
+ }
1296
+ }
1297
+
1298
+ reloadPlayer() {
1299
+ if (this.currentGame) {
1300
+ this.launchGame(this.currentGame.id);
1301
+ this.showToast("Reloading game...");
1302
+ }
1303
+ }
1304
+
1305
+ togglePlayerFullscreen() {
1306
+ const frameWrapper = document.querySelector('.player-frame-wrapper');
1307
+ if (!document.fullscreenElement) {
1308
+ frameWrapper.requestFullscreen().catch(e => console.error(e));
1309
+ } else {
1310
+ document.exitFullscreen();
1311
+ }
1312
+ }
1313
+
1314
+ async openGameAboutBlank() {
1315
+ if (!this.currentGame) return;
1316
+ const filename = this.currentGame.url.split('/').pop();
1317
+ const win = window.open('about:blank', '_blank');
1318
+ if (win) {
1319
+ const doc = win.document;
1320
+ doc.title = "Google Classroom";
1321
+ const link = doc.createElement('link');
1322
+ link.rel = 'icon';
1323
+ link.href = 'https://ssl.gstatic.com/classroom/favicon.png';
1324
+ doc.head.appendChild(link);
1325
+
1326
+ const frame = doc.createElement('iframe');
1327
+ frame.style.border = 'none';
1328
+ frame.style.width = '100vw';
1329
+ frame.style.height = '100vh';
1330
+ frame.style.margin = '0';
1331
+ frame.style.position = 'fixed';
1332
+ frame.style.top = '0';
1333
+ frame.style.left = '0';
1334
+ frame.allow = 'autoplay; fullscreen; gamepad';
1335
+
1336
+ try {
1337
+ const res = await fetch(`https://raw.githubusercontent.com/gn-math/html/main/${filename}`);
1338
+ if (res.ok) {
1339
+ let html = await res.text();
1340
+ if (html && html.length > 50) {
1341
+ html = html.replace(/https?:\/\/cdn\.jsdelivr\.net\/gh\//g, 'https://fastly.jsdelivr.net/gh/');
1342
+ html = html.replace(/https?:\/\/cdn\.jsdelivr\.net\/npm\//g, 'https://fastly.jsdelivr.net/npm/');
1343
+ frame.srcdoc = html;
1344
+ } else {
1345
+ frame.src = `https://fastly.jsdelivr.net/gh/gn-math/html@main/${filename}`;
1346
+ }
1347
+ } else {
1348
+ frame.src = `https://fastly.jsdelivr.net/gh/gn-math/html@main/${filename}`;
1349
+ }
1350
+ } catch (e) {
1351
+ frame.src = `https://fastly.jsdelivr.net/gh/gn-math/html@main/${filename}`;
1352
+ }
1353
+
1354
+ doc.body.style.margin = '0';
1355
+ doc.body.style.overflow = 'hidden';
1356
+ doc.body.appendChild(frame);
1357
+ this.showToast("Game launched in about:blank tab.");
1358
+ }
1359
+ }
1360
+
1361
+ openSiteAboutBlank() {
1362
+ const win = window.open('about:blank', '_blank');
1363
+ if (win) {
1364
+ const doc = win.document;
1365
+ doc.title = "Classes";
1366
+ const link = doc.createElement('link');
1367
+ link.rel = 'icon';
1368
+ link.href = 'https://ssl.gstatic.com/classroom/favicon.png';
1369
+ doc.head.appendChild(link);
1370
+
1371
+ const frame = doc.createElement('iframe');
1372
+ frame.style.border = 'none';
1373
+ frame.style.width = '100vw';
1374
+ frame.style.height = '100vh';
1375
+ frame.style.margin = '0';
1376
+ frame.style.position = 'fixed';
1377
+ frame.style.top = '0';
1378
+ frame.style.left = '0';
1379
+ frame.allow = 'autoplay; fullscreen; gamepad';
1380
+ frame.src = window.location.href;
1381
+ doc.body.style.margin = '0';
1382
+ doc.body.style.overflow = 'hidden';
1383
+ doc.body.appendChild(frame);
1384
+ this.showToast("Site cloaked in about:blank.");
1385
+ }
1386
+ }
1387
+
1388
+ handleOpiumSearch() {
1389
+ const input = document.getElementById('opium-search-input');
1390
+ if (!input || !input.value.trim()) return;
1391
+
1392
+ const query = input.value.trim();
1393
+
1394
+ const matchedGame = this.games.find(g => g.name.toLowerCase().includes(query.toLowerCase()));
1395
+ if (matchedGame) {
1396
+ this.launchGame(matchedGame.id);
1397
+ input.value = '';
1398
+ return;
1399
+ }
1400
+
1401
+ this.switchTab('games');
1402
+ const sInput = document.getElementById('search-games-input');
1403
+ if (sInput) {
1404
+ sInput.value = query;
1405
+ this.filterGames('all', query);
1406
+ }
1407
+ input.value = '';
1408
+ }
1409
+
1410
+ openUpdateLogsModal() {
1411
+ const modal = document.getElementById('update-logs-modal');
1412
+ if (modal) modal.style.display = 'flex';
1413
+ }
1414
+
1415
+ closeUpdateLogsModal() {
1416
+ const modal = document.getElementById('update-logs-modal');
1417
+ if (modal) modal.style.display = 'none';
1418
+ }
1419
+
1420
+ renderBanScreen(customReason) {
1421
+ document.body.innerHTML = `
1422
+ <div style="display: flex; height: 100vh; width: 100vw; background: #000; color: #ff2a2a; font-family: var(--font-mono); align-items: center; justify-content: center; text-align: center; padding: 2rem;">
1423
+ <div style="border: 2px solid #ff2a2a; padding: 3rem 2rem; max-width: 540px; background: #0c0202; border-radius: 10px; box-shadow: 0 0 40px rgba(255, 42, 42, 0.4);">
1424
+ <h1 style="font-size: 2.2rem; margin-bottom: 1rem; color: #ff3333;">[PERMANENT BAN]</h1>
1425
+ <p style="color: #ccc; margin-bottom: 1.5rem; font-size: 0.95rem; line-height: 1.6;">
1426
+ ${customReason || 'You have been permanently banned by the Reaper System.'}
1427
+ </p>
1428
+ <div style="font-size: 0.8rem; color: #772222; border-top: 1px solid #331111; padding-top: 1rem;">
1429
+ IDENTIFIER BLACKLISTED // ZERO TOLERANCE POLICY
1430
+ </div>
1431
+ </div>
1432
+ </div>
1433
+ `;
1434
+ }
1435
+
1436
+ renderPermanentShutdownScreen() {
1437
+ document.body.innerHTML = `
1438
+ <div style="display: flex; height: 100vh; width: 100vw; background: #020408; color: #fff; font-family: var(--font-main); align-items: center; justify-content: center; text-align: center; padding: 2rem; box-sizing: border-box;">
1439
+ <div style="border: 2px solid #ff3333; padding: 3.5rem 2.5rem; max-width: 580px; width: 100%; background: rgba(10, 2, 2, 0.96); border-radius: 12px; box-shadow: 0 0 50px rgba(255, 30, 30, 0.5);">
1440
+ <div style="font-size: 3.8rem; margin-bottom: 0.5rem; font-family: var(--font-logo); font-weight: 900; color: #ff4444; letter-spacing: 0.1em;">reaper</div>
1441
+ <h1 style="color: #ffffff; font-size: 1.85rem; font-family: var(--font-heading); margin-bottom: 1.25rem; letter-spacing: 0.05em; font-weight: 900;">WE ARE SHUTTING DOWN FOR GOOD.</h1>
1442
+ <p style="color: #ffaaaa; font-size: 0.95rem; margin-bottom: 2rem; line-height: 1.7;">
1443
+ Reaper Hub has been permanently closed by the Owner. All servers and services have been decommissioned. Thank you to everyone who played.
1444
+ </p>
1445
+ <div style="border-top: 1px solid #331111; padding-top: 1.5rem; display: flex; justify-content: center;">
1446
+ <button onclick="app.promptAdminLogin()" class="btn-stealth" style="color: #884444; border-color: #552222; font-size: 0.72rem; padding: 0.4rem 0.8rem;">
1447
+ OWNER EMERGENCY LOGIN
1448
+ </button>
1449
+ </div>
1450
+ </div>
1451
+ </div>
1452
+ `;
1453
+ }
1454
+
1455
+ renderLockdownScreen() {
1456
+ document.body.innerHTML = `
1457
+ <div style="display: flex; height: 100vh; width: 100vw; background: #050505; color: #fff; font-family: var(--font-main); align-items: center; justify-content: center; text-align: center; padding: 2rem;">
1458
+ <div style="border: 1.5px solid #0088ff; padding: 3rem 2rem; max-width: 550px; background: #041022; border-radius: 10px; box-shadow: 0 0 35px rgba(0, 136, 255, 0.3);">
1459
+ <div style="font-size: 2.5rem; margin-bottom: 1rem; font-family: var(--font-logo); font-weight: 900;">REAPER HUB</div>
1460
+ <h2 style="color: #00d2ff; margin-bottom: 1rem; font-family: var(--font-heading);">[PORTAL MAINTENANCE / LOCKDOWN]</h2>
1461
+ <p style="color: #88aacc; font-size: 0.9rem; margin-bottom: 2rem;">
1462
+ The portal is locked by the Owner.
1463
+ </p>
1464
+ <button onclick="app.promptAdminLogin()" class="btn-primary" style="padding: 0.75rem 1.5rem;">
1465
+ OWNER LOGIN
1466
+ </button>
1467
+ </div>
1468
+ </div>
1469
+ `;
1470
+ }
1471
+
1472
+ generateNewUsername() {
1473
+ const newName = typeof getRandomUsername === 'function' ? getRandomUsername() : "Reaper_" + Math.floor(Math.random() * 9999);
1474
+ this.currentUser.username = newName;
1475
+ localStorage.setItem('reaper_username', newName);
1476
+ this.playtimeSeconds = parseInt(localStorage.getItem('reaper_playtime_' + newName) || '0', 10);
1477
+ this.updateUserUI();
1478
+ this.syncPlaytimeToDB();
1479
+ this.showToast(`Identity: ${newName}`);
1480
+ }
1481
+
1482
+ updateUserUI() {
1483
+ const isOwner = this.currentUser.rank === 'owner';
1484
+ const isMod = this.currentUser.rank === 'chat_mod' || this.currentUser.rank === 'mod';
1485
+
1486
+ const userDisp = document.getElementById('header-username-display');
1487
+ if (userDisp) {
1488
+ userDisp.textContent = this.currentUser.username;
1489
+ userDisp.className = `bind-username ${isOwner ? 'owner-animated-name' : ''}`;
1490
+ }
1491
+
1492
+ const rankBadge = document.getElementById('header-rank-badge');
1493
+ if (rankBadge) {
1494
+ rankBadge.textContent = isOwner ? 'OWNER' : isMod ? 'CHAT MOD' : 'MEMBER';
1495
+ rankBadge.className = `bind-rank rank-pill rank-${isOwner ? 'owner' : isMod ? 'mod' : 'member'}`;
1496
+ }
1497
+
1498
+ const adminNavTab = document.getElementById('nav-tab-admin');
1499
+ if (adminNavTab) {
1500
+ adminNavTab.style.display = isOwner ? 'inline-block' : 'none';
1501
+ }
1502
+
1503
+ const modNavTab = document.getElementById('nav-tab-mod');
1504
+ if (modNavTab) {
1505
+ modNavTab.style.display = (isOwner || isMod) ? 'inline-block' : 'none';
1506
+ }
1507
+ }
1508
+
1509
+ updateStats() {
1510
+ const countEl = document.getElementById('stat-game-count');
1511
+ if (countEl) countEl.textContent = this.games.length;
1512
+
1513
+ const favCountEl = document.getElementById('stat-fav-count');
1514
+ if (favCountEl) favCountEl.textContent = this.favorites.length;
1515
+ }
1516
+
1517
+ // Admin / Owner Controls
1518
+ promptAdminLogin() {
1519
+ const key = prompt("ENTER MASTER OWNER KEY:");
1520
+ if (!key) return;
1521
+ const cleanKey = key.trim().toUpperCase();
1522
+
1523
+ if (cleanKey === "679-102" || cleanKey === "679102") {
1524
+ this.isAdmin = true;
1525
+ this.currentUser.rank = 'owner';
1526
+ localStorage.setItem('reaper_admin_auth', 'true');
1527
+ localStorage.setItem('reaper_rank', 'owner');
1528
+ this.updateUserUI();
1529
+ this.switchTab('admin');
1530
+ this.showToast("TOTAL MASTER OWNER AUTHENTICATED");
1531
+ } else {
1532
+ this.showToast("ACCESS DENIED");
1533
+ }
1534
+ }
1535
+
1536
+ async toggleRedirectGoogle(isRedirect) {
1537
+ this.redirectGoogle = isRedirect;
1538
+ if (window.reaperFirebase) {
1539
+ await window.reaperFirebase.setRedirectGoogle(isRedirect);
1540
+ }
1541
+ this.showToast(isRedirect ? "GOOGLE REDIRECT PANIC MODE ENABLED" : "GOOGLE REDIRECT DISABLED");
1542
+ }
1543
+
1544
+ async toggleKeysLocked(isLocked) {
1545
+ this.keysLocked = isLocked;
1546
+ if (window.reaperFirebase) {
1547
+ await window.reaperFirebase.setKeysLocked(isLocked);
1548
+ }
1549
+ this.showToast(isLocked ? "ALL ACCESS KEYS PAUSED (FROZEN)" : "ACCESS KEYS RESUMED");
1550
+ }
1551
+
1552
+ async toggleVerifyLockdown(isLocked) {
1553
+ this.verifyLockdown = isLocked;
1554
+ if (window.reaperFirebase) {
1555
+ await window.reaperFirebase.setVerifyLockdown(isLocked);
1556
+ }
1557
+ this.showToast(isLocked ? "VERIFY GATEWAY LOCKED" : "VERIFY GATEWAY UNLOCKED");
1558
+ }
1559
+
1560
+ async toggleSiteLockdown(isLocked) {
1561
+ this.siteLockdown = isLocked;
1562
+ if (window.reaperFirebase) {
1563
+ await window.reaperFirebase.setSiteLockdown(isLocked);
1564
+ }
1565
+ this.showToast(isLocked ? "MAIN PORTAL LOCKDOWN ACTIVE" : "MAIN PORTAL LOCKDOWN REMOVED");
1566
+ }
1567
+
1568
+ async toggleChatLock(isLocked) {
1569
+ this.isChatLocked = isLocked;
1570
+ if (window.reaperFirebase) {
1571
+ await window.reaperFirebase.setChatLock(isLocked);
1572
+ }
1573
+ this.updateChatStateUI();
1574
+ this.showToast(isLocked ? "CHAT LOCKED" : "CHAT UNLOCKED");
1575
+ }
1576
+
1577
+ async toggleShutdownMode(isShutdown) {
1578
+ this.siteShutdownMode = isShutdown;
1579
+ if (window.reaperFirebase) {
1580
+ await window.reaperFirebase.setShutdownMode(isShutdown);
1581
+ }
1582
+ this.showToast(isShutdown ? "PERMANENT SHUTDOWN MODE ACTIVATED" : "SHUTDOWN MODE DISABLED");
1583
+ }
1584
+
1585
+ async purgeAllServerData() {
1586
+ if (confirm("WARNING: This will permanently wipe ALL chat messages, playtime leaderboard scores, and access keys across the entire server.\n\nAre you sure you want to PURGE ALL?")) {
1587
+ if (window.reaperFirebase) {
1588
+ await window.reaperFirebase.purgeAllServerData();
1589
+ }
1590
+ this.chatMessages = [];
1591
+ this.leaderboardData = [{ username: this.currentUser.username, seconds: this.playtimeSeconds, rank: this.currentUser.rank }];
1592
+ this.renderChatMessages();
1593
+ this.renderAdminChatLogs();
1594
+ this.renderModChatLogs();
1595
+ this.renderLeaderboardUI();
1596
+ this.renderAdminKeyList({});
1597
+ this.showToast("ALL SERVER DATA PURGED SUCCESSFULLY");
1598
+ }
1599
+ }
1600
+
1601
+ async banUser(username) {
1602
+ if (!username || username === 'owner') return;
1603
+
1604
+ // Wipe locally
1605
+ if (!this.bannedUsers.includes(username)) {
1606
+ this.bannedUsers.push(username);
1607
+ }
1608
+ localStorage.setItem('reaper_banned_users', JSON.stringify(this.bannedUsers));
1609
+
1610
+ // Wipe from leaderboard array
1611
+ this.leaderboardData = this.leaderboardData.filter(u => u.username !== username);
1612
+ this.chatMessages = this.chatMessages.filter(m => m.username !== username);
1613
+ this.renderLeaderboardUI();
1614
+ this.renderChatMessages();
1615
+ this.renderAdminUsersDirectory();
1616
+ this.renderBannedUsersList();
1617
+
1618
+ // Broadcast ban and progress wipe to Firebase
1619
+ if (window.reaperFirebase) {
1620
+ await window.reaperFirebase.banUser(username);
1621
+ }
1622
+
1623
+ this.showToast(`User ${username} BANNED & ALL PROGRESS WIPED.`);
1624
+ }
1625
+
1626
+ async unbanUser(username) {
1627
+ this.bannedUsers = this.bannedUsers.filter(u => u !== username);
1628
+ localStorage.setItem('reaper_banned_users', JSON.stringify(this.bannedUsers));
1629
+ if (window.reaperFirebase) {
1630
+ await window.reaperFirebase.unbanUser(username);
1631
+ }
1632
+ this.showToast(`User ${username} Unbanned.`);
1633
+ this.renderBannedUsersList();
1634
+ }
1635
+
1636
+ permLocalBan() {
1637
+ const input = document.getElementById('admin-ban-username-input');
1638
+ const val = input && input.value.trim() ? input.value.trim() : prompt("ENTER USERNAME TO PERMANENTLY BAN:");
1639
+ if (val) {
1640
+ this.banUser(val.trim());
1641
+ if (input) input.value = '';
1642
+ }
1643
+ }
1644
+
1645
+ renderBannedUsersList() {
1646
+ const container = document.getElementById('admin-banned-list');
1647
+ if (!container) return;
1648
+ if (this.bannedUsers.length === 0) {
1649
+ container.innerHTML = `<div style="color: #88aacc; font-size: 0.82rem;">No banned users.</div>`;
1650
+ return;
1651
+ }
1652
+ container.innerHTML = this.bannedUsers.map(u => `
1653
+ <div style="display: flex; justify-content: space-between; align-items: center; background: #0a182c; padding: 0.4rem 0.6rem; border-radius: 4px; margin-bottom: 0.3rem; border: 1px solid rgba(0, 136, 255, 0.15);">
1654
+ <span style="color: #ff5555; font-weight: 700;">${u}</span>
1655
+ <button onclick="app.unbanUser('${u}')" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.72rem;">UNBAN</button>
1656
+ </div>
1657
+ `).join('');
1658
+ }
1659
+
1660
+ renderAdminChatLogs() {
1661
+ const logsContainer = document.getElementById('admin-chat-logs');
1662
+ if (!logsContainer) return;
1663
+
1664
+ if (this.chatMessages.length === 0) {
1665
+ logsContainer.innerHTML = `<div style="color: #88aacc; padding: 0.5rem;">No logs available.</div>`;
1666
+ return;
1667
+ }
1668
+
1669
+ logsContainer.innerHTML = this.chatMessages.map(msg => `
1670
+ <div style="display: flex; justify-content: space-between; align-items: center; padding: 0.35rem 0.5rem; border-bottom: 1px solid #041022; font-size: 0.78rem;">
1671
+ <div>
1672
+ <strong style="color: ${msg.rank === 'owner' ? '#ffd700' : '#00d2ff'};">${this.escapeHtml(msg.username || 'Anon')}:</strong>
1673
+ <span style="color: #ccc; margin-left: 0.3rem;">${this.escapeHtml(msg.message)}</span>
1674
+ </div>
1675
+ <div style="display: flex; gap: 0.35rem;">
1676
+ <button onclick="app.deleteMessage('${msg.id}', '${msg.username}')" style="color: #ff8888; background: none; border: none; cursor: pointer; font-size: 0.72rem;">DEL</button>
1677
+ <button onclick="app.banUser('${msg.username}')" style="color: #ff3333; background: none; border: none; cursor: pointer; font-size: 0.72rem;">BAN</button>
1678
+ </div>
1679
+ </div>
1680
+ `).join('');
1681
+ }
1682
+
1683
+ clearAllChat() {
1684
+ if (confirm("Clear all public chat messages?")) {
1685
+ localStorage.removeItem('reaper_local_chat');
1686
+ this.chatMessages = [];
1687
+ this.renderChatMessages();
1688
+ this.renderAdminChatLogs();
1689
+ this.showToast("Chat cleared.");
1690
+ }
1691
+ }
1692
+
1693
+ logoutAdmin() {
1694
+ this.isAdmin = false;
1695
+ localStorage.removeItem('reaper_admin_auth');
1696
+ this.currentUser.rank = 'member';
1697
+ localStorage.setItem('reaper_rank', 'member');
1698
+ this.updateUserUI();
1699
+ this.switchTab('home');
1700
+ this.showToast("Logged out of Admin.");
1701
+ }
1702
+
1703
+ setupAdminShortcuts() {
1704
+ document.addEventListener('keydown', (e) => {
1705
+ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'a') {
1706
+ e.preventDefault();
1707
+ this.switchTab('admin');
1708
+ }
1709
+ });
1710
+ }
1711
+
1712
+ showToast(msg) {
1713
+ const toast = document.getElementById('toast-box');
1714
+ if (!toast) return;
1715
+ toast.textContent = msg;
1716
+ toast.style.display = 'block';
1717
+ clearTimeout(this.toastTimer);
1718
+ this.toastTimer = setTimeout(() => { toast.style.display = 'none'; }, 3200);
1719
+ }
1720
+
1721
+ setupEventListeners() {
1722
+ const searchInput = document.getElementById('search-games-input');
1723
+ if (searchInput) {
1724
+ searchInput.addEventListener('input', (e) => {
1725
+ this.filterGames(this.activeCategory, e.target.value);
1726
+ });
1727
+ }
1728
+
1729
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1730
+ btn.addEventListener('click', () => {
1731
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
1732
+ btn.classList.add('active');
1733
+ const cat = btn.getAttribute('data-cat');
1734
+ this.filterGames(cat, searchInput ? searchInput.value : '');
1735
+ });
1736
+ });
1737
+
1738
+ const redChatInput = document.getElementById('red-chat-input');
1739
+ if (redChatInput) {
1740
+ redChatInput.addEventListener('keydown', (e) => {
1741
+ if (e.key === 'Enter') {
1742
+ this.handleSendChatMessage();
1743
+ }
1744
+ });
1745
+ }
1746
+ }
1747
+
1748
+ escapeHtml(str) {
1749
+ const div = document.createElement('div');
1750
+ div.textContent = str;
1751
+ return div.innerHTML;
1752
+ }
1753
+ }
1754
+
1755
+ window.app = new ReaperHubApp();
1756
+ window.addEventListener('DOMContentLoaded', () => {
1757
+ window.app.init();
1758
+ });