Lobby: leagues are play buttons w/ arrow; remove background music feature
- 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:
@@ -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,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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user