Files
HokmPlay/src/lib/ui-store.ts
T
soroush.asadi 7a18bc39e6
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m21s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
Achievements overhaul: 37 achievements, page with tabs, leagues, gating
Achievements (client + server mirror, metric-driven so the list is one source):
- 37 achievements across 6 categories (Victories, Kot, Streaks, Levels, Ranks,
  Veterancy) incl. 7–0 sweeps, kot milestones (1/5/10/25/50/100), win streaks
  (3/5/10/15), level milestones every 5 (5..50), rank floors, games/tricks.
- New AchievementsScreen with category tabs, progress bars, coin + sticker-unlock
  badges, and unlocked/locked states; summary header (unlocked count + coins).
- Some achievements unlock sticker packs: Seven–Zip→Hokm, 25 Kots→Taunts,
  100 Wins→Persian (ownedStickerPackIds now also honors profile.unlocked).
- Prestige titles added: Expert, Professional, Captain, Leader (+ existing).
- Tracks new stat shutoutWins; MatchSummary.shutout (7–0). Profile shows a
  6-item preview + "view all" link.

Leagues: 3 ranked entry tiers — Starter (100, lvl1), Pro (500, lvl10),
Expert (1000, lvl20). Higher league stakes more, so wins/losses swing bigger;
kot bonus now scales to the stake (40%). OnlineLobby shows league cards with
level gating.

Profile photo upload gated to level 25 (client button + server Update guard).

Win animation: PostMatchRewardsModal now shows an animated coins-won count-up
hero on a win.

Verified: dotnet build + tsc + next build clean; sim unlocks 26 achievements
over 500 matches; live server grants first_win/first_kot/shutout_1 and pays
2050 coins on an expert-league shutout+kot win. Images rebuilt on :1500/:1505.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:47:38 +03:30

110 lines
3.0 KiB
TypeScript

"use client";
import { create } from "zustand";
export type Screen =
| "home"
| "auth"
| "profile"
| "friends"
| "online" // online lobby (create room / play random)
| "room"
| "matchmaking"
| "leaderboard"
| "shop"
| "buycoins"
| "achievements"
| "chat"
| "notifications"
| "game"; // the table (used for both ai + online)
const ALL_SCREENS: Screen[] = [
"home", "auth", "profile", "friends", "online",
"room", "matchmaking", "leaderboard", "shop", "buycoins", "achievements", "chat", "notifications", "game",
];
/** Screens safe to restore from a URL on a cold load (no transient state needed). */
export const STATIC_SCREENS: Screen[] = [
"home", "auth", "profile", "friends", "online", "leaderboard", "shop", "buycoins", "achievements", "notifications",
];
export function screenFromHash(): Screen {
if (typeof window === "undefined") return "home";
const h = window.location.hash.replace(/^#\/?/, "");
return (ALL_SCREENS.includes(h as Screen) ? (h as Screen) : "home");
}
function urlFor(screen: Screen): string {
if (typeof window === "undefined") return "/";
const base = window.location.pathname + window.location.search;
return screen === "home" ? base : `${base}#/${screen}`;
}
function pushHistory(screen: Screen, replace = false) {
if (typeof window === "undefined") return;
const state = { screen };
if (replace) window.history.replaceState(state, "", urlFor(screen));
else window.history.pushState(state, "", urlFor(screen));
}
interface UIStore {
screen: Screen;
/** screen to return to from the game table */
returnTo: Screen;
dailyModalOpen: boolean;
go: (screen: Screen) => void;
goGame: (returnTo?: Screen) => void;
/** Browser/hardware back. */
back: (fallback?: Screen) => void;
/** Sync from a popstate event (does not touch history). */
syncFromPop: (screen: Screen) => void;
/** Establish a home base entry + restore a deep-linked screen on load. */
initHistory: () => void;
openDaily: () => void;
closeDaily: () => void;
}
export const useUIStore = create<UIStore>((set, get) => ({
screen: "home",
returnTo: "home",
dailyModalOpen: false,
go: (screen) => {
if (get().screen === screen) return;
pushHistory(screen);
set({ screen });
},
goGame: (returnTo = "home") => {
pushHistory("game");
set({ screen: "game", returnTo });
},
back: (fallback) => {
if (typeof window !== "undefined") {
window.history.back();
return;
}
if (fallback) set({ screen: fallback });
},
syncFromPop: (screen) => set({ screen }),
initHistory: () => {
// Always anchor a "home" base entry so back() has somewhere to land.
pushHistory("home", true);
const target = screenFromHash();
if (target !== "home" && STATIC_SCREENS.includes(target)) {
pushHistory(target);
set({ screen: target });
} else {
set({ screen: "home" });
}
},
openDaily: () => set({ dailyModalOpen: true }),
closeDaily: () => set({ dailyModalOpen: false }),
}));