diff --git a/.idea/libraries/Generated_files.xml b/.idea/libraries/Generated_files.xml new file mode 100644 index 0000000..4ca3a34 --- /dev/null +++ b/.idea/libraries/Generated_files.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..1decc5c --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/public/api/current.json b/public/api/current.json index 16aa752..b1e25ad 100644 --- a/public/api/current.json +++ b/public/api/current.json @@ -30,7 +30,8 @@ "portraitUrl": "/portraits/aelica.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "flow", @@ -38,7 +39,8 @@ "leader": "aelica", "name": "Flow", "portraitUrl": "/portraits/flow.png", - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "athetyz", @@ -59,7 +61,8 @@ "portraitUrl": "/portraits/athetyz.png", "turnsTotal": 1, "turnsLeft": 0, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "galvelle", @@ -67,7 +70,8 @@ "leader": "athetyz", "name": "Galvelle", "portraitUrl": "/portraits/galvelle.png", - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "echo", @@ -88,7 +92,8 @@ "portraitUrl": "/portraits/echo.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "gale", @@ -103,7 +108,8 @@ "portraitUrl": "/portraits/gale.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "gravitas", @@ -124,7 +130,8 @@ "portraitUrl": "/portraits/gravitas.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "calor", @@ -132,7 +139,8 @@ "leader": "gravitas", "name": "Calor", "portraitUrl": "/portraits/calor.png", - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "linnet", @@ -153,7 +161,8 @@ "portraitUrl": "/portraits/linnet.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "terra", @@ -161,12 +170,13 @@ "leader": "linnet", "name": "Terra", "portraitUrl": "/portraits/terra.png", - "statuses": [] + "statuses": [], + "privacy": "friend" }, { - "id": "prandia", + "id": "priva", "side": "ally", - "name": "Prandia", + "name": "Privá", "level": 32, "hp": 37, "maxHp": 90, @@ -180,50 +190,53 @@ "sp": 3, "spBank": 4, "spType": "Fabula", - "portraitUrl": "/portraits/prandia.png", + "portraitUrl": "/portraits/priva.png", "turnsTotal": 1, "turnsLeft": 1, - "statuses": [] + "statuses": [], + "privacy": "friend" }, { "id": "selia", "side": "ally", - "leader": "prandia", + "leader": "priva", "name": "Sélia", "portraitUrl": "/portraits/selia.png", - "statuses": [] + "statuses": [], + "privacy": "friend" }, { - "id": "werespider", - "side": "enemy", - "name": "Werespider", - "health": "KO", - "spType": "Ultima", - "sp": 5, - "turnsLeft": 3, - "turnsTotal": 3 + "id": "chelle", + "side": "ally", + "name": "Seychelle", + "level": 32, + "hp": 37, + "maxHp": 90, + "mp": 0, + "maxMp": 70, + "ip": 8, + "maxIp": 8, + "sp": 3, + "spBank": 4, + "spType": "Fabula", + "portraitUrl": "/portraits/seychelle.png", + "turnsTotal": 1, + "turnsLeft": 1, + "statuses": [], + "privacy": "friend" }, { - "id": "kaumari", + "id": "jalen", "side": "enemy", - "name": "Kaumari", - "level": 600, + "name": "Jalen", "health": "Full", "spType": "Ultima", - "sp": 15, - "turnsLeft": 6, - "turnsTotal": 6, - "portraitUrl": "/portraits/kaumari.png" - }, - { - "id": "amita", - "side": "enemy", - "name": "Amita", + "portraitUrl": "/portraits/jalen.png", "level": 50, - "health": "Full", - "turnsLeft": 6, - "turnsTotal": 6, - "portraitUrl": "/portraits/amita.png" + "sp": 5, + "turnsLeft": 7, + "turnsTotal": 7, + "privacy": "unscanned enemy" } ], "statuses": [{ diff --git a/public/backgrounds/desert-ground-storm.jpg b/public/backgrounds/desert-ground-storm.jpg new file mode 100644 index 0000000..4dd119d Binary files /dev/null and b/public/backgrounds/desert-ground-storm.jpg differ diff --git a/public/backgrounds/desert-sky-storm.jpg b/public/backgrounds/desert-sky-storm.jpg new file mode 100644 index 0000000..274e32e Binary files /dev/null and b/public/backgrounds/desert-sky-storm.jpg differ diff --git a/public/backgrounds/empty.jpg b/public/backgrounds/empty.jpg new file mode 100644 index 0000000..0eea0bd Binary files /dev/null and b/public/backgrounds/empty.jpg differ diff --git a/public/backgrounds/sewer.jpg b/public/backgrounds/sewer.jpg new file mode 100644 index 0000000..c9235fb Binary files /dev/null and b/public/backgrounds/sewer.jpg differ diff --git a/public/icons/accelerate.webp b/public/icons/accelerate.webp new file mode 100644 index 0000000..a36304d Binary files /dev/null and b/public/icons/accelerate.webp differ diff --git a/public/icons/captive.webp b/public/icons/captive.webp new file mode 100644 index 0000000..fe16f7c Binary files /dev/null and b/public/icons/captive.webp differ diff --git a/public/icons/dazed.webp b/public/icons/dazed.webp new file mode 100644 index 0000000..bc4b68c Binary files /dev/null and b/public/icons/dazed.webp differ diff --git a/public/icons/default.webp b/public/icons/default.webp new file mode 100644 index 0000000..691baa4 Binary files /dev/null and b/public/icons/default.webp differ diff --git a/public/icons/devoured.webp b/public/icons/devoured.webp new file mode 100644 index 0000000..beee424 Binary files /dev/null and b/public/icons/devoured.webp differ diff --git a/public/icons/enraged.webp b/public/icons/enraged.webp new file mode 100644 index 0000000..5903aee Binary files /dev/null and b/public/icons/enraged.webp differ diff --git a/public/icons/poisoned.webp b/public/icons/poisoned.webp new file mode 100644 index 0000000..2a03015 Binary files /dev/null and b/public/icons/poisoned.webp differ diff --git a/public/icons/revived.webp b/public/icons/revived.webp new file mode 100644 index 0000000..3062e25 Binary files /dev/null and b/public/icons/revived.webp differ diff --git a/public/icons/shaken.webp b/public/icons/shaken.webp new file mode 100644 index 0000000..cdce393 Binary files /dev/null and b/public/icons/shaken.webp differ diff --git a/public/icons/slow.webp b/public/icons/slow.webp new file mode 100644 index 0000000..de9417c Binary files /dev/null and b/public/icons/slow.webp differ diff --git a/public/icons/stop.webp b/public/icons/stop.webp new file mode 100644 index 0000000..16379e7 Binary files /dev/null and b/public/icons/stop.webp differ diff --git a/public/icons/super.webp b/public/icons/super.webp new file mode 100644 index 0000000..696472f Binary files /dev/null and b/public/icons/super.webp differ diff --git a/public/icons/weak.webp b/public/icons/weak.webp new file mode 100644 index 0000000..15fca7e Binary files /dev/null and b/public/icons/weak.webp differ diff --git a/public/portraits/aelica.png b/public/portraits/aelica.png new file mode 100644 index 0000000..350f0cd Binary files /dev/null and b/public/portraits/aelica.png differ diff --git a/public/portraits/airship.png b/public/portraits/airship.png new file mode 100644 index 0000000..dc08f88 Binary files /dev/null and b/public/portraits/airship.png differ diff --git a/public/portraits/amita.png b/public/portraits/amita.png new file mode 100644 index 0000000..0dfa5a5 Binary files /dev/null and b/public/portraits/amita.png differ diff --git a/public/portraits/athetyz-alt.png b/public/portraits/athetyz-alt.png new file mode 100644 index 0000000..0cabfc6 Binary files /dev/null and b/public/portraits/athetyz-alt.png differ diff --git a/public/portraits/athetyz-alt2.png b/public/portraits/athetyz-alt2.png new file mode 100644 index 0000000..af4a6eb Binary files /dev/null and b/public/portraits/athetyz-alt2.png differ diff --git a/public/portraits/athetyz.png b/public/portraits/athetyz.png new file mode 100644 index 0000000..b1563b7 Binary files /dev/null and b/public/portraits/athetyz.png differ diff --git a/public/portraits/blue-dragon.png b/public/portraits/blue-dragon.png new file mode 100644 index 0000000..1baac73 Binary files /dev/null and b/public/portraits/blue-dragon.png differ diff --git a/public/portraits/calor.png b/public/portraits/calor.png new file mode 100644 index 0000000..02a6f64 Binary files /dev/null and b/public/portraits/calor.png differ diff --git a/public/portraits/echo.png b/public/portraits/echo.png new file mode 100644 index 0000000..f508585 Binary files /dev/null and b/public/portraits/echo.png differ diff --git a/public/portraits/flow.png b/public/portraits/flow.png new file mode 100644 index 0000000..780dc34 Binary files /dev/null and b/public/portraits/flow.png differ diff --git a/public/portraits/gale.png b/public/portraits/gale.png new file mode 100644 index 0000000..9ea2310 Binary files /dev/null and b/public/portraits/gale.png differ diff --git a/public/portraits/galvelle.png b/public/portraits/galvelle.png new file mode 100644 index 0000000..82a0a1a Binary files /dev/null and b/public/portraits/galvelle.png differ diff --git a/public/portraits/gravitas.png b/public/portraits/gravitas.png new file mode 100644 index 0000000..73efec0 Binary files /dev/null and b/public/portraits/gravitas.png differ diff --git a/public/portraits/jalen.png b/public/portraits/jalen.png new file mode 100644 index 0000000..5e22ec5 Binary files /dev/null and b/public/portraits/jalen.png differ diff --git a/public/portraits/kaumari.png b/public/portraits/kaumari.png new file mode 100644 index 0000000..3c4a244 Binary files /dev/null and b/public/portraits/kaumari.png differ diff --git a/public/portraits/linnet.png b/public/portraits/linnet.png new file mode 100644 index 0000000..58e5a25 Binary files /dev/null and b/public/portraits/linnet.png differ diff --git a/public/portraits/portraits.zip b/public/portraits/portraits.zip new file mode 100644 index 0000000..9039877 Binary files /dev/null and b/public/portraits/portraits.zip differ diff --git a/public/portraits/prandia.png b/public/portraits/prandia.png new file mode 100644 index 0000000..9be8314 Binary files /dev/null and b/public/portraits/prandia.png differ diff --git a/public/portraits/priva.png b/public/portraits/priva.png new file mode 100644 index 0000000..775f150 Binary files /dev/null and b/public/portraits/priva.png differ diff --git a/public/portraits/selia.png b/public/portraits/selia.png new file mode 100644 index 0000000..24ecc8a Binary files /dev/null and b/public/portraits/selia.png differ diff --git a/public/portraits/seychelle.png b/public/portraits/seychelle.png new file mode 100644 index 0000000..227f45a Binary files /dev/null and b/public/portraits/seychelle.png differ diff --git a/public/portraits/soft-kaumari.png b/public/portraits/soft-kaumari.png new file mode 100644 index 0000000..3885bef Binary files /dev/null and b/public/portraits/soft-kaumari.png differ diff --git a/public/portraits/terra.png b/public/portraits/terra.png new file mode 100644 index 0000000..1a02f53 Binary files /dev/null and b/public/portraits/terra.png differ diff --git a/public/portraits/wing.png b/public/portraits/wing.png new file mode 100644 index 0000000..617ffe0 Binary files /dev/null and b/public/portraits/wing.png differ diff --git a/src/model/Character.ts b/src/model/Character.ts index b4483e8..5ee102c 100644 --- a/src/model/Character.ts +++ b/src/model/Character.ts @@ -485,7 +485,7 @@ export const CharacterResources: ResourceManipulator = { export function applyCharacterPrivacy(character: Character): Character|null { const privacySettings = CharacterPrivacySettings[character.privacy ?? CharacterPrivacy.Hidden] - if (!privacySettings.showCharacter) { + if (!privacySettings || !privacySettings.showCharacter) { return null } const out: {-readonly [Field in keyof Character]: Character[Field]} = Object.assign({}, character) @@ -599,4 +599,56 @@ export const CharacterStatuses = { } } } +} + +export interface CharacterWithMinions extends Character { + readonly minions: readonly CharacterWithMinions[] +} + +export function getCharacterWithMinions(id: string, characterList: readonly Character[]): CharacterWithMinions { + const idsToRecurse: string[] = [] + const idsToRevisit: string[] = [] + const rootCharacter = characterList.find((c) => c.id === id) + if (!rootCharacter) { + throw Error(`no character with id ${id} was found`) + } + const characterMap: {[id: string]: Character} = {[id]: rootCharacter} + const withMinionsMap: {[id: string]: CharacterWithMinions} = {} + const minionIdMap: {[id: string]: string[]} = {} + + for (let nextId:string|undefined = id; typeof nextId !== "undefined"; nextId = idsToRecurse.pop()) { + const character = characterMap[nextId] + const minions = characterList.filter((c) => c.leader === nextId) + if (minions.length === 0) { + console.log(`no minions: ${character.name}`) + withMinionsMap[nextId] = { + ...character, + minions: [] + } + continue + } + minionIdMap[nextId] = minions.map((c) => c.id) + for (const minion of minions) { + if (!(minion.id in characterMap) && !idsToRecurse.includes(minion.id)) { + characterMap[minion.id] = minion + idsToRecurse.push(minion.id) + } + } + idsToRevisit.push(nextId) + } + + for (let nextId: string|undefined = idsToRevisit.pop(); typeof nextId !== "undefined"; nextId = idsToRevisit.pop()) { + const minionIds = minionIdMap[nextId] + if (!minionIds.every((c) => c in withMinionsMap)) { + throw Error(`Possible loop detected; can't satisfy dependencies for ${nextId}`) + } + withMinionsMap[nextId] = { + ...characterMap[nextId], + minions: minionIds.map((cid) => withMinionsMap[cid]), + } + } + if (!withMinionsMap[id]) { + throw Error(`Could not construct list of minions for ${id}`) + } + return withMinionsMap[id] } \ No newline at end of file diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 5691271..1199bfb 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,12 +1,12 @@ import React, {useEffect, useMemo, useState} from 'react'; import './App.css'; import {Col, Container, Row, Stack} from "react-bootstrap"; -import {CharacterStatus} from "./CharacterStatus"; +import {CharacterDisplayType, CharacterStatus} from "./CharacterStatus"; import {GameState} from "../model/GameState"; import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import Tooltip from "react-bootstrap/Tooltip"; import {TurnTimer} from "./TurnTimer"; -import {CharacterSide} from "../model/Character"; +import {applyCharacterPrivacy, Character, CharacterSide, getCharacterWithMinions} from "../model/Character"; import {evaluate} from "../grammar/interpreter"; function useJson(url: string): T | null { @@ -63,8 +63,11 @@ function App() { console.log(ex) } }, [origState]) - const startTime = Date.now() - const endTime = Date.now() + 2 * 60 * 1000 + const characters = state?.characters.map((c) => applyCharacterPrivacy(c)) + .filter((c: Character|null): c is Character => c !== null) ?? [] + const statuses = state?.statuses ?? [] + const conflict = state?.conflict ?? null + const session = state?.session ?? null return @@ -77,13 +80,13 @@ function App() {
Fabula Points spent - {state?.session.usedSp.Fabula ?? 0}
+ {session?.usedSp.Fabula ?? 0}
The party earns 1 EXP for each (#-players) Fabula Points spent during the session.
} placement={"right"}> -
{state?.session.usedSp.Fabula ?? 0}
+
{session?.usedSp.Fabula ?? 0}
Points Spent
- - -
+ { && null}

Allies

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

Enemies

- {state && state.characters.filter((character) => !character.leader && character.side === CharacterSide.Enemy).map((character) => +

Enemies

+ {characters.filter((character) => + !characters.some((c) => c.id === character.leader) + && character.side === CharacterSide.Enemy).map((character) => minion.leader === character.id)} - statuses={state.statuses} - activeId={state.conflict?.activeCharacterId ?? ""} />)} + character={getCharacterWithMinions(character.id, characters)} + statuses={statuses} + displayMode={CharacterDisplayType.NORMAL} + activeId={conflict?.activeCharacterId ?? ""} />)}
diff --git a/src/ui/CharacterStatus.css b/src/ui/CharacterStatus.css index de02904..49e20cb 100644 --- a/src/ui/CharacterStatus.css +++ b/src/ui/CharacterStatus.css @@ -8,14 +8,14 @@ .characterStatus.minion { } -.characterStatus.minion.collapsed { +.characterStatus.portraitOnly { font-size: 0.80em; height: 6.25em; width: 6.25em; margin-left: 0; } -.characterStatus.minion.collapsed .characterPortrait { +.characterStatus.portraitOnly .characterPortrait { position: absolute; top: 0; left: 0; @@ -23,24 +23,24 @@ bottom: 0; } -.characterStatus.minion.collapsed .characterKOBar { +.characterStatus.portraitOnly .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 { +.characterStatus.portraitOnly .characterHeader, +.characterStatus.portraitOnly .characterTurns, +.characterStatus.portraitOnly .characterSp, +.characterStatus.portraitOnly .characterBp, +.characterStatus.portraitOnly .characterHp, +.characterStatus.portraitOnly .characterMp, +.characterStatus.portraitOnly .characterIp, +.characterStatus.portraitOnly .characterZeroGauge, +.characterStatus.portraitOnly .characterStatuses { display: none; } -.characterStatus .characterStatus.minion { +.characterStatus .characterStatus.portraitOnly { position: absolute; bottom: 1.5em; left: 5.625em; @@ -51,6 +51,7 @@ left: 10.25em; bottom: 2.75em; z-index: 4; + pointer-events: none; } .characterStatus/*.leader*/ .characterHeader { @@ -88,6 +89,10 @@ font-size: 0.75em; } +.characterHp, .characterMp, .characterIp { + pointer-events: none; +} + .characterHpBar, .characterMpBar, .characterIpBar { box-shadow: 0.1em 0.1em 0.1em rgba(0, 0, 0, 0.5); border: 0.1em solid black; @@ -98,6 +103,7 @@ margin: 0.1em 0.1em 0.4em 0.25em; bottom: 0; box-sizing: content-box; + pointer-events: auto; } .characterHpValue, .characterMpValue, .characterIpValue, .characterHealthText { diff --git a/src/ui/CharacterStatus.tsx b/src/ui/CharacterStatus.tsx index dd4d530..4d3a6a5 100644 --- a/src/ui/CharacterStatus.tsx +++ b/src/ui/CharacterStatus.tsx @@ -12,7 +12,7 @@ import {altTo as to} from "./FixedInterpolation"; import { Character, CharacterHealth, - CharacterTurnState, + CharacterTurnState, CharacterWithMinions, healthToBounds, healthToFraction, hpToHealth, spTypeToDescription, turnStateToDescription, @@ -21,6 +21,13 @@ import { import {StatusEffect} from "../model/GameState"; import React from "react"; +export enum CharacterDisplayType { + NORMAL = "normal", + MINION = "minion", + PORTRAIT_ONLY = "portraitOnly", + BOSS = "boss", +} + export function healthToColor(health: CharacterHealth | undefined): string { switch (health) { case CharacterHealth.Full: @@ -76,14 +83,14 @@ const ipBarStyle: SpringyValueInterpolatables = { }, } -export function CharacterStatus({character, minions, statuses, activeId, collapsed, click}: {character: Character, minions: readonly Character[], statuses: readonly StatusEffect[], activeId: string, collapsed?: boolean, click?: () => void}): ReactElement { +export function CharacterStatus({character, statuses, activeId, displayMode, click}: {character: CharacterWithMinions, statuses: readonly StatusEffect[], activeId: string, displayMode: CharacterDisplayType, 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 {id, name, altName, leader, level, health, koText, minions} = character const {hp, maxHp} = character const effectiveMaxHp = maxHp ?? 100 @@ -316,9 +323,9 @@ export function CharacterStatus({character, minions, statuses, activeId, collaps return -
0 && !showMinions ? " leader" : leader ? " minion" : "") + (collapsed ? " collapsed" : "")} onClick={() => click ? click() : null}> - {minions.length > 0 && !showMinions && } - +
+ {minions.length > 0 && !showMinions && } + {isDefined(maxZp) && isDefined(zp) &&
@@ -335,7 +342,7 @@ export function CharacterStatus({character, minions, statuses, activeId, collaps
= maxZp ? " active" : "")} />
} - {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && {koText ?? "KO"}} + {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && {koText ?? "KO"}} {isDefined(turnsState) && @@ -434,8 +441,8 @@ export function CharacterStatus({character, minions, statuses, activeId, collaps
}
{minions.length > 0 && showMinions && minions.map((minion) => - + )} }