diff --git a/src/characters/character-block.ts b/src/characters/character-block.ts new file mode 100644 index 00000000..3e2f2dfc --- /dev/null +++ b/src/characters/character-block.ts @@ -0,0 +1,215 @@ +import { html, render } from "lit-html"; +import { map } from "lit-html/directives/map.js"; +// import { range } from "lit-html/directives/range.js"; + +import IronVaultPlugin from "index"; +import { EventRef, TFile } from "obsidian"; +// import { vaultProcess } from "utils/obsidian"; +import { CharacterContext } from "../character-tracker"; +import { md } from "utils/ui/directives"; + +export default function registerCharacterBlock(plugin: IronVaultPlugin): void { + plugin.registerMarkdownCodeBlockProcessor( + "iron-vault-character", + async (source: string, el: CharacterContainerEl, ctx) => { + // We can't render blocks until datastore is ready. + await plugin.datastore.waitForReady; + if (!el.characterRenderer) { + el.characterRenderer = new CharacterRenderer(el, source, plugin); + } + const file = plugin.app.vault.getFileByPath(ctx.sourcePath); + await el.characterRenderer.render(file); + }, + ); +} + +interface CharacterContainerEl extends HTMLElement { + characterRenderer?: CharacterRenderer; +} + +class CharacterRenderer { + contentEl: HTMLElement; + source: string; + plugin: IronVaultPlugin; + fileWatcher?: EventRef; + + constructor(contentEl: HTMLElement, source: string, plugin: IronVaultPlugin) { + this.contentEl = contentEl; + this.source = source; + this.plugin = plugin; + } + + async render(file: TFile | undefined | null) { + if (!file) { + render( + html`
Error rendering character: no file found.
`, + this.contentEl, + ); + return; + } + const character = this.plugin.characters.get(file.path); + if (this.fileWatcher) { + this.plugin.app.metadataCache.offref(this.fileWatcher); + } + this.fileWatcher = this.plugin.app.metadataCache.on( + "changed", + (moddedFile) => { + if (moddedFile.path === file.path) { + this.render(moddedFile); + } + }, + ); + this.plugin.registerEvent(this.fileWatcher); + if (!character || character.isLeft()) { + render( + // TODO: we should preserve the error? + //html`
Error rendering clock: ${res.error.message}
`, + html`
+Error rendering character: character file is invalid${character
+            ? ": " + character.error.message
+            : ""}
`, + this.contentEl, + ); + return; + } else if (character.isRight()) { + await this.renderCharacter(character.value, file); + } + } + + async renderCharacter(charCtx: CharacterContext, file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + const tpl = html` +
+

${md(this.plugin, lens.name.get(raw), file.path)}

+
Stats
+ ${this.renderStats(charCtx, file)} +
Meters
+ ${this.renderMeters(charCtx, file)} +
Impacts
+ ${this.renderImpacts(charCtx, file)} +
Special Tracks
+ ${this.renderSpecialTracks(charCtx, file)} +
Assets
+ ${this.renderAssets(charCtx, file)} +
+ `; + render(tpl, this.contentEl); + } + + renderStats(charCtx: CharacterContext, _file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + return html` + + `; + } + + renderMeters(charCtx: CharacterContext, _file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + return html` + + `; + } + + renderImpacts(charCtx: CharacterContext, _file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + return html` + + `; + } + + renderSpecialTracks(charCtx: CharacterContext, _file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + return html` + + `; + } + + renderAssets(charCtx: CharacterContext, _file: TFile) { + const lens = charCtx.lens; + const raw = charCtx.character; + return html` + + `; + } + + // async updateClockProgress( + // file: TFile, + // { steps, progress }: { steps?: number; progress?: number }, + // ) { + // const newProg = await clockUpdater( + // vaultProcess(this.plugin.app, file.path), + // (clockFile) => + // clockFile.updatingClock((clock) => + // progress != null ? clock.withProgress(progress) : clock.tick(steps!), + // ), + // ); + + // await this.renderCharacter(newProg, file); + // } +} diff --git a/src/characters/css/assets.css b/src/characters/css/assets.css new file mode 100644 index 00000000..84b43ae9 --- /dev/null +++ b/src/characters/css/assets.css @@ -0,0 +1,2 @@ +.iron-vault-character .assets { +} diff --git a/src/characters/css/characters.css b/src/characters/css/characters.css new file mode 100644 index 00000000..eb42f79d --- /dev/null +++ b/src/characters/css/characters.css @@ -0,0 +1,9 @@ +@import url("assets.css"); +@import url("impacts.css"); +@import url("meters.css"); +@import url("special_tracks.css"); +@import url("assets.css"); + +.iron-vault-character { + +} diff --git a/src/characters/css/impacts.css b/src/characters/css/impacts.css new file mode 100644 index 00000000..9d7742ed --- /dev/null +++ b/src/characters/css/impacts.css @@ -0,0 +1,2 @@ +.iron-vault-character .impacts { +} diff --git a/src/characters/css/meters.css b/src/characters/css/meters.css new file mode 100644 index 00000000..76e4dc21 --- /dev/null +++ b/src/characters/css/meters.css @@ -0,0 +1,2 @@ +.iron-vault-character .meters { +} diff --git a/src/characters/css/special_tracks.css b/src/characters/css/special_tracks.css new file mode 100644 index 00000000..28d56eed --- /dev/null +++ b/src/characters/css/special_tracks.css @@ -0,0 +1,2 @@ +.iron-vault-character .special-tracks { +} diff --git a/src/characters/css/stats.css b/src/characters/css/stats.css new file mode 100644 index 00000000..9f422fc5 --- /dev/null +++ b/src/characters/css/stats.css @@ -0,0 +1,2 @@ +.iron-vault-character .stats { +} diff --git a/src/characters/lens.test.ts b/src/characters/lens.test.ts index 8aad0346..225ebbcc 100644 --- a/src/characters/lens.test.ts +++ b/src/characters/lens.test.ts @@ -7,7 +7,6 @@ import { Lens, updating } from "../utils/lens"; import { BaseIronVaultSchema, IronVaultSheetAssetInput, - ImpactStatus, characterLens, momentumOps, movesReader, @@ -171,40 +170,40 @@ describe("characterLens", () => { }); actsLikeLens(lens.impacts, character, { - wounded: ImpactStatus.Marked, - disappointed: ImpactStatus.Unmarked, + wounded: true, + disappointed: false, }); + // TODO(@zkat) it("treats any string other than ⬢ as unmarked", () => { expect(lens.impacts.get(character)).toEqual({ - wounded: ImpactStatus.Unmarked, - disappointed: ImpactStatus.Unmarked, + wounded: false, + disappointed: false, }); }); it("treats a missing key as unmarked", () => { const character = validater({ ...VALID_INPUT }); expect(lens.impacts.get(character)).toEqual({ - wounded: ImpactStatus.Unmarked, - disappointed: ImpactStatus.Unmarked, + wounded: false, + disappointed: false, }); }); + // TODO(@zkat) it("treats ⬢ as marked", () => { expect( - lens.impacts.get( - lens.impacts.update(character, { wounded: ImpactStatus.Marked }), - ), + lens.impacts.get(lens.impacts.update(character, { wounded: true })), ).toEqual({ - wounded: ImpactStatus.Marked, - disappointed: ImpactStatus.Unmarked, + wounded: true, + disappointed: false, }); }); it("rejects an invalid impact type", () => { - expect(() => - lens.impacts.update(character, { foobar: ImpactStatus.Marked }), - ).toThrow("unexpected key in impacts: foobar"); + expect(() => lens.impacts.update(character, { foobar: true })).toThrow( + "unexpected key in impacts: foobar", + ); }); }); }); @@ -248,9 +247,7 @@ describe("momentumOps", () => { const character = validater({ ...VALID_INPUT, momentum: 3, - ...Object.fromEntries( - impactKeys.map((key) => [key, ImpactStatus.Marked]), - ), + ...Object.fromEntries(impactKeys.map((key) => [key, true])), }); it(`caps momentum to ${max}`, () => { expect(lens.momentum.get(take(8)(character))).toBe(max); diff --git a/src/characters/lens.ts b/src/characters/lens.ts index f1345697..1d71c520 100644 --- a/src/characters/lens.ts +++ b/src/characters/lens.ts @@ -55,10 +55,10 @@ export const baseIronVaultSchema = z export type BaseIronVaultSchema = z.input; -export enum ImpactStatus { - Unmarked = "⬡", - Marked = "⬢", -} +// export enum ImpactStatus { +// Unmarked = "marked", +// Marked = "un", +// } export interface CharacterLens { name: Lens; @@ -66,7 +66,7 @@ export interface CharacterLens { stats: Record>; condition_meters: Record>; assets: Lens; - impacts: Lens>; + impacts: Lens>; special_tracks: Record>; ruleset: Ruleset; } @@ -156,7 +156,7 @@ export function validated( function createImpactLens( ruleset: Ruleset, -): Lens, Record> { +): Lens, Record> { // function createImpactLens(data: Record, ruleset: Ruleset): Lens, Record> { // const impactProps: Record = {}; // const dataKeys = Object.fromEntries(Object.keys(data).map((key) => [key.toLowerCase(), key])); @@ -169,17 +169,13 @@ function createImpactLens( return { get(source) { return objectMap(impactProps, (dataKey) => { - const val = source[dataKey]; - if (val === "⬢") { - return ImpactStatus.Marked; - } else { - return ImpactStatus.Unmarked; - } + // TODO(@zkat): this needs validation. + return !!source[dataKey]; }); }, update(source, newval) { const original = this.get(source); - const updates: [string, ImpactStatus][] = []; + const updates: [string, boolean][] = []; for (const key in newval) { if (!(key in impactProps)) { throw new Error(`unexpected key in impacts: ${key}`); @@ -283,10 +279,9 @@ export function momentumTrackerReader( return { get: momentumTrackerLens(characterLens).get }; } -export function countMarked(impacts: Record): number { +export function countMarked(impacts: Record): number { return Object.values(impacts).reduce( - (count, impactStatus) => - count + (impactStatus === ImpactStatus.Marked ? 1 : 0), + (count, impactStatus) => count + (impactStatus ? 1 : 0), 0, ); } diff --git a/src/index.ts b/src/index.ts index a15e348a..ce72cd3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { MoveModal } from "moves/move-modal"; import { ViewPlugin, ViewUpdate } from "@codemirror/view"; import registerTrackBlock from "tracks/track-block"; import registerClockBlock from "tracks/clock-block"; +import registerCharacterBlock from "characters/character-block"; export default class IronVaultPlugin extends Plugin { settings!: IronVaultPluginSettings; @@ -222,6 +223,7 @@ export default class IronVaultPlugin extends Plugin { registerMechanicsBlock(this); registerTrackBlock(this); registerClockBlock(this); + registerCharacterBlock(this); } async activateView() { diff --git a/src/styles.css b/src/styles.css index 5e0dfae4..e72197c1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -3,6 +3,7 @@ @import url("oracles/css/oracles.css"); @import url("tracks/css/tracks.css"); @import url("tracks/css/clocks.css"); +@import url("characters/css/characters.css"); .theme-light { --color-code: 0, 0, 0; diff --git a/test-vault/characters/Ash Barlowe.md b/test-vault/characters/Ash Barlowe.md index 65332123..ff845422 100644 --- a/test-vault/characters/Ash Barlowe.md +++ b/test-vault/characters/Ash Barlowe.md @@ -38,5 +38,6 @@ Bonds_Progress: 0 Discoveries_XPEarned: 0 Discoveries_Progress: 0 --- +```iron-vault-character +``` -asdf