|
|
import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react';
|
|
|
import { BuffType, type ActiveBuff, type BuffChargeState } from '../game/types';
|
|
|
import { BUFF_COOLDOWN_MS, buffMaxChargesForHero } from '../network/buffMap';
|
|
|
import { BUFF_VISUAL, buffUiStrings } from './buffMeta';
|
|
|
import { purchaseBuffRefill } from '../network/api';
|
|
|
import type { HeroResponse } from '../network/api';
|
|
|
import { getTelegramUserId } from '../shared/telegram';
|
|
|
import { useT, t } from '../i18n';
|
|
|
|
|
|
// ---- Types ----
|
|
|
|
|
|
interface BuffBarProps {
|
|
|
buffs: ActiveBuff[];
|
|
|
/** Client-side cooldown end timestamps (ms); backend does not persist these yet. */
|
|
|
cooldownEndsAt: Partial<Record<BuffType, number>>;
|
|
|
/** Per-buff charge quotas from the server. */
|
|
|
buffCharges: Partial<Record<BuffType, BuffChargeState>>;
|
|
|
/** When true, UI max charge labels use subscriber caps (×2). */
|
|
|
subscriptionActive?: boolean;
|
|
|
/** When true (e.g. hero dead), buff taps do nothing and refill flow is blocked. */
|
|
|
buffsLocked?: boolean;
|
|
|
nowMs: number;
|
|
|
onActivate: (type: BuffType) => void;
|
|
|
/** Called when a buff refill purchase returns an updated hero */
|
|
|
onHeroUpdated?: (hero: HeroResponse) => void;
|
|
|
}
|
|
|
|
|
|
interface BuffButtonProps {
|
|
|
buff: ActiveBuff;
|
|
|
charge: BuffChargeState | undefined;
|
|
|
maxCharges: number;
|
|
|
onActivate: () => void;
|
|
|
onRefill?: (type: BuffType) => void;
|
|
|
nowMs: number;
|
|
|
buffsLocked?: boolean;
|
|
|
}
|
|
|
|
|
|
// ---- Tooltip ----
|
|
|
|
|
|
const LONG_PRESS_MS = 400;
|
|
|
const TOOLTIP_AUTO_HIDE_MS = 2500;
|
|
|
|
|
|
/** When false, the green refill CTA in the hover tooltip is hidden (purchase still reachable from tap flow if any). */
|
|
|
const SHOW_BUFF_REFILL_BUTTON_IN_TOOLTIP = false;
|
|
|
|
|
|
const tooltipStyle: CSSProperties = {
|
|
|
position: 'absolute',
|
|
|
bottom: '110%',
|
|
|
left: '50%',
|
|
|
transform: 'translateX(-50%)',
|
|
|
padding: '6px 10px',
|
|
|
borderRadius: 8,
|
|
|
backgroundColor: 'rgba(10, 10, 20, 0.92)',
|
|
|
border: '1px solid rgba(255,255,255,0.15)',
|
|
|
color: '#eee',
|
|
|
fontSize: 11,
|
|
|
fontWeight: 600,
|
|
|
whiteSpace: 'nowrap',
|
|
|
pointerEvents: 'none',
|
|
|
zIndex: 100,
|
|
|
textAlign: 'center',
|
|
|
lineHeight: 1.4,
|
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
|
|
};
|
|
|
|
|
|
const tooltipArrow: CSSProperties = {
|
|
|
position: 'absolute',
|
|
|
bottom: -5,
|
|
|
left: '50%',
|
|
|
transform: 'translateX(-50%)',
|
|
|
width: 0,
|
|
|
height: 0,
|
|
|
borderLeft: '5px solid transparent',
|
|
|
borderRight: '5px solid transparent',
|
|
|
borderTop: '5px solid rgba(10, 10, 20, 0.92)',
|
|
|
};
|
|
|
|
|
|
// ---- Charge Badge ----
|
|
|
|
|
|
const chargeBadgeBase: CSSProperties = {
|
|
|
position: 'absolute',
|
|
|
top: -4,
|
|
|
right: -4,
|
|
|
minWidth: 16,
|
|
|
height: 16,
|
|
|
borderRadius: 8,
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
fontSize: 9,
|
|
|
fontWeight: 700,
|
|
|
lineHeight: 1,
|
|
|
padding: '0 3px',
|
|
|
zIndex: 10,
|
|
|
pointerEvents: 'none',
|
|
|
border: '1px solid rgba(0,0,0,0.4)',
|
|
|
};
|
|
|
|
|
|
// ---- Radial Cooldown SVG ----
|
|
|
|
|
|
function RadialCooldown({ progress }: { progress: number }) {
|
|
|
if (progress <= 0) return null;
|
|
|
|
|
|
const radius = 22;
|
|
|
const cx = 25;
|
|
|
const cy = 25;
|
|
|
const circumference = 2 * Math.PI * radius;
|
|
|
const offset = circumference * (1 - progress);
|
|
|
|
|
|
return (
|
|
|
<svg
|
|
|
style={{
|
|
|
position: 'absolute',
|
|
|
top: 0,
|
|
|
left: 0,
|
|
|
width: '100%',
|
|
|
height: '100%',
|
|
|
transform: 'rotate(-90deg)',
|
|
|
pointerEvents: 'none',
|
|
|
}}
|
|
|
viewBox="0 0 50 50"
|
|
|
>
|
|
|
<circle
|
|
|
cx={cx}
|
|
|
cy={cy}
|
|
|
r={radius}
|
|
|
fill="rgba(0,0,0,0.5)"
|
|
|
stroke="none"
|
|
|
strokeDasharray={circumference}
|
|
|
strokeDashoffset={offset}
|
|
|
/>
|
|
|
<circle
|
|
|
cx={cx}
|
|
|
cy={cy}
|
|
|
r={radius}
|
|
|
fill="none"
|
|
|
stroke="rgba(255,255,255,0.3)"
|
|
|
strokeWidth={2}
|
|
|
strokeDasharray={circumference}
|
|
|
strokeDashoffset={offset}
|
|
|
strokeLinecap="round"
|
|
|
/>
|
|
|
</svg>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// ---- Helpers ----
|
|
|
|
|
|
/** Format an ISO timestamp as HH:MM in local time. */
|
|
|
function formatTimeHHMM(iso: string): string {
|
|
|
const d = new Date(iso);
|
|
|
if (isNaN(d.getTime())) return '??:??';
|
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
|
}
|
|
|
|
|
|
// ---- Buff Button ----
|
|
|
|
|
|
const buttonBase: CSSProperties = {
|
|
|
position: 'relative',
|
|
|
border: '2px solid rgba(255,255,255,0.2)',
|
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
flexDirection: 'column',
|
|
|
cursor: 'pointer',
|
|
|
userSelect: 'none',
|
|
|
WebkitTapHighlightColor: 'transparent',
|
|
|
};
|
|
|
|
|
|
function BuffButton({ buff, charge, maxCharges, onActivate, onRefill, nowMs, buffsLocked }: BuffButtonProps) {
|
|
|
const tr = useT();
|
|
|
const visual = BUFF_VISUAL[buff.type];
|
|
|
const { label, desc } = buffUiStrings(tr, buff.type);
|
|
|
const meta = { ...visual, label, desc };
|
|
|
const [pressed, setPressed] = useState(false);
|
|
|
const [showTooltip, setShowTooltip] = useState(false);
|
|
|
const [showRefillConfirm, setShowRefillConfirm] = useState(false);
|
|
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
const autoHideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
const didLongPress = useRef(false);
|
|
|
|
|
|
const remainingEffectMs =
|
|
|
buff.expiresAtMs != null ? Math.max(0, buff.expiresAtMs - nowMs) : buff.remainingMs;
|
|
|
const isActive = remainingEffectMs > 0;
|
|
|
|
|
|
const isOnCooldown = buff.cooldownRemainingMs > 0;
|
|
|
const cooldownProgress =
|
|
|
buff.cooldownMs > 0 ? buff.cooldownRemainingMs / buff.cooldownMs : 0;
|
|
|
|
|
|
const cdSec = Math.ceil((buff.cooldownMs || 0) / 1000);
|
|
|
|
|
|
// Charge state
|
|
|
const remaining = charge?.remaining;
|
|
|
const hasChargeData = remaining != null;
|
|
|
const isOutOfCharges = hasChargeData && remaining === 0;
|
|
|
const isDisabled = isOnCooldown || (isOutOfCharges && !isActive) || !!buffsLocked;
|
|
|
|
|
|
useEffect(() => {
|
|
|
return () => {
|
|
|
if (longPressTimer.current) clearTimeout(longPressTimer.current);
|
|
|
if (autoHideTimer.current) clearTimeout(autoHideTimer.current);
|
|
|
};
|
|
|
}, []);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (buffsLocked) {
|
|
|
setShowTooltip(false);
|
|
|
setShowRefillConfirm(false);
|
|
|
}
|
|
|
}, [buffsLocked]);
|
|
|
|
|
|
const openTooltip = (): void => {
|
|
|
setShowTooltip(true);
|
|
|
if (autoHideTimer.current) clearTimeout(autoHideTimer.current);
|
|
|
autoHideTimer.current = setTimeout(() => setShowTooltip(false), TOOLTIP_AUTO_HIDE_MS);
|
|
|
};
|
|
|
|
|
|
const closeTooltip = (): void => {
|
|
|
setShowTooltip(false);
|
|
|
if (autoHideTimer.current) clearTimeout(autoHideTimer.current);
|
|
|
};
|
|
|
|
|
|
const handleTouchStart = (): void => {
|
|
|
if (buffsLocked) return;
|
|
|
didLongPress.current = false;
|
|
|
longPressTimer.current = setTimeout(() => {
|
|
|
didLongPress.current = true;
|
|
|
openTooltip();
|
|
|
}, LONG_PRESS_MS);
|
|
|
};
|
|
|
|
|
|
const handleTouchEnd = (): void => {
|
|
|
if (longPressTimer.current) {
|
|
|
clearTimeout(longPressTimer.current);
|
|
|
longPressTimer.current = null;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleClick = (): void => {
|
|
|
if (buffsLocked) return;
|
|
|
if (didLongPress.current) {
|
|
|
didLongPress.current = false;
|
|
|
return;
|
|
|
}
|
|
|
setPressed(true);
|
|
|
window.setTimeout(() => setPressed(false), 140);
|
|
|
|
|
|
// When out of charges, show refill confirmation instead of doing nothing
|
|
|
if (isOutOfCharges && !isOnCooldown && !isActive) {
|
|
|
setShowRefillConfirm(true);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (isDisabled) return;
|
|
|
onActivate();
|
|
|
};
|
|
|
|
|
|
const dimmedFaceOpacity = buffsLocked
|
|
|
? 0.4
|
|
|
: isDisabled
|
|
|
? (isOutOfCharges && !isOnCooldown ? 0.3 : 0.55)
|
|
|
: 1;
|
|
|
|
|
|
const hitStyle: CSSProperties = {
|
|
|
position: 'relative',
|
|
|
width: 44,
|
|
|
height: 50,
|
|
|
padding: 0,
|
|
|
border: 'none',
|
|
|
background: 'transparent',
|
|
|
pointerEvents: buffsLocked ? 'none' : 'auto',
|
|
|
cursor: buffsLocked || isDisabled ? 'not-allowed' : 'pointer',
|
|
|
transform: pressed ? 'scale(0.94)' : 'scale(1)',
|
|
|
transition: 'transform 80ms ease',
|
|
|
};
|
|
|
|
|
|
const activatorFaceStyle: CSSProperties = {
|
|
|
...buttonBase,
|
|
|
position: 'absolute',
|
|
|
inset: 0,
|
|
|
width: '100%',
|
|
|
height: '100%',
|
|
|
borderRadius: 10,
|
|
|
borderColor: isActive ? meta.color : pressed ? '#fff' : 'rgba(255,255,255,0.2)',
|
|
|
boxShadow: isActive
|
|
|
? `0 0 12px ${meta.color}`
|
|
|
: pressed
|
|
|
? `0 0 10px rgba(255,255,255,0.5), inset 0 0 12px ${meta.color}66`
|
|
|
: 'none',
|
|
|
opacity: dimmedFaceOpacity,
|
|
|
transition: 'opacity 150ms ease, box-shadow 80ms ease, border-color 80ms ease',
|
|
|
pointerEvents: 'none',
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<button
|
|
|
type="button"
|
|
|
style={hitStyle}
|
|
|
onClick={handleClick}
|
|
|
onTouchStart={handleTouchStart}
|
|
|
onTouchEnd={handleTouchEnd}
|
|
|
onTouchCancel={handleTouchEnd}
|
|
|
onMouseEnter={buffsLocked ? undefined : openTooltip}
|
|
|
onMouseLeave={buffsLocked ? undefined : closeTooltip}
|
|
|
aria-disabled={isDisabled}
|
|
|
aria-label={`${meta.label}: ${meta.desc}`}
|
|
|
>
|
|
|
{/* Dim only the activator (chrome + icon); tooltip is a sibling so it stays full opacity. */}
|
|
|
<div style={activatorFaceStyle}>
|
|
|
{/* Inner wrapper clips cooldown overlay to button bounds */}
|
|
|
<div
|
|
|
style={{
|
|
|
position: 'absolute',
|
|
|
inset: 0,
|
|
|
overflow: 'hidden',
|
|
|
borderRadius: 8,
|
|
|
}}
|
|
|
>
|
|
|
<RadialCooldown progress={cooldownProgress} />
|
|
|
</div>
|
|
|
<span style={{ fontSize: 18, lineHeight: 1, position: 'relative' }}>{meta.icon}</span>
|
|
|
<span style={{ fontSize: 8, color: '#ccc', marginTop: 1, position: 'relative' }}>{meta.label}</span>
|
|
|
{isActive && !isOnCooldown && (
|
|
|
<span
|
|
|
style={{
|
|
|
position: 'absolute',
|
|
|
bottom: 2,
|
|
|
fontSize: 9,
|
|
|
fontWeight: 700,
|
|
|
color: meta.color,
|
|
|
textShadow: '0 1px 2px rgba(0,0,0,0.9)',
|
|
|
}}
|
|
|
>
|
|
|
{Math.ceil(remainingEffectMs / 1000)}s
|
|
|
</span>
|
|
|
)}
|
|
|
{isOnCooldown && (
|
|
|
<span
|
|
|
style={{
|
|
|
position: 'absolute',
|
|
|
fontSize: 11,
|
|
|
fontWeight: 700,
|
|
|
color: '#fff',
|
|
|
textShadow: '0 1px 2px rgba(0,0,0,0.8)',
|
|
|
}}
|
|
|
>
|
|
|
{Math.ceil(buff.cooldownRemainingMs / 1000)}s
|
|
|
</span>
|
|
|
)}
|
|
|
|
|
|
{/* Charge badge */}
|
|
|
{hasChargeData && (
|
|
|
<span
|
|
|
style={{
|
|
|
...chargeBadgeBase,
|
|
|
backgroundColor: isOutOfCharges ? '#aa2222' : 'rgba(30, 90, 180, 0.9)',
|
|
|
color: isOutOfCharges ? '#ffaaaa' : '#fff',
|
|
|
}}
|
|
|
>
|
|
|
{remaining}
|
|
|
</span>
|
|
|
)}
|
|
|
</div>
|
|
|
|
|
|
{showTooltip && (
|
|
|
<div style={tooltipStyle}>
|
|
|
<div style={{ color: meta.color, fontWeight: 700, marginBottom: 2 }}>{meta.label}</div>
|
|
|
<div style={{ color: '#ddd', fontWeight: 400 }}>{meta.desc}</div>
|
|
|
{cdSec > 0 && <div style={{ color: '#999', fontSize: 10, marginTop: 2 }}>CD: {cdSec}s</div>}
|
|
|
{hasChargeData && (
|
|
|
<div
|
|
|
style={{
|
|
|
fontSize: 10,
|
|
|
marginTop: 3,
|
|
|
color: isOutOfCharges ? '#ff8844' : '#aaccff',
|
|
|
}}
|
|
|
>
|
|
|
{tr.charges}: {remaining}/{maxCharges}
|
|
|
</div>
|
|
|
)}
|
|
|
{isOutOfCharges && charge?.periodEnd && (
|
|
|
<div style={{ fontSize: 10, marginTop: 1, color: '#ff6644' }}>
|
|
|
{tr.refillsAt} {formatTimeHHMM(charge.periodEnd)}
|
|
|
</div>
|
|
|
)}
|
|
|
{SHOW_BUFF_REFILL_BUTTON_IN_TOOLTIP && isOutOfCharges && onRefill && (
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
setShowRefillConfirm(true);
|
|
|
closeTooltip();
|
|
|
}}
|
|
|
style={{
|
|
|
marginTop: 4,
|
|
|
padding: '3px 10px',
|
|
|
fontSize: 10,
|
|
|
fontWeight: 700,
|
|
|
color: '#fff',
|
|
|
backgroundColor: 'rgba(100, 180, 60, 0.85)',
|
|
|
border: '1px solid rgba(100, 180, 60, 0.6)',
|
|
|
borderRadius: 4,
|
|
|
cursor: 'pointer',
|
|
|
pointerEvents: 'auto',
|
|
|
}}
|
|
|
>
|
|
|
{tr.refill}
|
|
|
</button>
|
|
|
)}
|
|
|
<div style={tooltipArrow} />
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/* Refill confirmation popup */}
|
|
|
{showRefillConfirm && (
|
|
|
<div
|
|
|
style={{
|
|
|
position: 'absolute',
|
|
|
bottom: '115%',
|
|
|
left: '50%',
|
|
|
transform: 'translateX(-50%)',
|
|
|
padding: '8px 12px',
|
|
|
borderRadius: 8,
|
|
|
backgroundColor: 'rgba(10, 10, 20, 0.95)',
|
|
|
border: '1px solid rgba(100, 180, 60, 0.4)',
|
|
|
color: '#eee',
|
|
|
fontSize: 11,
|
|
|
fontWeight: 600,
|
|
|
whiteSpace: 'nowrap',
|
|
|
zIndex: 110,
|
|
|
textAlign: 'center',
|
|
|
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
|
|
}}
|
|
|
>
|
|
|
<div style={{ marginBottom: 6 }}>
|
|
|
{t(tr.refillQuestion, { label: meta.label })}
|
|
|
</div>
|
|
|
<div style={{ fontSize: 10, color: '#aaa', marginBottom: 8 }}>
|
|
|
{buff.type === BuffType.Resurrection ? '150\u20BD' : '50\u20BD'}
|
|
|
</div>
|
|
|
<div style={{ display: 'flex', gap: 6, justifyContent: 'center' }}>
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
setShowRefillConfirm(false);
|
|
|
onRefill?.(buff.type);
|
|
|
}}
|
|
|
style={{
|
|
|
padding: '4px 12px',
|
|
|
fontSize: 10,
|
|
|
fontWeight: 700,
|
|
|
color: '#fff',
|
|
|
backgroundColor: 'rgba(100, 180, 60, 0.85)',
|
|
|
border: 'none',
|
|
|
borderRadius: 4,
|
|
|
cursor: 'pointer',
|
|
|
}}
|
|
|
>
|
|
|
{tr.refill}
|
|
|
</button>
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
setShowRefillConfirm(false);
|
|
|
}}
|
|
|
style={{
|
|
|
padding: '4px 12px',
|
|
|
fontSize: 10,
|
|
|
fontWeight: 700,
|
|
|
color: '#aaa',
|
|
|
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
|
border: 'none',
|
|
|
borderRadius: 4,
|
|
|
cursor: 'pointer',
|
|
|
}}
|
|
|
>
|
|
|
{tr.cancel}
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</button>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// ---- Buff Bar ----
|
|
|
|
|
|
const barStyle: CSSProperties = {
|
|
|
display: 'flex',
|
|
|
gap: 4,
|
|
|
justifyContent: 'center',
|
|
|
flexWrap: 'wrap',
|
|
|
padding: '6px 0',
|
|
|
};
|
|
|
|
|
|
/** All buff types that should always be visible as buttons */
|
|
|
const ALL_BUFF_TYPES: BuffType[] = [
|
|
|
BuffType.Heal, BuffType.Rage, BuffType.Shield,
|
|
|
BuffType.Rush, BuffType.PowerPotion, BuffType.WarCry,
|
|
|
BuffType.Luck,
|
|
|
];
|
|
|
|
|
|
function getBuffEntry(
|
|
|
type: BuffType,
|
|
|
activeBuffs: ActiveBuff[],
|
|
|
cooldownEndsAt: Partial<Record<BuffType, number>>,
|
|
|
nowMs: number,
|
|
|
): ActiveBuff {
|
|
|
const active = activeBuffs.find((b) => b.type === type);
|
|
|
const cdEnd = cooldownEndsAt[type];
|
|
|
const cdRem = cdEnd != null ? Math.max(0, cdEnd - nowMs) : 0;
|
|
|
const cdFull = active?.cooldownMs || BUFF_COOLDOWN_MS[type];
|
|
|
|
|
|
if (active) {
|
|
|
const exp = active.expiresAtMs;
|
|
|
const remainingMs = exp != null ? Math.max(0, exp - nowMs) : active.remainingMs;
|
|
|
return {
|
|
|
...active,
|
|
|
remainingMs,
|
|
|
cooldownMs: active.cooldownMs || cdFull,
|
|
|
cooldownRemainingMs: cdRem,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
type,
|
|
|
remainingMs: 0,
|
|
|
durationMs: 0,
|
|
|
cooldownMs: cdFull,
|
|
|
cooldownRemainingMs: cdRem,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
export function BuffBar({
|
|
|
buffs,
|
|
|
cooldownEndsAt,
|
|
|
buffCharges,
|
|
|
subscriptionActive,
|
|
|
buffsLocked,
|
|
|
nowMs,
|
|
|
onActivate,
|
|
|
onHeroUpdated,
|
|
|
}: BuffBarProps) {
|
|
|
const handleActivate = useCallback(
|
|
|
(type: BuffType) => () => onActivate(type),
|
|
|
[onActivate],
|
|
|
);
|
|
|
|
|
|
const handleRefill = useCallback(
|
|
|
(type: BuffType) => {
|
|
|
const telegramId = getTelegramUserId() ?? undefined;
|
|
|
purchaseBuffRefill(type, telegramId)
|
|
|
.then((hero) => {
|
|
|
onHeroUpdated?.(hero);
|
|
|
})
|
|
|
.catch((err) => {
|
|
|
console.warn('[BuffBar] purchaseBuffRefill failed:', err);
|
|
|
});
|
|
|
},
|
|
|
[onHeroUpdated],
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<div style={barStyle}>
|
|
|
{ALL_BUFF_TYPES.map((type) => {
|
|
|
const buff = getBuffEntry(type, buffs, cooldownEndsAt, nowMs);
|
|
|
const charge = buffCharges[type];
|
|
|
const maxCharges = buffMaxChargesForHero(type, subscriptionActive);
|
|
|
return (
|
|
|
<BuffButton
|
|
|
key={type}
|
|
|
buff={buff}
|
|
|
charge={charge}
|
|
|
maxCharges={maxCharges}
|
|
|
onActivate={handleActivate(type)}
|
|
|
onRefill={handleRefill}
|
|
|
nowMs={nowMs}
|
|
|
buffsLocked={buffsLocked}
|
|
|
/>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
);
|
|
|
}
|