Skip to content

Commit

Permalink
Make "Advance clock" command support rolling instead of just advancing (
Browse files Browse the repository at this point in the history
#464)

Fixes #220
  • Loading branch information
cwegrzyn committed Aug 22, 2024
1 parent 14c30aa commit c2066c1
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 23 deletions.
2 changes: 2 additions & 0 deletions docs/Commands/Advance a clock.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Advances a [[Clocks|Clock]], modifying its frontmatter, and inserts a [[Mechanics Blocks#`clock`|`clock` mechanics node]] into your active journal. Clocks which are already filled, or which are completed, cannot be advanced (and will not show up at all in the picker for the command).

If your clock has a "default odds" value other than "no roll", before the clock is advanced, you will be prompted to pick an odds for advancing the clock, with your "default odds" value preselected. Picking "certain" will advance the clock without a roll; otherwise, you can choose a different odds value as the situation warrants, or hit enter to roll with your default odds. A 1d100 roll will be made, and if the resulting value is less than or equal to the chosen odds, the clock will be advanced.

Advancing a Clock to completion will not automatically be marked as resolved. You will be asked if you wish to mark the clock as resolved. If you choose not to resolve it now, you can always resolve it as needed with the [[Resolve a clock]] command.

![[Mechanics Blocks#`clock`#Example|iv-embed]]
Expand Down
7 changes: 7 additions & 0 deletions docs/Commands/Create a clock.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
Creates a [[Entities/Clocks|Clock]] file and appends a [[Mechanics Blocks#`clock`|`clock` mechanics node]] to the current journal. The Clock file will have all necessary frontmatter for commands like [[Advance a clock]] to work.

When creating a clock, you'll need to set two clock-specific options:

* Number of segments in the clock
* The "default odds" for the clock:
* If this is "no roll" (the default), then your clock will always be advanced automatically when you [[Advance a clock]].
* If you pick a different odds value, when you [[Advance a clock]], you will be presented with the option to make a roll whenever you advance, and the chosen value will be preselected.

This command will also write a [[Clock Blocks|Clock Block]] to the newly-made Clock file:

![[Mechanics Blocks#`clock`#Example|iv-embed]]
Expand Down
15 changes: 8 additions & 7 deletions docs/Entities/Clocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ You can create a clock using the [[Create a clock]] command. To advance the cloc

The following schema describes the expected structure of clock file frontmatter in order for Iron Vault to be able to treat it as a clock. It is automatically generated by the [[Create a clock]] command, and modified by the [[Advance a clock]] command.

| Key | Type | About |
| --------------- | ---------------------- | ------------------------------------------------------------------ |
| name | text | Name of the clock, or a brief description of what it's for. |
| segments | number | Total number of segments in the clock. |
| progress | number | Number of *filled in* segments in the clock. |
| tags | tags | Either `complete` or `incomplete`. Completed clocks are read-only. |
| iron-vault-kind | text (must be "clock") | Marks this as a clock file. |
| Key | Type | About |
| ----------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `name` | text | Name of the clock, or a brief description of what it's for. |
| `segments` | number | Total number of segments in the clock. |
| `progress` | number | Number of *filled in* segments in the clock. |
| `tags` | tags | Either `complete` or `incomplete`. Completed clocks are read-only. |
| `iron-vault-kind` | text (must be "clock") | Marks this as a clock file. |
| `default-odds` | (optional) "small chance" \| "unlikely" \| "50 50" \| "likely" \| "almost certain" \| "certain" \| "no roll" | If present, sets the default odds to advance the clock. If `"no roll"`, you will never be prompted to roll. |
128 changes: 124 additions & 4 deletions src/clocks/clock-create-modal.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { CampaignFile } from "campaigns/entity";
import IronVaultPlugin from "index";
import {
AbstractInputSuggest,
App,
ButtonComponent,
FuzzyMatch,
Modal,
Setting,
TextComponent,
debounce,
prepareFuzzySearch,
} from "obsidian";
import { generateObsidianFilename } from "utils/filename";
import { capitalize } from "utils/strings";
import { processMatches } from "utils/suggest";
import { CampaignSelectComponent } from "utils/ui/settings/campaign-suggest";
import { RelativeFolderSearchComponent } from "utils/ui/settings/relative-folder-search";
import { Clock } from "./clock";
import { ClockOdds, STANDARD_ODDS } from "./clock-file";

export type ClockCreateResultType = {
segments: number;
name: string;
fileName: string;
targetFolder: string;
defaultOdds: ClockOdds | undefined;
};

export class ClockCreateModal extends Modal {
Expand All @@ -25,17 +33,35 @@ export class ClockCreateModal extends Modal {
name: "",
fileName: "",
targetFolder: "",
defaultOdds: "no roll",
};

public accepted: boolean = false;

static async show(plugin: IronVaultPlugin): Promise<{
name: string;
targetFolder: string;
fileName: string;
defaultOdds: ClockOdds | undefined;
clock: Clock;
}> {
return await new Promise((onAccept, onReject) => {
try {
new ClockCreateModal(plugin, {}, onAccept, onReject).open();
} catch (e) {
onReject(e);
}
});
}

constructor(
readonly plugin: IronVaultPlugin,
defaults: Partial<ClockCreateResultType> = {},
protected readonly onAccept: (arg: {
name: string;
targetFolder: string;
fileName: string;
defaultOdds: ClockOdds | undefined;
clock: Clock;
}) => void,
protected readonly onCancel: () => void,
Expand Down Expand Up @@ -63,7 +89,8 @@ export class ClockCreateModal extends Modal {
const validate = debounce(() => {
const valid =
this.result.name.trim().length > 0 &&
this.result.fileName.trim().length > 0;
this.result.fileName.trim().length > 0 &&
this.result.defaultOdds != null;
createButton.setDisabled(!valid);
}, 0);

Expand Down Expand Up @@ -133,6 +160,33 @@ export class ClockCreateModal extends Modal {
}),
);

const frag = new DocumentFragment();
frag.append(
"Default odds to roll when advancing this clock.",
document.createElement("br"),
"Choose 'no roll' if this clock should advance unconditionally (without prompting).",
);
new Setting(contentEl)
.setName("Default odds")
.setDesc(frag)
.addSearch((search) => {
new OddsSuggest(this.app, search.inputEl);

search
.setValue(String(this.result.defaultOdds ?? ""))
.onChange((newOdds) => {
newOdds = newOdds.toLowerCase();
if (newOdds in STANDARD_ODDS) {
this.result.defaultOdds = newOdds as keyof typeof STANDARD_ODDS;
} else if (newOdds == "no roll") {
this.result.defaultOdds = "no roll";
} else {
this.result.defaultOdds = undefined;
}
validate();
});
});

onChangeCampaign();
validate();

Expand All @@ -158,9 +212,7 @@ export class ClockCreateModal extends Modal {
this.accepted = true;
this.close();
this.onAccept({
name: this.result.name,
fileName: this.result.fileName,
targetFolder: this.result.targetFolder,
...this.result,
clock: Clock.create({
name: this.result.name,
progress: 0,
Expand All @@ -177,3 +229,71 @@ export class ClockCreateModal extends Modal {
}
}
}

export class OddsSuggest extends AbstractInputSuggest<FuzzyMatch<string>> {
readonly items = [...Object.keys(STANDARD_ODDS), "no roll"];

constructor(
app: App,
readonly inputEl: HTMLInputElement,
) {
super(app, inputEl);
}

getSuggestions(inputStr: string): FuzzyMatch<string>[] {
const numberValue = Number.parseInt(inputStr);
if (!Number.isNaN(numberValue)) {
if (numberValue >= 0 && numberValue <= 100) {
return [
{ item: numberValue.toString(), match: { matches: [], score: 100 } },
];
} else {
return [];
}
}
const searchFn = prepareFuzzySearch(inputStr);
return this.items
.flatMap((item) => {
const match = searchFn(item);
if (match) {
return [{ item, match }];
} else {
return [];
}
})
.sort((a, b) => a.match.score - b.match.score);
}

renderSuggestion({ item, match }: FuzzyMatch<string>, el: HTMLElement): void {
if (item == null) return;

el.createDiv(undefined, (div) => {
processMatches(
capitalize(item),
match,
(text) => {
div.appendText(text);
},
(text) => {
div.createEl("strong", { text });
},
);

if (item.toLowerCase() in STANDARD_ODDS) {
div.appendText(
` (${(STANDARD_ODDS as Record<string, number>)[item.toLowerCase()]}%)`,
);
}
});
// if (renderExtras != null) {
// renderExtras(match, el);
// }
}

selectSuggestion({ item }: FuzzyMatch<string>): void {
this.setValue(item);
if (this.inputEl.instanceOf(HTMLInputElement))
this.inputEl.trigger("input");
this.close();
}
}
49 changes: 49 additions & 0 deletions src/clocks/clock-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,40 @@ import { Either, Left } from "../utils/either";
import { updater } from "../utils/update";
import { Clock } from "./clock";

export const clockOddsSchema = z.enum([
"small chance",
"unlikely",
"50 50",
"likely",
"almost certain",
"certain",
"no roll",
]);

export const namedOddsSchema = clockOddsSchema.exclude(["no roll"]);

export type OddsTable = Record<z.infer<typeof namedOddsSchema>, number>;

export const STANDARD_ODDS: OddsTable = {
"small chance": 10,
unlikely: 25,
"50 50": 50,
likely: 75,
"almost certain": 90,
certain: 100,
};

export type ClockOdds = z.infer<typeof clockOddsSchema>;

const clockSchema = z
.object({
name: z.string(),
segments: z.number().positive(),
progress: z.number().nonnegative(),

/** Default odds of advancing the clock. Choose 'no roll' if you do not wish to be prompted ever. */
"default-odds": clockOddsSchema.optional(),

tags: z
.union([z.string().transform((arg) => [arg]), z.array(z.string())])
.refine(
Expand All @@ -33,6 +62,7 @@ const clockSchema = z

export const normalizedClockSchema = normalizeKeys(clockSchema);

export type ClockInputSchema = z.input<typeof clockSchema>;
export type ClockSchema = z.output<typeof clockSchema>;

export class ClockFileAdapter {
Expand All @@ -48,14 +78,17 @@ export class ClockFileAdapter {
static newFromClock({
name,
clock,
defaultOdds,
}: {
name: string;
clock: Clock;
defaultOdds: ClockOdds | undefined;
}): Either<z.ZodError, ClockFileAdapter> {
return this.create({
name,
segments: clock.segments,
progress: clock.progress,
"default-odds": defaultOdds,
tags: !clock.active ? ["complete"] : ["incomplete"],
[PLUGIN_KIND_FIELD]: IronVaultKind.Clock,
} satisfies z.input<typeof clockSchema>);
Expand Down Expand Up @@ -100,6 +133,22 @@ export class ClockFileAdapter {
other,
);
}

/** Returns the normalized numeric default odds for this clock, or */
normalizedOdds(
oddsTable: OddsTable = STANDARD_ODDS,
): number | "no roll" | undefined {
const defaultOdds = this.raw["default-odds"];
if (
defaultOdds === undefined ||
defaultOdds == "no roll" ||
typeof defaultOdds == "number"
) {
return defaultOdds;
} else {
return oddsTable[defaultOdds];
}
}
}

export class ClockIndexer extends BaseIndexer<ClockFileAdapter, z.ZodError> {
Expand Down
Loading

0 comments on commit c2066c1

Please sign in to comment.