Skip to content

Commit

Permalink
mise en place
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed May 25, 2024
1 parent 002a975 commit 6eeba22
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 35 deletions.
215 changes: 215 additions & 0 deletions src/characters/character-block.ts
Original file line number Diff line number Diff line change
@@ -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`<pre>Error rendering character: no file found.</pre>`,
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`<pre>Error rendering clock: ${res.error.message}</pre>`,
html`<pre>
Error rendering character: character file is invalid${character
? ": " + character.error.message
: ""}</pre
>`,
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`
<article class="iron-vault-character">
<h3 class="name">${md(this.plugin, lens.name.get(raw), file.path)}</h3>
<h5>Stats</h5>
${this.renderStats(charCtx, file)}
<h5>Meters</h5>
${this.renderMeters(charCtx, file)}
<h5>Impacts</h5>
${this.renderImpacts(charCtx, file)}
<h5>Special Tracks</h5>
${this.renderSpecialTracks(charCtx, file)}
<h5>Assets</h5>
${this.renderAssets(charCtx, file)}
</article>
`;
render(tpl, this.contentEl);
}

renderStats(charCtx: CharacterContext, _file: TFile) {
const lens = charCtx.lens;
const raw = charCtx.character;
return html`
<ul class="stats">
${map(
Object.entries(lens.stats),
([stat, value]) => html`
<li>
<dl>
<dt data-value=${stat}>${stat}</dt>
<dd data-value=${value.get(raw)}>${value.get(raw)}</dd>
</dl>
</li>
`,
)}
</ul>
`;
}

renderMeters(charCtx: CharacterContext, _file: TFile) {
const lens = charCtx.lens;
const raw = charCtx.character;
return html`
<ul class="meters">
${map(
Object.entries(lens.condition_meters),
([meter, value]) => html`
<li>
<dl>
<dt data-value=${meter}>${meter}</dt>
<dd data-value=${value.get(raw)}>${value.get(raw)}</dd>
</dl>
</li>
`,
)}
</ul>
`;
}

renderImpacts(charCtx: CharacterContext, _file: TFile) {
const lens = charCtx.lens;
const raw = charCtx.character;
return html`
<ul class="impacts">
${map(
Object.entries(lens.impacts.get(raw)),
([impact, value]) => html`
<li>
<dl>
<dt data-value=${impact}>${impact}</dt>
<dd data-value=${value}>${value}</dd>
</dl>
</li>
`,
)}
</ul>
`;
}

renderSpecialTracks(charCtx: CharacterContext, _file: TFile) {
const lens = charCtx.lens;
const raw = charCtx.character;
return html`
<ul class="special-tracks">
${map(
Object.entries(lens.special_tracks),
([track, value]) => html`
<li>
<dl>
<dt data-value=${track}>${track}</dt>
<dd>${JSON.stringify(value.get(raw))}</dd>
</dl>
</li>
`,
)}
</ul>
`;
}

renderAssets(charCtx: CharacterContext, _file: TFile) {
const lens = charCtx.lens;
const raw = charCtx.character;
return html`
<ul class="assets">
${map(
Object.values(lens.assets.get(raw)),
(asset) => html`
<li>
<dl>
<dt data-value=${asset.id}>${asset.id}</dt>
<dd>${JSON.stringify(asset)}</dd>
</dl>
</li>
`,
)}
</ul>
`;
}

// 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);
// }
}
2 changes: 2 additions & 0 deletions src/characters/css/assets.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.iron-vault-character .assets {
}
9 changes: 9 additions & 0 deletions src/characters/css/characters.css
Original file line number Diff line number Diff line change
@@ -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 {

}
2 changes: 2 additions & 0 deletions src/characters/css/impacts.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.iron-vault-character .impacts {
}
2 changes: 2 additions & 0 deletions src/characters/css/meters.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.iron-vault-character .meters {
}
2 changes: 2 additions & 0 deletions src/characters/css/special_tracks.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.iron-vault-character .special-tracks {
}
2 changes: 2 additions & 0 deletions src/characters/css/stats.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.iron-vault-character .stats {
}
33 changes: 15 additions & 18 deletions src/characters/lens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Lens, updating } from "../utils/lens";
import {
BaseIronVaultSchema,
IronVaultSheetAssetInput,
ImpactStatus,
characterLens,
momentumOps,
movesReader,
Expand Down Expand Up @@ -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",
);
});
});
});
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 11 additions & 16 deletions src/characters/lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,18 @@ export const baseIronVaultSchema = z

export type BaseIronVaultSchema = z.input<typeof baseIronVaultSchema>;

export enum ImpactStatus {
Unmarked = "",
Marked = "",
}
// export enum ImpactStatus {
// Unmarked = "marked",
// Marked = "un",
// }

export interface CharacterLens {
name: Lens<ValidatedCharacter, string>;
momentum: Lens<ValidatedCharacter, number>;
stats: Record<string, Lens<ValidatedCharacter, number>>;
condition_meters: Record<string, Lens<ValidatedCharacter, number>>;
assets: Lens<ValidatedCharacter, IronVaultSheetAssetSchema[]>;
impacts: Lens<ValidatedCharacter, Record<string, ImpactStatus>>;
impacts: Lens<ValidatedCharacter, Record<string, boolean>>;
special_tracks: Record<string, Lens<ValidatedCharacter, ProgressTrack>>;
ruleset: Ruleset;
}
Expand Down Expand Up @@ -156,7 +156,7 @@ export function validated(

function createImpactLens(
ruleset: Ruleset,
): Lens<Record<string, unknown>, Record<string, ImpactStatus>> {
): Lens<Record<string, unknown>, Record<string, boolean>> {
// function createImpactLens(data: Record<string, unknown>, ruleset: Ruleset): Lens<Record<string, unknown>, Record<string, ImpactStatus>> {
// const impactProps: Record<string, string> = {};
// const dataKeys = Object.fromEntries(Object.keys(data).map((key) => [key.toLowerCase(), key]));
Expand All @@ -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}`);
Expand Down Expand Up @@ -283,10 +279,9 @@ export function momentumTrackerReader(
return { get: momentumTrackerLens(characterLens).get };
}

export function countMarked(impacts: Record<string, ImpactStatus>): number {
export function countMarked(impacts: Record<string, boolean>): number {
return Object.values(impacts).reduce(
(count, impactStatus) =>
count + (impactStatus === ImpactStatus.Marked ? 1 : 0),
(count, impactStatus) => count + (impactStatus ? 1 : 0),
0,
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -222,6 +223,7 @@ export default class IronVaultPlugin extends Plugin {
registerMechanicsBlock(this);
registerTrackBlock(this);
registerClockBlock(this);
registerCharacterBlock(this);
}

async activateView() {
Expand Down
Loading

0 comments on commit 6eeba22

Please sign in to comment.