Skip to content

Commit 7f25fcc

Browse files
committed
cleaner types
1 parent 023aaad commit 7f25fcc

File tree

5 files changed

+101
-67
lines changed

5 files changed

+101
-67
lines changed

data/adversaries.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4209,7 +4209,7 @@
42094209
"motives": "Demolish, devour, undermine",
42104210
"hp": 7,
42114211
"stress": 5,
4212-
"attack": null,
4212+
"attack": "+2d4",
42134213
"weapon": "Massive Pseudopod",
42144214
"range": "Very Close",
42154215
"damage": "4d6+13 mag",

esbuild.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import esbuild from "esbuild";
22
import process from "process";
3-
import builtins from "builtin-modules";
3+
import { builtinModules } from "node:module";
44

55
const banner =
66
`/*
@@ -31,7 +31,7 @@ const context = await esbuild.context({
3131
"@lezer/common",
3232
"@lezer/highlight",
3333
"@lezer/lr",
34-
...builtins],
34+
...builtinModules],
3535
format: "cjs",
3636
target: "es2018",
3737
logLevel: "info",

src/main.ts

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Editor, Plugin, setTooltip, parseYaml, Menu, Notice, type TFolder } from 'obsidian';
1+
import { Editor, Plugin, setTooltip, parseYaml, Menu, Notice, type TFolder, debounce, type Debouncer } from 'obsidian';
22
import { SettingTab, type PluginSettings, DEFAULT_SETTINGS } from './settings';
3-
import { ADV_LIBRARY, ENV_LIBRARY, ADV_TEMPLATE, ENV_TEMPLATE, processAdversary, walkFolder } from './utils';
4-
import { AdversaryCard, AdversaryModal, type Adversary } from './ui';
3+
import { ADV_LIBRARY, ENV_LIBRARY, ADV_TEMPLATE, ENV_TEMPLATE, walkFolder } from './utils';
4+
import { AdversaryCard, AdversaryModal, type RawAdversary } from './ui';
55

66
export type PluginState = {
77
settings: PluginSettings;
@@ -25,7 +25,8 @@ export default class BeastVault extends Plugin {
2525
saveTimer?: number;
2626
saving?: Promise<void>;
2727
battlePoints: HTMLElement;
28-
library: Adversary[] = [];
28+
library: RawAdversary[] = [];
29+
updateState: Debouncer<[], Promise<void>>;
2930

3031
updateStatusBar() {
3132
const file = this.app.workspace.getActiveFile();
@@ -75,9 +76,9 @@ export default class BeastVault extends Plugin {
7576
}
7677
return;
7778
}
78-
const newLibrary: Adversary[] = [];
79+
const newLibrary: RawAdversary[] = [];
7980
await walkFolder(folder, async (file) => {
80-
let content: Adversary | Adversary[];
81+
let content: RawAdversary | RawAdversary[];
8182

8283
if (file.extension == 'json') {
8384
content = JSON.parse(await this.app.vault.read(file));
@@ -90,15 +91,18 @@ export default class BeastVault extends Plugin {
9091
const lines = (await this.app.vault.read(file)).split('\n');
9192
content = codeblocks
9293
.filter(sec => lines[sec.position.start.line].trim() === '```daggerheart')
93-
.map(sec => parseYaml(lines.slice(sec.position.start.line + 1, sec.position.end.line).join("\n")));
94+
.map(sec => {
95+
const targetLines = lines.slice(sec.position.start.line + 1, sec.position.end.line).join("\n");
96+
return { raw: targetLines, ...parseYaml(targetLines) };
97+
});
9498
} else {
9599
return;
96100
}
97101

98102
if (!Array.isArray(content)) content = [content];
99103
for (const item of content) {
100104
if (item && typeof item == 'object' && typeof item.name == 'string') {
101-
newLibrary.push(processAdversary({ source: file.path, ...item }, file.path));
105+
newLibrary.push({ source: 'homebrew', ...item });
102106
}
103107
}
104108
})
@@ -123,18 +127,19 @@ export default class BeastVault extends Plugin {
123127
if (length == 0) {
124128
new Notice(`No valid stat blocks found in ${this.state.settings.libraryFolder}`);
125129
} else {
130+
// TODO: message about duplicates?
126131
new Notice(`Loaded ${length} stat block${length != 1 ? 's' : ''}`)
127132
}
128133
}
129134

130135
return newLibrary;
131136
}
132137

133-
allAdversaries(): Adversary[] {
138+
allAdversaries(): RawAdversary[] {
134139
return this.library.filter(adv => (adv.hp && adv.hp > 0) || (adv.stress && adv.stress > 0)).concat(ADV_LIBRARY);
135140
}
136141

137-
allEnvironments(): Adversary[] {
142+
allEnvironments(): RawAdversary[] {
138143
return this.library.filter(adv => (!adv.hp || adv.hp == 0) && (!adv.stress || adv.stress == 0)).concat(ENV_LIBRARY);
139144
}
140145

@@ -144,10 +149,10 @@ export default class BeastVault extends Plugin {
144149
this.battlePoints = this.addStatusBarItem();
145150
this.registerEvent(this.app.workspace.on('active-leaf-change', () => this.updateStatusBar()));
146151
this.app.workspace.onLayoutReady(() => this.scanLibrary(false, 'no'));
152+
this.updateState = debounce(() => this.saveData(this.state), 1000, true);
147153

148154
this.registerMarkdownCodeBlockProcessor("daggerheart", (src, el, ctx) => {
149-
const adv = processAdversary(parseYaml(src) ?? {}, this.app.workspace.getActiveFile()!.path);
150-
const child = new AdversaryCard(el, adv, this);
155+
const child = new AdversaryCard(el, parseYaml(src) ?? {}, this);
151156
ctx.addChild(child);
152157
child.render();
153158
// Track it so we can refresh on settings change:
@@ -250,9 +255,7 @@ export default class BeastVault extends Plugin {
250255
}
251256

252257
onunload() {
253-
if (this.saveTimer != null) {
254-
void this.flushSave();
255-
}
258+
void this.updateState.run();
256259
}
257260

258261
renderAll() {
@@ -261,23 +264,6 @@ export default class BeastVault extends Plugin {
261264
}
262265
}
263266

264-
updateState() {
265-
// Debounce writes
266-
if (this.saveTimer != null) window.clearTimeout(this.saveTimer);
267-
this.saveTimer = window.setTimeout(() => { void this.flushSave(); }, 1000);
268-
}
269-
270-
async flushSave() {
271-
if (this.saveTimer != null) {
272-
window.clearTimeout(this.saveTimer);
273-
this.saveTimer = undefined;
274-
}
275-
const run = async () => await this.saveData(this.state);
276-
// Chain to the previous save if one is in-flight
277-
this.saving = (this.saving ?? Promise.resolve()).then(run, run);
278-
await this.saving;
279-
}
280-
281267
updateCard(keys: (string | number)[], value: string | number) {
282268
type Data = { [key: string]: Data | number | string };
283269
let data: Data = this.state.cards;
@@ -294,12 +280,11 @@ export default class BeastVault extends Plugin {
294280
getCardState(keys: (string | number)[]): number | undefined {
295281
type Data = { [key: string]: Data | string | number }
296282
let data: Data = this.state.cards;
297-
for (const key of keys) {
283+
for (const [i, key] of keys.entries()) {
298284
if (!data[key]) return undefined;
285+
if (i === keys.length - 1) return data[key] as number;
299286
data = data[key] as Data;
300287
}
301-
return data as any;
302288
}
303289
}
304290

305-

src/ui.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { App, Editor, SuggestModal, Notice, MarkdownRenderChild, stringifyYaml, setIcon, MarkdownRenderer } from 'obsidian';
22
import { roll } from '@airjp73/dice-notation';
33
import BeastVault from './main';
4-
import { hexToRgb, DICE_PATTERN } from './utils';
4+
import { hexToRgb, DICE_PATTERN, processAdversary } from './utils';
55

66
type Feature = {
77
name?: string;
@@ -12,25 +12,27 @@ type Feature = {
1212
flavor?: string;
1313
}
1414

15-
export type Adversary = {
15+
// Stored in library, as entered by user.
16+
// Pasted into editor when inserted.
17+
export type RawAdversary = {
1618
name?: string;
1719
tier?: number;
1820
type?: string;
19-
difficulty?: string;
2021
desc?: string;
21-
features: Feature[];
22+
difficulty?: string | number;
23+
features?: Feature[];
2224

2325
// these are for environments
2426
tone?: string;
2527
impulses?: string;
2628
adversaries?: string;
2729

2830
// these are for adversaries
29-
hp: number;
30-
stress: number;
31-
thresholds: number[];
32-
attack?: string;
33-
xp: string[];
31+
hp?: number;
32+
stress?: number;
33+
thresholds?: string | number | number[];
34+
attack?: string | number;
35+
xp?: string | string[];
3436
motives?: string;
3537

3638
weapon?: string;
@@ -39,48 +41,68 @@ export type Adversary = {
3941

4042
// these are not rendered
4143
source?: string;
42-
id: string;
44+
id?: string;
45+
raw?: string;
4346
};
4447

48+
// Used when rendering.
49+
// 'id' is not rendered but required to track state.
50+
export type Adversary = Omit<RawAdversary, 'source' | 'raw'> & {
51+
difficulty?: string;
52+
features: Feature[];
53+
hp: number; // 0 for environments
54+
stress: number; // 0 for environments
55+
thresholds: number[];
56+
attack?: string;
57+
xp: string[];
58+
id: string;
59+
}
60+
4561
function subTitle(tier?: number, type?: string) {
4662
return (tier ? `Tier ${tier} ` : '') + (type ? type : '');
4763
}
4864

49-
export class AdversaryModal extends SuggestModal<Adversary> {
50-
constructor(app: App, private editor: Editor, private library: Adversary[]) {
65+
export class AdversaryModal extends SuggestModal<RawAdversary> {
66+
constructor(app: App, private editor: Editor, private library: RawAdversary[]) {
5167
super(app);
5268
this.limit = 200;
5369
}
5470

55-
getSuggestions(query: string): Adversary[] {
71+
getSuggestions(query: string): RawAdversary[] {
5672
return this.library.filter((adv: Adversary) =>
5773
adv.name!.toLowerCase().includes(query.toLowerCase())
5874
);
5975
}
6076

61-
renderSuggestion(adv: Adversary, el: HTMLElement) {
77+
renderSuggestion(adv: RawAdversary, el: HTMLElement) {
6278
const heading = el.createDiv({ cls: 'bv-spreadout' });
6379
heading.createEl('b', { text: adv.name?.toUpperCase() || '' });
6480
heading.createSpan({ text: subTitle(adv.tier, adv.type), cls: 'bv-smaller' });
65-
el.createSpan({ text: adv.desc || '', cls: 'bv-smaller bv-muted' });
81+
el.createSpan({ text: adv.desc ?? '', cls: 'bv-smaller bv-muted' });
6682
}
6783

68-
onChooseSuggestion(adv: Adversary, evt: MouseEvent | KeyboardEvent) {
69-
adv.id = Math.random().toString(36).slice(2);
70-
delete adv.source;
71-
this.editor.replaceSelection(`\`\`\`daggerheart\n${stringifyYaml(adv)}\`\`\`\n`);
84+
onChooseSuggestion(adv: RawAdversary, evt: MouseEvent | KeyboardEvent) {
85+
const copy = { ... adv };
86+
copy.id = Math.random().toString(36).slice(2);
87+
delete copy.source;
88+
delete copy.raw;
89+
this.editor.replaceSelection(`\`\`\`daggerheart\n${adv.raw ? adv.raw : stringifyYaml(copy)}\`\`\`\n`);
7290
}
7391
}
7492

7593
export class AdversaryCard extends MarkdownRenderChild {
7694
count: number;
95+
filePath: string;
96+
public adv: Adversary;
7797

7898
constructor(
7999
private container: HTMLElement,
80-
public adv: Adversary,
100+
public raw: RawAdversary,
81101
private plugin: BeastVault
82102
) {
83103
super(container);
104+
this.filePath = this.plugin.app.workspace.getActiveFile()?.path ?? '/';
105+
this.adv = processAdversary(raw, this.filePath);
84106
this.count = this.plugin.state.cards[this.adv.id]?.count || 1;
85107
}
86108

@@ -153,7 +175,7 @@ export class AdversaryCard extends MarkdownRenderChild {
153175
.replace(/\b([mM])ark a [sS]tress\b/g, "<b>$1ark a Stress</b>")
154176
.replace(DICE_PATTERN, `<span class=bv-rollable>$&</span>`),
155177
featureDiv,
156-
this.plugin.app.workspace.getActiveFile()!.path,
178+
this.filePath,
157179
this
158180
);
159181
}
@@ -275,7 +297,7 @@ export class AdversaryCard extends MarkdownRenderChild {
275297
setIcon(remove, 'minus')
276298

277299
// hacky but works for now
278-
setTimeout(() => {
300+
window.setTimeout(() => {
279301
const editable = card.parentElement?.nextElementSibling?.classList.contains('edit-block-button');
280302
if (editable) {
281303
add.addClass('bv-lower-1');

src/utils.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Adversary } from './ui';
1+
import type { Adversary, RawAdversary } from './ui';
22
import { TFile, TFolder } from "obsidian";
33
import ADV_LIBRARY_DATA from '../data/adversaries.json';
44
import ENV_LIBRARY_DATA from '../data/environments.json';
55

6-
export const ADV_LIBRARY: Adversary[] = ADV_LIBRARY_DATA as any;
7-
export const ENV_LIBRARY: Adversary[] = ENV_LIBRARY_DATA as any;
6+
export const ADV_LIBRARY: RawAdversary[] = ADV_LIBRARY_DATA;
7+
export const ENV_LIBRARY: RawAdversary[] = ENV_LIBRARY_DATA;
88

99
export const ADV_TEMPLATE = `\`\`\`daggerheart
1010
name:
@@ -60,19 +60,24 @@ export function hexToRgb(hex: string) {
6060
return `${rgb[0]}, ${rgb[1]}, ${rgb[2]}`;
6161
}
6262

63-
export function processAdversary(obj: any, filePath: string): Adversary {
63+
export function processAdversary(obj: RawAdversary, filePath: string): Adversary {
6464
if (typeof obj.attack === 'number') {
6565
obj.attack = obj.attack > 0 ? `+${obj.attack}` : `${obj.attack}`;
6666
}
6767
if (typeof obj.thresholds === "string") {
68-
obj.thresholds = obj.thresholds.split(/[,/]/).filter((s: string) => s.trim().toLowerCase() != 'none');
68+
obj.thresholds = obj.thresholds.split(/[,/]/)
69+
.filter((s: string) => s.trim().toLowerCase() != 'none')
70+
.map((s: string) => parseInt(s));
6971
}
7072
if (typeof obj.thresholds === "number") {
7173
obj.thresholds = [obj.thresholds];
7274
}
7375
if (typeof obj.xp === "string") {
7476
obj.xp = obj.xp.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
7577
}
78+
if (typeof obj.difficulty === "number") {
79+
obj.difficulty = obj.difficulty.toString();
80+
}
7681

7782
obj.id ??= `${filePath}::${obj.name || ''}`;
7883
obj.hp ??= 0;
@@ -81,9 +86,31 @@ export function processAdversary(obj: any, filePath: string): Adversary {
8186
obj.thresholds ??= [];
8287
obj.features ??= [];
8388

84-
// TODO: clean up stray keys
89+
return {
90+
name: obj.name,
91+
tier: obj.tier,
92+
type: obj.type,
93+
desc: obj.desc,
94+
difficulty: obj.difficulty,
95+
features: obj.features,
96+
97+
tone: obj.tone,
98+
impulses: obj.impulses,
99+
adversaries: obj.adversaries,
100+
101+
hp: obj.hp,
102+
stress: obj.stress,
103+
thresholds: obj.thresholds,
104+
attack: obj.attack,
105+
xp: obj.xp,
106+
motives: obj.motives,
107+
108+
weapon: obj.weapon,
109+
range: obj.range,
110+
damage: obj.damage,
85111

86-
return obj;
112+
id: obj.id,
113+
};
87114
}
88115

89116
export async function walkFolder(folder: TFolder, callback: (file: TFile) => Promise<void>) {

0 commit comments

Comments
 (0)