53 lines
3.1 KiB
TypeScript
53 lines
3.1 KiB
TypeScript
/** Coin Madness: manual, sequential stages. All dates are UTC epoch milliseconds. */
|
|
export const PRODUCT = 'coin_madness';
|
|
export interface CoinConfig {
|
|
enabled: boolean; approved: boolean; periodId: string; startsAt: number; endsAt: number;
|
|
minLevel: number; priceCents: number; title: string;
|
|
stages: Array<{ wins: number; coins: number }>;
|
|
}
|
|
export interface CoinState {
|
|
config: CoinConfig; paidAt: number; orderNo: string; reservedOrder: string;
|
|
stage: number; progress: number; lastLevel: number; settled: boolean;
|
|
}
|
|
export function parse<T>(value: any): T | null {
|
|
try { return typeof value === 'string' ? JSON.parse(value) : value || null; } catch { return null; }
|
|
}
|
|
export function validConfig(raw: any): raw is CoinConfig {
|
|
const integer = (n: any, min: number) => Number.isSafeInteger(n) && n >= min;
|
|
return !!raw && raw.enabled === true && raw.approved === true
|
|
&& typeof raw.periodId === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(raw.periodId)
|
|
&& integer(raw.startsAt, 1) && integer(raw.endsAt, raw.startsAt + 1)
|
|
&& integer(raw.minLevel, 0) && integer(raw.priceCents, 1)
|
|
&& typeof raw.title === 'string' && raw.title.length > 0 && raw.title.length <= 40
|
|
&& Array.isArray(raw.stages) && raw.stages.length > 0 && raw.stages.length <= 50
|
|
&& raw.stages.every((s: any) => integer(s.wins, 1) && integer(s.coins, 1))
|
|
&& Number.isSafeInteger(raw.stages.reduce((n: number, s: any) => n + s.coins, 0));
|
|
}
|
|
export function createState(config: CoinConfig, level: number): CoinState {
|
|
return { config: JSON.parse(JSON.stringify(config)), paidAt: 0, orderNo: '', reservedOrder: '',
|
|
stage: 0, progress: 0, lastLevel: level, settled: false };
|
|
}
|
|
export function active(state: CoinState, now: number): boolean {
|
|
return !!state && now >= state.config.startsAt && now < state.config.endsAt && !state.settled;
|
|
}
|
|
export function win(state: CoinState, level: number, now: number, isVictory: boolean, infinite: boolean): CoinState {
|
|
const next: CoinState = JSON.parse(JSON.stringify(state));
|
|
if (!Number.isSafeInteger(level) || level <= next.lastLevel) return next;
|
|
const singleNewLevel = level === next.lastLevel + 1;
|
|
next.lastLevel = level; // Includes waiting-for-claim wins and syncs: never bank them.
|
|
const target = next.config.stages[next.stage];
|
|
if (active(next, now) && next.paidAt > 0 && now >= next.paidAt && target
|
|
&& isVictory && !infinite && singleNewLevel && next.progress < target.wins) next.progress++;
|
|
return next;
|
|
}
|
|
export function reward(state: CoinState, now: number, expired: boolean): { state: CoinState; coins: number } {
|
|
const next: CoinState = JSON.parse(JSON.stringify(state));
|
|
if (expired ? now < next.config.endsAt : !active(next, now)) throw new Error('活动时间不允许此操作');
|
|
const target = next.config.stages[next.stage];
|
|
const coins = next.paidAt > 0 && !next.settled && target && next.progress >= target.wins ? target.coins : 0;
|
|
if (!expired && !coins) throw new Error('当前阶段尚未达标');
|
|
if (coins) { next.stage++; next.progress = 0; }
|
|
if (expired) next.settled = true;
|
|
return { state: next, coins };
|
|
}
|