Lobby: leagues are play buttons w/ arrow; remove background music feature
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 35s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m14s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 58s

- OnlineLobbyScreen: each league row is now a tappable play button (queues a
  ranked match at that league's stake) with a forward arrow; the cheapest
  enterable league is highlighted gold. Drops the redundant separate "ranked
  random" CTA and the select-then-play step.
- Remove the background-music feature entirely: deleted the floating MusicToggle,
  the TopBar music button, and the Profile audio music toggle + style picker.
  sound.startMusic() is now an inert no-op so music never plays (sfx unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 17:23:26 +03:30
parent deb83cf77c
commit efefbcec3d
5 changed files with 66 additions and 198 deletions
-38
View File
@@ -1,38 +0,0 @@
"use client";
import { Music } from "lucide-react";
import { useSoundStore } from "@/lib/sound-store";
import { useUIStore } from "@/lib/ui-store";
import { useI18n } from "@/lib/i18n";
/**
* Always-available music mute toggle (enable/disable from anywhere). Floats in a
* corner on every screen except the game table, which has its own audio control
* in its HUD.
*/
export function MusicToggle() {
const { t } = useI18n();
const music = useSoundStore((s) => s.music);
const toggleMusic = useSoundStore((s) => s.toggleMusic);
const screen = useUIStore((s) => s.screen);
if (screen === "game") return null;
return (
<button
onClick={toggleMusic}
title={music ? t("settings.music") : t("settings.music")}
aria-label={t("settings.music")}
className="fixed z-[55] bottom-[max(0.75rem,env(safe-area-inset-bottom))] ltr:left-3 rtl:right-3 glass rounded-full min-h-11 min-w-11 grid place-items-center hover:bg-navy-800/80 transition"
>
{music ? (
<Music className="size-4 text-gold-400" />
) : (
<span className="relative grid place-items-center">
<Music className="size-4 text-cream/40" />
<span className="absolute block h-0.5 w-5 rotate-45 rounded bg-rose-400" />
</span>
)}
</button>
);
}
+1 -19
View File
@@ -1,8 +1,7 @@
"use client"; "use client";
import { Bell, Crown, Gift, Music, Store } from "lucide-react"; import { Bell, Crown, Gift, Store } from "lucide-react";
import { useSessionStore } from "@/lib/session-store"; import { useSessionStore } from "@/lib/session-store";
import { useSoundStore } from "@/lib/sound-store";
import { useUIStore } from "@/lib/ui-store"; import { useUIStore } from "@/lib/ui-store";
import { useNotifStore } from "@/lib/notification-store"; import { useNotifStore } from "@/lib/notification-store";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
@@ -15,8 +14,6 @@ export function TopBar() {
const go = useUIStore((s) => s.go); const go = useUIStore((s) => s.go);
const openDaily = useUIStore((s) => s.openDaily); const openDaily = useUIStore((s) => s.openDaily);
const unread = useNotifStore((s) => s.unread); const unread = useNotifStore((s) => s.unread);
const music = useSoundStore((s) => s.music);
const toggleMusic = useSoundStore((s) => s.toggleMusic);
const { t } = useI18n(); const { t } = useI18n();
if (!profile) return null; if (!profile) return null;
@@ -54,21 +51,6 @@ export function TopBar() {
</button> </button>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<button
onClick={toggleMusic}
className="glass rounded-full p-1.5 hover:bg-navy-800/80 transition"
title={t("settings.music")}
aria-label={t("settings.music")}
>
{music ? (
<Music className="size-4 text-gold-400" />
) : (
<span className="relative grid place-items-center">
<Music className="size-4 text-cream/40" />
<span className="absolute block h-0.5 w-5 rotate-45 rounded bg-rose-400" />
</span>
)}
</button>
<button <button
onClick={() => go("notifications")} onClick={() => go("notifications")}
className="glass rounded-full p-2 hover:bg-navy-800/80 transition relative" className="glass rounded-full p-2 hover:bg-navy-800/80 transition relative"
+33 -51
View File
@@ -1,11 +1,10 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Coins, Lock, Trophy } from "lucide-react"; import { ChevronLeft, Coins, Lock, Trophy } from "lucide-react";
import { useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader"; import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { CoinsPill } from "@/components/online/CoinsPill"; import { CoinsPill } from "@/components/online/CoinsPill";
import { MATCH_LEAGUES, leagueById } from "@/lib/online/gamification"; import { MATCH_LEAGUES } from "@/lib/online/gamification";
import { useOnlineStore } from "@/lib/online-store"; import { useOnlineStore } from "@/lib/online-store";
import { useSessionStore } from "@/lib/session-store"; import { useSessionStore } from "@/lib/session-store";
import { useUIStore } from "@/lib/ui-store"; import { useUIStore } from "@/lib/ui-store";
@@ -37,20 +36,19 @@ export function OnlineLobbyScreen() {
const profile = useSessionStore((s) => s.profile); const profile = useSessionStore((s) => s.profile);
const coins = profile?.coins ?? 0; const coins = profile?.coins ?? 0;
const level = profile?.level ?? 1; const level = profile?.level ?? 1;
const [leagueId, setLeagueId] = useState(MATCH_LEAGUES[0].id);
const league = leagueById(leagueId);
const entry = league.entry;
const lockedLeague = level < league.minLevel;
// Ranked random always costs the entry (you stake it). // The cheapest league you can enter is highlighted as the default pick.
const onRandom = async () => { const featuredId = MATCH_LEAGUES.find((l) => level >= l.minLevel)?.id;
// Each league is its own play button — tap to queue a ranked match at its stake.
const playLeague = async (l: (typeof MATCH_LEAGUES)[number]) => {
if (guardActiveMatch()) return; if (guardActiveMatch()) return;
if (lockedLeague) return; if (level < l.minLevel) return;
if (coins < entry) { if (coins < l.entry) {
go("buycoins"); go("buycoins");
return; return;
} }
await startMatchmaking({ ranked: true, stake: entry }); await startMatchmaking({ ranked: true, stake: l.entry });
go("matchmaking"); go("matchmaking");
}; };
@@ -58,80 +56,64 @@ export function OnlineLobbyScreen() {
<ScreenShell> <ScreenShell>
<ScreenHeader title={t("lobby.title")} right={<CoinsPill />} /> <ScreenHeader title={t("lobby.title")} right={<CoinsPill />} />
{/* league pick (only for ranked) */} <div className="flex items-center gap-1.5 text-sm text-cream/70 mb-3">
<div className="panel rounded-2xl p-4 mb-4">
<div className="flex items-center gap-1.5 text-sm text-cream/70 mb-2.5">
<Trophy className="size-4 text-gold-400" /> <Trophy className="size-4 text-gold-400" />
{t("lobby.chooseLeague")} {t("lobby.chooseLeague")}
</div> </div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="grid gap-3">
{MATCH_LEAGUES.map((l) => { {MATCH_LEAGUES.map((l) => {
const locked = level < l.minLevel; const locked = level < l.minLevel;
const active = l.id === leagueId; const gold = !locked && l.id === featuredId;
const poor = !locked && coins < l.entry;
return ( return (
<button <motion.button
key={l.id} key={l.id}
whileTap={locked ? undefined : { scale: 0.985 }}
disabled={locked} disabled={locked}
onClick={() => setLeagueId(l.id)} onClick={() => playLeague(l)}
className={cn( className={cn(
"w-full rounded-2xl p-3 flex items-center gap-3 border text-start transition", "w-full rounded-3xl p-4 flex items-center gap-3 text-start transition",
active gold ? "btn-gold" : "press-3d panel hover:border-gold-500/40",
? "border-gold-500/70 bg-gold-500/10"
: "border-navy-700/60 bg-navy-900/50 hover:border-navy-600",
locked && "opacity-50 cursor-not-allowed" locked && "opacity-50 cursor-not-allowed"
)} )}
> >
<span <span
className="size-10 rounded-xl flex items-center justify-center text-xl shrink-0" className="grid size-12 place-items-center rounded-2xl text-2xl shrink-0"
style={{ background: l.color + "22" }} style={{ background: gold ? "rgba(0,0,0,.12)" : l.color + "22" }}
> >
{l.icon} {l.icon}
</span> </span>
<span className="flex-1 min-w-0"> <span className="flex-1 min-w-0">
<span className="block text-sm font-black text-cream"> <span className={cn("block text-base font-black", gold ? "text-[#2a1f04]" : "text-cream")}>
{locale === "fa" ? l.nameFa : l.nameEn} {locale === "fa" ? l.nameFa : l.nameEn}
</span> </span>
<span className="block text-[11px] text-cream/55"> <span className={cn("block text-[11px]", gold ? "text-[#2a1f04]/70" : "text-cream/55")}>
{locale === "fa" ? l.descFa : l.descEn} {locale === "fa" ? l.descFa : l.descEn}
</span> </span>
{poor && (
<span className="block text-[10px] text-rose-300 mt-0.5">{t("lobby.needCoins")}</span>
)}
</span> </span>
{locked ? ( {locked ? (
<span className="text-[11px] text-rose-300 flex items-center gap-1 shrink-0"> <span className="text-[11px] text-rose-300 flex items-center gap-1 shrink-0">
<Lock className="size-3.5" /> <Lock className="size-3.5" />
{t("lobby.lvl")} {l.minLevel} {t("lobby.lvl")} {l.minLevel}
</span> </span>
) : ( ) : (
<span className="flex items-center gap-1 text-gold-300 font-black text-sm shrink-0"> <span className="flex items-center gap-2.5 shrink-0">
<span className={cn("flex items-center gap-1 font-black text-sm", gold ? "text-[#2a1f04]" : "text-gold-300")}>
{l.entry.toLocaleString()} {l.entry.toLocaleString()}
<Coins className="size-3.5" /> <Coins className="size-3.5" />
</span> </span>
<ChevronLeft className={cn("size-5 ltr:rotate-180", gold ? "text-[#2a1f04]" : "text-gold-400")} />
</span>
)} )}
</button> </motion.button>
); );
})} })}
</div> </div>
{!lockedLeague && coins < entry && (
<p className="text-rose-300 text-xs mt-2 text-center">{t("lobby.needCoins")}</p>
)}
</div>
<motion.button
whileTap={{ scale: 0.985 }}
onClick={onRandom}
className="btn-gold w-full rounded-3xl p-5 flex items-center gap-4 text-start"
>
<span className="grid size-12 place-items-center rounded-2xl bg-black/15 text-[#2a1f04]">
<Trophy className="size-6" />
</span>
<span className="flex-1">
<span className="block text-lg font-black text-[#2a1f04]">{t("lobby.random")}</span>
<span className="block text-xs text-[#2a1f04]/70">{t("lobby.randomDesc")}</span>
</span>
<span className="flex items-center gap-1 text-[#2a1f04] font-black">
{entry}
<Coins className="size-4" />
</span>
</motion.button>
</ScreenShell> </ScreenShell>
); );
} }
+2 -25
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Check, ChevronLeft, Crown, Eye, EyeOff, Lock, LogOut, Music, Pencil, Star, Upload, Users, Volume2 } from "lucide-react"; import { Check, ChevronLeft, Crown, Eye, EyeOff, Lock, LogOut, Pencil, Star, Upload, Users, Volume2 } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader"; import { ScreenHeader, ScreenShell } from "@/components/online/ScreenHeader";
import { RankBadge } from "@/components/online/RankBadge"; import { RankBadge } from "@/components/online/RankBadge";
@@ -509,34 +509,11 @@ function SocialSettings() {
function SoundSettings() { function SoundSettings() {
const { t } = useI18n(); const { t } = useI18n();
const { sfx, music, musicTrack, toggleSfx, toggleMusic, setMusicTrack } = useSoundStore(); const { sfx, toggleSfx } = useSoundStore();
const tracks = [
{ id: "santoor" as const, label: t("settings.trackSantoor") },
{ id: "playful" as const, label: t("settings.trackPlayful") },
];
return ( return (
<div className="panel rounded-2xl p-4"> <div className="panel rounded-2xl p-4">
<h3 className="text-sm font-bold text-cream/80 mb-2">{t("settings.audio")}</h3> <h3 className="text-sm font-bold text-cream/80 mb-2">{t("settings.audio")}</h3>
<ToggleRow icon={<Volume2 className="size-4 text-gold-400" />} label={t("settings.sound")} on={sfx} onClick={toggleSfx} /> <ToggleRow icon={<Volume2 className="size-4 text-gold-400" />} label={t("settings.sound")} on={sfx} onClick={toggleSfx} />
<ToggleRow icon={<Music className="size-4 text-gold-400" />} label={t("settings.music")} on={music} onClick={toggleMusic} />
{/* music style picker */}
<div className="mt-3">
<div className="text-[11px] text-cream/55 mb-1.5">{t("settings.musicStyle")}</div>
<div className="grid grid-cols-2 gap-2">
{tracks.map((tr) => (
<button
key={tr.id}
onClick={() => setMusicTrack(tr.id)}
className={cn(
"press-3d rounded-xl py-2.5 text-sm font-bold",
musicTrack === tr.id ? "btn-gold" : "bg-navy-900/70 gold-border text-cream/70"
)}
>
{tr.label}
</button>
))}
</div>
</div>
</div> </div>
); );
} }
+2 -37
View File
@@ -210,43 +210,8 @@ class SoundManager {
}, },
}; };
startMusic() { /** Background music was removed from the game — these are inert no-ops. */
if (!this.musicEnabled || this.musicTimer || !this.ctx || !this.musicGain) return; startMusic() {}
const playNote = () => {
if (!this.ctx || !this.musicGain) return;
const cfg = this.TRACKS[this.musicTrack];
const freq = cfg.notes[this.step % cfg.notes.length];
this.step++;
const osc = this.ctx.createOscillator();
const g = this.ctx.createGain();
const t = this.ctx.currentTime;
osc.type = cfg.type;
osc.frequency.value = freq;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(cfg.peak, t + cfg.attack);
g.gain.exponentialRampToValueAtTime(0.0001, t + cfg.dur);
osc.connect(g);
g.connect(this.musicGain);
osc.start(t);
osc.stop(t + cfg.dur + 0.1);
// soft fifth harmony (santoor) every other note
if (cfg.fifth && this.step % 2 === 0) {
const o2 = this.ctx.createOscillator();
const g2 = this.ctx.createGain();
o2.type = "sine";
o2.frequency.value = freq * 1.5;
g2.gain.setValueAtTime(0.0001, t);
g2.gain.exponentialRampToValueAtTime(0.22, t + 0.3);
g2.gain.exponentialRampToValueAtTime(0.0001, t + 1.4);
o2.connect(g2);
g2.connect(this.musicGain);
o2.start(t);
o2.stop(t + 1.5);
}
};
playNote();
this.musicTimer = setInterval(playNote, this.TRACKS[this.musicTrack].gap);
}
stopMusic() { stopMusic() {
if (this.musicTimer) { if (this.musicTimer) {