Skip to content

Commit

Permalink
Use boolean array for marked abilities
Browse files Browse the repository at this point in the history
Fixes #16
  • Loading branch information
cwegrzyn committed May 17, 2024
1 parent 16af6a3 commit dc7d3e3
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 55 deletions.
16 changes: 12 additions & 4 deletions src/character-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class CharacterTracker implements ReadonlyMap<string, CharacterResult> {
key: string,
map: ReadonlyMap<string, CharacterResult>,
) => void,
thisArg?: any,
thisArg?: unknown,
): void {
this.index.forEach(callbackfn, thisArg);
}
Expand Down Expand Up @@ -57,20 +57,28 @@ export class CharacterTracker implements ReadonlyMap<string, CharacterResult> {

activeCharacter(): [string, CharacterContext] {
if (this.size == 0) {
throw new Error("no valid characters found");
throw new MissingCharacterError("no valid characters found");
} else if (this.size > 1) {
throw new Error("we don't yet support multiple characters");
throw new MissingCharacterError(
"we don't yet support multiple characters",
);
}

const [[key, val]] = this.entries();
if (val.isLeft()) {
throw new Error("character is invalid", { cause: val.error });
throw val.error;
}

return [key, val.value];
}
}

export class CharacterError extends Error {}

export class MissingCharacterError extends Error {}

export class InvalidCharacterError extends Error {}

export class CharacterIndexer extends BaseIndexer<CharacterResult> {
readonly id: string = "character";

Expand Down
26 changes: 21 additions & 5 deletions src/characters/action-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ export class CharacterActionContext implements IActionContext {

export type ActionContext = CharacterActionContext | NoCharacterActionConext;

function renderError(e: Error, el: HTMLElement): void {
el.createEl("pre", { text: `${e.name}: ${e.message}` });
if (e.cause instanceof Error) {
el.createEl("p", { text: "Caused by:" });
renderError(e.cause, el);
}
}

export async function determineCharacterActionContext(
plugin: ForgedPlugin,
): Promise<ActionContext | undefined> {
Expand All @@ -147,11 +155,19 @@ export async function determineCharacterActionContext(
characterContext,
);
} catch (e) {
// TODO: probably want to show character parse errors in full glory
await InfoModal.show(
plugin.app,
`An error occurred while finding your active character.\n\n${e}`,
);
const div = document.createElement("div");
div.createEl("p", {
text: `An error occurred while finding your active character`,
});
if (e instanceof Error) {
renderError(e, div);
} else {
div.createEl("pre", {
text: `${e}`,
});
}

await InfoModal.show(plugin.app, div);
return undefined;
}
} else {
Expand Down
44 changes: 28 additions & 16 deletions src/characters/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type AnyOptionField = AssetOptionField | AssetAbilityOptionField;

function traverseAssetControls(
asset: Asset,
markedAbilities?: number[],
markedAbilities: boolean[],
): Pathed<AnyControlField>[] {
function conditionMeterControls(
controls: AssetConditionMeter["controls"],
Expand All @@ -84,8 +84,14 @@ function traverseAssetControls(
return [curControl, ...conditionMeterFields];
});

const abilityControls: Pathed<AnyControlField>[] = (markedAbilities ?? [])
.map((abilityNum) => asset.abilities[abilityNum - 1]) // TODO: marked abilities 0-indexing FOR-13
if (markedAbilities.length != asset.abilities.length) {
throw new Error(
`Asset has ${asset.abilities.length} abilities, but marked abilities only ${markedAbilities.length}`,
);
}

const abilityControls: Pathed<AnyControlField>[] = asset.abilities
.filter((_ability, index) => markedAbilities[index])
.flatMap((ability) => {
return Object.entries(ability.controls ?? {}).map(([key, field]) =>
pathed([ability.id, key], field),
Expand All @@ -97,14 +103,20 @@ function traverseAssetControls(

export function traverseAssetOptions(
asset: Asset,
markedAbilities?: number[],
markedAbilities: boolean[],
): Pathed<AnyOptionField>[] {
const baseControls: Pathed<AnyOptionField>[] = Object.entries(
asset.options ?? {},
).map(([key, field]) => pathed([asset.id, key], field));

const abilityOptions: Pathed<AnyOptionField>[] = (markedAbilities ?? [])
.map((abilityNum) => asset.abilities[abilityNum - 1]) // TODO: marked abilities 0-indexing FOR-13
if (markedAbilities.length != asset.abilities.length) {
throw new Error(
`Asset has ${asset.abilities.length} abilities, but marked abilities only ${markedAbilities.length}`,
);
}

const abilityOptions: Pathed<AnyOptionField>[] = asset.abilities
.filter((_ability, index) => markedAbilities[index])
.flatMap((ability) => {
return Object.entries(ability.options ?? {}).map(([key, field]) =>
pathed([ability.id, key], field),
Expand All @@ -114,7 +126,10 @@ export function traverseAssetOptions(
return [...baseControls, ...abilityOptions];
}

export function samePath(left: Pathed<any>, right: Pathed<any>): boolean {
export function samePath(
left: Pathed<unknown>,
right: Pathed<unknown>,
): boolean {
if (left.path == right.path) return true;
if (left.path.length != right.path.length) return false;

Expand All @@ -125,21 +140,18 @@ export function samePath(left: Pathed<any>, right: Pathed<any>): boolean {
return true;
}

export function getPathLabel(pathed: Pathed<any>): string {
export function getPathLabel(pathed: Pathed<unknown>): string {
// Skip first element (which is asset/ability id)
return pathed.path.slice(1).join("/");
}

export function defaultMarkedAbilitiesForAsset(asset: Asset): number[] {
// TODO: 0-index FOR-13
return asset.abilities.flatMap(({ enabled }, index) =>
enabled ? [index + 1] : [],
);
export function defaultMarkedAbilitiesForAsset(asset: Asset): boolean[] {
return asset.abilities.map(({ enabled }) => enabled);
}

export function updateAssetWithOptions(
asset: Asset,
options: Record<string, any>,
options: Record<string, string>,
): Asset {
return produce(asset, (draft) => {
for (const pathed of traverseAssetOptions(
Expand Down Expand Up @@ -178,7 +190,7 @@ export function addAsset(charLens: CharacterLens): CharWriter<Asset> {

return charLens.assets.update(character, [
...currentAssets,
{ id: newAsset.id, marked_abilities, controls, options },
{ id: newAsset.id, abilities: marked_abilities, controls, options },
]);
});
}
Expand All @@ -188,7 +200,7 @@ export class MissingAssetError extends Error {}
export function assetMeters(
charLens: CharacterLens,
asset: Asset,
markedAbilities: number[],
markedAbilities: boolean[],
): {
key: string;
definition: ConditionMeterDefinition;
Expand Down
30 changes: 24 additions & 6 deletions src/characters/lens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,25 @@ describe("characterLens", () => {
const character = validater({
...VALID_INPUT,
assets: [
{ id: "asset_id", controls: { integrity: 2 } },
{
id: "asset_id",
abilities: [true, false, false],
controls: { integrity: 2 },
},
] as ForgedSheetAssetInput[],
});
actsLikeLens(lens.assets, character, [
{ id: "new_asset", controls: { integrity: 3 }, options: {} },
{
id: "new_asset",
abilities: [true, false, false],
controls: { integrity: 3 },
options: {},
},
]);

it("requires a valid asset definition", () => {
expect(() =>
lens.assets.update(character, [{ foo: "bar" }] as any),
lens.assets.update(character, [{ foo: "bar" }] as never),
).toThrow(/invalid_type/);
});

Expand Down Expand Up @@ -405,11 +414,17 @@ describe("movesReader", () => {
movesReader(lens, mockIndex).get(
validater({
...VALID_INPUT,
assets: [{ id: "starforged/assets/path/empath" }],
assets: [
{
id: "starforged/assets/path/empath",
abilities: [false, false, false],
},
],
} satisfies BaseForgedSchema),
),
).toEqual(Right.create([]));
});

it("includes moves for marked asset abilities", () => {
// This ability has no additional moves.
expect(
Expand All @@ -418,7 +433,10 @@ describe("movesReader", () => {
validater({
...VALID_INPUT,
assets: [
{ id: "starforged/assets/path/empath", marked_abilities: [2] },
{
id: "starforged/assets/path/empath",
abilities: [false, true, false],
},
],
} satisfies BaseForgedSchema),
)
Expand All @@ -434,7 +452,7 @@ describe("movesReader", () => {
assets: [
{
id: "starforged/assets/path/empath",
marked_abilities: [1, 2],
abilities: [true, true, false],
},
],
} satisfies BaseForgedSchema),
Expand Down
32 changes: 10 additions & 22 deletions src/characters/lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ import {
reader,
updating,
} from "../utils/lens";
import {
AssetError,
assetMeters,
assetWithDefnReader,
defaultMarkedAbilitiesForAsset,
} from "./assets";
import { AssetError, assetMeters, assetWithDefnReader } from "./assets";

const ValidationTag: unique symbol = Symbol("validated ruleset");

Expand All @@ -39,7 +34,7 @@ export type ValidatedCharacter = {

export const characterAssetSchema = z.object({
id: z.string(),
marked_abilities: z.array(z.number().int().positive()).optional(),
abilities: z.array(z.boolean()),
controls: z
.record(z.union([z.string(), z.number().int(), z.boolean()]).nullable())
.default({}),
Expand Down Expand Up @@ -310,16 +305,13 @@ export function movesReader(
const assetReader = assetWithDefnReader(charLens, index);
return reader((source) => {
return collectEither(assetReader.get(source)).map((assets) =>
assets.flatMap(({ asset, defn }) => {
const moveList: Move[] = [];
const marked_abilities = asset.marked_abilities ?? [];
for (const [idx, ability] of defn.abilities.entries()) {
if (marked_abilities.includes(idx + 1)) {
moveList.push(...Object.values(ability.moves ?? {}));
}
}
return moveList;
}),
assets.flatMap(({ asset: assetConfig, defn }) =>
defn.abilities
// Take only enabled abilities
.filter((_ability, index) => assetConfig.abilities[index])
// Gather moves
.flatMap((ability) => Object.values(ability.moves ?? {})),
),
);
});
}
Expand Down Expand Up @@ -377,11 +369,7 @@ export function meterLenses(
return [];
} else {
const { asset, defn } = assetResult.value;
return assetMeters(
charLens,
defn,
asset.marked_abilities ?? defaultMarkedAbilitiesForAsset(defn),
);
return assetMeters(charLens, defn, asset.abilities);
}
})
.map((val) => [val.key, val]);
Expand Down
4 changes: 2 additions & 2 deletions src/utils/ui/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { type App } from "obsidian";

/** Modal to render an informative prompt to the user. */
export class InfoModal extends Modal {
static async show(app: App, content: string): Promise<void> {
static async show(app: App, content: string | HTMLElement): Promise<void> {
return await new Promise((resolve, _reject) => {
new this(app, content, resolve).open();
});
}

private constructor(
app: App,
public readonly content: string,
public readonly content: string | HTMLElement,
public readonly accept: () => void,
) {
super(app);
Expand Down

0 comments on commit dc7d3e3

Please sign in to comment.