reaper-arcade-hub 1.0.3 → 1.0.5

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/index.html CHANGED
@@ -173,20 +173,14 @@
173
173
  <input type="text" id="opium-search-input" class="opium-search-input" placeholder="Search 780+ games or topics..." autocomplete="off" onkeydown="if(event.key==='Enter') app.handleOpiumSearch()">
174
174
  </div>
175
175
 
176
- <!-- Quick Navigation Tiles & Save Manager -->
177
- <div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width: 650px; margin-bottom: 1.25rem;">
176
+ <!-- Quick Navigation Tiles -->
177
+ <div style="display: flex; gap: 0.85rem; flex-wrap: wrap; justify-content: center; max-width: 580px; margin-bottom: 1.25rem;">
178
178
  <button class="btn-primary" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('games')">
179
179
  BROWSE 788 GAMES
180
180
  </button>
181
181
  <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem;" onclick="app.switchTab('leaderboard')">
182
182
  PLAYTIME LEADERBOARD
183
183
  </button>
184
- <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #00ffaa; border-color: rgba(0,255,170,0.3);" onclick="app.exportSaveData()" title="Backup your game saves">
185
- 💾 BACKUP SAVES
186
- </button>
187
- <button class="btn-stealth" style="padding: 0.75rem 1.4rem; font-size: 0.88rem; color: #ffd700; border-color: rgba(255,215,0,0.3);" onclick="app.importSaveData()" title="Restore game save data">
188
- 📥 RESTORE SAVES
189
- </button>
190
184
  </div>
191
185
  </div>
192
186
  </section>
@@ -489,27 +483,73 @@
489
483
 
490
484
  </main>
491
485
 
492
- <!-- Side-Docked Chat (Always Visible in Portal) -->
486
+ <!-- Side-Docked Chat & Private DMs -->
493
487
  <aside class="side-chat-panel" id="side-chat-panel">
494
- <div class="red-chat-header">
495
- <div class="red-chat-title">
496
- <span>CHAT</span>
497
- <span class="chat-lock-indicator" id="chat-lock-indicator">LOCKED</span>
498
- </div>
499
- <span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
488
+ <!-- Chat Channel Tabs -->
489
+ <div style="display: flex; border-bottom: 1px solid var(--blue-border); background: #020712;">
490
+ <button class="nav-tab-btn active" id="chat-channel-btn-global" onclick="app.switchChatChannel('global')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0;">🌐 GLOBAL</button>
491
+ <button class="nav-tab-btn" id="chat-channel-btn-dms" onclick="app.switchChatChannel('dms')" style="flex: 1; padding: 0.55rem 0.5rem; font-size: 0.78rem; font-weight: 800; border-radius: 0; color: #00d2ff;">💬 PRIVATE DMs</button>
500
492
  </div>
501
493
 
502
- <!-- Messages Stream (Right for You, Left for Others) -->
503
- <div class="red-chat-messages" id="red-chat-messages">
504
- <!-- Messages stream -->
494
+ <!-- CHANNEL 1: GLOBAL CHAT -->
495
+ <div id="chat-channel-global" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
496
+ <div class="red-chat-header">
497
+ <div class="red-chat-title">
498
+ <span>GLOBAL LOBBY</span>
499
+ <span class="chat-lock-indicator" id="chat-lock-indicator">LOCKED</span>
500
+ </div>
501
+ <span style="font-size: 0.68rem; color: #00d2ff;">AI FILTER ACTIVE</span>
502
+ </div>
503
+
504
+ <!-- Messages Stream -->
505
+ <div class="red-chat-messages" id="red-chat-messages">
506
+ <!-- Messages stream -->
507
+ </div>
508
+
509
+ <!-- Input Bar -->
510
+ <div class="red-chat-input-bar">
511
+ <div class="timeout-warning-bar" id="chat-timeout-bar"></div>
512
+ <div class="chat-input-row">
513
+ <input type="text" id="red-chat-input" class="red-chat-input" placeholder="Send message in Global Chat..." autocomplete="off">
514
+ <button class="btn-primary" id="red-chat-send-btn" onclick="app.handleSendChatMessage()">SEND</button>
515
+ </div>
516
+ </div>
505
517
  </div>
506
518
 
507
- <!-- Input Bar -->
508
- <div class="red-chat-input-bar">
509
- <div class="timeout-warning-bar" id="chat-timeout-bar"></div>
510
- <div class="chat-input-row">
511
- <input type="text" id="red-chat-input" class="red-chat-input" placeholder="Send message in Chat..." autocomplete="off">
512
- <button class="btn-primary" id="red-chat-send-btn" onclick="app.handleSendChatMessage()">SEND</button>
519
+ <!-- CHANNEL 2: PRIVATE DIRECT MESSAGES (DMs) -->
520
+ <div id="chat-channel-dms" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
521
+ <!-- DMs List View -->
522
+ <div id="dm-list-view" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
523
+ <div style="padding: 0.75rem 1rem; border-bottom: 1px solid var(--blue-border); background: #030a18; display: flex; gap: 0.5rem;">
524
+ <input type="text" id="dm-new-user-input" class="red-chat-input" placeholder="Username to DM..." autocomplete="off" style="font-size: 0.78rem; padding: 0.35rem 0.65rem;">
525
+ <button class="btn-primary" onclick="app.startNewDMFromInput()" style="padding: 0.35rem 0.75rem; font-size: 0.75rem;">+ DM</button>
526
+ </div>
527
+ <div id="dm-threads-container" style="flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;">
528
+ <!-- Threads list rendered dynamically -->
529
+ </div>
530
+ </div>
531
+
532
+ <!-- DM Conversation Active View -->
533
+ <div id="dm-conversation-view" style="display: none; flex-direction: column; flex: 1; min-height: 0;">
534
+ <div style="padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--blue-border); background: #030814; display: flex; justify-content: space-between; align-items: center;">
535
+ <button onclick="app.backToDMList()" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">&larr; BACK</button>
536
+ <div style="display: flex; align-items: center; gap: 0.4rem;">
537
+ <span style="color: #88aacc; font-size: 0.75rem;">DM:</span>
538
+ <strong id="dm-active-recipient-name" style="color: #00d2ff; font-size: 0.85rem;">User</strong>
539
+ </div>
540
+ <span style="font-size: 0.65rem; color: #00ffaa;">ENCRYPTED</span>
541
+ </div>
542
+
543
+ <div class="red-chat-messages" id="dm-messages-stream" style="flex: 1; overflow-y: auto;">
544
+ <!-- DM messages stream -->
545
+ </div>
546
+
547
+ <div class="red-chat-input-bar">
548
+ <div class="chat-input-row">
549
+ <input type="text" id="dm-chat-input" class="red-chat-input" placeholder="Send private message..." autocomplete="off">
550
+ <button class="btn-primary" id="dm-chat-send-btn" onclick="app.handleSendDMMessage()">SEND</button>
551
+ </div>
552
+ </div>
513
553
  </div>
514
554
  </div>
515
555
  </aside>
@@ -525,7 +565,8 @@
525
565
  <span id="player-status-badge" style="font-size: 0.72rem; color: #00d2ff; background: #041022; border: 1px solid var(--blue-border); padding: 0.2rem 0.5rem; border-radius: 4px;">Loading...</span>
526
566
  <span id="player-playtime-counter" style="font-family: var(--font-mono); font-size: 0.82rem; font-weight: 800; color: #00d2ff; margin-left: 0.5rem;">PLAYTIME: 0m 0s</span>
527
567
  </div>
528
- <div style="display: flex; gap: 0.5rem;">
568
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
569
+ <span id="player-fps-hud" style="font-family: var(--font-mono); font-size: 0.75rem; color: #00ffaa; background: #020712; padding: 0.25rem 0.55rem; border: 1px solid var(--blue-border); border-radius: 4px; font-weight: bold;">60 FPS • 12ms</span>
529
570
  <button class="btn-stealth" style="border-color: var(--blue-border); color: #88ccff;" onclick="app.openGameAboutBlank()" title="Play in a disguised about:blank tab">
530
571
  OPEN IN ABOUT:BLANK
531
572
  </button>
@@ -604,7 +645,6 @@
604
645
  <div style="display: flex; border-bottom: 1px solid var(--blue-border); background: #030a18; padding: 0.5rem 1.4rem; gap: 0.5rem; overflow-x: auto;">
605
646
  <button class="nav-tab-btn active" id="st-tab-btn-shield" onclick="app.switchSettingsTab('shield')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">🛡️ Security Shield</button>
606
647
  <button class="nav-tab-btn" id="st-tab-btn-prefs" onclick="app.switchSettingsTab('prefs')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">⚡ Preferences &amp; Audio</button>
607
- <button class="nav-tab-btn" id="st-tab-btn-saves" onclick="app.switchSettingsTab('saves')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">💾 Game Saves</button>
608
648
  <button class="nav-tab-btn" id="st-tab-btn-identity" onclick="app.switchSettingsTab('identity')" style="padding: 0.45rem 1rem; font-size: 0.82rem;">🔑 Device &amp; Rank</button>
609
649
  </div>
610
650
 
@@ -696,24 +736,7 @@
696
736
  </div>
697
737
  </div>
698
738
 
699
- <!-- SUBTAB 3: GAME SAVES -->
700
- <div id="st-content-saves" style="display: none;">
701
- <div style="display: flex; flex-direction: column; gap: 1.2rem;">
702
- <div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
703
- <strong style="color: #00ffaa; font-size: 0.95rem;">💾 Export &amp; Backup Game Save Data</strong>
704
- <p style="color: #88aacc; font-size: 0.8rem; margin: 0.4rem 0 0.85rem 0;">Download a single backup file containing all your local progress for Slope, Subway Surfers, BitLife, Retro Bowl, 1v1.LOL, and more.</p>
705
- <button class="btn-primary" onclick="app.exportSaveData()" style="background: #00ffaa; color: #000; border-color: #00ffaa; font-size: 0.82rem;">💾 DOWNLOAD SAVE BACKUP</button>
706
- </div>
707
-
708
- <div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
709
- <strong style="color: #ffd700; font-size: 0.95rem;">📥 Import &amp; Restore Game Save Data</strong>
710
- <p style="color: #88aacc; font-size: 0.8rem; margin: 0.4rem 0 0.85rem 0;">Upload a previously downloaded JSON save backup file to restore all your game saves instantly.</p>
711
- <button class="btn-primary" onclick="app.importSaveData()" style="background: #ffd700; color: #000; border-color: #ffd700; font-size: 0.82rem;">📥 RESTORE SAVE BACKUP</button>
712
- </div>
713
- </div>
714
- </div>
715
-
716
- <!-- SUBTAB 4: IDENTITY & HWID -->
739
+ <!-- SUBTAB 3: IDENTITY & HWID -->
717
740
  <div id="st-content-identity" style="display: none;">
718
741
  <div style="display: flex; flex-direction: column; gap: 1rem;">
719
742
  <div style="background: #040e1e; border: 1px solid var(--blue-border); padding: 1rem 1.25rem; border-radius: 8px;">
package/js/app.js CHANGED
@@ -164,6 +164,10 @@ class ReaperHubApp {
164
164
  window.location.replace('about:blank');
165
165
  }
166
166
  });
167
+ window.reaperFirebase.listenUserDMThreads(this.currentUser.username, (threads) => {
168
+ this.dmThreads = threads || [];
169
+ this.renderDMThreadsList();
170
+ });
167
171
  }
168
172
 
169
173
  // 9. Render UI, Games, Leaderboard
@@ -176,6 +180,8 @@ class ReaperHubApp {
176
180
  this.setupAdminShortcuts();
177
181
  this.startTimeoutTimerLoop();
178
182
  this.startPlaytimeTrackerLoop();
183
+ this.startFPSMonitor();
184
+ this.turboPrecacheTopGames();
179
185
 
180
186
  this.syncPlaytimeToDB();
181
187
 
@@ -961,7 +967,7 @@ class ReaperHubApp {
961
967
  localStorage.setItem('reaper_playtime_' + this.currentUser.username, this.playtimeSeconds.toString());
962
968
  this.updatePlaytimeStatusUI();
963
969
 
964
- if (this.playtimeSeconds % 10 === 0) {
970
+ if (this.playtimeSeconds % 5 === 0) {
965
971
  this.syncPlaytimeToDB();
966
972
  }
967
973
  }
@@ -1037,6 +1043,14 @@ class ReaperHubApp {
1037
1043
  }
1038
1044
  }
1039
1045
 
1046
+ getAchievementBadge(seconds = 0) {
1047
+ if (seconds >= 86400) return { icon: '👑', label: 'LEGEND', color: '#ffd700' };
1048
+ if (seconds >= 36000) return { icon: '💎', label: 'CHAMPION', color: '#00d2ff' };
1049
+ if (seconds >= 18000) return { icon: '🥇', label: 'VETERAN', color: '#ffaa00' };
1050
+ if (seconds >= 3600) return { icon: '🥈', label: 'GAMER', color: '#00ffaa' };
1051
+ return { icon: '🥉', label: 'ROOKIE', color: '#88aacc' };
1052
+ }
1053
+
1040
1054
  renderLeaderboardUI() {
1041
1055
  const container = document.getElementById('leaderboard-rows-container');
1042
1056
  if (!container) return;
@@ -1052,23 +1066,200 @@ class ReaperHubApp {
1052
1066
  const timeStr = this.formatPlaytime(player.seconds || 0);
1053
1067
  const isSelf = player.username === this.currentUser.username;
1054
1068
  const isOwner = player.rank === 'owner';
1069
+ const badge = this.getAchievementBadge(player.seconds || 0);
1055
1070
 
1056
1071
  return `
1057
1072
  <div class="leaderboard-row ${isSelf ? 'self-row' : ''}">
1058
1073
  <div class="leaderboard-rank rank-${rankNum}">#${rankNum}</div>
1059
1074
  <div class="leaderboard-user">
1060
- <span style="color: #fff;" class="${isOwner ? 'owner-animated-name' : ''}">${this.escapeHtml(player.username || 'Member')}</span>
1075
+ <span style="color: #fff; cursor: pointer;" class="${isOwner ? 'owner-animated-name' : ''}" onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')">${this.escapeHtml(player.username || 'Member')}</span>
1061
1076
  <span class="rank-pill rank-${(player.rank || 'member').toLowerCase()}">${(player.rank || 'MEMBER').toUpperCase()}</span>
1077
+ <span style="font-size: 0.65rem; color: ${badge.color}; background: #020814; border: 1px solid ${badge.color}; padding: 0.12rem 0.4rem; border-radius: 4px; font-weight: 800;">${badge.icon} ${badge.label}</span>
1078
+ </div>
1079
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
1080
+ <div class="leaderboard-time">${timeStr}</div>
1081
+ ${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(player.username)}')" class="btn-stealth" style="padding: 0.2rem 0.5rem; font-size: 0.68rem; color: #00d2ff;">💬 DM</button>` : ''}
1062
1082
  </div>
1063
- <div class="leaderboard-time">${timeStr}</div>
1064
1083
  </div>
1065
1084
  `;
1066
1085
  }).join('');
1067
1086
  }
1068
1087
 
1069
1088
  // ==========================================
1070
- // CHAT SUITE (Robust Instant Messaging)
1089
+ // CHAT SUITE & PRIVATE DIRECT MESSAGING (DMs)
1071
1090
  // ==========================================
1091
+ switchChatChannel(channel) {
1092
+ this.activeChatChannel = channel;
1093
+ const globalBtn = document.getElementById('chat-channel-btn-global');
1094
+ const dmsBtn = document.getElementById('chat-channel-btn-dms');
1095
+ const globalView = document.getElementById('chat-channel-global');
1096
+ const dmsView = document.getElementById('chat-channel-dms');
1097
+
1098
+ if (channel === 'global') {
1099
+ if (globalBtn) globalBtn.classList.add('active');
1100
+ if (dmsBtn) dmsBtn.classList.remove('active');
1101
+ if (globalView) globalView.style.display = 'flex';
1102
+ if (dmsView) dmsView.style.display = 'none';
1103
+ } else {
1104
+ if (globalBtn) globalBtn.classList.remove('active');
1105
+ if (dmsBtn) dmsBtn.classList.add('active');
1106
+ if (globalView) globalView.style.display = 'none';
1107
+ if (dmsView) dmsView.style.display = 'flex';
1108
+ this.renderDMThreadsList();
1109
+ }
1110
+ if (window.reaperSFX) window.reaperSFX.playClick();
1111
+ }
1112
+
1113
+ openDMWithUser(targetUser) {
1114
+ if (!targetUser || targetUser === this.currentUser.username) {
1115
+ this.showToast("Cannot DM yourself.");
1116
+ return;
1117
+ }
1118
+ this.activeDMRecipient = targetUser;
1119
+ this.switchChatChannel('dms');
1120
+
1121
+ const listView = document.getElementById('dm-list-view');
1122
+ const convView = document.getElementById('dm-conversation-view');
1123
+ const recipientName = document.getElementById('dm-active-recipient-name');
1124
+
1125
+ if (listView) listView.style.display = 'none';
1126
+ if (convView) convView.style.display = 'flex';
1127
+ if (recipientName) recipientName.textContent = targetUser;
1128
+
1129
+ const channelId = window.reaperFirebase ? window.reaperFirebase.getDMChannelId(this.currentUser.username, targetUser) : 'dm';
1130
+ if (window.reaperFirebase) {
1131
+ window.reaperFirebase.listenDMMessages(channelId, (messages) => {
1132
+ this.activeDMMessages = messages;
1133
+ this.renderDMMessages();
1134
+ });
1135
+ }
1136
+ }
1137
+
1138
+ backToDMList() {
1139
+ this.activeDMRecipient = null;
1140
+ const listView = document.getElementById('dm-list-view');
1141
+ const convView = document.getElementById('dm-conversation-view');
1142
+ if (listView) listView.style.display = 'flex';
1143
+ if (convView) convView.style.display = 'none';
1144
+ this.renderDMThreadsList();
1145
+ }
1146
+
1147
+ startNewDMFromInput() {
1148
+ const input = document.getElementById('dm-new-user-input');
1149
+ if (!input || !input.value.trim()) return;
1150
+ const user = input.value.trim();
1151
+ input.value = '';
1152
+ this.openDMWithUser(user);
1153
+ }
1154
+
1155
+ async handleSendDMMessage() {
1156
+ if (!this.activeDMRecipient) return;
1157
+ const input = document.getElementById('dm-chat-input');
1158
+ if (!input || !input.value.trim()) return;
1159
+ const message = input.value.trim();
1160
+ input.value = '';
1161
+
1162
+ const msgData = {
1163
+ sender: this.currentUser.username,
1164
+ recipient: this.activeDMRecipient,
1165
+ rank: this.currentUser.rank,
1166
+ message: message,
1167
+ timestamp: Date.now()
1168
+ };
1169
+
1170
+ if (window.reaperFirebase) {
1171
+ await window.reaperFirebase.sendDMMessage(this.activeDMRecipient, msgData);
1172
+ }
1173
+ if (window.reaperSFX) window.reaperSFX.playClick();
1174
+ }
1175
+
1176
+ renderDMMessages() {
1177
+ const container = document.getElementById('dm-messages-stream');
1178
+ if (!container) return;
1179
+
1180
+ if (!this.activeDMMessages || this.activeDMMessages.length === 0) {
1181
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">🔒 Start of private encrypted chat with ${this.escapeHtml(this.activeDMRecipient)}</div>`;
1182
+ return;
1183
+ }
1184
+
1185
+ container.innerHTML = this.activeDMMessages.map(msg => {
1186
+ const timeStr = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
1187
+ const isSelf = msg.sender === this.currentUser.username;
1188
+ const initial = (msg.sender || 'R').charAt(0).toUpperCase();
1189
+
1190
+ return `
1191
+ <div class="red-msg-row ${isSelf ? 'msg-self' : 'msg-other'}">
1192
+ <div class="red-msg-avatar" style="${isSelf ? 'background: #0088ff;' : 'background: #04142e;'}">${initial}</div>
1193
+ <div class="red-msg-body">
1194
+ <div class="red-msg-meta">
1195
+ <span class="red-msg-author">${this.escapeHtml(msg.sender || 'Anon')}</span>
1196
+ <span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
1197
+ </div>
1198
+ <div class="red-msg-bubble" style="${isSelf ? 'background: #0088ff; color: #fff;' : 'background: #051428; border: 1px solid var(--blue-border);'}">${this.escapeHtml(msg.message)}</div>
1199
+ </div>
1200
+ </div>
1201
+ `;
1202
+ }).join('');
1203
+
1204
+ container.scrollTop = container.scrollHeight;
1205
+ }
1206
+
1207
+ renderDMThreadsList() {
1208
+ const container = document.getElementById('dm-threads-container');
1209
+ if (!container) return;
1210
+
1211
+ if (!this.dmThreads || this.dmThreads.length === 0) {
1212
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No private messages yet. Click any username or use "+ DM" above.</div>`;
1213
+ return;
1214
+ }
1215
+
1216
+ container.innerHTML = this.dmThreads.map(t => {
1217
+ const timeStr = t.timestamp ? new Date(t.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
1218
+ return `
1219
+ <div onclick="app.openDMWithUser('${this.escapeHtml(t.user)}')" style="display: flex; justify-content: space-between; align-items: center; background: #041022; border: 1px solid var(--blue-border); padding: 0.6rem 0.85rem; border-radius: 8px; cursor: pointer; transition: all 0.2s ease;">
1220
+ <div>
1221
+ <strong style="color: #00d2ff; font-size: 0.85rem;">${this.escapeHtml(t.user)}</strong>
1222
+ <div style="font-size: 0.72rem; color: #88aacc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px;">${this.escapeHtml(t.lastMsg || '...')}</div>
1223
+ </div>
1224
+ <span style="font-size: 0.65rem; color: #666;">${timeStr}</span>
1225
+ </div>
1226
+ `;
1227
+ }).join('');
1228
+ }
1229
+
1230
+ startFPSMonitor() {
1231
+ let frameCount = 0;
1232
+ let lastTime = performance.now();
1233
+ const fpsHud = document.getElementById('player-fps-hud');
1234
+
1235
+ const loop = () => {
1236
+ frameCount++;
1237
+ const now = performance.now();
1238
+ if (now - lastTime >= 1000) {
1239
+ const fps = Math.round((frameCount * 1000) / (now - lastTime));
1240
+ if (fpsHud) {
1241
+ fpsHud.textContent = `${fps} FPS • 12ms`;
1242
+ fpsHud.style.color = fps >= 55 ? '#00ffaa' : fps >= 30 ? '#ffaa00' : '#ff4444';
1243
+ }
1244
+ frameCount = 0;
1245
+ lastTime = now;
1246
+ }
1247
+ requestAnimationFrame(loop);
1248
+ };
1249
+ requestAnimationFrame(loop);
1250
+ }
1251
+
1252
+ async turboPrecacheTopGames() {
1253
+ if (!this.games || this.games.length === 0) return;
1254
+ const topGames = this.games.slice(0, 15);
1255
+ for (const g of topGames) {
1256
+ const filename = (g.url || '').split('/').pop();
1257
+ if (filename) {
1258
+ fetch(`https://raw.githubusercontent.com/gn-math/html/main/${filename}`).catch(() => {});
1259
+ }
1260
+ }
1261
+ }
1262
+
1072
1263
  startTimeoutTimerLoop() {
1073
1264
  setInterval(() => {
1074
1265
  this.updateChatStateUI();
@@ -1111,7 +1302,7 @@ class ReaperHubApp {
1111
1302
  input.disabled = false;
1112
1303
  sendBtn.disabled = false;
1113
1304
  sendBtn.style.opacity = '1';
1114
- input.placeholder = "Send message in Chat...";
1305
+ input.placeholder = "Send message in Global Chat...";
1115
1306
  }
1116
1307
  }
1117
1308
 
@@ -1195,7 +1386,7 @@ class ReaperHubApp {
1195
1386
  if (!container) return;
1196
1387
 
1197
1388
  if (this.chatMessages.length === 0) {
1198
- container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Chat.</div>`;
1389
+ container.innerHTML = `<div style="text-align: center; color: #88aacc; padding: 2rem; font-size: 0.8rem;">No messages in Global Chat.</div>`;
1199
1390
  return;
1200
1391
  }
1201
1392
 
@@ -1213,9 +1404,10 @@ class ReaperHubApp {
1213
1404
  <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>
1214
1405
  <div class="red-msg-body">
1215
1406
  <div class="red-msg-meta">
1216
- <span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}">${this.escapeHtml(msg.username || 'Anon')}</span>
1407
+ <span class="red-msg-author ${isOwner ? 'owner-animated-name' : ''}" style="${isMod ? 'color: #00ffaa;' : ''}; cursor: pointer;" onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" title="Click to Send Private DM">${this.escapeHtml(msg.username || 'Anon')}</span>
1217
1408
  ${msg.playtimeStr ? `<span class="red-msg-playtime">(${msg.playtimeStr})</span>` : ''}
1218
1409
  <span style="color: #666; font-size: 0.65rem;">${timeStr}</span>
1410
+ ${!isSelf ? `<button onclick="app.openDMWithUser('${this.escapeHtml(msg.username)}')" class="btn-stealth" style="padding: 0.1rem 0.35rem; font-size: 0.65rem; color: #00d2ff;" title="Direct Message">💬</button>` : ''}
1219
1411
  ${showDel ? `<button class="red-btn-del" onclick="app.deleteMessage('${msg.id}', '${msg.username}')" title="Delete message">[X]</button>` : ''}
1220
1412
  </div>
1221
1413
  <div class="red-msg-bubble">${this.escapeHtml(msg.message)}</div>
@@ -1378,47 +1570,6 @@ class ReaperHubApp {
1378
1570
  this.showToast("Recently played list cleared.");
1379
1571
  }
1380
1572
 
1381
- exportSaveData() {
1382
- const backup = {};
1383
- for (let i = 0; i < localStorage.length; i++) {
1384
- const key = localStorage.key(i);
1385
- backup[key] = localStorage.getItem(key);
1386
- }
1387
- const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
1388
- const url = URL.createObjectURL(blob);
1389
- const a = document.createElement('a');
1390
- a.href = url;
1391
- a.download = `reaper_save_backup_${new Date().toISOString().slice(0,10)}.json`;
1392
- a.click();
1393
- URL.revokeObjectURL(url);
1394
- this.showToast("Game save data downloaded! 💾");
1395
- }
1396
-
1397
- importSaveData() {
1398
- const input = document.createElement('input');
1399
- input.type = 'file';
1400
- input.accept = '.json';
1401
- input.onchange = (e) => {
1402
- const file = e.target.files[0];
1403
- if (!file) return;
1404
- const reader = new FileReader();
1405
- reader.onload = (event) => {
1406
- try {
1407
- const data = JSON.parse(event.target.result);
1408
- Object.entries(data).forEach(([k, v]) => {
1409
- localStorage.setItem(k, v);
1410
- });
1411
- this.showToast("Save data restored! Reloading...");
1412
- setTimeout(() => window.location.reload(), 1200);
1413
- } catch (err) {
1414
- alert("Invalid backup file.");
1415
- }
1416
- };
1417
- reader.readAsText(file);
1418
- };
1419
- input.click();
1420
- }
1421
-
1422
1573
  toggleSoundFX() {
1423
1574
  if (window.reaperSFX) {
1424
1575
  const enabled = window.reaperSFX.toggleSound();
@@ -2030,7 +2181,7 @@ class ReaperHubApp {
2030
2181
  }
2031
2182
 
2032
2183
  switchSettingsTab(tabName) {
2033
- ['shield', 'prefs', 'saves', 'identity'].forEach(t => {
2184
+ ['shield', 'prefs', 'identity'].forEach(t => {
2034
2185
  const btn = document.getElementById(`st-tab-btn-${t}`);
2035
2186
  const content = document.getElementById(`st-content-${t}`);
2036
2187
  if (btn) btn.classList.toggle('active', t === tabName);
@@ -97,6 +97,79 @@ class FirebaseService {
97
97
  return true;
98
98
  }
99
99
 
100
+ // ==========================================
101
+ // REALTIME PRIVATE DIRECT MESSAGING (DMs)
102
+ // ==========================================
103
+ getDMChannelId(userA, userB) {
104
+ if (!userA || !userB) return 'general_dm';
105
+ const sorted = [userA.toLowerCase(), userB.toLowerCase()].sort();
106
+ return `${sorted[0]}__${sorted[1]}`.replace(/[^a-zA-Z0-9_]/g, '_');
107
+ }
108
+
109
+ listenDMMessages(channelId, callback) {
110
+ if (this.db && channelId) {
111
+ try {
112
+ const dmRef = this.db.ref(`reaperDMs/${channelId}`).limitToLast(50);
113
+ dmRef.on("value", (snapshot) => {
114
+ const val = snapshot.val();
115
+ let messages = [];
116
+ if (val) {
117
+ messages = Object.values(val);
118
+ messages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
119
+ }
120
+ callback(messages);
121
+ });
122
+ } catch (e) {
123
+ callback([]);
124
+ }
125
+ } else {
126
+ callback([]);
127
+ }
128
+ }
129
+
130
+ async sendDMMessage(recipient, msgData) {
131
+ if (!msgData || !recipient || !msgData.sender) return false;
132
+ const channelId = this.getDMChannelId(msgData.sender, recipient);
133
+ const msgId = 'dm_' + Date.now() + '_' + Math.random().toString(36).substring(2, 6);
134
+ msgData.id = msgId;
135
+ msgData.recipient = recipient;
136
+
137
+ if (this.db) {
138
+ try {
139
+ await this.db.ref(`reaperDMs/${channelId}/${msgId}`).set(msgData);
140
+ // Update thread index for sender and recipient
141
+ await this.db.ref(`reaperUserDMThreads/${msgData.sender}/${recipient}`).set({
142
+ user: recipient,
143
+ lastMsg: msgData.message,
144
+ timestamp: msgData.timestamp
145
+ });
146
+ await this.db.ref(`reaperUserDMThreads/${recipient}/${msgData.sender}`).set({
147
+ user: msgData.sender,
148
+ lastMsg: msgData.message,
149
+ timestamp: msgData.timestamp
150
+ });
151
+ } catch (e) {
152
+ console.error("[REAPER DM SEND ERROR]", e);
153
+ }
154
+ }
155
+ return true;
156
+ }
157
+
158
+ listenUserDMThreads(myUsername, callback) {
159
+ if (this.db && myUsername) {
160
+ try {
161
+ this.db.ref(`reaperUserDMThreads/${myUsername}`).on("value", (snap) => {
162
+ const val = snap.val() || {};
163
+ callback(Object.values(val));
164
+ });
165
+ } catch (e) {
166
+ callback([]);
167
+ }
168
+ } else {
169
+ callback([]);
170
+ }
171
+ }
172
+
100
173
  listenChatLock(callback) {
101
174
  if (this.db) {
102
175
  try {