mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
rename world features to features so it fits
This commit is contained in:
parent
b1da4c3115
commit
08f67d00de
4 changed files with 2062 additions and 1681 deletions
|
|
@ -1,452 +1,543 @@
|
|||
import { App, Notice, PluginSettingTab, Setting, TFile } from "obsidian";
|
||||
import type DuckmagePlugin from "./DuckmagePlugin";
|
||||
import { normalizeFolder } from "./utils";
|
||||
|
||||
export class DuckmageSettingTab extends PluginSettingTab {
|
||||
plugin: DuckmagePlugin;
|
||||
|
||||
constructor(app: App, plugin: DuckmagePlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("World notes folder")
|
||||
.setDesc("Vault-relative path. Scopes the file search when adding links to hexes.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world")
|
||||
.setValue(this.plugin.settings.worldFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.worldFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Set up folders")
|
||||
.setDesc("Populates any blank folder settings below with defaults under the world folder, then creates those folders in your vault. Only blank fields are affected — manually set values are left untouched.")
|
||||
.addButton(btn =>
|
||||
btn.setButtonText("Generate folders").setCta().onClick(async () => {
|
||||
const world = normalizeFolder(this.plugin.settings.worldFolder) || "world";
|
||||
const defaults: [keyof typeof this.plugin.settings, string][] = [
|
||||
["hexFolder", `${world}/hexes`],
|
||||
["townsFolder", `${world}/towns`],
|
||||
["dungeonsFolder", `${world}/dungeons`],
|
||||
["questsFolder", `${world}/quests`],
|
||||
["featuresFolder", `${world}/features`],
|
||||
["factionsFolder", `${world}/factions`],
|
||||
["tablesFolder", `${world}/tables`],
|
||||
];
|
||||
for (const [key, path] of defaults) {
|
||||
if (!this.plugin.settings[key]) {
|
||||
(this.plugin.settings as unknown as Record<string, unknown>)[key] = path;
|
||||
try {
|
||||
if (!this.app.vault.getAbstractFileByPath(path)) {
|
||||
await this.app.vault.createFolder(path);
|
||||
}
|
||||
} catch { /* folder already exists */ }
|
||||
}
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
new Notice("Folders generated.");
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex notes folder")
|
||||
.setDesc("Vault-relative path where hex notes (x_y.md) are stored.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/hexes")
|
||||
.setValue(this.plugin.settings.hexFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Towns folder")
|
||||
.setDesc("Vault-relative folder to populate the Towns dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/towns")
|
||||
.setValue(this.plugin.settings.townsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.townsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Dungeons folder")
|
||||
.setDesc("Vault-relative folder to populate the Dungeons dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/dungeons")
|
||||
.setValue(this.plugin.settings.dungeonsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.dungeonsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Quests folder")
|
||||
.setDesc("Vault-relative folder to populate the Quests dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/quests")
|
||||
.setValue(this.plugin.settings.questsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.questsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Features folder")
|
||||
.setDesc("Vault-relative folder to populate the Features dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/features")
|
||||
.setValue(this.plugin.settings.featuresFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.featuresFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Factions folder")
|
||||
.setDesc("Vault-relative folder to populate the Factions dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/factions")
|
||||
.setValue(this.plugin.settings.factionsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.factionsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Tables folder")
|
||||
.setDesc("Vault-relative folder for random table notes. Used by the Encounters Table section and the Random Tables view.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/tables")
|
||||
.setValue(this.plugin.settings.tablesFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.tablesFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default die for new tables")
|
||||
.setDesc("Die size used when creating new random table notes (d6, d20, d100, etc.).")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("4", "d4")
|
||||
.addOption("6", "d6")
|
||||
.addOption("8", "d8")
|
||||
.addOption("10", "d10")
|
||||
.addOption("12", "d12")
|
||||
.addOption("20", "d20")
|
||||
.addOption("100", "d100")
|
||||
.addOption("200", "d200")
|
||||
.addOption("500", "d500")
|
||||
.addOption("1000", "d1000")
|
||||
.setValue(String(this.plugin.settings.defaultTableDice ?? 100))
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.defaultTableDice = parseInt(value, 10);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Generate world data" });
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description duckmage-generate-warning",
|
||||
text: "⚠️ Configure all folder settings above before selecting Generate. This will create terrain table notes, add roller links to all table notes, and link each hex note to its terrain's encounters table. Safe to run multiple times — existing notes and links are not overwritten.",
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Generate terrain tables & hex links")
|
||||
.setDesc("Creates missing terrain table notes, adds roller links to all table notes (so they can be opened in the Duckmage Roller from within Obsidian), and links each hex note's terrain encounters table into its Encounters Table section.")
|
||||
.addButton(btn =>
|
||||
btn.setButtonText("Generate").setCta().onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
btn.setButtonText("Generating…");
|
||||
try {
|
||||
await this.plugin.ensureTerrainTables();
|
||||
await this.plugin.ensureAllRollerLinks();
|
||||
await this.plugin.backfillTerrainLinks();
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
btn.setButtonText("Generate");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex editor sections start collapsed")
|
||||
.setDesc("Choose which sections open collapsed by default in the right-click hex editor.")
|
||||
.then(setting => {
|
||||
const addCb = (label: string, get: () => boolean, set: (v: boolean) => void) => {
|
||||
const lbl = setting.controlEl.createEl("label", { cls: "duckmage-collapse-cb-label" });
|
||||
const cb = lbl.createEl("input") as HTMLInputElement;
|
||||
cb.type = "checkbox";
|
||||
cb.checked = get();
|
||||
cb.addEventListener("change", async () => { set(cb.checked); await this.plugin.saveSettings(); });
|
||||
lbl.appendText(label);
|
||||
};
|
||||
addCb("Terrain", () => this.plugin.settings.hexEditorTerrainCollapsed, v => { this.plugin.settings.hexEditorTerrainCollapsed = v; });
|
||||
addCb("World features", () => this.plugin.settings.hexEditorFeaturesCollapsed, v => { this.plugin.settings.hexEditorFeaturesCollapsed = v; });
|
||||
addCb("Notes", () => this.plugin.settings.hexEditorNotesCollapsed, v => { this.plugin.settings.hexEditorNotesCollapsed = v; });
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Template path")
|
||||
.setDesc("Vault-relative path to a hex note template. Supports {{x}}, {{y}}, {{title}}. Include ## Towns, ## Dungeons, and ## Features headings for the link sections.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("templates/hex.md")
|
||||
.setValue(this.plugin.settings.templatePath)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.templatePath = (value ?? "").replace(/^\/+|\/+$/g, "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex orientation")
|
||||
.setDesc("Pointy-top: points face north/south, flat sides east/west. Flat-top: flat sides face north/south, points east/west.")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("pointy", "Pointy-top")
|
||||
.addOption("flat", "Flat-top")
|
||||
.setValue(this.plugin.settings.hexOrientation ?? "pointy")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexOrientation = value as "pointy" | "flat";
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Grid width")
|
||||
.setDesc("Number of hex columns.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("20")
|
||||
.setValue(String(this.plugin.settings.gridSize.cols))
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.gridSize.cols = Number(value.trim()) || 20;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Grid height")
|
||||
.setDesc("Number of hex rows.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("16")
|
||||
.setValue(String(this.plugin.settings.gridSize.rows))
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.gridSize.rows = Number(value.trim()) || 16;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex cell spacing")
|
||||
.setDesc("Gap between hex cells (0 – 0.5 em).")
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(0, 0.5, 0.01)
|
||||
.setValue(parseFloat(this.plugin.settings.hexGap ?? "0.15") || 0.15)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexGap = String(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Custom icons folder")
|
||||
.setDesc("Vault-relative folder containing additional icon images (PNG, JPG, SVG, etc.) for the terrain palette and hex icon override. These are merged with the built-in icons.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("icons")
|
||||
.setValue(this.plugin.settings.iconsFolder ?? "")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.iconsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.loadAvailableIcons();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Roads & rivers" });
|
||||
new Setting(containerEl)
|
||||
.setName("Road color")
|
||||
.setDesc("Color used to draw road lines between connected road hexes.")
|
||||
.addColorPicker(color =>
|
||||
color
|
||||
.setValue(this.plugin.settings.roadColor ?? "#a16207")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.roadColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("River color")
|
||||
.setDesc("Color used to draw river lines between connected river hexes.")
|
||||
.addColorPicker(color =>
|
||||
color
|
||||
.setValue(this.plugin.settings.riverColor ?? "#3b82f6")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.riverColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Terrain palette" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Right-click a hex to set terrain. Each type can have a fill color and an icon from the plugin's icons folder or your custom icons folder.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
const listEl = containerEl.createDiv({ cls: "duckmage-palette-list" });
|
||||
const palette = this.plugin.settings.terrainPalette ?? [];
|
||||
|
||||
let dragSrcIndex = -1;
|
||||
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
const entry = palette[i];
|
||||
const itemEl = listEl.createDiv({ cls: "duckmage-palette-item" });
|
||||
itemEl.draggable = true;
|
||||
|
||||
// Drag handle
|
||||
itemEl.createSpan({ cls: "duckmage-palette-drag-handle", text: "⠿" });
|
||||
|
||||
itemEl.addEventListener("dragstart", (e: DragEvent) => {
|
||||
dragSrcIndex = i;
|
||||
itemEl.addClass("duckmage-palette-dragging");
|
||||
e.dataTransfer?.setDragImage(itemEl, 0, 0);
|
||||
});
|
||||
itemEl.addEventListener("dragend", () => {
|
||||
itemEl.removeClass("duckmage-palette-dragging");
|
||||
listEl.querySelectorAll(".duckmage-palette-drop-target").forEach(el =>
|
||||
el.classList.remove("duckmage-palette-drop-target"),
|
||||
);
|
||||
});
|
||||
itemEl.addEventListener("dragover", (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
listEl.querySelectorAll(".duckmage-palette-drop-target").forEach(el =>
|
||||
el.classList.remove("duckmage-palette-drop-target"),
|
||||
);
|
||||
itemEl.addClass("duckmage-palette-drop-target");
|
||||
});
|
||||
itemEl.addEventListener("drop", async (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
const dropIndex = i;
|
||||
if (dragSrcIndex === -1 || dragSrcIndex === dropIndex) return;
|
||||
const pal = this.plugin.settings.terrainPalette;
|
||||
const [moved] = pal.splice(dragSrcIndex, 1);
|
||||
pal.splice(dropIndex, 0, moved);
|
||||
dragSrcIndex = -1;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
|
||||
new Setting(itemEl)
|
||||
.addText(text => {
|
||||
let nameBeforeEdit = entry.name;
|
||||
text
|
||||
.setPlaceholder("Name")
|
||||
.setValue(entry.name)
|
||||
.onChange(async value => {
|
||||
const trimmed = (value ?? "").trim();
|
||||
const isDuplicate = trimmed.length > 0 && this.plugin.settings.terrainPalette.some(
|
||||
(e) => e !== entry && e.name.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (isDuplicate) {
|
||||
text.inputEl.addClass("duckmage-input-error");
|
||||
text.inputEl.title = `"${trimmed}" is already used by another terrain.`;
|
||||
return;
|
||||
}
|
||||
text.inputEl.removeClass("duckmage-input-error");
|
||||
text.inputEl.title = "";
|
||||
entry.name = trimmed || entry.name;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.addEventListener("focus", () => { nameBeforeEdit = entry.name; });
|
||||
text.inputEl.addEventListener("blur", async () => {
|
||||
if (text.inputEl.hasClass("duckmage-input-error")) return;
|
||||
if (entry.name !== nameBeforeEdit) {
|
||||
await this.renameTerrainTables(nameBeforeEdit, entry.name);
|
||||
}
|
||||
});
|
||||
})
|
||||
.addColorPicker(color =>
|
||||
color.setValue(entry.color).onChange(async value => {
|
||||
entry.color = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshHexMap();
|
||||
}),
|
||||
)
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption("", "— no icon —");
|
||||
for (const icon of this.plugin.availableIcons) {
|
||||
const label = icon.replace(/^bw-/, "").replace(/\.png$/, "").replace(/-/g, " ");
|
||||
dropdown.addOption(icon, label);
|
||||
}
|
||||
dropdown.setValue(entry.icon ?? "");
|
||||
dropdown.onChange(async value => {
|
||||
entry.icon = value || undefined;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshHexMap();
|
||||
});
|
||||
})
|
||||
.addExtraButton(btn =>
|
||||
btn.setIcon("trash-2").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.splice(i, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl).addButton(btn =>
|
||||
btn.setButtonText("Add terrain type").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.push({ name: "New", color: "#888888" });
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.ensureTerrainTables();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getTerrainTablePath(terrainName: string, tableType: "description" | "encounters"): string {
|
||||
const folder = normalizeFolder(this.plugin.settings.tablesFolder);
|
||||
const subfolder = folder ? `${folder}/terrain` : "terrain";
|
||||
return `${subfolder}/${tableType}/${terrainName}.md`;
|
||||
}
|
||||
|
||||
private async renameTerrainTables(oldName: string, newName: string): Promise<void> {
|
||||
for (const tableType of ["description", "encounters"] as const) {
|
||||
const oldPath = this.getTerrainTablePath(oldName, tableType);
|
||||
const newPath = this.getTerrainTablePath(newName, tableType);
|
||||
const file = this.app.vault.getAbstractFileByPath(oldPath);
|
||||
if (file instanceof TFile) {
|
||||
try { await this.app.vault.rename(file, newPath); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import { App, Notice, PluginSettingTab, Setting, TFile } from "obsidian";
|
||||
import type DuckmagePlugin from "./DuckmagePlugin";
|
||||
import { normalizeFolder } from "./utils";
|
||||
|
||||
export class DuckmageSettingTab extends PluginSettingTab {
|
||||
plugin: DuckmagePlugin;
|
||||
|
||||
constructor(app: App, plugin: DuckmagePlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("World notes folder")
|
||||
.setDesc(
|
||||
"Vault-relative path. Scopes the file search when adding links to hexes.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world")
|
||||
.setValue(this.plugin.settings.worldFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.worldFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Set up folders")
|
||||
.setDesc(
|
||||
"Populates any blank folder settings below with defaults under the world folder, then creates those folders in your vault. Only blank fields are affected — manually set values are left untouched.",
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Generate folders")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const world =
|
||||
normalizeFolder(this.plugin.settings.worldFolder) || "world";
|
||||
const defaults: [keyof typeof this.plugin.settings, string][] = [
|
||||
["hexFolder", `${world}/hexes`],
|
||||
["townsFolder", `${world}/towns`],
|
||||
["dungeonsFolder", `${world}/dungeons`],
|
||||
["questsFolder", `${world}/quests`],
|
||||
["featuresFolder", `${world}/features`],
|
||||
["factionsFolder", `${world}/factions`],
|
||||
["tablesFolder", `${world}/tables`],
|
||||
];
|
||||
for (const [key, path] of defaults) {
|
||||
if (!this.plugin.settings[key]) {
|
||||
(this.plugin.settings as unknown as Record<string, unknown>)[
|
||||
key
|
||||
] = path;
|
||||
try {
|
||||
if (!this.app.vault.getAbstractFileByPath(path)) {
|
||||
await this.app.vault.createFolder(path);
|
||||
}
|
||||
} catch {
|
||||
/* folder already exists */
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
new Notice("Folders generated.");
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex notes folder")
|
||||
.setDesc("Vault-relative path where hex notes (x_y.md) are stored.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/hexes")
|
||||
.setValue(this.plugin.settings.hexFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Towns folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Towns dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/towns")
|
||||
.setValue(this.plugin.settings.townsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.townsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Dungeons folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Dungeons dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/dungeons")
|
||||
.setValue(this.plugin.settings.dungeonsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dungeonsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Quests folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Quests dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/quests")
|
||||
.setValue(this.plugin.settings.questsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.questsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Features folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Features dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/features")
|
||||
.setValue(this.plugin.settings.featuresFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.featuresFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Factions folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Factions dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/factions")
|
||||
.setValue(this.plugin.settings.factionsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.factionsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Tables folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder for random table notes. Used by the Encounters Table section and the Random Tables view.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/tables")
|
||||
.setValue(this.plugin.settings.tablesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.tablesFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default die for new tables")
|
||||
.setDesc(
|
||||
"Die size used when creating new random table notes (d6, d20, d100, etc.).",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("4", "d4")
|
||||
.addOption("6", "d6")
|
||||
.addOption("8", "d8")
|
||||
.addOption("10", "d10")
|
||||
.addOption("12", "d12")
|
||||
.addOption("20", "d20")
|
||||
.addOption("100", "d100")
|
||||
.addOption("200", "d200")
|
||||
.addOption("500", "d500")
|
||||
.addOption("1000", "d1000")
|
||||
.setValue(String(this.plugin.settings.defaultTableDice ?? 100))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultTableDice = parseInt(value, 10);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Generate world data" });
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description duckmage-generate-warning",
|
||||
text: "⚠️ Configure all folder settings above before selecting Generate. This will create terrain table notes, add roller links to all table notes, and link each hex note to its terrain's encounters table. Safe to run multiple times — existing notes and links are not overwritten.",
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Generate terrain tables & hex links")
|
||||
.setDesc(
|
||||
"Creates missing terrain table notes, adds roller links to all table notes (so they can be opened in the Duckmage Roller from within Obsidian), and links each hex note's terrain encounters table into its Encounters Table section.",
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Generate")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
btn.setButtonText("Generating…");
|
||||
try {
|
||||
await this.plugin.ensureTerrainTables();
|
||||
await this.plugin.ensureAllRollerLinks();
|
||||
await this.plugin.backfillTerrainLinks();
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
btn.setButtonText("Generate");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex editor sections start collapsed")
|
||||
.setDesc(
|
||||
"Choose which sections open collapsed by default in the right-click hex editor.",
|
||||
)
|
||||
.then((setting) => {
|
||||
const addCb = (
|
||||
label: string,
|
||||
get: () => boolean,
|
||||
set: (v: boolean) => void,
|
||||
) => {
|
||||
const lbl = setting.controlEl.createEl("label", {
|
||||
cls: "duckmage-collapse-cb-label",
|
||||
});
|
||||
const cb = lbl.createEl("input") as HTMLInputElement;
|
||||
cb.type = "checkbox";
|
||||
cb.checked = get();
|
||||
cb.addEventListener("change", async () => {
|
||||
set(cb.checked);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
lbl.appendText(label);
|
||||
};
|
||||
addCb(
|
||||
"Terrain",
|
||||
() => this.plugin.settings.hexEditorTerrainCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorTerrainCollapsed = v;
|
||||
},
|
||||
);
|
||||
addCb(
|
||||
"Features",
|
||||
() => this.plugin.settings.hexEditorFeaturesCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorFeaturesCollapsed = v;
|
||||
},
|
||||
);
|
||||
addCb(
|
||||
"Notes",
|
||||
() => this.plugin.settings.hexEditorNotesCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorNotesCollapsed = v;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Template path")
|
||||
.setDesc(
|
||||
"Vault-relative path to a hex note template. Supports {{x}}, {{y}}, {{title}}. Include ## Towns, ## Dungeons, and ## Features headings for the link sections.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("templates/hex.md")
|
||||
.setValue(this.plugin.settings.templatePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templatePath = (value ?? "").replace(
|
||||
/^\/+|\/+$/g,
|
||||
"",
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex orientation")
|
||||
.setDesc(
|
||||
"Pointy-top: points face north/south, flat sides east/west. Flat-top: flat sides face north/south, points east/west.",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("pointy", "Pointy-top")
|
||||
.addOption("flat", "Flat-top")
|
||||
.setValue(this.plugin.settings.hexOrientation ?? "pointy")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexOrientation = value as "pointy" | "flat";
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Grid width")
|
||||
.setDesc("Number of hex columns.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("20")
|
||||
.setValue(String(this.plugin.settings.gridSize.cols))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.gridSize.cols = Number(value.trim()) || 20;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Grid height")
|
||||
.setDesc("Number of hex rows.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("16")
|
||||
.setValue(String(this.plugin.settings.gridSize.rows))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.gridSize.rows = Number(value.trim()) || 16;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex cell spacing")
|
||||
.setDesc("Gap between hex cells (0 – 0.5 em).")
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 0.5, 0.01)
|
||||
.setValue(parseFloat(this.plugin.settings.hexGap ?? "0.15") || 0.15)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexGap = String(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Custom icons folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder containing additional icon images (PNG, JPG, SVG, etc.) for the terrain palette and hex icon override. These are merged with the built-in icons.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("icons")
|
||||
.setValue(this.plugin.settings.iconsFolder ?? "")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.iconsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.loadAvailableIcons();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Roads & rivers" });
|
||||
new Setting(containerEl)
|
||||
.setName("Road color")
|
||||
.setDesc("Color used to draw road lines between connected road hexes.")
|
||||
.addColorPicker((color) =>
|
||||
color
|
||||
.setValue(this.plugin.settings.roadColor ?? "#a16207")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.roadColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("River color")
|
||||
.setDesc("Color used to draw river lines between connected river hexes.")
|
||||
.addColorPicker((color) =>
|
||||
color
|
||||
.setValue(this.plugin.settings.riverColor ?? "#3b82f6")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.riverColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
containerEl.createEl("h3", { text: "Terrain palette" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Right-click a hex to set terrain. Each type can have a fill color and an icon from the plugin's icons folder or your custom icons folder.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
const listEl = containerEl.createDiv({ cls: "duckmage-palette-list" });
|
||||
const palette = this.plugin.settings.terrainPalette ?? [];
|
||||
|
||||
let dragSrcIndex = -1;
|
||||
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
const entry = palette[i];
|
||||
const itemEl = listEl.createDiv({ cls: "duckmage-palette-item" });
|
||||
itemEl.draggable = true;
|
||||
|
||||
// Drag handle
|
||||
itemEl.createSpan({ cls: "duckmage-palette-drag-handle", text: "⠿" });
|
||||
|
||||
itemEl.addEventListener("dragstart", (e: DragEvent) => {
|
||||
dragSrcIndex = i;
|
||||
itemEl.addClass("duckmage-palette-dragging");
|
||||
e.dataTransfer?.setDragImage(itemEl, 0, 0);
|
||||
});
|
||||
itemEl.addEventListener("dragend", () => {
|
||||
itemEl.removeClass("duckmage-palette-dragging");
|
||||
listEl
|
||||
.querySelectorAll(".duckmage-palette-drop-target")
|
||||
.forEach((el) => el.classList.remove("duckmage-palette-drop-target"));
|
||||
});
|
||||
itemEl.addEventListener("dragover", (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
listEl
|
||||
.querySelectorAll(".duckmage-palette-drop-target")
|
||||
.forEach((el) => el.classList.remove("duckmage-palette-drop-target"));
|
||||
itemEl.addClass("duckmage-palette-drop-target");
|
||||
});
|
||||
itemEl.addEventListener("drop", async (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
const dropIndex = i;
|
||||
if (dragSrcIndex === -1 || dragSrcIndex === dropIndex) return;
|
||||
const pal = this.plugin.settings.terrainPalette;
|
||||
const [moved] = pal.splice(dragSrcIndex, 1);
|
||||
pal.splice(dropIndex, 0, moved);
|
||||
dragSrcIndex = -1;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
|
||||
new Setting(itemEl)
|
||||
.addText((text) => {
|
||||
let nameBeforeEdit = entry.name;
|
||||
text
|
||||
.setPlaceholder("Name")
|
||||
.setValue(entry.name)
|
||||
.onChange(async (value) => {
|
||||
const trimmed = (value ?? "").trim();
|
||||
const isDuplicate =
|
||||
trimmed.length > 0 &&
|
||||
this.plugin.settings.terrainPalette.some(
|
||||
(e) =>
|
||||
e !== entry &&
|
||||
e.name.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (isDuplicate) {
|
||||
text.inputEl.addClass("duckmage-input-error");
|
||||
text.inputEl.title = `"${trimmed}" is already used by another terrain.`;
|
||||
return;
|
||||
}
|
||||
text.inputEl.removeClass("duckmage-input-error");
|
||||
text.inputEl.title = "";
|
||||
entry.name = trimmed || entry.name;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.addEventListener("focus", () => {
|
||||
nameBeforeEdit = entry.name;
|
||||
});
|
||||
text.inputEl.addEventListener("blur", async () => {
|
||||
if (text.inputEl.hasClass("duckmage-input-error")) return;
|
||||
if (entry.name !== nameBeforeEdit) {
|
||||
await this.renameTerrainTables(nameBeforeEdit, entry.name);
|
||||
}
|
||||
});
|
||||
})
|
||||
.addColorPicker((color) =>
|
||||
color.setValue(entry.color).onChange(async (value) => {
|
||||
entry.color = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshHexMap();
|
||||
}),
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("", "— no icon —");
|
||||
for (const icon of this.plugin.availableIcons) {
|
||||
const label = icon
|
||||
.replace(/^bw-/, "")
|
||||
.replace(/\.png$/, "")
|
||||
.replace(/-/g, " ");
|
||||
dropdown.addOption(icon, label);
|
||||
}
|
||||
dropdown.setValue(entry.icon ?? "");
|
||||
dropdown.onChange(async (value) => {
|
||||
entry.icon = value || undefined;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshHexMap();
|
||||
});
|
||||
})
|
||||
.addExtraButton((btn) =>
|
||||
btn.setIcon("trash-2").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.splice(i, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl).addButton((btn) =>
|
||||
btn.setButtonText("Add terrain type").onClick(async () => {
|
||||
this.plugin.settings.terrainPalette.push({
|
||||
name: "New",
|
||||
color: "#888888",
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.ensureTerrainTables();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getTerrainTablePath(
|
||||
terrainName: string,
|
||||
tableType: "description" | "encounters",
|
||||
): string {
|
||||
const folder = normalizeFolder(this.plugin.settings.tablesFolder);
|
||||
const subfolder = folder ? `${folder}/terrain` : "terrain";
|
||||
return `${subfolder}/${tableType}/${terrainName}.md`;
|
||||
}
|
||||
|
||||
private async renameTerrainTables(
|
||||
oldName: string,
|
||||
newName: string,
|
||||
): Promise<void> {
|
||||
for (const tableType of ["description", "encounters"] as const) {
|
||||
const oldPath = this.getTerrainTablePath(oldName, tableType);
|
||||
const newPath = this.getTerrainTablePath(newName, tableType);
|
||||
const file = this.app.vault.getAbstractFileByPath(oldPath);
|
||||
if (file instanceof TFile) {
|
||||
try {
|
||||
await this.app.vault.rename(file, newPath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,390 +3,500 @@ import type HexmakerPlugin from "./HexmakerPlugin";
|
|||
import { normalizeFolder } from "./utils";
|
||||
|
||||
export class HexmakerSettingTab extends PluginSettingTab {
|
||||
plugin: HexmakerPlugin;
|
||||
plugin: HexmakerPlugin;
|
||||
|
||||
constructor(app: App, plugin: HexmakerPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
constructor(app: App, plugin: HexmakerPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("World notes folder")
|
||||
.setDesc("Vault-relative path. Scopes the file search when adding links to hexes.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world")
|
||||
.setValue(this.plugin.settings.worldFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.worldFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("World notes folder")
|
||||
.setDesc(
|
||||
"Vault-relative path. Scopes the file search when adding links to hexes.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world")
|
||||
.setValue(this.plugin.settings.worldFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.worldFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Set up folders")
|
||||
.setDesc("Populates any blank folder settings below with defaults under the world folder, then creates those folders in your vault. Only blank fields are affected — manually set values are left untouched.")
|
||||
.addButton(btn =>
|
||||
btn.setButtonText("Generate folders").setCta().onClick(async () => {
|
||||
const world = normalizeFolder(this.plugin.settings.worldFolder) || "world";
|
||||
const defaults: [keyof typeof this.plugin.settings, string][] = [
|
||||
["hexFolder", `${world}/hexes`],
|
||||
["townsFolder", `${world}/towns`],
|
||||
["dungeonsFolder", `${world}/dungeons`],
|
||||
["questsFolder", `${world}/quests`],
|
||||
["featuresFolder", `${world}/features`],
|
||||
["factionsFolder", `${world}/factions`],
|
||||
["tablesFolder", `${world}/tables`],
|
||||
["workflowsFolder", `${world}/workflows`],
|
||||
];
|
||||
for (const [key, path] of defaults) {
|
||||
if (!this.plugin.settings[key]) {
|
||||
(this.plugin.settings as unknown as Record<string, unknown>)[key] = path;
|
||||
try {
|
||||
if (!this.app.vault.getAbstractFileByPath(path)) {
|
||||
await this.app.vault.createFolder(path);
|
||||
}
|
||||
} catch { /* folder already exists */ }
|
||||
}
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
// Ensure all region subfolders exist and generate hex notes
|
||||
const hexF = normalizeFolder(this.plugin.settings.hexFolder);
|
||||
if (hexF) {
|
||||
let totalCreated = 0;
|
||||
for (const region of this.plugin.settings.regions) {
|
||||
const regionFolder = `${hexF}/${region.name}`;
|
||||
if (!this.app.vault.getAbstractFileByPath(regionFolder)) {
|
||||
try { await this.app.vault.createFolder(regionFolder); } catch { /* exists */ }
|
||||
}
|
||||
const { cols, rows } = region.gridSize;
|
||||
const { x: ox, y: oy } = region.gridOffset;
|
||||
const xs = Array.from({ length: cols }, (_, i) => ox + i);
|
||||
const ys = Array.from({ length: rows }, (_, i) => oy + i);
|
||||
totalCreated += await this.plugin.generateHexNotes(region.name, xs, ys);
|
||||
}
|
||||
if (totalCreated > 0) new Notice(`Hexmaker: generated ${totalCreated} hex note${totalCreated !== 1 ? "s" : ""}.`);
|
||||
}
|
||||
new Notice("Folders generated.");
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Set up folders")
|
||||
.setDesc(
|
||||
"Populates any blank folder settings below with defaults under the world folder, then creates those folders in your vault. Only blank fields are affected — manually set values are left untouched.",
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Generate folders")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const world =
|
||||
normalizeFolder(this.plugin.settings.worldFolder) || "world";
|
||||
const defaults: [keyof typeof this.plugin.settings, string][] = [
|
||||
["hexFolder", `${world}/hexes`],
|
||||
["townsFolder", `${world}/towns`],
|
||||
["dungeonsFolder", `${world}/dungeons`],
|
||||
["questsFolder", `${world}/quests`],
|
||||
["featuresFolder", `${world}/features`],
|
||||
["factionsFolder", `${world}/factions`],
|
||||
["tablesFolder", `${world}/tables`],
|
||||
["workflowsFolder", `${world}/workflows`],
|
||||
];
|
||||
for (const [key, path] of defaults) {
|
||||
if (!this.plugin.settings[key]) {
|
||||
(this.plugin.settings as unknown as Record<string, unknown>)[
|
||||
key
|
||||
] = path;
|
||||
try {
|
||||
if (!this.app.vault.getAbstractFileByPath(path)) {
|
||||
await this.app.vault.createFolder(path);
|
||||
}
|
||||
} catch {
|
||||
/* folder already exists */
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
// Ensure all region subfolders exist and generate hex notes
|
||||
const hexF = normalizeFolder(this.plugin.settings.hexFolder);
|
||||
if (hexF) {
|
||||
let totalCreated = 0;
|
||||
for (const region of this.plugin.settings.regions) {
|
||||
const regionFolder = `${hexF}/${region.name}`;
|
||||
if (!this.app.vault.getAbstractFileByPath(regionFolder)) {
|
||||
try {
|
||||
await this.app.vault.createFolder(regionFolder);
|
||||
} catch {
|
||||
/* exists */
|
||||
}
|
||||
}
|
||||
const { cols, rows } = region.gridSize;
|
||||
const { x: ox, y: oy } = region.gridOffset;
|
||||
const xs = Array.from({ length: cols }, (_, i) => ox + i);
|
||||
const ys = Array.from({ length: rows }, (_, i) => oy + i);
|
||||
totalCreated += await this.plugin.generateHexNotes(
|
||||
region.name,
|
||||
xs,
|
||||
ys,
|
||||
);
|
||||
}
|
||||
if (totalCreated > 0)
|
||||
new Notice(
|
||||
`Hexmaker: generated ${totalCreated} hex note${totalCreated !== 1 ? "s" : ""}.`,
|
||||
);
|
||||
}
|
||||
new Notice("Folders generated.");
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex notes folder")
|
||||
.setDesc("Vault-relative path where hex notes (x_y.md) are stored.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/hexes")
|
||||
.setValue(this.plugin.settings.hexFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Hex notes folder")
|
||||
.setDesc("Vault-relative path where hex notes (x_y.md) are stored.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/hexes")
|
||||
.setValue(this.plugin.settings.hexFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Towns folder")
|
||||
.setDesc("Vault-relative folder to populate the Towns dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/towns")
|
||||
.setValue(this.plugin.settings.townsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.townsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Towns folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Towns dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/towns")
|
||||
.setValue(this.plugin.settings.townsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.townsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Dungeons folder")
|
||||
.setDesc("Vault-relative folder to populate the Dungeons dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/dungeons")
|
||||
.setValue(this.plugin.settings.dungeonsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.dungeonsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Dungeons folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Dungeons dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/dungeons")
|
||||
.setValue(this.plugin.settings.dungeonsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dungeonsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Quests folder")
|
||||
.setDesc("Vault-relative folder to populate the Quests dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/quests")
|
||||
.setValue(this.plugin.settings.questsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.questsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Quests folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Quests dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/quests")
|
||||
.setValue(this.plugin.settings.questsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.questsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Features folder")
|
||||
.setDesc("Vault-relative folder to populate the Features dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/features")
|
||||
.setValue(this.plugin.settings.featuresFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.featuresFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Features folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Features dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/features")
|
||||
.setValue(this.plugin.settings.featuresFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.featuresFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Factions folder")
|
||||
.setDesc("Vault-relative folder to populate the Factions dropdown in the hex editor. Files starting with _ are excluded.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/factions")
|
||||
.setValue(this.plugin.settings.factionsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.factionsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Factions folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder to populate the Factions dropdown in the hex editor. Files starting with _ are excluded.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/factions")
|
||||
.setValue(this.plugin.settings.factionsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.factionsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Tables folder")
|
||||
.setDesc("Vault-relative folder for random table notes. Used by the Encounters Table section and the Random Tables view.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/tables")
|
||||
.setValue(this.plugin.settings.tablesFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.tablesFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Tables folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder for random table notes. Used by the Encounters Table section and the Random Tables view.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/tables")
|
||||
.setValue(this.plugin.settings.tablesFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.tablesFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Workflows folder")
|
||||
.setDesc("Vault-relative folder for workflow notes. Browsable from the Random Tables view via the Workflows tab.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("world/workflows")
|
||||
.setValue(this.plugin.settings.workflowsFolder)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.workflowsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Workflows folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder for workflow notes. Browsable from the Random Tables view via the Workflows tab.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("world/workflows")
|
||||
.setValue(this.plugin.settings.workflowsFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.workflowsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default die for new tables")
|
||||
.setDesc("Die size used when creating new random table notes (d6, d20, d100, etc.).")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("4", "d4")
|
||||
.addOption("6", "d6")
|
||||
.addOption("8", "d8")
|
||||
.addOption("10", "d10")
|
||||
.addOption("12", "d12")
|
||||
.addOption("20", "d20")
|
||||
.addOption("100", "d100")
|
||||
.addOption("200", "d200")
|
||||
.addOption("500", "d500")
|
||||
.addOption("1000", "d1000")
|
||||
.setValue(String(this.plugin.settings.defaultTableDice ?? 100))
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.defaultTableDice = parseInt(value, 10);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Default die for new tables")
|
||||
.setDesc(
|
||||
"Die size used when creating new random table notes (d6, d20, d100, etc.).",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("4", "d4")
|
||||
.addOption("6", "d6")
|
||||
.addOption("8", "d8")
|
||||
.addOption("10", "d10")
|
||||
.addOption("12", "d12")
|
||||
.addOption("20", "d20")
|
||||
.addOption("100", "d100")
|
||||
.addOption("200", "d200")
|
||||
.addOption("500", "d500")
|
||||
.addOption("1000", "d1000")
|
||||
.setValue(String(this.plugin.settings.defaultTableDice ?? 100))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultTableDice = parseInt(value, 10);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex editor sections start collapsed")
|
||||
.setDesc("Choose which sections open collapsed by default in the right-click hex editor.")
|
||||
.then(setting => {
|
||||
const addCb = (label: string, get: () => boolean, set: (v: boolean) => void) => {
|
||||
const lbl = setting.controlEl.createEl("label", { cls: "duckmage-collapse-cb-label" });
|
||||
const cb = lbl.createEl("input") as HTMLInputElement;
|
||||
cb.type = "checkbox";
|
||||
cb.checked = get();
|
||||
cb.addEventListener("change", async () => { set(cb.checked); await this.plugin.saveSettings(); });
|
||||
lbl.appendText(label);
|
||||
};
|
||||
addCb("Terrain", () => this.plugin.settings.hexEditorTerrainCollapsed, v => { this.plugin.settings.hexEditorTerrainCollapsed = v; });
|
||||
addCb("World features", () => this.plugin.settings.hexEditorFeaturesCollapsed, v => { this.plugin.settings.hexEditorFeaturesCollapsed = v; });
|
||||
addCb("Notes", () => this.plugin.settings.hexEditorNotesCollapsed, v => { this.plugin.settings.hexEditorNotesCollapsed = v; });
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Hex editor sections start collapsed")
|
||||
.setDesc(
|
||||
"Choose which sections open collapsed by default in the right-click hex editor.",
|
||||
)
|
||||
.then((setting) => {
|
||||
const addCb = (
|
||||
label: string,
|
||||
get: () => boolean,
|
||||
set: (v: boolean) => void,
|
||||
) => {
|
||||
const lbl = setting.controlEl.createEl("label", {
|
||||
cls: "duckmage-collapse-cb-label",
|
||||
});
|
||||
const cb = lbl.createEl("input") as HTMLInputElement;
|
||||
cb.type = "checkbox";
|
||||
cb.checked = get();
|
||||
cb.addEventListener("change", async () => {
|
||||
set(cb.checked);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
lbl.appendText(label);
|
||||
};
|
||||
addCb(
|
||||
"Terrain",
|
||||
() => this.plugin.settings.hexEditorTerrainCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorTerrainCollapsed = v;
|
||||
},
|
||||
);
|
||||
addCb(
|
||||
"Features",
|
||||
() => this.plugin.settings.hexEditorFeaturesCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorFeaturesCollapsed = v;
|
||||
},
|
||||
);
|
||||
addCb(
|
||||
"Notes",
|
||||
() => this.plugin.settings.hexEditorNotesCollapsed,
|
||||
(v) => {
|
||||
this.plugin.settings.hexEditorNotesCollapsed = v;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Template path")
|
||||
.setDesc("Vault-relative path to a hex note template. Supports {{x}}, {{y}}, {{title}}. Include ## Towns, ## Dungeons, and ## Features headings for the link sections.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("templates/hex.md")
|
||||
.setValue(this.plugin.settings.templatePath)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.templatePath = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Template path")
|
||||
.setDesc(
|
||||
"Vault-relative path to a hex note template. Supports {{x}}, {{y}}, {{title}}. Include ## Towns, ## Dungeons, and ## Features headings for the link sections.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("templates/hex.md")
|
||||
.setValue(this.plugin.settings.templatePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templatePath = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default region")
|
||||
.setDesc("The region opened when the hex map is launched.")
|
||||
.addDropdown(dropdown => {
|
||||
for (const r of this.plugin.settings.regions) {
|
||||
dropdown.addOption(r.name, r.name);
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.defaultRegion ?? this.plugin.settings.regions[0]?.name ?? "")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.defaultRegion = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Default region")
|
||||
.setDesc("The region opened when the hex map is launched.")
|
||||
.addDropdown((dropdown) => {
|
||||
for (const r of this.plugin.settings.regions) {
|
||||
dropdown.addOption(r.name, r.name);
|
||||
}
|
||||
dropdown
|
||||
.setValue(
|
||||
this.plugin.settings.defaultRegion ??
|
||||
this.plugin.settings.regions[0]?.name ??
|
||||
"",
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultRegion = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex orientation")
|
||||
.setDesc("Pointy-top: points face north/south, flat sides east/west. Flat-top: flat sides face north/south, points east/west.")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("pointy", "Pointy-top")
|
||||
.addOption("flat", "Flat-top")
|
||||
.setValue(this.plugin.settings.hexOrientation ?? "pointy")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexOrientation = value as "pointy" | "flat";
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Hex orientation")
|
||||
.setDesc(
|
||||
"Pointy-top: points face north/south, flat sides east/west. Flat-top: flat sides face north/south, points east/west.",
|
||||
)
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("pointy", "Pointy-top")
|
||||
.addOption("flat", "Flat-top")
|
||||
.setValue(this.plugin.settings.hexOrientation ?? "pointy")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexOrientation = value as "pointy" | "flat";
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hex cell spacing")
|
||||
.setDesc("Gap between hex cells (0 – 0.5 em).")
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(0, 0.5, 0.01)
|
||||
.setValue(parseFloat(this.plugin.settings.hexGap ?? "0.15") || 0.15)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.hexGap = String(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Hex cell spacing")
|
||||
.setDesc("Gap between hex cells (0 – 0.5 em).")
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 0.5, 0.01)
|
||||
.setValue(parseFloat(this.plugin.settings.hexGap ?? "0.15") || 0.15)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hexGap = String(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Custom icons folder")
|
||||
.setDesc("Vault-relative folder containing additional icon images (PNG, JPG, SVG, etc.) for the terrain palette and hex icon override. These are merged with the built-in icons.")
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder("icons")
|
||||
.setValue(this.plugin.settings.iconsFolder ?? "")
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.iconsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.loadAvailableIcons();
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Custom icons folder")
|
||||
.setDesc(
|
||||
"Vault-relative folder containing additional icon images (PNG, JPG, SVG, etc.) for the terrain palette and hex icon override. These are merged with the built-in icons.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("icons")
|
||||
.setValue(this.plugin.settings.iconsFolder ?? "")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.iconsFolder = normalizeFolder(value ?? "");
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.loadAvailableIcons();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl).setName("Generate world data").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description duckmage-generate-warning",
|
||||
text: "⚠️ Configure all folder settings above before selecting Generate. This will create terrain table notes, add roller links to all table notes, and link each hex note to its terrain's encounters table. Safe to run multiple times — existing notes and links are not overwritten.",
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Generate terrain tables & hex links")
|
||||
.setDesc("Creates missing terrain table notes, adds roller links to all table notes (so they can be opened in the Hexmaker Roller from within Obsidian), and links each hex note's terrain encounters table into its Encounters Table section.")
|
||||
.addButton(btn =>
|
||||
btn.setButtonText("Generate").setCta().onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
btn.setButtonText("Generating…");
|
||||
try {
|
||||
await this.plugin.ensureTerrainTables();
|
||||
await this.plugin.ensureAllRollerLinks();
|
||||
await this.plugin.backfillTerrainLinks();
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
btn.setButtonText("Generate");
|
||||
}
|
||||
}),
|
||||
);
|
||||
new Setting(containerEl).setName("Generate world data").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
cls: "setting-item-description duckmage-generate-warning",
|
||||
text: "⚠️ Configure all folder settings above before selecting Generate. This will create terrain table notes, add roller links to all table notes, and link each hex note to its terrain's encounters table. Safe to run multiple times — existing notes and links are not overwritten.",
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Generate terrain tables & hex links")
|
||||
.setDesc(
|
||||
"Creates missing terrain table notes, adds roller links to all table notes (so they can be opened in the Hexmaker Roller from within Obsidian), and links each hex note's terrain encounters table into its Encounters Table section.",
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Generate")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
btn.setButtonText("Generating…");
|
||||
try {
|
||||
await this.plugin.ensureTerrainTables();
|
||||
await this.plugin.ensureAllRollerLinks();
|
||||
await this.plugin.backfillTerrainLinks();
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
btn.setButtonText("Generate");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl).setName("Path types").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
text: "Path types define the available drawing tools (roads, rivers, etc.). Edit them from the Path tool on the hex map.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
const pathList = containerEl.createDiv({ cls: "duckmage-path-type-list" });
|
||||
for (const pt of this.plugin.settings.pathTypes) {
|
||||
const row = pathList.createDiv({ cls: "duckmage-path-type-row" });
|
||||
const swatch = row.createSpan({ cls: "duckmage-path-type-swatch" });
|
||||
swatch.style.backgroundColor = pt.color;
|
||||
row.createSpan({ text: `${pt.name} (${pt.width}px, ${pt.lineStyle}, ${pt.routing})` });
|
||||
}
|
||||
new Setting(containerEl).setName("Path types").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
text: "Path types define the available drawing tools (roads, rivers, etc.). Edit them from the Path tool on the hex map.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
const pathList = containerEl.createDiv({ cls: "duckmage-path-type-list" });
|
||||
for (const pt of this.plugin.settings.pathTypes) {
|
||||
const row = pathList.createDiv({ cls: "duckmage-path-type-row" });
|
||||
const swatch = row.createSpan({ cls: "duckmage-path-type-swatch" });
|
||||
swatch.style.backgroundColor = pt.color;
|
||||
row.createSpan({
|
||||
text: `${pt.name} (${pt.width}px, ${pt.lineStyle}, ${pt.routing})`,
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(containerEl).setName("Terrain palettes").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
text: "Each region uses one palette. Assign a palette when creating a region — it cannot be changed after. Edit palette contents from the terrain tool on the hex map.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
new Setting(containerEl).setName("Terrain palettes").setHeading();
|
||||
containerEl.createEl("p", {
|
||||
text: "Each region uses one palette. Assign a palette when creating a region — it cannot be changed after. Edit palette contents from the terrain tool on the hex map.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
const palettes = this.plugin.settings.terrainPalettes;
|
||||
const palettes = this.plugin.settings.terrainPalettes;
|
||||
|
||||
const renderPaletteList = () => {
|
||||
const existingList = containerEl.querySelector(".duckmage-palette-mgmt-list");
|
||||
if (existingList) existingList.remove();
|
||||
const renderPaletteList = () => {
|
||||
const existingList = containerEl.querySelector(
|
||||
".duckmage-palette-mgmt-list",
|
||||
);
|
||||
if (existingList) existingList.remove();
|
||||
|
||||
const listEl = containerEl.createDiv({ cls: "duckmage-palette-mgmt-list" });
|
||||
const listEl = containerEl.createDiv({
|
||||
cls: "duckmage-palette-mgmt-list",
|
||||
});
|
||||
|
||||
for (let i = 0; i < palettes.length; i++) {
|
||||
const pal = palettes[i];
|
||||
const usedBy = this.plugin.settings.regions.filter(r => r.paletteName === pal.name).length;
|
||||
const rowEl = listEl.createDiv({ cls: "duckmage-palette-mgmt-row" });
|
||||
for (let i = 0; i < palettes.length; i++) {
|
||||
const pal = palettes[i];
|
||||
const usedBy = this.plugin.settings.regions.filter(
|
||||
(r) => r.paletteName === pal.name,
|
||||
).length;
|
||||
const rowEl = listEl.createDiv({ cls: "duckmage-palette-mgmt-row" });
|
||||
|
||||
const nameInput = rowEl.createEl("input", { type: "text", value: pal.name }) as HTMLInputElement;
|
||||
nameInput.addClass("duckmage-palette-mgmt-name");
|
||||
nameInput.addEventListener("blur", async () => {
|
||||
const trimmed = nameInput.value.trim();
|
||||
if (!trimmed || trimmed === pal.name) { nameInput.value = pal.name; return; }
|
||||
const isDupe = palettes.some((p, j) => j !== i && p.name.toLowerCase() === trimmed.toLowerCase());
|
||||
if (isDupe) {
|
||||
new Notice(`Palette "${trimmed}" already exists.`);
|
||||
nameInput.value = pal.name;
|
||||
return;
|
||||
}
|
||||
// Update any regions using this palette
|
||||
for (const r of this.plugin.settings.regions) {
|
||||
if (r.paletteName === pal.name) r.paletteName = trimmed;
|
||||
}
|
||||
pal.name = trimmed;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
const nameInput = rowEl.createEl("input", {
|
||||
type: "text",
|
||||
value: pal.name,
|
||||
}) as HTMLInputElement;
|
||||
nameInput.addClass("duckmage-palette-mgmt-name");
|
||||
nameInput.addEventListener("blur", async () => {
|
||||
const trimmed = nameInput.value.trim();
|
||||
if (!trimmed || trimmed === pal.name) {
|
||||
nameInput.value = pal.name;
|
||||
return;
|
||||
}
|
||||
const isDupe = palettes.some(
|
||||
(p, j) => j !== i && p.name.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (isDupe) {
|
||||
new Notice(`Palette "${trimmed}" already exists.`);
|
||||
nameInput.value = pal.name;
|
||||
return;
|
||||
}
|
||||
// Update any regions using this palette
|
||||
for (const r of this.plugin.settings.regions) {
|
||||
if (r.paletteName === pal.name) r.paletteName = trimmed;
|
||||
}
|
||||
pal.name = trimmed;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
|
||||
rowEl.createSpan({ cls: "duckmage-palette-mgmt-badge", text: `(${usedBy} region${usedBy !== 1 ? "s" : ""})` });
|
||||
rowEl.createSpan({
|
||||
cls: "duckmage-palette-mgmt-badge",
|
||||
text: `(${usedBy} region${usedBy !== 1 ? "s" : ""})`,
|
||||
});
|
||||
|
||||
const deleteBtn = rowEl.createEl("button", { text: "Delete" });
|
||||
deleteBtn.disabled = usedBy > 0 || palettes.length <= 1;
|
||||
deleteBtn.title = usedBy > 0 ? "Cannot delete — in use by a region" : palettes.length <= 1 ? "Cannot delete the last palette" : "";
|
||||
deleteBtn.addEventListener("click", async () => {
|
||||
palettes.splice(i, 1);
|
||||
await this.plugin.saveSettings();
|
||||
renderPaletteList();
|
||||
});
|
||||
}
|
||||
const deleteBtn = rowEl.createEl("button", { text: "Delete" });
|
||||
deleteBtn.disabled = usedBy > 0 || palettes.length <= 1;
|
||||
deleteBtn.title =
|
||||
usedBy > 0
|
||||
? "Cannot delete — in use by a region"
|
||||
: palettes.length <= 1
|
||||
? "Cannot delete the last palette"
|
||||
: "";
|
||||
deleteBtn.addEventListener("click", async () => {
|
||||
palettes.splice(i, 1);
|
||||
await this.plugin.saveSettings();
|
||||
renderPaletteList();
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(listEl).addButton(btn =>
|
||||
btn.setButtonText("Add palette").onClick(async () => {
|
||||
palettes.push({
|
||||
name: "New Palette",
|
||||
terrains: this.plugin.settings.terrainPalettes[0]?.terrains.map(t => ({ ...t })) ?? [],
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
renderPaletteList();
|
||||
}),
|
||||
);
|
||||
};
|
||||
new Setting(listEl).addButton((btn) =>
|
||||
btn.setButtonText("Add palette").onClick(async () => {
|
||||
palettes.push({
|
||||
name: "New Palette",
|
||||
terrains:
|
||||
this.plugin.settings.terrainPalettes[0]?.terrains.map((t) => ({
|
||||
...t,
|
||||
})) ?? [],
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
renderPaletteList();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
renderPaletteList();
|
||||
}
|
||||
renderPaletteList();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { App, Notice, TFile } from "obsidian";
|
||||
import { HexmakerModal } from "../HexmakerModal";
|
||||
import type HexmakerPlugin from "../HexmakerPlugin";
|
||||
import { getIconUrl, normalizeFolder, makeTableTemplate, createIconEl } from "../utils";
|
||||
import {
|
||||
getIconUrl,
|
||||
normalizeFolder,
|
||||
makeTableTemplate,
|
||||
createIconEl,
|
||||
} from "../utils";
|
||||
import {
|
||||
getTerrainFromFile,
|
||||
setTerrainInFile,
|
||||
|
|
@ -92,7 +97,8 @@ export class HexEditorModal extends HexmakerModal {
|
|||
});
|
||||
centerBtn.addEventListener("click", () => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_HEX_MAP);
|
||||
if (leaves.length > 0) (leaves[0].view as any).centerOnHex?.(this.x, this.y);
|
||||
if (leaves.length > 0)
|
||||
(leaves[0].view as any).centerOnHex?.(this.x, this.y);
|
||||
});
|
||||
|
||||
// "Open note" can be determined synchronously from the vault index
|
||||
|
|
@ -131,19 +137,28 @@ export class HexEditorModal extends HexmakerModal {
|
|||
s.hexEditorTerrainCollapsed ?? false,
|
||||
);
|
||||
const paletteEntry = directTerrain
|
||||
? this.plugin.getRegionPalette(this.regionName).find(p => p.name === directTerrain)
|
||||
? this.plugin
|
||||
.getRegionPalette(this.regionName)
|
||||
.find((p) => p.name === directTerrain)
|
||||
: undefined;
|
||||
const iconToShow = directIcon ?? paletteEntry?.icon;
|
||||
if (paletteEntry || iconToShow) {
|
||||
const preview = terrainHeader.createSpan({ cls: "duckmage-terrain-header-preview" });
|
||||
const swatch = preview.createSpan({ cls: "duckmage-terrain-header-swatch" });
|
||||
const preview = terrainHeader.createSpan({
|
||||
cls: "duckmage-terrain-header-preview",
|
||||
});
|
||||
const swatch = preview.createSpan({
|
||||
cls: "duckmage-terrain-header-swatch",
|
||||
});
|
||||
if (paletteEntry) swatch.style.backgroundColor = paletteEntry.color;
|
||||
if (iconToShow) {
|
||||
const img = swatch.createEl("img");
|
||||
img.src = getIconUrl(this.plugin, iconToShow);
|
||||
}
|
||||
if (paletteEntry) {
|
||||
preview.createSpan({ text: paletteEntry.name, cls: "duckmage-terrain-header-name" });
|
||||
preview.createSpan({
|
||||
text: paletteEntry.name,
|
||||
cls: "duckmage-terrain-header-name",
|
||||
});
|
||||
}
|
||||
}
|
||||
this.renderTerrainSection(terrainBody, path, directTerrain, directIcon);
|
||||
|
|
@ -156,22 +171,70 @@ export class HexEditorModal extends HexmakerModal {
|
|||
s.hexEditorNotesCollapsed ?? false,
|
||||
);
|
||||
for (const { key, label } of TEXT_SECTIONS) {
|
||||
this.renderTextSection(notesBody, path, key, label, allText.get(key) ?? "");
|
||||
this.renderTextSection(
|
||||
notesBody,
|
||||
path,
|
||||
key,
|
||||
label,
|
||||
allText.get(key) ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
bodyEl.createEl("hr", { cls: "duckmage-editor-divider" });
|
||||
|
||||
const { body: featuresBody } = this.makeCollapsible(
|
||||
bodyEl,
|
||||
"World features",
|
||||
"Features",
|
||||
s.hexEditorFeaturesCollapsed ?? false,
|
||||
);
|
||||
this.renderDropdownSection(featuresBody, path, "Encounters Table", hexExists, s.tablesFolder, allLinks.get("encounters table") ?? []);
|
||||
this.renderDropdownSection(featuresBody, path, "Towns", hexExists, s.townsFolder, allLinks.get("towns") ?? []);
|
||||
this.renderDropdownSection(featuresBody, path, "Dungeons", hexExists, s.dungeonsFolder, allLinks.get("dungeons") ?? []);
|
||||
this.renderDropdownSection(featuresBody, path, "Quests", hexExists, s.questsFolder, allLinks.get("quests") ?? []);
|
||||
this.renderDropdownSection(featuresBody, path, "Factions", hexExists, s.factionsFolder, allLinks.get("factions") ?? []);
|
||||
this.renderDropdownSection(featuresBody, path, "Features", hexExists, s.featuresFolder, allLinks.get("features") ?? []);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Encounters Table",
|
||||
hexExists,
|
||||
s.tablesFolder,
|
||||
allLinks.get("encounters table") ?? [],
|
||||
);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Towns",
|
||||
hexExists,
|
||||
s.townsFolder,
|
||||
allLinks.get("towns") ?? [],
|
||||
);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Dungeons",
|
||||
hexExists,
|
||||
s.dungeonsFolder,
|
||||
allLinks.get("dungeons") ?? [],
|
||||
);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Quests",
|
||||
hexExists,
|
||||
s.questsFolder,
|
||||
allLinks.get("quests") ?? [],
|
||||
);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Factions",
|
||||
hexExists,
|
||||
s.factionsFolder,
|
||||
allLinks.get("factions") ?? [],
|
||||
);
|
||||
this.renderDropdownSection(
|
||||
featuresBody,
|
||||
path,
|
||||
"Features",
|
||||
hexExists,
|
||||
s.featuresFolder,
|
||||
allLinks.get("features") ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
|
@ -179,7 +242,6 @@ export class HexEditorModal extends HexmakerModal {
|
|||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
|
||||
private isOnMap(nx: number, ny: number): boolean {
|
||||
const region = this.plugin.getOrCreateRegion(this.regionName);
|
||||
const { gridOffset, gridSize } = region;
|
||||
|
|
@ -191,28 +253,34 @@ export class HexEditorModal extends HexmakerModal {
|
|||
);
|
||||
}
|
||||
|
||||
private renderNeighborWidget(container: HTMLElement, x: number, y: number): void {
|
||||
private renderNeighborWidget(
|
||||
container: HTMLElement,
|
||||
x: number,
|
||||
y: number,
|
||||
): void {
|
||||
const isFlat = this.plugin.settings.hexOrientation === "flat";
|
||||
const paletteMap = new Map(this.plugin.getRegionPalette(this.regionName).map(p => [p.name, p]));
|
||||
const paletteMap = new Map(
|
||||
this.plugin.getRegionPalette(this.regionName).map((p) => [p.name, p]),
|
||||
);
|
||||
const widget = container.createDiv({ cls: "duckmage-neighbor-widget" });
|
||||
|
||||
type NeighborDef = { l: number; t: number; nx: number; ny: number };
|
||||
const defs: NeighborDef[] = isFlat
|
||||
? [
|
||||
{ l: 22, t: 2, nx: x, ny: y - 1 }, // N
|
||||
{ l: 42, t: 13, nx: x + 1, ny: x % 2 === 0 ? y - 1 : y }, // NE
|
||||
{ l: 42, t: 32, nx: x + 1, ny: x % 2 === 0 ? y : y + 1 }, // SE
|
||||
{ l: 22, t: 40, nx: x, ny: y + 1 }, // S
|
||||
{ l: 2, t: 32, nx: x - 1, ny: x % 2 === 0 ? y : y + 1 }, // SW
|
||||
{ l: 2, t: 13, nx: x - 1, ny: x % 2 === 0 ? y - 1 : y }, // NW
|
||||
{ l: 22, t: 2, nx: x, ny: y - 1 }, // N
|
||||
{ l: 42, t: 13, nx: x + 1, ny: x % 2 === 0 ? y - 1 : y }, // NE
|
||||
{ l: 42, t: 32, nx: x + 1, ny: x % 2 === 0 ? y : y + 1 }, // SE
|
||||
{ l: 22, t: 40, nx: x, ny: y + 1 }, // S
|
||||
{ l: 2, t: 32, nx: x - 1, ny: x % 2 === 0 ? y : y + 1 }, // SW
|
||||
{ l: 2, t: 13, nx: x - 1, ny: x % 2 === 0 ? y - 1 : y }, // NW
|
||||
]
|
||||
: [
|
||||
{ l: 10, t: 1, nx: y % 2 === 0 ? x - 1 : x, ny: y - 1 }, // NW
|
||||
{ l: 34, t: 1, nx: y % 2 === 0 ? x : x + 1, ny: y - 1 }, // NE
|
||||
{ l: 0, t: 18, nx: x - 1, ny: y }, // W
|
||||
{ l: 44, t: 18, nx: x + 1, ny: y }, // E
|
||||
{ l: 10, t: 35, nx: y % 2 === 0 ? x - 1 : x, ny: y + 1 }, // SW
|
||||
{ l: 34, t: 35, nx: y % 2 === 0 ? x : x + 1, ny: y + 1 }, // SE
|
||||
{ l: 10, t: 1, nx: y % 2 === 0 ? x - 1 : x, ny: y - 1 }, // NW
|
||||
{ l: 34, t: 1, nx: y % 2 === 0 ? x : x + 1, ny: y - 1 }, // NE
|
||||
{ l: 0, t: 18, nx: x - 1, ny: y }, // W
|
||||
{ l: 44, t: 18, nx: x + 1, ny: y }, // E
|
||||
{ l: 10, t: 35, nx: y % 2 === 0 ? x - 1 : x, ny: y + 1 }, // SW
|
||||
{ l: 34, t: 35, nx: y % 2 === 0 ? x : x + 1, ny: y + 1 }, // SE
|
||||
];
|
||||
|
||||
for (const { l, t, nx, ny } of defs) {
|
||||
|
|
@ -282,9 +350,16 @@ export class HexEditorModal extends HexmakerModal {
|
|||
|
||||
// Clear terrain — always first in the grid
|
||||
if (currentTerrain) {
|
||||
const clearBtn = grid.createDiv({ cls: "duckmage-terrain-option duckmage-terrain-option-clear" });
|
||||
clearBtn.createDiv({ cls: "duckmage-terrain-preview duckmage-terrain-preview-clear" });
|
||||
clearBtn.createSpan({ text: "Clear", cls: "duckmage-terrain-option-name" });
|
||||
const clearBtn = grid.createDiv({
|
||||
cls: "duckmage-terrain-option duckmage-terrain-option-clear",
|
||||
});
|
||||
clearBtn.createDiv({
|
||||
cls: "duckmage-terrain-preview duckmage-terrain-preview-clear",
|
||||
});
|
||||
clearBtn.createSpan({
|
||||
text: "Clear",
|
||||
cls: "duckmage-terrain-option-name",
|
||||
});
|
||||
clearBtn.addEventListener("click", async () => {
|
||||
await setTerrainInFile(this.app, path, null);
|
||||
void this.plugin.syncHexEncounterTableLink(path, null);
|
||||
|
|
@ -302,7 +377,13 @@ export class HexEditorModal extends HexmakerModal {
|
|||
preview.style.backgroundColor = entry.color;
|
||||
|
||||
if (entry.icon) {
|
||||
createIconEl(preview, getIconUrl(this.plugin, entry.icon), entry.name, entry.iconColor, "duckmage-terrain-preview-icon");
|
||||
createIconEl(
|
||||
preview,
|
||||
getIconUrl(this.plugin, entry.icon),
|
||||
entry.name,
|
||||
entry.iconColor,
|
||||
"duckmage-terrain-preview-icon",
|
||||
);
|
||||
}
|
||||
|
||||
btn.createSpan({ text: entry.name, cls: "duckmage-terrain-option-name" });
|
||||
|
|
@ -346,7 +427,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
iconSelect.addEventListener("change", async () => {
|
||||
await this.ensureHexNote();
|
||||
await setIconOverrideInFile(this.app, path, iconSelect.value || null);
|
||||
this.onChanged(terrainOverrides, new Map([[path, iconSelect.value || null]]));
|
||||
this.onChanged(
|
||||
terrainOverrides,
|
||||
new Map([[path, iconSelect.value || null]]),
|
||||
);
|
||||
});
|
||||
const clearIconBtn = iconRow.createEl("button", {
|
||||
text: "Clear",
|
||||
|
|
@ -367,7 +451,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
});
|
||||
}
|
||||
|
||||
private getFilesForDropdown(folder: string, filterType?: "roll-filter" | "encounter-filter"): TFile[] {
|
||||
private getFilesForDropdown(
|
||||
folder: string,
|
||||
filterType?: "roll-filter" | "encounter-filter",
|
||||
): TFile[] {
|
||||
const normalized = normalizeFolder(folder);
|
||||
const all = this.app.vault.getMarkdownFiles();
|
||||
const scoped = normalized
|
||||
|
|
@ -375,9 +462,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
: all;
|
||||
let filtered = scoped.filter((f) => !f.basename.startsWith("_"));
|
||||
if (filterType) {
|
||||
const excluded = filterType === "encounter-filter"
|
||||
? this.plugin.settings.encounterTableExcludedFolders
|
||||
: this.plugin.settings.rollTableExcludedFolders;
|
||||
const excluded =
|
||||
filterType === "encounter-filter"
|
||||
? this.plugin.settings.encounterTableExcludedFolders
|
||||
: this.plugin.settings.rollTableExcludedFolders;
|
||||
filtered = this.plugin.filterTableFiles(filtered, filterType, excluded);
|
||||
}
|
||||
return filtered.sort((a, b) => a.basename.localeCompare(b.basename));
|
||||
|
|
@ -391,8 +479,13 @@ export class HexEditorModal extends HexmakerModal {
|
|||
sourceFolder: string,
|
||||
initialLinks: string[],
|
||||
): void {
|
||||
const sectionEl = container.createDiv({ cls: "duckmage-editor-link-section" });
|
||||
sectionEl.createEl("h4", { text: section, cls: "duckmage-link-section-title" });
|
||||
const sectionEl = container.createDiv({
|
||||
cls: "duckmage-editor-link-section",
|
||||
});
|
||||
sectionEl.createEl("h4", {
|
||||
text: section,
|
||||
cls: "duckmage-link-section-title",
|
||||
});
|
||||
|
||||
// ── Combo box ──────────────────────────────────────────────────────────
|
||||
const comboWrap = sectionEl.createDiv({ cls: "duckmage-link-combo" });
|
||||
|
|
@ -407,19 +500,26 @@ export class HexEditorModal extends HexmakerModal {
|
|||
cls: "duckmage-link-combo-arrow",
|
||||
title: "Show all",
|
||||
});
|
||||
const dropdown = comboWrap.createDiv({ cls: "duckmage-link-combo-dropdown" });
|
||||
const dropdown = comboWrap.createDiv({
|
||||
cls: "duckmage-link-combo-dropdown",
|
||||
});
|
||||
dropdown.style.display = "none";
|
||||
|
||||
// ── Link list ──────────────────────────────────────────────────────────
|
||||
const linksEl = sectionEl.createDiv({ cls: "duckmage-link-list" });
|
||||
|
||||
let currentLinks = hexExists ? [...initialLinks] : [];
|
||||
const filterType = section === "Encounters Table" ? "encounter-filter" as const : undefined;
|
||||
const filterType =
|
||||
section === "Encounters Table"
|
||||
? ("encounter-filter" as const)
|
||||
: undefined;
|
||||
|
||||
const onItemClick =
|
||||
section === "Encounters Table"
|
||||
? async (_link: string, file: TFile) => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_RANDOM_TABLES);
|
||||
const leaves = this.app.workspace.getLeavesOfType(
|
||||
VIEW_TYPE_RANDOM_TABLES,
|
||||
);
|
||||
if (leaves.length > 0) {
|
||||
this.app.workspace.revealLeaf(leaves[0]);
|
||||
(leaves[0].view as any).openTable?.(file.path);
|
||||
|
|
@ -435,11 +535,16 @@ export class HexEditorModal extends HexmakerModal {
|
|||
const onRollClick =
|
||||
section === "Encounters Table"
|
||||
? (file: TFile) =>
|
||||
new RandomTableModal(this.app, this.plugin, undefined, file.path).open()
|
||||
new RandomTableModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
undefined,
|
||||
file.path,
|
||||
).open()
|
||||
: undefined;
|
||||
|
||||
const onRemove = async (link: string) => {
|
||||
currentLinks = currentLinks.filter(l => l !== link);
|
||||
currentLinks = currentLinks.filter((l) => l !== link);
|
||||
refresh();
|
||||
await removeLinkFromSection(this.app, path, section, link);
|
||||
this.onChanged();
|
||||
|
|
@ -447,7 +552,14 @@ export class HexEditorModal extends HexmakerModal {
|
|||
|
||||
const refresh = () => {
|
||||
linksEl.empty();
|
||||
this.renderLinkList(linksEl, currentLinks, path, onRemove, onItemClick, onRollClick);
|
||||
this.renderLinkList(
|
||||
linksEl,
|
||||
currentLinks,
|
||||
path,
|
||||
onRemove,
|
||||
onItemClick,
|
||||
onRollClick,
|
||||
);
|
||||
};
|
||||
|
||||
refresh();
|
||||
|
|
@ -459,7 +571,7 @@ export class HexEditorModal extends HexmakerModal {
|
|||
const files = this.getFilesForDropdown(sourceFolder, filterType);
|
||||
if (!query) return files;
|
||||
const q = query.toLowerCase();
|
||||
return files.filter(f => f.basename.toLowerCase().includes(q));
|
||||
return files.filter((f) => f.basename.toLowerCase().includes(q));
|
||||
};
|
||||
|
||||
const populateDropdown = (query: string) => {
|
||||
|
|
@ -468,7 +580,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
const files = getFiltered(trimmed);
|
||||
|
||||
if (files.length === 0 && !trimmed) {
|
||||
dropdown.createDiv({ cls: "duckmage-link-combo-empty", text: "No files in folder" });
|
||||
dropdown.createDiv({
|
||||
cls: "duckmage-link-combo-empty",
|
||||
text: "No files in folder",
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
|
|
@ -480,7 +595,9 @@ export class HexEditorModal extends HexmakerModal {
|
|||
});
|
||||
}
|
||||
|
||||
const exactMatch = files.some(f => f.basename.toLowerCase() === trimmed.toLowerCase());
|
||||
const exactMatch = files.some(
|
||||
(f) => f.basename.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (trimmed && !exactMatch) {
|
||||
const createItem = dropdown.createDiv({
|
||||
cls: "duckmage-link-combo-item duckmage-link-combo-create",
|
||||
|
|
@ -508,7 +625,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
closeDropdown();
|
||||
input.value = "";
|
||||
const hexFile = await this.ensureHexNote();
|
||||
if (!hexFile) { new Notice("Could not create hex note."); return; }
|
||||
if (!hexFile) {
|
||||
new Notice("Could not create hex note.");
|
||||
return;
|
||||
}
|
||||
const linkPath = this.app.metadataCache.fileToLinktext(file, path);
|
||||
currentLinks = [...currentLinks, linkPath];
|
||||
refresh();
|
||||
|
|
@ -540,8 +660,14 @@ export class HexEditorModal extends HexmakerModal {
|
|||
}
|
||||
}
|
||||
const hexFile = await this.ensureHexNote();
|
||||
if (!hexFile) { new Notice("Could not create hex note."); return; }
|
||||
const linkPath = this.app.metadataCache.fileToLinktext(file as TFile, path);
|
||||
if (!hexFile) {
|
||||
new Notice("Could not create hex note.");
|
||||
return;
|
||||
}
|
||||
const linkPath = this.app.metadataCache.fileToLinktext(
|
||||
file as TFile,
|
||||
path,
|
||||
);
|
||||
currentLinks = [...currentLinks, linkPath];
|
||||
refresh();
|
||||
void addLinkToSection(this.app, path, section, `[[${linkPath}]]`);
|
||||
|
|
@ -550,18 +676,26 @@ export class HexEditorModal extends HexmakerModal {
|
|||
};
|
||||
|
||||
input.addEventListener("focus", () => openDropdown());
|
||||
input.addEventListener("blur", () => setTimeout(() => closeDropdown(), 150));
|
||||
input.addEventListener("blur", () =>
|
||||
setTimeout(() => closeDropdown(), 150),
|
||||
);
|
||||
input.addEventListener("input", () => {
|
||||
if (!isOpen) openDropdown();
|
||||
else populateDropdown(input.value);
|
||||
});
|
||||
input.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") { closeDropdown(); input.blur(); return; }
|
||||
if (e.key === "Escape") {
|
||||
closeDropdown();
|
||||
input.blur();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
const trimmed = input.value.trim();
|
||||
if (!trimmed) return;
|
||||
const files = getFiltered(trimmed);
|
||||
const exact = files.find(f => f.basename.toLowerCase() === trimmed.toLowerCase());
|
||||
const exact = files.find(
|
||||
(f) => f.basename.toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
if (exact) void selectFile(exact);
|
||||
else if (files.length === 1) void selectFile(files[0]);
|
||||
else void createAndLink(trimmed);
|
||||
|
|
@ -571,7 +705,10 @@ export class HexEditorModal extends HexmakerModal {
|
|||
arrowBtn.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
arrowBtn.addEventListener("click", () => {
|
||||
if (isOpen) closeDropdown();
|
||||
else { input.focus(); openDropdown(); }
|
||||
else {
|
||||
input.focus();
|
||||
openDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -686,7 +823,9 @@ export class HexEditorModal extends HexmakerModal {
|
|||
});
|
||||
|
||||
// 📖 button: terrain description table (description section) or section-specific table
|
||||
const tablesFolder = normalizeFolder(this.plugin.settings.tablesFolder ?? "");
|
||||
const tablesFolder = normalizeFolder(
|
||||
this.plugin.settings.tablesFolder ?? "",
|
||||
);
|
||||
let previewTablePath: string | null = null;
|
||||
let previewTitle = "";
|
||||
|
||||
|
|
@ -720,7 +859,9 @@ export class HexEditorModal extends HexmakerModal {
|
|||
});
|
||||
|
||||
if (previewTablePath) {
|
||||
const btnGroup = labelRow.createDiv({ cls: "duckmage-text-section-btn-group" });
|
||||
const btnGroup = labelRow.createDiv({
|
||||
cls: "duckmage-text-section-btn-group",
|
||||
});
|
||||
const previewBtn = btnGroup.createEl("button", {
|
||||
text: "📖",
|
||||
cls: "duckmage-section-desc-table-btn",
|
||||
|
|
@ -728,11 +869,16 @@ export class HexEditorModal extends HexmakerModal {
|
|||
previewBtn.title = previewTitle;
|
||||
const capturedPath = previewTablePath;
|
||||
previewBtn.addEventListener("click", () => {
|
||||
new RandomTableModal(this.app, this.plugin, (result) => {
|
||||
if (textarea.value && !textarea.value.endsWith("\n"))
|
||||
textarea.value += "\n";
|
||||
textarea.value += result;
|
||||
}, capturedPath).open();
|
||||
new RandomTableModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
(result) => {
|
||||
if (textarea.value && !textarea.value.endsWith("\n"))
|
||||
textarea.value += "\n";
|
||||
textarea.value += result;
|
||||
},
|
||||
capturedPath,
|
||||
).open();
|
||||
});
|
||||
}
|
||||
textarea.rows = 3;
|
||||
|
|
|
|||
Loading…
Reference in a new issue