export const MANASTORM_BUBBLE_DEFAULT_RADIUS = 9; export const MANASTORM_BUBBLE_DANGER_DISTANCE = 1.5; export const MANASTORM_BUBBLE_WARNING_DISTANCE = 3; export type ManastormBubbleSeverity = "safe" | "warning" | "danger"; export interface ManastormSafetyBubbleState { readonly active: boolean; readonly radius: number; readonly distanceRemaining: number; readonly severity: ManastormBubbleSeverity; } export interface ManastormClientOverlayState { readonly nextWaveCountdownSeconds: number | null; readonly safetyBubble: ManastormSafetyBubbleState; } export interface ManastormSafetyBubbleUpdate { readonly active: boolean; readonly radius?: number; readonly distanceRemaining?: number; } function finiteOr(value: number | undefined, fallback: number): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } export function manastormBubbleSeverity(distanceRemaining: number): ManastormBubbleSeverity { if (distanceRemaining <= MANASTORM_BUBBLE_DANGER_DISTANCE) return "danger"; if (distanceRemaining <= MANASTORM_BUBBLE_WARNING_DISTANCE) return "warning"; return "safe"; } export function createManastormClientOverlayState(): ManastormClientOverlayState { return { nextWaveCountdownSeconds: null, safetyBubble: { active: false, radius: MANASTORM_BUBBLE_DEFAULT_RADIUS, distanceRemaining: MANASTORM_BUBBLE_DEFAULT_RADIUS, severity: "safe", }, }; } export function withManastormNextWaveCountdown( state: ManastormClientOverlayState, seconds: number | null, ): ManastormClientOverlayState { const normalized = typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0 ? Math.floor(seconds) : null; if (state.nextWaveCountdownSeconds === normalized) return state; return { ...state, nextWaveCountdownSeconds: normalized }; } export function withManastormSafetyBubble( state: ManastormClientOverlayState, update: ManastormSafetyBubbleUpdate, ): ManastormClientOverlayState { if (!update.active) { const initial = createManastormClientOverlayState().safetyBubble; if (!state.safetyBubble.active) return state; return { ...state, safetyBubble: initial }; } const radius = Math.max( Number.EPSILON, finiteOr(update.radius, MANASTORM_BUBBLE_DEFAULT_RADIUS), ); const distanceRemaining = Math.max( 0, Math.min(radius, finiteOr(update.distanceRemaining, radius)), ); return { ...state, safetyBubble: { active: true, radius, distanceRemaining, severity: manastormBubbleSeverity(distanceRemaining), }, }; } export function manastormBubbleFillPercent(bubble: ManastormSafetyBubbleState): number { if (!bubble.active || bubble.radius <= 0) return 0; return Math.max(0, Math.min(100, bubble.distanceRemaining / bubble.radius * 100)); }