Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
ef96bfe
notification system
ryanbarlow97 Dec 21, 2025
caa98bb
prettier + eslint fix
ryanbarlow97 Dec 21, 2025
9008211
followed coderabbit suggestion
ryanbarlow97 Dec 21, 2025
99250cd
added more logs
ryanbarlow97 Dec 21, 2025
72a92b2
drop the hardcoded /w0/ path and connect via the host root instead
ryanbarlow97 Dec 21, 2025
d854204
drop the trailing slash
ryanbarlow97 Dec 21, 2025
dbe7a7c
coderabbit suggestions
ryanbarlow97 Dec 21, 2025
eeffa1d
now using 'notificationCleanupInterval'
ryanbarlow97 Dec 21, 2025
ba8cf6f
temp fix for localdev
ryanbarlow97 Dec 21, 2025
05bd7f5
prettier
ryanbarlow97 Dec 21, 2025
dd926f5
port 3000
ryanbarlow97 Dec 21, 2025
bd831da
persist throughout the session and handles registration/unregistratio…
ryanbarlow97 Dec 21, 2025
440e85d
create notification worker
ryanbarlow97 Dec 21, 2025
4c8fc4a
should trigger for back to back ffas/teams
ryanbarlow97 Dec 21, 2025
ee7f10e
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 21, 2025
c99095c
changed to en.json text
ryanbarlow97 Dec 21, 2025
1955532
added /w0 fallback
ryanbarlow97 Dec 21, 2025
b0f7de5
hijack polling already done instead of websockets
ryanbarlow97 Dec 21, 2025
a318a42
revert changes
ryanbarlow97 Dec 21, 2025
3a8572d
remove websocketness
ryanbarlow97 Dec 21, 2025
15a4675
revert change
ryanbarlow97 Dec 21, 2025
d62a231
spacing
ryanbarlow97 Dec 21, 2025
9e73769
spacing
ryanbarlow97 Dec 21, 2025
d76201d
remove blank line
ryanbarlow97 Dec 21, 2025
52d0af8
remove clientregisternotifications
ryanbarlow97 Dec 21, 2025
cb21fcf
removed schema stuff
ryanbarlow97 Dec 21, 2025
d20f828
replaced with en.json
ryanbarlow97 Dec 21, 2025
deabb94
updated to use lang
ryanbarlow97 Dec 21, 2025
a1c295d
remove popup
ryanbarlow97 Dec 21, 2025
bac4b56
remove unused import
ryanbarlow97 Dec 21, 2025
8e3a2ba
remove unused
ryanbarlow97 Dec 21, 2025
a8cf332
reuse some language
ryanbarlow97 Dec 21, 2025
8c0f8bb
stripped back as we dont need to track
ryanbarlow97 Dec 21, 2025
bba8da0
prettier fix
ryanbarlow97 Dec 21, 2025
c98a8c2
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 21, 2025
62328e2
prettier fix
ryanbarlow97 Dec 22, 2025
92fd8f2
added tests
ryanbarlow97 Dec 22, 2025
76def68
fixed prettier+eslint
ryanbarlow97 Dec 22, 2025
8447c43
calling this.lobbyNotificationManager?.destroy() in the beforeunload …
ryanbarlow97 Dec 22, 2025
001bccd
fixed whitespace issue
ryanbarlow97 Dec 22, 2025
bcd2eb3
Extracting to a shared constant
ryanbarlow97 Dec 22, 2025
bd0f9c0
comment
ryanbarlow97 Dec 22, 2025
02bef33
Remove duplicate beforeunload listener
ryanbarlow97 Dec 22, 2025
12ff2d4
Use bg-transparent instead
ryanbarlow97 Dec 22, 2025
9f09677
add back gamestop
ryanbarlow97 Dec 22, 2025
faa0f86
relocate gamestop
ryanbarlow97 Dec 22, 2025
c2398af
Use border-0 instead.
ryanbarlow97 Dec 22, 2025
91b7dfa
change setting types
ryanbarlow97 Dec 22, 2025
d62f24a
update modal
ryanbarlow97 Dec 22, 2025
cbb90b3
Update tests/client/LobbyNotificationManager.test.ts
ryanbarlow97 Dec 22, 2025
b3dbb59
added humans v nations
ryanbarlow97 Dec 22, 2025
d63797b
updated tests
ryanbarlow97 Dec 22, 2025
fa152ec
updated test
ryanbarlow97 Dec 22, 2025
71686c6
Update tests/client/LobbyNotificationManager.test.ts
ryanbarlow97 Dec 22, 2025
30096c6
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 28, 2025
bf42b40
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 29, 2025
1c1ac04
Merge remote-tracking branch 'origin/main' into lobbynotify
ryanbarlow97 Dec 29, 2025
ffea152
migrated tests away from jest
ryanbarlow97 Dec 29, 2025
3bc1bd6
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 29, 2025
56b54a3
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 29, 2025
feda16a
fix handleBeforeUnload issue
ryanbarlow97 Dec 30, 2025
a962dcc
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 31, 2025
9e0a430
Merge branch 'main' into lobbynotify
ryanbarlow97 Dec 31, 2025
46abaae
Merge branch 'main' into lobbynotify
ryanbarlow97 Jan 1, 2026
e74cb12
conflict resolution
ryanbarlow97 Jan 1, 2026
ee2b2ce
Merge branch 'main' into lobbynotify
ryanbarlow97 Jan 1, 2026
1f42c80
conflict resolution
ryanbarlow97 Jan 1, 2026
b40dd86
Merge branch 'main' into lobbynotify
ryanbarlow97 Jan 2, 2026
2d31581
Merge branch 'main' into lobbynotify
iiamlewis Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@
<help-modal></help-modal>
<game-info-modal></game-info-modal>
<dark-mode-button></dark-mode-button>
<lobby-notification-button></lobby-notification-button>
<lobby-notification-modal></lobby-notification-modal>
<stats-button></stats-button>
<alert-frame></alert-frame>
<chat-modal></chat-modal>
Expand Down
8 changes: 8 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -861,5 +861,13 @@
"stats_games_played": "Games Played",
"mode_ffa": "Free-for-All",
"mode_team": "Team"
},
"lobby_notification_modal": {
"title": "Lobby Notifications",
"min": "Min:",
"max": "Max:",
"sound_notifications": "Sound Notifications",
"enable_hint": "Select a game mode to enable notifications",
"team_count_range": "Players per Team"
}
}
1 change: 1 addition & 0 deletions src/client/LangSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export class LangSelector extends LitElement {
"o-modal",
"o-button",
"territory-patterns-modal",
"lobby-notification-modal",
];

document.title = this.translateText("main.title") ?? document.title;
Expand Down
30 changes: 30 additions & 0 deletions src/client/LobbyNotificationButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { translateText } from "./Utils";

@customElement("lobby-notification-button")
export class LobbyNotificationButton extends LitElement {
createRenderRoot() {
return this;
}

private openModal() {
const event = new CustomEvent("open-notification-modal", {
bubbles: true,
composed: true,
});
window.dispatchEvent(event);
}

render() {
return html`
<button
title="${translateText("lobby_notification_modal.title")}"
class="absolute top-0 left-[50px] md:top-[10px] md:left-[60px] border-0 bg-transparent cursor-pointer text-2xl"
@click=${this.openModal}
>
🔔
</button>
`;
}
}
176 changes: 176 additions & 0 deletions src/client/LobbyNotificationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { GameConfig, GameInfo } from "../core/Schemas";
import { GameMode } from "../core/game/Game";

interface NotificationSettings {
ffaEnabled: boolean;
teamEnabled: boolean;
soundEnabled: boolean;
minTeamCount: number;
maxTeamCount: number;
}

export class LobbyNotificationManager {
private settings: NotificationSettings | null = null;
private audioContext: AudioContext | null = null;
private seenLobbies: Set<string> = new Set();

constructor() {
this.loadSettings();
this.setupEventListeners();
}

private setupEventListeners() {
window.addEventListener(
"notification-settings-changed",
this.handleSettingsChanged,
);
window.addEventListener("lobbies-updated", this.handleLobbiesUpdated);
}

private handleSettingsChanged = (e: Event) => {
const event = e as CustomEvent<NotificationSettings>;
this.settings = event.detail;
};

private loadSettings() {
try {
const saved = localStorage.getItem("lobbyNotificationSettings");
if (saved) {
this.settings = JSON.parse(saved);
}
} catch (error) {
console.error("Failed to load notification settings:", error);
}
}

private handleLobbiesUpdated = (e: Event) => {
const event = e as CustomEvent<GameInfo[]>;
const lobbies = event.detail || [];

// Check for new lobbies
lobbies.forEach((lobby) => {
if (!this.seenLobbies.has(lobby.gameID) && lobby.gameConfig) {
this.seenLobbies.add(lobby.gameID);

// Check if this lobby matches user preferences
if (this.matchesPreferences(lobby.gameConfig)) {
this.playNotificationSound();
}
}
});

// Clean up old lobbies no longer in the list
const currentIds = new Set(lobbies.map((l) => l.gameID));
for (const id of this.seenLobbies) {
if (!currentIds.has(id)) {
this.seenLobbies.delete(id);
}
}
};

private matchesPreferences(config: GameConfig): boolean {
if (!this.settings) return false;

// Check FFA
if (this.settings.ffaEnabled && config.gameMode === GameMode.FFA) {
return true;
}

// Check Team
if (this.settings.teamEnabled && config.gameMode === GameMode.Team) {
const maxPlayers = config.maxPlayers ?? 0;
const playerTeams = config.playerTeams;

if (maxPlayers === 0) return false;

// Map fixed modes to players per team
const playersPerTeamMap: Record<string, number> = {
Duos: 2,
Trios: 3,
Quads: 4,
"Humans Vs Nations": 1,
};

// Calculate players per team
// If playerTeams is a string (Duos/Trios/Quads), use the map
// If it's a number, it's the number of teams, so calculate players per team
const playersPerTeam =
typeof playerTeams === "string"
? (playersPerTeamMap[playerTeams] ?? 0)
: typeof playerTeams === "number"
? Math.floor(maxPlayers / playerTeams)
: 0;

if (playersPerTeam === 0) return false;

if (playersPerTeam < this.settings.minTeamCount) return false;
if (playersPerTeam > this.settings.maxTeamCount) return false;

return true;
}

return false;
}

private playNotificationSound() {
if (!this.settings?.soundEnabled) {
return;
}

this.playBeepSound();
}

private getAudioContext(): AudioContext | null {
if (this.audioContext) {
return this.audioContext;
}

try {
this.audioContext = new (window.AudioContext ||
(window as any).webkitAudioContext)();
return this.audioContext;
} catch (error) {
console.error("Failed to create AudioContext:", error);
return null;
}
}

private playBeepSound() {
try {
const audioContext = this.getAudioContext();
if (!audioContext) return;

const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();

oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);

oscillator.frequency.value = 800;
oscillator.type = "sine";

gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + 0.3,
);

oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (error) {
console.error("Failed to play beep sound:", error);
}
}

public destroy() {
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
window.removeEventListener(
"notification-settings-changed",
this.handleSettingsChanged,
);
window.removeEventListener("lobbies-updated", this.handleLobbiesUpdated);
}
}
Loading
Loading