import type { DailyChallenge, DailyChallengeState, DailyChallengeType, GameState, } from "@elysium/types"; import { DAILY_CHALLENGE_TEMPLATES } from "../data/dailyChallenges.js"; // Use the server's PST/PDT timezone so challenges roll over at PST midnight const getTodayString = (): string => new Intl.DateTimeFormat("en-CA", { timeZone: "America/Los_Angeles" }).format(new Date()); /** Simple deterministic pseudo-random based on a numeric seed. */ const seededRandom = (seed: number): number => { const x = Math.sin(seed + 1) * 10_000; return x - Math.floor(x); }; /** Converts a date string into a stable numeric seed. */ const dateSeed = (dateStr: string): number => dateStr.split("").reduce((acc, char, i) => acc + char.charCodeAt(0) * (i + 1), 0); /** Deterministically shuffles an array using a numeric seed. */ const shuffleWithSeed = (arr: T[], seed: number): T[] => { const result = [...arr]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(seededRandom(seed + i) * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; }; const CHALLENGE_TYPES: DailyChallengeType[] = [ "clicks", "bossesDefeated", "questsCompleted", "prestige", ]; /** * Generates 3 daily challenges for the given date string, deterministically. * Picks one challenge from 3 different randomly-selected types. */ export const generateDailyChallenges = (dateStr: string): DailyChallenge[] => { const seed = dateSeed(dateStr); const selectedTypes = shuffleWithSeed([...CHALLENGE_TYPES], seed).slice(0, 3); return selectedTypes.map((type, index) => { const templates = DAILY_CHALLENGE_TEMPLATES.filter((t) => t.type === type); const templateIndex = Math.floor(seededRandom(seed + index * 100) * templates.length); const template = templates[templateIndex]; return { id: `${dateStr}_${type}`, type: template.type, label: template.label, target: template.target, progress: 0, completed: false, rewardCrystals: template.rewardCrystals, }; }); }; /** * Returns the current daily challenge state, generating fresh challenges if * the stored date doesn't match today (i.e. a new day has begun). */ export const getOrResetDailyChallenges = (state: GameState): DailyChallengeState => { const today = getTodayString(); if (state.dailyChallenges?.date === today) { return state.dailyChallenges; } return { date: today, challenges: generateDailyChallenges(today) }; }; /** * Increments progress for challenges matching the given type. * Returns the updated challenge state and total crystals awarded for newly completed challenges. */ export const updateChallengeProgress = ( challengeState: DailyChallengeState, type: DailyChallengeType, amount: number, ): { updatedChallenges: DailyChallengeState; crystalsAwarded: number } => { let crystalsAwarded = 0; const updatedChallenges: DailyChallengeState = { ...challengeState, challenges: challengeState.challenges.map((challenge) => { if (challenge.type !== type || challenge.completed) return challenge; const newProgress = Math.min(challenge.progress + amount, challenge.target); const nowCompleted = newProgress >= challenge.target; if (nowCompleted) crystalsAwarded += challenge.rewardCrystals; return { ...challenge, progress: newProgress, completed: nowCompleted }; }), }; return { updatedChallenges, crystalsAwarded }; };