|
|
|
@ -1,5 +1,5 @@ |
|
|
|
|
import {animated, useSpring} from "@react-spring/web"; |
|
|
|
|
import {ReactElement, useMemo} from "react"; |
|
|
|
|
import {ReactElement, useCallback, useMemo, useState} from "react"; |
|
|
|
|
import {evaluateResourceBarStyles, ResourceBarColors, ResourceBarStyles} from "./ResourceBarGradient"; |
|
|
|
|
import {isDefined} from "../types/type_check"; |
|
|
|
|
import {SpringyValueInterpolatables, useSpringyValue} from "./SpringyValueHook"; |
|
|
|
@ -19,6 +19,7 @@ import { |
|
|
|
|
turnStateToTitle |
|
|
|
|
} from "../model/Character"; |
|
|
|
|
import {StatusEffect} from "../model/GameState"; |
|
|
|
|
import React from "react"; |
|
|
|
|
|
|
|
|
|
export function healthToColor(health: CharacterHealth | undefined): string { |
|
|
|
|
switch (health) { |
|
|
|
@ -75,8 +76,14 @@ const ipBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = { |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function CharacterStatus({character, statuses, active}: {character: Character, statuses: readonly StatusEffect[], active: boolean}): ReactElement { |
|
|
|
|
const {name, altName, minion, level, health, koText} = character |
|
|
|
|
export function CharacterStatus({character, minions, statuses, activeId, collapsed, click}: {character: Character, minions: readonly Character[], statuses: readonly StatusEffect[], activeId: string, collapsed?: boolean, click?: () => void}): ReactElement { |
|
|
|
|
const [showMinions, setShowMinions] = useState<boolean>(false) |
|
|
|
|
|
|
|
|
|
const toggleMinions = useCallback(() => { |
|
|
|
|
setShowMinions(!showMinions) |
|
|
|
|
}, [showMinions, setShowMinions]) |
|
|
|
|
|
|
|
|
|
const {id, name, altName, leader, level, health, koText} = character |
|
|
|
|
|
|
|
|
|
const {hp, maxHp} = character |
|
|
|
|
const effectiveMaxHp = maxHp ?? 100 |
|
|
|
@ -199,7 +206,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara |
|
|
|
|
return { |
|
|
|
|
turnsState: CharacterTurnState.None |
|
|
|
|
} |
|
|
|
|
} else if (active) { |
|
|
|
|
} else if (activeId === id) { |
|
|
|
|
return { |
|
|
|
|
turnsState: CharacterTurnState.Active, |
|
|
|
|
turnsText: "🞂", |
|
|
|
@ -228,7 +235,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara |
|
|
|
|
turnsState: CharacterTurnState.Ready, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, [active, effectiveHp, canAct, turnsLeft, turnsTotal]) |
|
|
|
|
}, [activeId, id, effectiveHp, canAct, turnsLeft, turnsTotal]) |
|
|
|
|
|
|
|
|
|
const {portraitUrl} = character |
|
|
|
|
const effectivePortraitUrl = portraitUrl ?? DefaultPortrait |
|
|
|
@ -308,120 +315,127 @@ export function CharacterStatus({character, statuses, active}: {character: Chara |
|
|
|
|
</Tooltip> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return <div className={"characterStatus" + (minion ? " minion" : "")}> |
|
|
|
|
<animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} /> |
|
|
|
|
{isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>Zero Charge</span> |
|
|
|
|
<span className={"characterHelpValue"}>{(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}</span></div> |
|
|
|
|
{<div className={"characterHelpDescription"}> |
|
|
|
|
The amount of energy stored up towards unleashing the might of a Zero Power. When the gauge is full, let loose! |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<div className={"characterZeroGauge"}> |
|
|
|
|
<div className={"characterZeroBarBack"} /> |
|
|
|
|
<animated.div className={"characterZeroBar"} style={zpStyle} /> |
|
|
|
|
<div className={"characterZeroBarPulse" + (zp >= maxZp ? " active" : "")} /> |
|
|
|
|
</div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
{isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && <animated.div className={"characterKOBar"} style={characterKOBarStyleInterpolated}>{koText ?? "KO"}</animated.div>} |
|
|
|
|
{isDefined(turnsState) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
return <React.Fragment> |
|
|
|
|
<div className={"characterStatus" + (minions.length > 0 && !showMinions ? " leader" : leader ? " minion" : "") + (collapsed ? " collapsed" : "")} onClick={() => click ? click() : null}> |
|
|
|
|
{minions.length > 0 && !showMinions && <CharacterStatus character={minions[0]} minions={[]} statuses={statuses} activeId={activeId} collapsed={true} click={toggleMinions} />} |
|
|
|
|
<animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} /> |
|
|
|
|
{isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>{turnStateToTitle(turnsState)}</span> |
|
|
|
|
{isDefined(turnsTotal) && isDefined(turnsLeft) && turnsTotal > 1 && <span className={"characterHelpValue"}>{turnsLeft}/{turnsTotal}</span>}</div> |
|
|
|
|
<span className={"characterHelpName"}>Zero Charge</span> |
|
|
|
|
<span className={"characterHelpValue"}>{(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}</span></div> |
|
|
|
|
{<div className={"characterHelpDescription"}> |
|
|
|
|
{isDefined(turnsTotal) && isDefined(turnsLeft) && |
|
|
|
|
turnStateToDescription(turnsState) |
|
|
|
|
.replaceAll("%c%", turnsLeft.toFixed(0)) |
|
|
|
|
.replaceAll("%m%", turnsTotal.toFixed(0))} |
|
|
|
|
The amount of energy stored up towards unleashing the might of a Zero Power. When the gauge is full, let loose! |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<div className={"characterTurns characterTurns" + turnsState}>{turnsText}</div> |
|
|
|
|
<div className={"characterZeroGauge"}> |
|
|
|
|
<div className={"characterZeroBarBack"} /> |
|
|
|
|
<animated.div className={"characterZeroBar"} style={zpStyle} /> |
|
|
|
|
<div className={"characterZeroBarPulse" + (zp >= maxZp ? " active" : "")} /> |
|
|
|
|
</div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
<div className={"characterHeader"}> |
|
|
|
|
<div className="characterLevel"> |
|
|
|
|
<span className="characterLevelLabel">Lv</span> |
|
|
|
|
<span className="characterLevelValue">{level ?? "??"}</span> |
|
|
|
|
{isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && <animated.div className={"characterKOBar"} style={characterKOBarStyleInterpolated}>{koText ?? "KO"}</animated.div>} |
|
|
|
|
{isDefined(turnsState) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>{turnStateToTitle(turnsState)}</span> |
|
|
|
|
{isDefined(turnsTotal) && isDefined(turnsLeft) && turnsTotal > 1 && <span className={"characterHelpValue"}>{turnsLeft}/{turnsTotal}</span>}</div> |
|
|
|
|
{<div className={"characterHelpDescription"}> |
|
|
|
|
{isDefined(turnsTotal) && isDefined(turnsLeft) && |
|
|
|
|
turnStateToDescription(turnsState) |
|
|
|
|
.replaceAll("%c%", turnsLeft.toFixed(0)) |
|
|
|
|
.replaceAll("%m%", turnsTotal.toFixed(0))} |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<div className={"characterTurns characterTurns" + turnsState}>{turnsText}</div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
<div className={"characterHeader"}> |
|
|
|
|
<div className="characterLevel"> |
|
|
|
|
<span className="characterLevelLabel">Lv</span> |
|
|
|
|
<span className="characterLevelValue">{level ?? "??"}</span> |
|
|
|
|
</div> |
|
|
|
|
<div className={"characterName"}>{name ?? altName ?? "???"}</div> |
|
|
|
|
</div> |
|
|
|
|
<div className={"characterName"}>{name ?? altName ?? "???"}</div> |
|
|
|
|
</div> |
|
|
|
|
{isDefined(hpText) && |
|
|
|
|
<div className={"characterHp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={hpTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterHpBar"} style={hpBarStyleInterpolated} /> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
{isDefined(hp) && <animated.div |
|
|
|
|
className={"characterHpValue"} |
|
|
|
|
style={hpTextStyleInterpolated}>{hpText}</animated.div>} |
|
|
|
|
</div>} |
|
|
|
|
{isDefined(mpText) && |
|
|
|
|
<div className={"characterMp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={mpTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterMpBar"} style={mpBarStyleInterpolated} /> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
<animated.div className={"characterMpValue"}>{mpText}</animated.div> |
|
|
|
|
</div>} |
|
|
|
|
{isDefined(ipText) && |
|
|
|
|
<div className={"characterIp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ipTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterIpBar"} style={ipBarStyleInterpolated} /> |
|
|
|
|
{isDefined(hpText) && |
|
|
|
|
<div className={"characterHp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={hpTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterHpBar"} style={hpBarStyleInterpolated} /> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
<animated.div className={"characterIpValue"}>{ipText}</animated.div> |
|
|
|
|
</div> |
|
|
|
|
} |
|
|
|
|
{isDefined(spText) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>{spType} Points</span> |
|
|
|
|
<span className={"characterHelpValue"}>{sp}{typeof spBank === "number" ? ` / ${spBank} banked`: null}</span></div> |
|
|
|
|
{isDefined(spType) && <div className={"characterHelpDescription"}> |
|
|
|
|
{spTypeToDescription(spType)} |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<animated.div className={"characterSp characterSp" + spType}> |
|
|
|
|
<animated.span className={"characterSpValue characterSpValue" + spType}> |
|
|
|
|
{spText}</animated.span> |
|
|
|
|
</animated.div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
{isDefined(bpText) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>Blood Points</span> |
|
|
|
|
<span className={"characterHelpValue"}>{bp}/{maxBp}</span></div> |
|
|
|
|
{isDefined(spType) && <div className={"characterHelpDescription"}> |
|
|
|
|
The current number of blood points, used for Vampire abilities. |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<animated.div className={"characterBp"}> |
|
|
|
|
<animated.span className={"characterBpValue"}> |
|
|
|
|
{bpText}</animated.span> |
|
|
|
|
</animated.div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
{isDefined(effectiveStatuses) && effectiveStatuses.length > 0 && |
|
|
|
|
<div className={"characterStatuses"}> |
|
|
|
|
{effectiveStatuses.map(({id, name, count, description, iconUrl}) => |
|
|
|
|
<OverlayTrigger key={id} delay={{show: 300, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterStatusHeader"}> |
|
|
|
|
<span className={"characterStatusName"}>{name}</span> |
|
|
|
|
{isDefined(count) && count > 0 && <span className={"characterStatusCount"}>{count}</span>}</div> |
|
|
|
|
{isDefined(description) && <div className={"characterStatusDescription"}> |
|
|
|
|
{count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description} |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"bottom"}> |
|
|
|
|
<div className={"characterStatusIcon"} style={{backgroundImage: `url("${iconUrl ?? DefaultStatus}")`}}>{count > 0 && <span className={"characterStatusIconCountBadge"}>{count}</span>}</div> |
|
|
|
|
{isDefined(hp) && <animated.div |
|
|
|
|
className={"characterHpValue"} |
|
|
|
|
style={hpTextStyleInterpolated}>{hpText}</animated.div>} |
|
|
|
|
</div>} |
|
|
|
|
{isDefined(mpText) && |
|
|
|
|
<div className={"characterMp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={mpTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterMpBar"} style={mpBarStyleInterpolated} /> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
)} |
|
|
|
|
</div>} |
|
|
|
|
</div> |
|
|
|
|
<animated.div className={"characterMpValue"}>{mpText}</animated.div> |
|
|
|
|
</div>} |
|
|
|
|
{isDefined(ipText) && |
|
|
|
|
<div className={"characterIp"}> |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ipTooltip} placement={"top"}> |
|
|
|
|
<animated.div className={"characterIpBar"} style={ipBarStyleInterpolated} /> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
<animated.div className={"characterIpValue"}>{ipText}</animated.div> |
|
|
|
|
</div> |
|
|
|
|
} |
|
|
|
|
{isDefined(spText) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>{spType} Points</span> |
|
|
|
|
<span className={"characterHelpValue"}>{sp}{typeof spBank === "number" ? ` / ${spBank} banked`: null}</span></div> |
|
|
|
|
{isDefined(spType) && <div className={"characterHelpDescription"}> |
|
|
|
|
{spTypeToDescription(spType)} |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<animated.div className={"characterSp characterSp" + spType}> |
|
|
|
|
<animated.span className={"characterSpValue characterSpValue" + spType}> |
|
|
|
|
{spText}</animated.span> |
|
|
|
|
</animated.div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
{isDefined(bpText) && |
|
|
|
|
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterHelpHeader"}> |
|
|
|
|
<span className={"characterHelpName"}>Blood Points</span> |
|
|
|
|
<span className={"characterHelpValue"}>{bp}/{maxBp}</span></div> |
|
|
|
|
{isDefined(spType) && <div className={"characterHelpDescription"}> |
|
|
|
|
The current number of blood points, used for Vampire abilities. |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"right"}> |
|
|
|
|
<animated.div className={"characterBp"}> |
|
|
|
|
<animated.span className={"characterBpValue"}> |
|
|
|
|
{bpText}</animated.span> |
|
|
|
|
</animated.div> |
|
|
|
|
</OverlayTrigger>} |
|
|
|
|
{isDefined(effectiveStatuses) && effectiveStatuses.length > 0 && |
|
|
|
|
<div className={"characterStatuses"}> |
|
|
|
|
{effectiveStatuses.map(({id, name, count, description, iconUrl}) => |
|
|
|
|
<OverlayTrigger key={id} delay={{show: 300, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ |
|
|
|
|
<Tooltip> |
|
|
|
|
<div className={"characterStatusHeader"}> |
|
|
|
|
<span className={"characterStatusName"}>{name}</span> |
|
|
|
|
{isDefined(count) && count > 0 && <span className={"characterStatusCount"}>{count}</span>}</div> |
|
|
|
|
{isDefined(description) && <div className={"characterStatusDescription"}> |
|
|
|
|
{count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description} |
|
|
|
|
</div>} |
|
|
|
|
</Tooltip> |
|
|
|
|
} placement={"bottom"}> |
|
|
|
|
<div className={"characterStatusIcon"} style={{backgroundImage: `url("${iconUrl ?? DefaultStatus}")`}}>{count > 0 && <span className={"characterStatusIconCountBadge"}>{count}</span>}</div> |
|
|
|
|
</OverlayTrigger> |
|
|
|
|
)} |
|
|
|
|
</div>} |
|
|
|
|
</div> |
|
|
|
|
{minions.length > 0 && showMinions && minions.map((minion) => |
|
|
|
|
<CharacterStatus character={minion} minions={[]} statuses={statuses} key={minion.id} |
|
|
|
|
activeId={activeId} collapsed={false} click={toggleMinions} /> |
|
|
|
|
)} |
|
|
|
|
</React.Fragment> |
|
|
|
|
} |
|
|
|
|