You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

168 lines
4.9 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}