Skip to content

Commit

Permalink
Support for "enhance asset" in asset attributes (#459)
Browse files Browse the repository at this point in the history
Fixes #417
  • Loading branch information
cwegrzyn authored Aug 17, 2024
1 parent dc454a7 commit f11635e
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 129 deletions.
2 changes: 0 additions & 2 deletions src/character-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export class MissingCharacterError extends CharacterError {}

export class MissingCampaignError extends CharacterError {}

export class InvalidCharacterError extends CharacterError {}

export class CharacterIndexer extends BaseIndexer<
CharacterContext,
z.ZodError
Expand Down
43 changes: 26 additions & 17 deletions src/characters/action-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StandardIndex } from "datastore/data-indexer";
import { DataswornTypes } from "datastore/datasworn-indexer";
import { moveOrigin } from "datastore/datasworn-symbols";
import { produce } from "immer";
import { App, MarkdownFileInfo } from "obsidian";
import { App, MarkdownFileInfo, Notice } from "obsidian";
import { OracleRoller } from "oracles/roller";
import { ConditionMeterDefinition, Ruleset } from "rules/ruleset";
import { vaultProcess } from "utils/obsidian";
Expand All @@ -27,6 +27,7 @@ import {
import { type Datastore } from "../datastore";
import IronVaultPlugin from "../index";
import { InfoModal } from "../utils/ui/info";
import { InvalidCharacterError } from "./errors";

export interface IActionContext extends IDataContext {
readonly campaignContext: CampaignDataContext;
Expand Down Expand Up @@ -142,23 +143,31 @@ export class CharacterActionContext implements IActionContext {

get moves(): StandardIndex<DataswornTypes["move"]> {
if (!this.#moves) {
// TODO(@cwegrzyn): we should let the user know if they have a missing move, I think
const characterMoves = movesReader(this.characterContext.lens, this)
.get(this.characterContext.character)
.expect("unexpected failure finding assets for moves");

this.#moves = this.campaignContext.moves.projected((move) => {
if (move[moveOrigin].assetId == null) return move;
const assetMove = characterMoves.find(
({ move: characterMove }) => move._id === characterMove._id,
);
if (assetMove) {
return produce(move, (draft) => {
draft.name = `${assetMove.asset.name}: ${draft.name}`;
});
try {
const characterMoves = movesReader(
this.characterContext.lens,
this,
).get(this.characterContext.character);

this.#moves = this.campaignContext.moves.projected((move) => {
if (move[moveOrigin].assetId == null) return move;
const assetMove = characterMoves.find(
({ move: characterMove }) => move._id === characterMove._id,
);
if (assetMove) {
return produce(move, (draft) => {
draft.name = `${assetMove.asset.name}: ${draft.name}`;
});
}
return undefined;
});
} catch (err) {
if (err instanceof InvalidCharacterError) {
console.error(err);
new Notice(`Invalid character definition: ${err.message}`, 0);
}
return undefined;
});
throw err;
}
}
return this.#moves;
}
Expand Down
19 changes: 0 additions & 19 deletions src/characters/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,25 +280,6 @@ describe("integratedAssetLens", () => {
).toHaveProperty("controls.integrity.controls.battered.value", true);
});

it("integrates meter subfield values", () => {
expect(
integratedAssetLens(dataContext).get({
id: starship()._id,
abilities: [true, false, false],
options: {},
controls: {},
}),
).toHaveProperty("controls.integrity.controls.battered.value", false);
expect(
integratedAssetLens(dataContext).get({
id: starship()._id,
abilities: [true, false, false],
options: {},
controls: { "integrity/battered": true },
}),
).toHaveProperty("controls.integrity.controls.battered.value", true);
});

it("integrates ability values", () => {
expect(
integratedAssetLens(dataContext).get({
Expand Down
68 changes: 35 additions & 33 deletions src/characters/assets.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
import { type Datasworn } from "@datasworn/core";
import { IDataContext } from "datastore/data-context";
import { produce } from "immer";
import merge from "lodash.merge";
import { ConditionMeterDefinition } from "rules/ruleset";
import { Either, Left, Right } from "../utils/either";
import { Lens, addOrUpdateMatching, reader, writer } from "../utils/lens";
import { Lens, addOrUpdateMatching, writer } from "../utils/lens";
import { MissingAssetError } from "./errors";
import {
CharLens,
CharReader,
CharWriter,
CharacterLens,
IronVaultSheetAssetSchema,
} from "./lens";

export class AssetError extends Error {}

export function assetWithDefnReader(
charLens: CharacterLens,
dataContext: IDataContext,
): CharReader<
Array<
Either<
AssetError,
{ asset: IronVaultSheetAssetSchema; defn: Datasworn.Asset }
>
>
> {
return reader((source) => {
return charLens.assets.get(source).map((asset) => {
const defn = dataContext.assets.get(asset.id);
if (defn) {
return Right.create({ asset, defn });
} else {
return Left.create(new AssetError(`missing asset with id ${asset.id}`));
}
});
});
}
// export function assetWithDefnReader(
// charLens: CharacterLens,
// dataContext: IDataContext,
// ): CharReader<
// Array<
// Either<
// AssetError,
// { asset: IronVaultSheetAssetSchema; defn: Datasworn.Asset }
// >
// >
// > {
// return reader((source) => {
// return charLens.assets.get(source).map((asset) => {
// const defn = dataContext.assets.get(asset.id);
// if (defn) {
// return Right.create({ asset, defn });
// } else {
// return Left.create(new AssetError(`missing asset with id ${asset.id}`));
// }
// });
// });
// }

export type AssetWalker = {
onAnyOption?: (
Expand Down Expand Up @@ -169,26 +167,31 @@ export function addOrUpdateViaDataswornAsset(
});
}

export class MissingAssetError extends Error {}

function assetKey(key: string, parentKey: string | number | undefined): string {
return parentKey != null ? `${parentKey}/${key}` : key;
}

/** A lens that takes a character asset choice and produces an asset w/ choices merged in. */
export function integratedAssetLens(
dataContext: IDataContext,
): Lens<IronVaultSheetAssetSchema, Datasworn.Asset> {
return {
get(assetData) {
const dataswornAsset = dataContext.assets.get(assetData.id);
if (!dataswornAsset) {
throw new AssetError(`unable to find asset ${assetData.id}`);
throw new MissingAssetError(`unable to find asset ${assetData.id}`);
}
return produce(dataswornAsset, (draft) => {
assetData.abilities.forEach((enabled, index) => {
if (enabled) {
if (enabled != null) {
draft.abilities[index].enabled = enabled;
}
if (
draft.abilities[index].enabled &&
draft.abilities[index].enhance_asset
) {
draft = merge(draft, draft.abilities[index].enhance_asset);
}
});
walkAsset(draft, {
onAnyOption(key, option, parentKey) {
Expand Down Expand Up @@ -230,7 +233,6 @@ export function integratedAssetLens(
export function assetMeters(
charLens: CharacterLens,
asset: Datasworn.Asset,
markedAbilities: boolean[],
): {
key: string;
definition: ConditionMeterDefinition;
Expand All @@ -246,7 +248,7 @@ export function assetMeters(
}
},
},
markedAbilities,
asset.abilities.map(({ enabled }) => enabled),
);

return meters.map(([key, control]) => {
Expand Down
4 changes: 4 additions & 0 deletions src/characters/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Base class for errors indicating an invalid character. */

export class InvalidCharacterError extends Error {}
export class MissingAssetError extends InvalidCharacterError {}
98 changes: 69 additions & 29 deletions src/characters/lens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import starforgedData from "@datasworn/starforged/json/starforged.json" with { t
import { IDataContext, MockDataContext } from "datastore/data-context";
import { Ruleset } from "../rules/ruleset";
import { ChallengeRanks } from "../tracks/progress";
import { Right } from "../utils/either";
import { Lens, updating } from "../utils/lens";
import {
BaseIronVaultSchema,
Expand Down Expand Up @@ -283,7 +282,7 @@ describe("movesReader", () => {
movesReader(lens, mockDataContext).get(
validater({ ...VALID_INPUT }).unwrap(),
),
).toEqual(Right.create([]));
).toEqual([]);
});

it("does not include moves for unmarked asset abilities", () => {
Expand All @@ -299,42 +298,38 @@ describe("movesReader", () => {
],
} satisfies BaseIronVaultSchema).unwrap(),
),
).toEqual(Right.create([]));
).toEqual([]);
});

it("includes moves for marked asset abilities", () => {
// This ability has no additional moves.
expect(
movesReader(lens, mockDataContext)
.get(
validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/path/empath",
abilities: [false, true, false],
},
],
} satisfies BaseIronVaultSchema).unwrap(),
)
.unwrap(),
movesReader(lens, mockDataContext).get(
validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/path/empath",
abilities: [false, true, false],
},
],
} satisfies BaseIronVaultSchema).unwrap(),
),
).toHaveLength(0);

// This ability adds one extra move.
expect(
movesReader(lens, mockDataContext)
.get(
validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/path/empath",
abilities: [true, true, false],
},
],
} satisfies BaseIronVaultSchema).unwrap(),
)
.unwrap(),
movesReader(lens, mockDataContext).get(
validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/path/empath",
abilities: [true, true, false],
},
],
} satisfies BaseIronVaultSchema).unwrap(),
),
).toMatchObject([
{
move: {
Expand All @@ -354,6 +349,8 @@ describe("meterLenses", () => {
mockDataContext = createMockDataContext(
starforgedData.assets.companion.contents
.protocol_bot as unknown as Datasworn.Asset,
starforgedData.assets.companion.contents
.symbiote as unknown as Datasworn.Asset,
);
});

Expand Down Expand Up @@ -398,13 +395,56 @@ describe("meterLenses", () => {

expect(meters.length).toBe(new Set(meters.map(({ key }) => key)).size);
});

it("updates meters according to enhance_assets", () => {
const character1 = validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/companion/symbiote",
abilities: [true, false, false],
},
],
}).expect("valid character");
const result1 = meterLenses(lens, character1, mockDataContext);
expect(result1).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "asset:starforged/companion/symbiote@health",
parent: { label: "Symbiote" },
definition: expect.objectContaining({ max: 2 }),
}),
]),
);

const character2 = validater({
...VALID_INPUT,
assets: [
{
id: "asset:starforged/companion/symbiote",
abilities: [true, false, true],
},
],
}).expect("valid character");
const result2 = meterLenses(lens, character2, mockDataContext);
expect(result2).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "asset:starforged/companion/symbiote@health",
parent: { label: "Symbiote" },
definition: expect.objectContaining({ max: 3 }),
}),
]),
);
});
});

describe("Special Tracks", () => {
const { validater, lens } = characterLens(
TEST_RULESET.merge({
_id: "moar",
type: "expansion",
ruleset: "test",
rules: {
special_tracks: {
quests_legacy: {
Expand Down
Loading

0 comments on commit f11635e

Please sign in to comment.