Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make "Advance clock" command support rolling instead of just advancing #464

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading