A tabletop RPG world-building plugin for Obsidian. Three interconnected views (hex map, hex table, random tables/workflows) operate on a shared vault of Markdown notes.
The primary interactive surface. Renders a hex grid where every cell corresponds to a `{hexFolder}/{region}/{x}_{y}.md` note on disk. Multiple regions are supported; the active region is tracked as `activeRegionName`.
4. Right-click (on any hex) → `exit*Mode()` → `drawingMode = null`
5.`updateToolbarButtonStates()` syncs active CSS and button labels
**Write strategies:**
- Terrain/icon: synchronous DOM patch + coalescing async write queue (`scheduleTerrainWrite` / `scheduleIconWrite`). Only the latest value per hex is flushed, preventing stale writes.
- Link tools: purely async, no DOM patch needed (badge added directly after `await addLinkToSection`).
- Roads/rivers: written to `plugin.settings` and persisted via `saveSettings()`.
Current columns in order: Description, Landmark, Towns, Dungeons, Features, Quests, Factions, Enc. Table, Hidden, Secret, Weather, Hooks & Rumors.
The **Enc. Table** cell displays the basename of the linked table file (not the full path). Clicking a populated cell opens `RandomTableView` at that file via `openTable()`.
- Terrain multi-select modal — left-click to include, right-click to exclude (strikethrough + red)
- Has Town / Has Dungeon / Has Feature / Has Quest / Has Faction checkboxes
- Sort priority (X→Y or Y→X) and direction (Asc / Desc)
Filter state is stored as `dataset` attributes on each `<tr>` (`data-terrain`, `data-has-town`, etc.) and read by `applyFilters()` — no file I/O on filter change.
**Jump button (◎):** Calls `HexMapView.centerOnHex(x, y)` on the first open map leaf, panning the viewport to that hex.
- Left: folder tree of workflow files + new-workflow creator. New workflows created with "New workflow with this table" (from a table detail pane) pre-populate the first step including auto-derived label.
- Right: selected workflow — each step shown as a clickable link (opens that table in Tables mode) with `×N` roll count, "Roll workflow" button, Edit button opens `WorkflowEditorModal`.
-`openTable(filePath)` — switches to Tables mode, expands all ancestor folders of the target file so it is visible in the tree, refreshes the list, and loads the specified table. Called from `HexTableView` (Enc. Table cell), `HexEditorModal`, workflow step links, and the `obsidian://duckmage-roll` protocol handler.
Stored as `string[][]` in `settings.roadChains` / `settings.riverChains`. Each chain is an ordered array of `"x_y"` keys. Rendered as SVG polylines connecting adjacent hexes.
### Random tables
Markdown files with:
```yaml
---
dice: 100
---
| Result | Weight |
|--------|--------|
| Dragons | 1 |
| Goblins | 5 |
```
Parsed by `randomTable.ts` into `{ dice, entries: [{result, weight}] }`. Terrain tables generated at `{tablesFolder}/terrain/{name} - description.md` and `…encounters.md`.
Parsed/serialized by `workflow.ts`. Template files live at `{workflowsFolder}/templates/{name}.md` and use `$label` / `$label_N` placeholders (multi-roll steps). `WorkflowEditorModal` auto-syncs the template when steps change: adding a step appends its section, changing a label updates the `## heading` and all `$var`/`$var_N` references, changing roll count adds or removes `_N`-suffixed placeholders. `WorkflowWizardModal` fills placeholders with roll results and saves the output as a new note.
- Metadata-cache race after `vault.modify`: `HexEditorModal.onChanged()` defers `renderGrid()` by 300 ms when no terrain/icon overrides are passed.
- Modals that preload file content accept an optional `preloaded` / `initialContent` parameter to avoid redundant vault reads when the caller already holds the content.
- Notes whose `basename` starts with `"_"` are excluded from all dropdowns, file trees, and auto-generated lists. Pattern: `.filter(f => !f.basename.startsWith("_"))`.
- Draggable modals call `makeDraggable()` in `onOpen`, add the `duckmage-editor-modal-drag` class, and restrict drag to the title-bar area.