diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index ced0867a9..871fc2350 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -19,6 +19,7 @@ import { BattleTextParser, type Args } from './battle-text-parser'; import type { BattleRoom } from './panel-battle'; import { Teams } from './battle-teams'; import type preact from '../js/lib/preact'; +import { PSHeader } from './panel-topbar'; declare const BattleTextAFD: any; declare const BattleTextNotAFD: any; @@ -1044,6 +1045,11 @@ export class PSRoom extends PSStreamModel implements RoomOptions { } } } catch {} + // extra check because the roomIsFocused is not accurate for some reason + if (document.visibilityState !== 'visible') { + PS.isNotifying = true; + PSHeader.updateFavicon(); + } } if (options.noAutoDismiss && !options.id) { throw new Error(`Must specify id for manual dismissing`); @@ -1073,6 +1079,7 @@ export class PSRoom extends PSStreamModel implements RoomOptions { this.notifications[i].notification?.close(); } catch {} this.notifications.splice(i, 1); + PSHeader.updateFavicon(); } dismissNotification(id: string) { const index = this.notifications.findIndex(n => n.id === id); @@ -1504,6 +1511,12 @@ export class PSRoom extends PSStreamModel implements RoomOptions { PS.prefs.set('nounlink', false); this.add('||Locked/banned users\' chat messages: HIDDEN'); }, + 'whatsnew'() { + const success = PSHeader.clickCounter.click(); + if (success) { + PS.join('patchnotes' as RoomID); + } + }, 'hl,highlight'(target) { let highlights = PS.prefs.highlights || {}; if (target.includes(' ')) { @@ -1930,6 +1943,8 @@ export const PS = new class extends PSModel { /** Tracks whether or not to display the "Use arrow keys" hint */ arrowKeysUsed = false; + isNotifying = false; + newsHTML = document.querySelector('#room-news .readable-bg')?.innerHTML || ''; newsId = document.getElementById('room-news')?.getAttribute('data-newsid') || null; @@ -2790,6 +2805,15 @@ export const PS = new class extends PSModel { this.prefs.set('autojoin', autojoin); } } + getNotificationsCount() { + let count = 0; + const notificationRooms = [...PS.leftRoomList, ...PS.rightRoomList, ...PS.miniRoomList]; + for (const roomid of notificationRooms) { + const miniNotifications = PS.rooms[roomid]?.notifications; + if (miniNotifications?.length) count++; + } + return count; + } requestNotifications() { try { if (window.webkitNotifications?.requestPermission) { diff --git a/play.pokemonshowdown.com/src/panel-popups.tsx b/play.pokemonshowdown.com/src/panel-popups.tsx index 67eab3d8a..d90215e64 100644 --- a/play.pokemonshowdown.com/src/panel-popups.tsx +++ b/play.pokemonshowdown.com/src/panel-popups.tsx @@ -1928,6 +1928,102 @@ class RulesPanel extends PSRoomPanel { } } +class PatchNotesPanel extends PSRoomPanel { + static readonly id = 'patchnotes'; + static readonly routes = ['patchnotes']; + static readonly location = 'semimodal-popup'; + static readonly noURL = true; + static readonly Model = PopupRoom; + + getRandomPatchNotes() { + const allTitles = [ + "Patch Notes (because chaos needs documentation)", + "Patch Notes: Now with 30% fewer bugs (No promises)", + "Patch Notes — We Swear It’s Better This Time", + "Patch Notes: Definitely Didn’t Break Anything Else", + "Patch Notes: Technically “Improvements”", + "Patch Notes — Version: “It Works on My Machine”", + "Patch Notes (We Fixed Things You Didn’t Know Were Broken)", + "Patch Notes: Stability? Never Heard of It.", + "Patch Notes — Slightly More Functional Than Yesterday", + "Patch Notes: The Bugs Strike Back", + "Patch Notes (A Love Letter to Debugging)", + "Patch Notes: Powered by Caffeine and Regret", + "Patch Notes — “Trust Me Bro” Edition", + ]; + const allNotes = [ + "Fixed a bug where clicking really hard didn’t make things load faster.", + "Improved stability by removing one line of code we didn’t understand.", + "Added a 'quantum uncertainty' mode — the UI may or may not respond.", + "Performance improved by at least 0.0001% (scientifically proven).", + "Added new AI: 'Emotionally Unstable Bot' — it forfeits if you’re mean.", + "Pokémon now occasionally question the meaning of their existence mid-battle.", + "Critical hits are now determined by your karma.", + "Fixed a bug where RNG was too fair.", + "Improved matchmaking: you’ll now face someone just as tilted as you.", + "Added experimental 'Therapist Mode': the game listens, you vent.", + "The app now looks 30% faster, even if it isn’t.", + "Chat filters now detect sarcasm with 4% accuracy.", + "Fixed a bug where players were sometimes right on the internet.", + "Improved mod tools: moderators can now sense chaos before it happens.", + "Backend now powered by pure friendship and caffeine.", + "Optimized the code to run slightly worse, but with more confidence.", + "Fixed an issue where the bug fix introduced more bugs.", + "Added 'Are you sure?' dialog. It doesn’t do anything, but feels safe.", + "Reduced load times by moving progress bar faster.", + "Replaced all semicolons with good vibes.", + "New setting: 'Developer Tears Mode' — enhances realism.", + "You can now press any key to feel like you’re helping.", + "Made UI 15% shinier for improved morale.", + "Bug reports are now automatically forwarded to the void.", + "Buffed RNG. It’s now personal.", + "Lucario’s aura now glows brighter when you lie.", + "Fixed crash where Trainer couldn’t accept defeat gracefully.", + "Buffed memes. Nerfed logic.", + "New mechanic: 'Disconnect Immunity' — doesn’t exist, but sounds nice.", + "Fixed an issue where players believed in fair matchups.", + "Nerfed stall strategies. (You’re welcome.)", + "Fixed an exploit where players had hope during battles.", + "Blissey now charges for therapy sessions.", + "Togekiss’s Air Slash now comes with a free rage quit.", + "Increased Confusion duration — the devs thought it was funny.", + "Garchomp’s Sand Veil now works indoors, somehow.", + "Rebalanced RNG: it now hates everyone equally.", + "Pikachu’s Thunderbolt now powered by real electricity bills.", + "Machamp now flexes every 3 turns automatically.", + "Buffed Magikarp’s Splash — it now causes mild emotional distress.", + "Nerfed Toxic stall teams. We’re doing this for humanity.", + "Alakazam now needs a PhD to understand its own Special Attack stat.", + "Zoroark’s Illusion now also fools itself sometimes.", + "Fixed a bug where Ditto copied your emotional damage too.", + "Gengar now giggles 20% more menacingly.", + "Nerfed RNG. It was getting too self-aware.", + "Charizard finally acknowledged that it’s not a Dragon-type. Therapy helped.", + ]; + + const title = allTitles.sort(() => 0.5 - Math.random())[0]; + const notes = allNotes.sort(() => 0.5 - Math.random()).slice(0, 12); + return { title, notes }; + } + + override render() { + const room = this.props.room; + const patchNotesData = this.getRandomPatchNotes(); + return +
+

{patchNotesData.title}

+ + {patchNotesData.notes + .map((note, i) =>

{i + 1}. {note}

)} +

+
+
; + } +} + PS.addRoomType( UserPanel, UserOptionsPanel, @@ -1947,5 +2043,6 @@ PS.addRoomType( RoomTabListPanel, BattleOptionsPanel, BattleTimerPanel, - RulesPanel + RulesPanel, + PatchNotesPanel ); diff --git a/play.pokemonshowdown.com/src/panel-topbar.tsx b/play.pokemonshowdown.com/src/panel-topbar.tsx index a00c54163..8740c39e9 100644 --- a/play.pokemonshowdown.com/src/panel-topbar.tsx +++ b/play.pokemonshowdown.com/src/panel-topbar.tsx @@ -21,6 +21,28 @@ window.addEventListener('dragover', e => { }); export class PSHeader extends preact.Component { + static clickCounter = { + count: 0, + lastClick: 0, + maxGap: 500, + click() { + const now = Date.now(); + + if (now - this.lastClick > this.maxGap) { + this.count = 1; + } else { + this.count++; + } + + this.lastClick = now; + + if (this.count === 10) { + this.count = 0; + return true; + } + return false; + }, + }; static toggleMute = (e: Event) => { PS.prefs.set('mute', !PS.prefs.mute); PS.update(); @@ -152,6 +174,13 @@ export class PSHeader extends preact.Component { {closeButton} ; } + static updateFavicon() { + const favicon = document.querySelector('#dynamic-favicon'); + if (favicon instanceof HTMLLinkElement) { + favicon.href = `${window.Dex.resourcePrefix}/${PS.isNotifying ? 'favicon-notify.ico' : 'favicon.ico'}`; + favicon.dataset.on = PS.isNotifying ? '1' : ''; + } + } handleResize = () => { if (!this.base) return; @@ -228,6 +257,7 @@ export class PSHeader extends preact.Component { src={`https://${Config.routes.client}/favicon-256.png`} alt="Pokémon Showdown! (beta)" width="50" height="50" + data-cmd="/whatsnew" />
    @@ -269,6 +299,7 @@ export class PSHeader extends preact.Component { src={`https://${Config.routes.client}/favicon-256.png`} alt="Pokémon Showdown! (beta)" width="48" height="48" + data-cmd="/whatsnew" /> {PSHeader.renderRoomTab(PS.leftRoomList[0])} @@ -308,13 +339,7 @@ export class PSMiniHeader extends preact.Component { }; override render() { if (PS.leftPanelWidth !== null) return null; - - let notificationsCount = 0; - const notificationRooms = [...PS.leftRoomList, ...PS.rightRoomList]; - for (const roomid of notificationRooms) { - const miniNotifications = PS.rooms[roomid]?.notifications; - if (miniNotifications?.length) notificationsCount++; - } + const notificationsCount = PS.getNotificationsCount(); const { icon, title } = PSHeader.roomInfo(PS.panel); const userColor = window.BattleLog && `color:${PS.user.away ? '#888' : BattleLog.usernameColor(PS.user.userid)}`; const showMenuButton = PSView.narrowMode; diff --git a/play.pokemonshowdown.com/src/panels.tsx b/play.pokemonshowdown.com/src/panels.tsx index 5f6cd8506..a211fb2b6 100644 --- a/play.pokemonshowdown.com/src/panels.tsx +++ b/play.pokemonshowdown.com/src/panels.tsx @@ -646,6 +646,11 @@ export class PSView extends preact.Component { PS.dragging = null; }); + window.addEventListener('focus', () => { + PS.isNotifying = false; + PSHeader.updateFavicon(); + }); + const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)'); if (colorSchemeQuery?.media !== 'not all') { colorSchemeQuery.addEventListener('change', cs => {