import { BuffType, DebuffType, type ActiveBuff, type ActiveDebuff } from '../game/types'; /** Mirrors backend/internal/model/buff.go CooldownDuration */ export const BUFF_COOLDOWN_MS: Record = { [BuffType.Rush]: 15 * 60_000, // 15 min [BuffType.Rage]: 10 * 60_000, // 10 min [BuffType.Shield]: 12 * 60_000, // 12 min [BuffType.Luck]: 2 * 60 * 60_000, // 2 hours [BuffType.Resurrection]: 30 * 60_000, // 30 min [BuffType.Heal]: 5 * 60_000, // 5 min [BuffType.PowerPotion]: 20 * 60_000, // 20 min [BuffType.WarCry]: 10 * 60_000, // 10 min }; /** Max buff charges per 24h period (mirrors backend BuffFreeChargesPerType). */ export const BUFF_MAX_CHARGES: Record = { [BuffType.Rush]: 3, [BuffType.Rage]: 2, [BuffType.Shield]: 2, [BuffType.Luck]: 1, [BuffType.Resurrection]: 1, [BuffType.Heal]: 3, [BuffType.PowerPotion]: 1, [BuffType.WarCry]: 2, }; /** Subscriber caps (mirrors backend BuffSubscriberChargesPerType, ×2). */ export const BUFF_MAX_CHARGES_SUBSCRIBER: Record = { [BuffType.Rush]: 6, [BuffType.Rage]: 4, [BuffType.Shield]: 4, [BuffType.Luck]: 2, [BuffType.Resurrection]: 2, [BuffType.Heal]: 6, [BuffType.PowerPotion]: 2, [BuffType.WarCry]: 4, }; export function buffMaxChargesForHero(type: BuffType, subscriptionActive: boolean | undefined): number { if (subscriptionActive) { return BUFF_MAX_CHARGES_SUBSCRIBER[type]; } return BUFF_MAX_CHARGES[type]; } /** Mirrors backend/internal/model/buff.go Duration */ export const BUFF_DURATION_MS: Record = { [BuffType.Rush]: 5 * 60_000, // 5 min [BuffType.Rage]: 3 * 60_000, // 3 min [BuffType.Shield]: 5 * 60_000, // 5 min [BuffType.Luck]: 30 * 60_000, // 30 min [BuffType.Resurrection]: 10 * 60_000, // 10 min [BuffType.Heal]: 1_000, // instant [BuffType.PowerPotion]: 5 * 60_000, // 5 min [BuffType.WarCry]: 3 * 60_000, // 3 min }; // ---- Server row shapes (Go JSON) ---- export interface ServerBuffRow { id?: number; type: string; name?: string; duration?: number; magnitude?: number; cooldownDuration?: number; } export interface ServerActiveBuffRow { buff: ServerBuffRow; appliedAt: string; expiresAt: string; } export interface ServerDebuffRow { id?: number; type: string; name?: string; duration?: number; magnitude?: number; } export interface ServerActiveDebuffRow { debuff: ServerDebuffRow; appliedAt: string; expiresAt: string; } function durationToMs(raw: unknown): number { if (typeof raw === 'number' && Number.isFinite(raw)) { return Math.round(raw / 1_000_000); } return 0; } function isBuffType(s: string): s is BuffType { return Object.values(BuffType).includes(s as BuffType); } function isDebuffType(s: string): s is DebuffType { return Object.values(DebuffType).includes(s as DebuffType); } /** Parse one active buff row from the API into client ActiveBuff (with expiresAtMs for UI ticking). */ export function mapServerActiveBuff(row: ServerActiveBuffRow, nowMs: number): ActiveBuff | null { const t = row.buff?.type; if (!t || !isBuffType(t)) return null; const exp = Date.parse(row.expiresAt); const app = Date.parse(row.appliedAt); if (!Number.isFinite(exp) || !Number.isFinite(app)) return null; const cooldownMs = durationToMs(row.buff.cooldownDuration) || BUFF_COOLDOWN_MS[t]; const durationMs = Math.max(0, exp - app); return { type: t, remainingMs: Math.max(0, exp - nowMs), durationMs, cooldownMs, cooldownRemainingMs: 0, expiresAtMs: exp, }; } export function mapServerActiveDebuff(row: ServerActiveDebuffRow, nowMs: number): ActiveDebuff | null { const t = row.debuff?.type; if (!t || !isDebuffType(t)) return null; const exp = Date.parse(row.expiresAt); const app = Date.parse(row.appliedAt); if (!Number.isFinite(exp) || !Number.isFinite(app)) return null; const durationMs = Math.max(0, exp - app); return { type: t, remainingMs: Math.max(0, exp - nowMs), durationMs, expiresAtMs: exp, }; } export function mapHeroBuffsFromServer(rows: ServerActiveBuffRow[] | undefined, nowMs: number): ActiveBuff[] { if (!rows?.length) return []; const out: ActiveBuff[] = []; for (const row of rows) { const b = mapServerActiveBuff(row, nowMs); if (b && b.remainingMs > 0) out.push(b); } return out; } export function mapHeroDebuffsFromServer(rows: ServerActiveDebuffRow[] | undefined, nowMs: number): ActiveDebuff[] { if (!rows?.length) return []; const out: ActiveDebuff[] = []; for (const row of rows) { const d = mapServerActiveDebuff(row, nowMs); if (d && d.remainingMs > 0) out.push(d); } return out; } export function cooldownMsFromActivateBuffRow(row: ServerActiveBuffRow): number { const t = row.buff?.type; const fromApi = durationToMs(row.buff?.cooldownDuration); if (fromApi > 0) return fromApi; if (t && isBuffType(t)) return BUFF_COOLDOWN_MS[t]; return 45_000; }