|
|
import { BuffType, DebuffType, type ActiveBuff, type ActiveDebuff } from '../game/types';
|
|
|
|
|
|
/** Mirrors backend/internal/model/buff.go CooldownDuration */
|
|
|
export const BUFF_COOLDOWN_MS: Record<BuffType, number> = {
|
|
|
[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, number> = {
|
|
|
[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, number> = {
|
|
|
[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, number> = {
|
|
|
[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;
|
|
|
}
|