daughter friendship

main
Mari 8 months ago
parent 84f70f86d8
commit 8ae259951b
  1. 15
      public/api/current.json
  2. 20
      src/grammar/grammar.ohm
  3. 63
      src/grammar/interpreter.ts
  4. 9
      src/model/Character.ts
  5. 25
      src/ui/App.tsx
  6. 77
      src/ui/CharacterStatus.css
  7. 236
      src/ui/CharacterStatus.tsx

@ -35,7 +35,7 @@
{ {
"id": "flow", "id": "flow",
"side": "ally", "side": "ally",
"minion": true, "leader": "aelica",
"name": "Flow", "name": "Flow",
"portraitUrl": "/portraits/flow.png", "portraitUrl": "/portraits/flow.png",
"statuses": [] "statuses": []
@ -64,8 +64,9 @@
{ {
"id": "galvelle", "id": "galvelle",
"side": "ally", "side": "ally",
"minion": true, "leader": "athetyz",
"name": "Galvelle", "name": "Galvelle",
"portraitUrl": "/portraits/galvelle.png",
"statuses": [] "statuses": []
}, },
{ {
@ -92,10 +93,10 @@
{ {
"id": "gale", "id": "gale",
"side": "ally", "side": "ally",
"minion": true, "leader": "echo",
"name": "Gale", "name": "Gale",
"level": 5, "level": 5,
"hp": 70, "hp": 0,
"maxHp": 70, "maxHp": 70,
"mp": 58, "mp": 58,
"maxMp": 58, "maxMp": 58,
@ -128,7 +129,7 @@
{ {
"id": "calor", "id": "calor",
"side": "ally", "side": "ally",
"minion": true, "leader": "gravitas",
"name": "Calor", "name": "Calor",
"portraitUrl": "/portraits/calor.png", "portraitUrl": "/portraits/calor.png",
"statuses": [] "statuses": []
@ -157,7 +158,7 @@
{ {
"id": "terra", "id": "terra",
"side": "ally", "side": "ally",
"minion": true, "leader": "linnet",
"name": "Terra", "name": "Terra",
"portraitUrl": "/portraits/terra.png", "portraitUrl": "/portraits/terra.png",
"statuses": [] "statuses": []
@ -187,7 +188,7 @@
{ {
"id": "selia", "id": "selia",
"side": "ally", "side": "ally",
"minion": true, "leader": "prandia",
"name": "Sélia", "name": "Sélia",
"portraitUrl": "/portraits/selia.png", "portraitUrl": "/portraits/selia.png",
"statuses": [] "statuses": []

@ -191,21 +191,25 @@ FabulaDSL {
StatusOrItemCounter = StatusOrItemCounterWrapped | StatusOrItemCounterUnwrapped StatusOrItemCounter = StatusOrItemCounterWrapped | StatusOrItemCounterUnwrapped
StatusOrItemDeltaWrapped = "(" Delta ")" StatusOrItemDeltaWrapped = "(" Delta ")"
StatusOrItemDelta = StatusOrItemDeltaWrapped | Delta StatusOrItemDelta = StatusOrItemDeltaWrapped | Delta
StatusSeparator = colon colon
// StatusOrItemAddOperation: operands, identifier, numberValue, evaluate, renderMarkdown // StatusOrItemAddOperation: operands, identifier, numberValue, evaluate, renderMarkdown
StatusOrItemAddOperation = Operands colon plus identifier StatusOrItemCounter? StatusOrItemAddOperation = Operands StatusSeparator plus identifier StatusOrItemCounter?
// TODO: StatusOrItemRemoveOperation: operands, identifier, evaluate, renderMarkdown // 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 // TODO: StatusOrItemDeltaOperation/Alternate: operands, identifier, numberValue, evaluate, renderMarkdown
StatusOrItemCounterDeltaOperation = Operands colon identifier StatusOrItemDelta StatusOrItemCounterDeltaOperation = Operands #wordSep identifier StatusSeparator StatusOrItemDelta
StatusOrItemCounterDeltaOperationAlternate = Operands colon StatusOrItemDelta identifier StatusOrItemCounterDeltaOperationAlternate = Operands StatusSeparator StatusOrItemDelta identifier
StatusOrItemCounterDeltaOperationAlternate2 = Operands StatusSeparator identifier StatusOrItemDelta
// TODO: Fix conflict with resource mutators (use brackets?) // TODO: Fix conflict with resource mutators (use brackets?)
// TODO: StatusOrItemSetOperation/Alternate: operands, identifier, numberValue, evaluate, renderMarkdown // TODO: StatusOrItemSetOperation/Alternate: operands, identifier, numberValue, evaluate, renderMarkdown
StatusOrItemCounterSetOperation = Operands colon identifier StatusOrItemCounter? StatusOrItemCounterSetOperation = Operands #wordSep identifier StatusSeparator StatusOrItemCounter?
StatusOrItemCounterSetOperationAlternate = Operands colon StatusOrItemCounter identifier StatusOrItemCounterSetOperationAlternate = Operands StatusSeparator StatusOrItemCounter identifier
StatusOrItemCounterSetOperationAlternate2 = Operands StatusSeparator identifier StatusOrItemCounter?
// TODO: continue implementation list from here // TODO: continue implementation list from here
@ -312,8 +316,8 @@ FabulaDSL {
PrintOperationWithOperands = Operands colon printOperator textToEndOfLine PrintOperationWithOperands = Operands colon printOperator textToEndOfLine
Operation = StatusOrItemAddOperation | StatusOrItemRemoveOperation Operation = StatusOrItemAddOperation | StatusOrItemRemoveOperation
| StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate | StatusOrItemCounterDeltaOperation | StatusOrItemCounterDeltaOperationAlternate | StatusOrItemCounterDeltaOperationAlternate2
| StatusOrItemCounterSetOperation | StatusOrItemCounterSetOperationAlternate | StatusOrItemCounterSetOperation | StatusOrItemCounterSetOperationAlternate | StatusOrItemCounterSetOperationAlternate2
| DeltaOperation | DeltaOperationAlternate | DeltaOperationAlternate2 | DeltaOperation | DeltaOperationAlternate | DeltaOperationAlternate2
| SetMeteredOperation | SetMeteredOperationAlternate | SetMeteredOperationAlternate2 | SetMeteredOperation | SetMeteredOperationAlternate | SetMeteredOperationAlternate2
| SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2 | SetValueOperation | SetValueOperationAlternate | SetValueOperationAlternate2

@ -203,9 +203,6 @@ interpreter.addAttribute<NumberSign>("sign", {
minus(_: TerminalNode): NumberSign.Negative { minus(_: TerminalNode): NumberSign.Negative {
return NumberSign.Negative return NumberSign.Negative
}, },
StatusOrItemCounterDeltaOperation(operands: NonterminalNode, colon: NonterminalNode, identifier: NonterminalNode, delta: NonterminalNode): NumberSign {
return (delta as InterpreterNode).sign
},
_iter(): never { _iter(): never {
throw Error(`No idea what to say ${this.ctorName} iteration node's number sign is`) throw Error(`No idea what to say ${this.ctorName} iteration node's number sign is`)
}, },
@ -282,10 +279,16 @@ interpreter.addAttribute<number>("numberValue", {
} }
return (counter.child(0)).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 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 return (counter as InterpreterNode).numberValue
}, },
_iter(): never { _iter(): never {
@ -324,10 +327,16 @@ interpreter.addAttribute<string>("identifier", {
StatusOrItemRemoveOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, identifier: NonterminalNode): string { StatusOrItemRemoveOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, identifier: NonterminalNode): string {
return (identifier as InterpreterNode).identifier; 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 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 return (identifier as InterpreterNode).identifier
}, },
_iter(): never { _iter(): never {
@ -602,10 +611,16 @@ interpreter.addAttribute<Operands>("operands", {
SetValueOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _value: NonterminalNode): Operands { SetValueOperationAlternate2(operands: NonterminalNode, _colon: NonterminalNode, _resource: NonterminalNode, _value: NonterminalNode): Operands {
return (operands as InterpreterNode).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 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 return (operands as InterpreterNode).operands
}, },
StatusOrItemAddOperation(operands: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _statusOrItem: NonterminalNode, _counter: IterationNode): Operands { StatusOrItemAddOperation(operands: NonterminalNode, _colon: NonterminalNode, _delta: NonterminalNode, _statusOrItem: NonterminalNode, _counter: IterationNode): Operands {
@ -737,10 +752,10 @@ interpreter.addAttribute<number>("currentValue", {
SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number { SetMeteredOperationAlternate2(operands: NonterminalNode, colon: NonterminalNode, resource: NonterminalNode, value: NonterminalNode): number {
return (value as InterpreterNode).currentValue; 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 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 return (counter as InterpreterNode).numberValue
}, },
_iter(): never { _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<EvaluationContext>("evaluate(ctx)", { interpreter.addOperation<EvaluationContext>("evaluate(ctx)", {
CodeSegment(items: IterationNode): EvaluationContext { CodeSegment(items: IterationNode): EvaluationContext {
let ctx = (this as EvaluationNode).args.ctx let ctx = (this as EvaluationNode).args.ctx
@ -1094,6 +1125,16 @@ interpreter.addOperation<EvaluationContext>("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 { ClearOperation(_arg0: NonterminalNode, _arg1: NonterminalNode, _arg2: NonterminalNode, _arg3: NonterminalNode, _arg4: NonterminalNode): EvaluationContext {
return EvaluateClear(this as EvaluationNode); return EvaluateClear(this as EvaluationNode);
}, },

@ -200,7 +200,7 @@ export enum CharacterSide {
export interface Character { export interface Character {
readonly id: string readonly id: string
readonly side?: CharacterSide readonly side?: CharacterSide
readonly minion?: boolean readonly leader?: string
readonly portraitUrl?: string readonly portraitUrl?: string
readonly name?: string readonly name?: string
readonly altName?: string readonly altName?: string
@ -474,6 +474,13 @@ export const CharacterResources: ResourceManipulator<Character, Resource> = {
return result 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 { export function applyCharacterPrivacy(character: Character): Character|null {

@ -41,7 +41,7 @@ function App() {
@: ZP 5/6 @: ZP 5/6
@: ZP +1 @: ZP +1
&: 20 HP &: 20 HP
gale, calor,gravitas HP: 3 calor,gravitas HP: 3
&: ZP 5 &: ZP 5
&: ZP /7 &: ZP /7
&: /99 IP &: /99 IP
@ -50,12 +50,13 @@ function App() {
athetyz: - MP athetyz: - MP
linnet: HP - linnet: HP -
&: Lv - &: Lv -
gale, echo, aelica: +Digesting gale, echo, aelica:: +Digesting
gale: -Digesting flow:: +Digesting 2
flow: +Digesting 2 echo, flow:: +Digesting 3
echo, flow: +Digesting 3 gale:: -Digesting
calor, flow, echo: Digesting +1 calor Digesting:: +1
gale, echo: +1 Digesting flow, gale:: Digesting +1
echo, flow, gale:: -1 Digesting
End`) End`)
} }
} catch (ex) { } catch (ex) {
@ -107,23 +108,25 @@ function App() {
<Col xs={{span: true}} xxl={{span: true, order: "first"}}> <Col xs={{span: true}} xxl={{span: true, order: "first"}}>
<Stack direction="vertical" className={"align-items-center"}> <Stack direction="vertical" className={"align-items-center"}>
<h1 className={"ally-head " + (state?.conflict?.activeSide === CharacterSide.Ally ? "active" : state?.conflict?.activeSide === CharacterSide.Enemy ? "inactive" : "")}>Allies</h1> <h1 className={"ally-head " + (state?.conflict?.activeSide === CharacterSide.Ally ? "active" : state?.conflict?.activeSide === CharacterSide.Enemy ? "inactive" : "")}>Allies</h1>
{state && state.characters.filter((character) => character.side === CharacterSide.Ally).map((character) => {state && state.characters.filter((character) => !character.leader && character.side === CharacterSide.Ally).map((character) =>
<CharacterStatus <CharacterStatus
key={character.id} key={character.id}
character={character} character={character}
minions={state.characters.filter((minion) => minion.leader === character.id)}
statuses={state.statuses} statuses={state.statuses}
active={character.id === state.conflict?.activeCharacterId} />)} activeId={state.conflict?.activeCharacterId ?? ""} />)}
</Stack> </Stack>
</Col> </Col>
<Col xs={{span: true}} xxl={{span: true, order: "last"}}> <Col xs={{span: true}} xxl={{span: true, order: "last"}}>
<Stack direction="vertical" className={"align-items-center"}> <Stack direction="vertical" className={"align-items-center"}>
<h1 className={"enemy-head " + (state?.conflict?.activeSide === CharacterSide.Enemy ? "active" : state?.conflict?.activeSide === CharacterSide.Ally ? "inactive" : "")}>Enemies</h1> <h1 className={"enemy-head " + (state?.conflict?.activeSide === CharacterSide.Enemy ? "active" : state?.conflict?.activeSide === CharacterSide.Ally ? "inactive" : "")}>Enemies</h1>
{state && state.characters.filter((character) => character.side === CharacterSide.Enemy).map((character) => {state && state.characters.filter((character) => !character.leader && character.side === CharacterSide.Enemy).map((character) =>
<CharacterStatus <CharacterStatus
key={character.id} key={character.id}
character={character} character={character}
minions={state.characters.filter((minion) => minion.leader === character.id)}
statuses={state.statuses} statuses={state.statuses}
active={character.id === state.conflict?.activeCharacterId} />)} activeId={state.conflict?.activeCharacterId ?? ""} />)}
</Stack> </Stack>
</Col> </Col>
</Row> </Row>

@ -1,13 +1,49 @@
.characterStatus { .characterStatus {
height: 7.5em; height: 7.5em;
width: 27.25em; width: 30.25em;
position: relative; position: relative;
box-sizing: content-box; box-sizing: content-box;
} }
.characterStatus.minion { .characterStatus.minion {
margin-left: 6.8125em; }
.characterStatus.minion.collapsed {
font-size: 0.80em; 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 { .characterHeader {
@ -17,6 +53,10 @@
z-index: 4; z-index: 4;
} }
.characterStatus/*.leader*/ .characterHeader {
left: 13.25em;
}
.characterLevel { .characterLevel {
display: inline; display: inline;
color: white; color: white;
@ -44,6 +84,10 @@
user-select: none; user-select: none;
} }
.characterName .minion {
font-size: 0.75em;
}
.characterHpBar, .characterMpBar, .characterIpBar { .characterHpBar, .characterMpBar, .characterIpBar {
box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5); box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5);
border: 0.1em solid black; border: 0.1em solid black;
@ -79,6 +123,10 @@
box-sizing: content-box; box-sizing: content-box;
} }
.characterStatus/*.leader*/ .characterHp {
left: 10.85em;
}
.characterHpBar { .characterHpBar {
height: 1.25em; height: 1.25em;
} }
@ -111,6 +159,10 @@
z-index: 3; z-index: 3;
} }
.characterStatus/*.leader*/ .characterMp {
left: 10em;
}
.characterIp { .characterIp {
width: 7.5em; width: 7.5em;
right: 1em; right: 1em;
@ -141,23 +193,28 @@
z-index: 0; z-index: 0;
} }
.characterStatus/*.leader*/ .characterPortrait {
left: 7.5em;
}
.characterZeroGauge { .characterZeroGauge {
position: absolute; position: absolute;
top: 0.25em; top: 0.25em;
bottom: 0.5em; bottom: 0.5em;
left: 2.5em; left: 2.5em;
width: 3.75em; width: 2.5em;
} }
.characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{ .characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: -1.25em;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: left bottom; background-position: left bottom;
background-size: auto 6.75em; background-size: auto 6.75em;
z-index: 0; z-index: 0;
pointer-events: none;
} }
.characterZeroBarBack { .characterZeroBarBack {
@ -197,8 +254,8 @@
.characterKOBar { .characterKOBar {
position: absolute; position: absolute;
top: 1.592em; height: 1.5em;
bottom: 2.518em; bottom: 1.962em;
left: 3.333em; left: 3.333em;
width: 4.629em; width: 4.629em;
background-color: black; background-color: black;
@ -213,6 +270,10 @@
border-radius: 0.555em 0; border-radius: 0.555em 0;
} }
.characterStatus/*.leader*/ .characterKOBar {
left: 5.555em;
}
.characterTurns { .characterTurns {
position: absolute; position: absolute;
top: 0.125em; top: 0.125em;
@ -311,6 +372,10 @@
overflow-x: auto; overflow-x: auto;
} }
.characterStatus/*.leader*/ .characterStatuses {
left: 14.25em;
}
.characterStatusIcon { .characterStatusIcon {
position: relative; position: relative;
flex: 0 0 1.8em; flex: 0 0 1.8em;

@ -1,5 +1,5 @@
import {animated, useSpring} from "@react-spring/web"; 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 {evaluateResourceBarStyles, ResourceBarColors, ResourceBarStyles} from "./ResourceBarGradient";
import {isDefined} from "../types/type_check"; import {isDefined} from "../types/type_check";
import {SpringyValueInterpolatables, useSpringyValue} from "./SpringyValueHook"; import {SpringyValueInterpolatables, useSpringyValue} from "./SpringyValueHook";
@ -19,6 +19,7 @@ import {
turnStateToTitle turnStateToTitle
} from "../model/Character"; } from "../model/Character";
import {StatusEffect} from "../model/GameState"; import {StatusEffect} from "../model/GameState";
import React from "react";
export function healthToColor(health: CharacterHealth | undefined): string { export function healthToColor(health: CharacterHealth | undefined): string {
switch (health) { switch (health) {
@ -75,8 +76,14 @@ const ipBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = {
}, },
} }
export function CharacterStatus({character, statuses, active}: {character: Character, statuses: readonly StatusEffect[], active: boolean}): ReactElement { 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 {name, altName, minion, level, health, koText} = character 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 {hp, maxHp} = character
const effectiveMaxHp = maxHp ?? 100 const effectiveMaxHp = maxHp ?? 100
@ -199,7 +206,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
return { return {
turnsState: CharacterTurnState.None turnsState: CharacterTurnState.None
} }
} else if (active) { } else if (activeId === id) {
return { return {
turnsState: CharacterTurnState.Active, turnsState: CharacterTurnState.Active,
turnsText: "🞂", turnsText: "🞂",
@ -228,7 +235,7 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
turnsState: CharacterTurnState.Ready, turnsState: CharacterTurnState.Ready,
} }
} }
}, [active, effectiveHp, canAct, turnsLeft, turnsTotal]) }, [activeId, id, effectiveHp, canAct, turnsLeft, turnsTotal])
const {portraitUrl} = character const {portraitUrl} = character
const effectivePortraitUrl = portraitUrl ?? DefaultPortrait const effectivePortraitUrl = portraitUrl ?? DefaultPortrait
@ -308,120 +315,127 @@ export function CharacterStatus({character, statuses, active}: {character: Chara
</Tooltip> </Tooltip>
return <div className={"characterStatus" + (minion ? " minion" : "")}> return <React.Fragment>
<animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} /> <div className={"characterStatus" + (minions.length > 0 && !showMinions ? " leader" : leader ? " minion" : "") + (collapsed ? " collapsed" : "")} onClick={() => click ? click() : null}>
{isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ {minions.length > 0 && !showMinions && <CharacterStatus character={minions[0]} minions={[]} statuses={statuses} activeId={activeId} collapsed={true} click={toggleMinions} />}
<Tooltip> <animated.div className={"characterPortrait"} style={characterPortraitStyleInterpolated} />
<div className={"characterHelpHeader"}> {isDefined(maxZp) && isDefined(zp) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={
<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={
<Tooltip> <Tooltip>
<div className={"characterHelpHeader"}> <div className={"characterHelpHeader"}>
<span className={"characterHelpName"}>{turnStateToTitle(turnsState)}</span> <span className={"characterHelpName"}>Zero Charge</span>
{isDefined(turnsTotal) && isDefined(turnsLeft) && turnsTotal > 1 && <span className={"characterHelpValue"}>{turnsLeft}/{turnsTotal}</span>}</div> <span className={"characterHelpValue"}>{(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}</span></div>
{<div className={"characterHelpDescription"}> {<div className={"characterHelpDescription"}>
{isDefined(turnsTotal) && isDefined(turnsLeft) && The amount of energy stored up towards unleashing the might of a Zero Power. When the gauge is full, let loose!
turnStateToDescription(turnsState)
.replaceAll("%c%", turnsLeft.toFixed(0))
.replaceAll("%m%", turnsTotal.toFixed(0))}
</div>} </div>}
</Tooltip> </Tooltip>
} placement={"right"}> } 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>} </OverlayTrigger>}
<div className={"characterHeader"}> {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && <animated.div className={"characterKOBar"} style={characterKOBarStyleInterpolated}>{koText ?? "KO"}</animated.div>}
<div className="characterLevel"> {isDefined(turnsState) &&
<span className="characterLevelLabel">Lv</span> <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={
<span className="characterLevelValue">{level ?? "??"}</span> <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>
<div className={"characterName"}>{name ?? altName ?? "???"}</div> {isDefined(hpText) &&
</div> <div className={"characterHp"}>
{isDefined(hpText) && <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={hpTooltip} placement={"top"}>
<div className={"characterHp"}> <animated.div className={"characterHpBar"} style={hpBarStyleInterpolated} />
<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} />
</OverlayTrigger> </OverlayTrigger>
<animated.div className={"characterIpValue"}>{ipText}</animated.div> {isDefined(hp) && <animated.div
</div> className={"characterHpValue"}
} style={hpTextStyleInterpolated}>{hpText}</animated.div>}
{isDefined(spText) && </div>}
<OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={ {isDefined(mpText) &&
<Tooltip> <div className={"characterMp"}>
<div className={"characterHelpHeader"}> <OverlayTrigger delay={{show: 750, hide: 0}} trigger={["hover", "click", "focus"]} overlay={mpTooltip} placement={"top"}>
<span className={"characterHelpName"}>{spType} Points</span> <animated.div className={"characterMpBar"} style={mpBarStyleInterpolated} />
<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> </OverlayTrigger>
)} <animated.div className={"characterMpValue"}>{mpText}</animated.div>
</div>} </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>
} }

Loading…
Cancel
Save