From 8ae259951bf1857d2ea5daaecdf3fa2d9232c583 Mon Sep 17 00:00:00 2001 From: Mari Date: Thu, 21 Sep 2023 19:24:21 -0700 Subject: [PATCH] daughter friendship --- public/api/current.json | 15 +-- src/grammar/grammar.ohm | 20 ++-- src/grammar/interpreter.ts | 63 ++++++++-- src/model/Character.ts | 9 +- src/ui/App.tsx | 25 ++-- src/ui/CharacterStatus.css | 77 +++++++++++- src/ui/CharacterStatus.tsx | 236 ++++++++++++++++++++----------------- 7 files changed, 290 insertions(+), 155 deletions(-) diff --git a/public/api/current.json b/public/api/current.json index d46061b..16aa752 100644 --- a/public/api/current.json +++ b/public/api/current.json @@ -35,7 +35,7 @@ { "id": "flow", "side": "ally", - "minion": true, + "leader": "aelica", "name": "Flow", "portraitUrl": "/portraits/flow.png", "statuses": [] @@ -64,8 +64,9 @@ { "id": "galvelle", "side": "ally", - "minion": true, + "leader": "athetyz", "name": "Galvelle", + "portraitUrl": "/portraits/galvelle.png", "statuses": [] }, { @@ -92,10 +93,10 @@ { "id": "gale", "side": "ally", - "minion": true, + "leader": "echo", "name": "Gale", "level": 5, - "hp": 70, + "hp": 0, "maxHp": 70, "mp": 58, "maxMp": 58, @@ -128,7 +129,7 @@ { "id": "calor", "side": "ally", - "minion": true, + "leader": "gravitas", "name": "Calor", "portraitUrl": "/portraits/calor.png", "statuses": [] @@ -157,7 +158,7 @@ { "id": "terra", "side": "ally", - "minion": true, + "leader": "linnet", "name": "Terra", "portraitUrl": "/portraits/terra.png", "statuses": [] @@ -187,7 +188,7 @@ { "id": "selia", "side": "ally", - "minion": true, + "leader": "prandia", "name": "Sélia", "portraitUrl": "/portraits/selia.png", "statuses": [] diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 84dd17d..cc21f82 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -191,21 +191,25 @@ FabulaDSL { StatusOrItemCounter = StatusOrItemCounterWrapped | StatusOrItemCounterUnwrapped StatusOrItemDeltaWrapped = "(" Delta ")" StatusOrItemDelta = StatusOrItemDeltaWrapped | Delta + StatusSeparator = colon colon // StatusOrItemAddOperation: operands, identifier, numberValue, evaluate, renderMarkdown - StatusOrItemAddOperation = Operands colon plus identifier StatusOrItemCounter? + StatusOrItemAddOperation = Operands StatusSeparator plus identifier StatusOrItemCounter? // TODO: StatusOrItemRemoveOperation: operands, identifier, evaluate, renderMarkdown - StatusOrItemRemoveOperation = Operands colon minus identifier + StatusOrItemRemoveOperation = Operands StatusSeparator minus identifier + StatusOrItemRemoveOperationAlternate = Operands #wordSep identifier StatusSeparator null // TODO: StatusOrItemDeltaOperation/Alternate: operands, identifier, numberValue, evaluate, renderMarkdown - StatusOrItemCounterDeltaOperation = Operands colon identifier StatusOrItemDelta - StatusOrItemCounterDeltaOperationAlternate = Operands colon StatusOrItemDelta identifier + StatusOrItemCounterDeltaOperation = Operands #wordSep identifier StatusSeparator StatusOrItemDelta + StatusOrItemCounterDeltaOperationAlternate = Operands StatusSeparator StatusOrItemDelta identifier + StatusOrItemCounterDeltaOperationAlternate2 = Operands StatusSeparator identifier StatusOrItemDelta // TODO: Fix conflict with resource mutators (use brackets?) // TODO: StatusOrItemSetOperation/Alternate: operands, identifier, numberValue, evaluate, renderMarkdown - StatusOrItemCounterSetOperation = Operands colon identifier StatusOrItemCounter? - StatusOrItemCounterSetOperationAlternate = Operands colon StatusOrItemCounter identifier + StatusOrItemCounterSetOperation = Operands #wordSep identifier StatusSeparator StatusOrItemCounter? + StatusOrItemCounterSetOperationAlternate = Operands StatusSeparator StatusOrItemCounter identifier + StatusOrItemCounterSetOperationAlternate2 = Operands StatusSeparator identifier StatusOrItemCounter? // TODO: continue implementation list from here @@ -312,8 +316,8 @@ FabulaDSL { PrintOperationWithOperands = Operands colon printOperator textToEndOfLine Operation = StatusOrItemAddOperation | StatusOrItemRemoveOperation - | StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate - | StatusOrItemCounterSetOperation | StatusOrItemCounterSetOperationAlternate + | StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate | StatusOrItemCounterDeltaOperationAlternate2 + | StatusOrItemCounterSetOperation | StatusOrItemCounterSetOperationAlternate | StatusOrItemCounterSetOperationAlternate2 | DeltaOperation | DeltaOperationAlternate | DeltaOperationAlternate2 | SetMeteredOperation | SetMeteredOperationAlternate | SetMeteredOperationAlternate2 | SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2 diff --git a/src/grammar/interpreter.ts b/src/grammar/interpreter.ts index 0e344eb..68047b0 100644 --- a/src/grammar/interpreter.ts +++ b/src/grammar/interpreter.ts @@ -203,9 +203,6 @@ interpreter.addAttribute("sign", { minus(_: TerminalNode): NumberSign.Negative { return NumberSign.Negative }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): NumberSign { - return (delta as InterpreterNode).sign - }, _iter(): never { throw Error(`No idea what to say ${this.ctorName} iteration node's number sign is`) }, @@ -282,10 +279,16 @@ interpreter.addAttribute("numberValue", { } return (counter.child(0)).numberValue }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): number { + StatusOrItemCounterDeltaOperation(operands: NonterminalNode, wordSep: NonterminalNode, identifier: NonterminalNode, sep: NonterminalNode, delta: NonterminalNode): number { + return (delta as InterpreterNode).numberValue + }, + StatusOrItemCounterDeltaOperationAlternate(operands: NonterminalNode, colon: NonterminalNode, delta: NonterminalNode, _identifier: NonterminalNode): number { return (delta as InterpreterNode).numberValue }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { + StatusOrItemCounterDeltaOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): number { + return (delta as InterpreterNode).numberValue + }, + StatusOrItemCounterSetOperation(operands: NonterminalNode, wordSep: NonterminalNode, identifier: NonterminalNode, sep: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, _iter(): never { @@ -324,10 +327,16 @@ interpreter.addAttribute("identifier", { StatusOrItemRemoveOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, identifier: NonterminalNode): string { return (identifier as InterpreterNode).identifier; }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, _delta: NonterminalNode): string { + StatusOrItemCounterDeltaOperation(operands: NonterminalNode, wordSep: NonterminalNode, identifier: NonterminalNode, sep: NonterminalNode, _delta: NonterminalNode): string { + return (identifier as InterpreterNode).identifier + }, + StatusOrItemCounterDeltaOperationAlternate(operands: NonterminalNode, _sep: NonterminalNode, _delta: NonterminalNode, identifier: NonterminalNode): string { return (identifier as InterpreterNode).identifier }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, _counter: NonterminalNode): string { + StatusOrItemCounterDeltaOperationAlternate2(operands: NonterminalNode, _sep: NonterminalNode, identifier: NonterminalNode, _delta: NonterminalNode): string { + return (identifier as InterpreterNode).identifier + }, + StatusOrItemCounterSetOperation(operands: NonterminalNode, wordSep: NonterminalNode, identifier: NonterminalNode, sep: NonterminalNode, _delta: NonterminalNode): string { return (identifier as InterpreterNode).identifier }, _iter(): never { @@ -602,10 +611,16 @@ interpreter.addAttribute("operands", { SetValueOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _value: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, - StatusOrItemCounterDeltaOperation(operands: NonterminalNode, _colon: NonterminalNode, _statusOrItem: NonterminalNode, _delta: NonterminalNode): Operands { + StatusOrItemCounterDeltaOperation(operands: NonterminalNode, _wordSep: NonterminalNode, _statusOrItem: NonterminalNode, _sep: NonterminalNode, _delta: NonterminalNode): Operands { + return (operands as InterpreterNode).operands + }, + StatusOrItemCounterDeltaOperationAlternate(operands: NonterminalNode, _sep: NonterminalNode, _delta: NonterminalNode, _statusOrItem: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, _colon: NonterminalNode, _statusOrItem: NonterminalNode, _value: NonterminalNode): Operands { + StatusOrItemCounterDeltaOperationAlternate2(operands: NonterminalNode, _sep: NonterminalNode, _statusOrItem: NonterminalNode, _delta: NonterminalNode): Operands { + return (operands as InterpreterNode).operands + }, + StatusOrItemCounterSetOperation(operands: NonterminalNode, _wordSep: NonterminalNode, _statusOrItem: NonterminalNode, _sep: NonterminalNode, _value: NonterminalNode): Operands { return (operands as InterpreterNode).operands }, StatusOrItemAddOperation(operands: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _statusOrItem: NonterminalNode, _counter: IterationNode): Operands { @@ -737,10 +752,10 @@ interpreter.addAttribute("currentValue", { SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { return (value as InterpreterNode).currentValue; }, - StatusOrItemAddOperation(operands: NonterminalNode, colon: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { + StatusOrItemAddOperation(operands: NonterminalNode, sep: NonterminalNode, deltaOperator: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, - StatusOrItemCounterSetOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, counter: NonterminalNode): number { + StatusOrItemCounterSetOperation(operands: NonterminalNode, wordSep: NonterminalNode, identifier: NonterminalNode, sep: NonterminalNode, counter: NonterminalNode): number { return (counter as InterpreterNode).numberValue }, _iter(): never { @@ -996,6 +1011,22 @@ function EvaluateClear(node: EvaluationNode): EvaluationContext { } } +function EvaluateStatusCounterDelta(node: EvaluationNode): EvaluationContext { + const ctx = node.args.ctx + const operands = evaluateOperands(node.operands, ctx) + const statusOrItemId = node.identifier + const stacks = node.numberValue + + return { + ...ctx, + game: { + ...ctx.game, + characters: ctx.game.characters.map((c) => + operands.has(c.id) ? CharacterStatuses.applyStatusDelta(c, statusOrItemId, stacks) : c), + } + } +} + interpreter.addOperation("evaluate(ctx)", { CodeSegment(items: IterationNode): EvaluationContext { let ctx = (this as EvaluationNode).args.ctx @@ -1094,6 +1125,16 @@ interpreter.addOperation("evaluate(ctx)", { } } }, + StatusOrItemCounterDeltaOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { + return EvaluateStatusCounterDelta(this as EvaluationNode) + }, + StatusOrItemCounterDeltaOperationAlternate(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { + return EvaluateStatusCounterDelta(this as EvaluationNode) + }, + StatusOrItemCounterDeltaOperationAlternate2(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode): EvaluationContext { + return EvaluateStatusCounterDelta(this as EvaluationNode) + }, + ClearOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext { return EvaluateClear(this as EvaluationNode); }, diff --git a/src/model/Character.ts b/src/model/Character.ts index 3887b95..b4483e8 100644 --- a/src/model/Character.ts +++ b/src/model/Character.ts @@ -200,7 +200,7 @@ export enum CharacterSide { export interface Character { readonly id: string readonly side?: CharacterSide - readonly minion?: boolean + readonly leader?: string readonly portraitUrl?: string readonly name?: string readonly altName?: string @@ -474,6 +474,13 @@ export const CharacterResources: ResourceManipulator = { return result } }, + clear(object: Character, resource: Resource): Character { + const result: {-readonly [key in keyof Character]: Character[key]} = baseCharacterResource.clear(object, resource) + if (resource === MeteredResource.Health) { + delete result.health + } + return result + } } export function applyCharacterPrivacy(character: Character): Character|null { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 6b6271e..5691271 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -41,7 +41,7 @@ function App() { @: ZP 5/6 @: ZP +1 &: 20 HP - gale, calor,gravitas HP: 3 + calor,gravitas HP: 3 &: ZP 5 &: ZP /7 &: /99 IP @@ -50,12 +50,13 @@ function App() { athetyz: - MP linnet: HP - &: Lv - - gale, echo, aelica: +Digesting - gale: -Digesting - flow: +Digesting 2 - echo, flow: +Digesting 3 - calor, flow, echo: Digesting +1 - gale, echo: +1 Digesting + gale, echo, aelica:: +Digesting + flow:: +Digesting 2 + echo, flow:: +Digesting 3 + gale:: -Digesting + calor Digesting:: +1 + flow, gale:: Digesting +1 + echo, flow, gale:: -1 Digesting End`) } } catch (ex) { @@ -107,23 +108,25 @@ function App() {

Allies

- {state && state.characters.filter((character) => character.side === CharacterSide.Ally).map((character) => + {state && state.characters.filter((character) => !character.leader && character.side === CharacterSide.Ally).map((character) => minion.leader === character.id)} statuses={state.statuses} - active={character.id === state.conflict?.activeCharacterId} />)} + activeId={state.conflict?.activeCharacterId ?? ""} />)}

Enemies

- {state && state.characters.filter((character) => character.side === CharacterSide.Enemy).map((character) => + {state && state.characters.filter((character) => !character.leader && character.side === CharacterSide.Enemy).map((character) => minion.leader === character.id)} statuses={state.statuses} - active={character.id === state.conflict?.activeCharacterId} />)} + activeId={state.conflict?.activeCharacterId ?? ""} />)}
diff --git a/src/ui/CharacterStatus.css b/src/ui/CharacterStatus.css index 4440902..de02904 100644 --- a/src/ui/CharacterStatus.css +++ b/src/ui/CharacterStatus.css @@ -1,13 +1,49 @@ .characterStatus { height: 7.5em; - width: 27.25em; + width: 30.25em; position: relative; box-sizing: content-box; } .characterStatus.minion { - margin-left: 6.8125em; +} + +.characterStatus.minion.collapsed { font-size: 0.80em; + height: 6.25em; + width: 6.25em; + margin-left: 0; +} + +.characterStatus.minion.collapsed .characterPortrait { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.characterStatus.minion.collapsed .characterKOBar { + left: 0; + bottom: 1.406em; +} + +.characterStatus.minion.collapsed .characterHeader, +.characterStatus.minion.collapsed .characterTurns, +.characterStatus.minion.collapsed .characterSp, +.characterStatus.minion.collapsed .characterBp, +.characterStatus.minion.collapsed .characterHp, +.characterStatus.minion.collapsed .characterMp, +.characterStatus.minion.collapsed .characterIp, +.characterStatus.minion.collapsed .characterZeroGauge, +.characterStatus.minion.collapsed .characterStatuses { + display: none; +} + +.characterStatus .characterStatus.minion { + position: absolute; + bottom: 1.5em; + left: 5.625em; } .characterHeader { @@ -17,6 +53,10 @@ z-index: 4; } +.characterStatus/*.leader*/ .characterHeader { + left: 13.25em; +} + .characterLevel { display: inline; color: white; @@ -44,6 +84,10 @@ user-select: none; } +.characterName .minion { + font-size: 0.75em; +} + .characterHpBar, .characterMpBar, .characterIpBar { box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5); border: 0.1em solid black; @@ -79,6 +123,10 @@ box-sizing: content-box; } +.characterStatus/*.leader*/ .characterHp { + left: 10.85em; +} + .characterHpBar { height: 1.25em; } @@ -111,6 +159,10 @@ z-index: 3; } +.characterStatus/*.leader*/ .characterMp { + left: 10em; +} + .characterIp { width: 7.5em; right: 1em; @@ -141,23 +193,28 @@ z-index: 0; } +.characterStatus/*.leader*/ .characterPortrait { + left: 7.5em; +} + .characterZeroGauge { position: absolute; top: 0.25em; bottom: 0.5em; left: 2.5em; - width: 3.75em; + width: 2.5em; } .characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{ position: absolute; top: 0; bottom: 0; left: 0; - right: 0; + right: -1.25em; background-repeat: no-repeat; background-position: left bottom; background-size: auto 6.75em; z-index: 0; + pointer-events: none; } .characterZeroBarBack { @@ -197,8 +254,8 @@ .characterKOBar { position: absolute; - top: 1.592em; - bottom: 2.518em; + height: 1.5em; + bottom: 1.962em; left: 3.333em; width: 4.629em; background-color: black; @@ -213,6 +270,10 @@ border-radius: 0.555em 0; } +.characterStatus/*.leader*/ .characterKOBar { + left: 5.555em; +} + .characterTurns { position: absolute; top: 0.125em; @@ -311,6 +372,10 @@ overflow-x: auto; } +.characterStatus/*.leader*/ .characterStatuses { + left: 14.25em; +} + .characterStatusIcon { position: relative; flex: 0 0 1.8em; diff --git a/src/ui/CharacterStatus.tsx b/src/ui/CharacterStatus.tsx index 648ede0..dd4d530 100644 --- a/src/ui/CharacterStatus.tsx +++ b/src/ui/CharacterStatus.tsx @@ -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 = { }, } -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(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 - return
- - {isDefined(maxZp) && isDefined(zp) && -
- Zero Charge - {(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}
- {
- The amount of energy stored up towards unleashing the might of a Zero Power. When the gauge is full, let loose! -
} - - } placement={"right"}> -
-
- -
= maxZp ? " active" : "")} /> -
- } - {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && {koText ?? "KO"}} - {isDefined(turnsState) && - +
0 && !showMinions ? " leader" : leader ? " minion" : "") + (collapsed ? " collapsed" : "")} onClick={() => click ? click() : null}> + {minions.length > 0 && !showMinions && } + + {isDefined(maxZp) && isDefined(zp) &&
- {turnStateToTitle(turnsState)} - {isDefined(turnsTotal) && isDefined(turnsLeft) && turnsTotal > 1 && {turnsLeft}/{turnsTotal}}
+ Zero Charge + {(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}
{
- {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!
} } placement={"right"}> -
{turnsText}
+
+
+ +
= maxZp ? " active" : "")} /> +
} -
-
- Lv - {level ?? "??"} + {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && {koText ?? "KO"}} + {isDefined(turnsState) && + +
+ {turnStateToTitle(turnsState)} + {isDefined(turnsTotal) && isDefined(turnsLeft) && turnsTotal > 1 && {turnsLeft}/{turnsTotal}}
+ {
+ {isDefined(turnsTotal) && isDefined(turnsLeft) && + turnStateToDescription(turnsState) + .replaceAll("%c%", turnsLeft.toFixed(0)) + .replaceAll("%m%", turnsTotal.toFixed(0))} +
} + + } placement={"right"}> +
{turnsText}
+
} +
+
+ Lv + {level ?? "??"} +
+
{name ?? altName ?? "???"}
-
{name ?? altName ?? "???"}
-
- {isDefined(hpText) && -
- - - - {isDefined(hp) && {hpText}} -
} - {isDefined(mpText) && -
- - - - {mpText} -
} - {isDefined(ipText) && -
- - + {isDefined(hpText) && +
+ + - {ipText} -
-} - {isDefined(spText) && - -
- {spType} Points - {sp}{typeof spBank === "number" ? ` / ${spBank} banked`: null}
- {isDefined(spType) &&
- {spTypeToDescription(spType)} -
} - - } placement={"right"}> - - - {spText} - -
} - {isDefined(bpText) && - -
- Blood Points - {bp}/{maxBp}
- {isDefined(spType) &&
- The current number of blood points, used for Vampire abilities. -
} - - } placement={"right"}> - - - {bpText} - -
} - {isDefined(effectiveStatuses) && effectiveStatuses.length > 0 && -
- {effectiveStatuses.map(({id, name, count, description, iconUrl}) => - -
- {name} - {isDefined(count) && count > 0 && {count}}
- {isDefined(description) &&
- {count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description} -
} - - } placement={"bottom"}> -
{count > 0 && {count}}
+ {isDefined(hp) && {hpText}} +
} + {isDefined(mpText) && +
+ + - )} -
} -
+ {mpText} +
} + {isDefined(ipText) && +
+ + + + {ipText} +
+ } + {isDefined(spText) && + +
+ {spType} Points + {sp}{typeof spBank === "number" ? ` / ${spBank} banked`: null}
+ {isDefined(spType) &&
+ {spTypeToDescription(spType)} +
} + + } placement={"right"}> + + + {spText} + +
} + {isDefined(bpText) && + +
+ Blood Points + {bp}/{maxBp}
+ {isDefined(spType) &&
+ The current number of blood points, used for Vampire abilities. +
} + + } placement={"right"}> + + + {bpText} + +
} + {isDefined(effectiveStatuses) && effectiveStatuses.length > 0 && +
+ {effectiveStatuses.map(({id, name, count, description, iconUrl}) => + +
+ {name} + {isDefined(count) && count > 0 && {count}}
+ {isDefined(description) &&
+ {count > 0 ? description.replaceAll("%c%", count.toFixed(0)) : description} +
} + + } placement={"bottom"}> +
{count > 0 && {count}}
+
+ )} +
} +
+ {minions.length > 0 && showMinions && minions.map((minion) => + + )} + }