@jjlmoya/utils-tabletop 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -0
- package/src/entries.ts +4 -0
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/spanish_leakage.test.ts +25 -3
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/decision-wheel/i18n/de.ts +10 -10
- package/src/tool/decision-wheel/i18n/es.ts +10 -10
- package/src/tool/decision-wheel/i18n/fr.ts +10 -10
- package/src/tool/decision-wheel/i18n/id.ts +10 -10
- package/src/tool/decision-wheel/i18n/it.ts +10 -10
- package/src/tool/decision-wheel/i18n/ja.ts +10 -10
- package/src/tool/decision-wheel/i18n/ko.ts +10 -10
- package/src/tool/decision-wheel/i18n/nl.ts +10 -10
- package/src/tool/decision-wheel/i18n/pl.ts +10 -10
- package/src/tool/decision-wheel/i18n/pt.ts +10 -10
- package/src/tool/decision-wheel/i18n/sv.ts +10 -10
- package/src/tool/decision-wheel/i18n/tr.ts +10 -10
- package/src/tool/decision-wheel/i18n/zh.ts +10 -10
- package/src/tool/dungeon-map-generator/bibliography.astro +16 -0
- package/src/tool/dungeon-map-generator/bibliography.ts +12 -0
- package/src/tool/dungeon-map-generator/component.astro +134 -0
- package/src/tool/dungeon-map-generator/controller.ts +242 -0
- package/src/tool/dungeon-map-generator/document.ts +72 -0
- package/src/tool/dungeon-map-generator/dom-views.ts +139 -0
- package/src/tool/dungeon-map-generator/editor.ts +57 -0
- package/src/tool/dungeon-map-generator/entry.ts +28 -0
- package/src/tool/dungeon-map-generator/evaluator.ts +40 -0
- package/src/tool/dungeon-map-generator/i18n/de.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/en.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/es.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/fr.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/id.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/it.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/ja.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/ko.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/nl.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/pl.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/pt.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/ru.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/sv.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/tr.ts +172 -0
- package/src/tool/dungeon-map-generator/i18n/zh.ts +172 -0
- package/src/tool/dungeon-map-generator/index.ts +11 -0
- package/src/tool/dungeon-map-generator/logic.test.ts +75 -0
- package/src/tool/dungeon-map-generator/logic.ts +255 -0
- package/src/tool/dungeon-map-generator/random-dungeon-map-generator.css +678 -0
- package/src/tool/dungeon-map-generator/seo.astro +16 -0
- package/src/tool/dungeon-map-generator/storage.ts +18 -0
- package/src/tool/dungeon-map-generator/ui.ts +53 -0
- package/src/tool/dungeon-map-generator/validation.ts +40 -0
- package/src/tool/hidden-role-dealer/i18n/de.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/id.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/it.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/ja.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/ko.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/nl.ts +6 -6
- package/src/tool/hidden-role-dealer/i18n/pl.ts +7 -7
- package/src/tool/hidden-role-dealer/i18n/pt.ts +7 -7
- package/src/tool/hidden-role-dealer/i18n/ru.ts +7 -7
- package/src/tool/hidden-role-dealer/i18n/sv.ts +7 -7
- package/src/tool/hidden-role-dealer/i18n/tr.ts +7 -7
- package/src/tool/hidden-role-dealer/i18n/zh.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/de.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/es.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/fr.ts +12 -12
- package/src/tool/scatter-direction-selector/i18n/id.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/it.ts +11 -11
- package/src/tool/scatter-direction-selector/i18n/ja.ts +8 -8
- package/src/tool/scatter-direction-selector/i18n/ko.ts +8 -8
- package/src/tool/scatter-direction-selector/i18n/nl.ts +9 -10
- package/src/tool/scatter-direction-selector/i18n/pl.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/pt.ts +12 -12
- package/src/tool/scatter-direction-selector/i18n/ru.ts +7 -7
- package/src/tool/scatter-direction-selector/i18n/sv.ts +9 -9
- package/src/tool/scatter-direction-selector/i18n/tr.ts +8 -8
- package/src/tool/scatter-direction-selector/i18n/zh.ts +11 -11
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { evaluateMap } from './evaluator';
|
|
2
|
+
import type { DungeonEdit } from './document';
|
|
3
|
+
import type { Door, DungeonMap, Tile } from './logic';
|
|
4
|
+
import type { DungeonMapGeneratorUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface RenderContext {
|
|
7
|
+
map: DungeonMap;
|
|
8
|
+
root: HTMLElement;
|
|
9
|
+
ui: DungeonMapGeneratorUI;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CELL = 20;
|
|
13
|
+
|
|
14
|
+
export function buildMapSvg(map: DungeonMap, standalone = false): string {
|
|
15
|
+
const width = map.config.columns * CELL;
|
|
16
|
+
const height = map.config.rows * CELL;
|
|
17
|
+
const cells = map.tiles.flatMap((row, y) =>
|
|
18
|
+
row.map((tile, x) => tileSvg(tile, x, y, map.config.style))).join('');
|
|
19
|
+
const palette = standalone ? standaloneStyle(map.config.style) : '';
|
|
20
|
+
const doors = map.doors.map(doorSvg).join('');
|
|
21
|
+
const hitGrid = standalone ? '' : interactionGridSvg(map);
|
|
22
|
+
return `<svg class="dmg-map dmg-map-${map.config.style}" viewBox="0 0 ${width} ${height}" role="img" aria-label="Generated dungeon map" xmlns="http://www.w3.org/2000/svg">${palette}<rect class="dmg-map-void" width="${width}" height="${height}"/>${cells}${doors}${compassSvg(width, height)}${hitGrid}</svg>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tileSvg(tile: Tile, x: number, y: number, style: string): string {
|
|
26
|
+
if (tile === 0) return '';
|
|
27
|
+
const px = x * CELL;
|
|
28
|
+
const py = y * CELL;
|
|
29
|
+
const radius = style === 'cavern' ? 7 : 1;
|
|
30
|
+
return `<rect class="dmg-map-floor" x="${px}" y="${py}" width="${CELL + 0.5}" height="${CELL + 0.5}" rx="${radius}"/>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function doorSvg(door: Door): string {
|
|
34
|
+
const x = door.x * CELL;
|
|
35
|
+
const y = door.y * CELL;
|
|
36
|
+
if (door.orientation === 'vertical') {
|
|
37
|
+
return `<g class="dmg-map-door"><line x1="${x + 10}" y1="${y + 2}" x2="${x + 10}" y2="${y + 18}"/><circle cx="${x + 10}" cy="${y + 4}" r="1.5"/></g>`;
|
|
38
|
+
}
|
|
39
|
+
return `<g class="dmg-map-door"><line x1="${x + 2}" y1="${y + 10}" x2="${x + 18}" y2="${y + 10}"/><circle cx="${x + 4}" cy="${y + 10}" r="1.5"/></g>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compassSvg(width: number, height: number): string {
|
|
43
|
+
if (width < 400 || height < 300) return '';
|
|
44
|
+
const x = width - 34;
|
|
45
|
+
const y = height - 34;
|
|
46
|
+
return `<g class="dmg-map-compass" transform="translate(${x} ${y})"><circle r="18"/><path d="M0 -14 L4 0 L0 14 L-4 0 Z"/><circle r="3"/></g>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function interactionGridSvg(map: DungeonMap): string {
|
|
50
|
+
return map.tiles.flatMap((row, y) => row.map((_tile, x) =>
|
|
51
|
+
`<rect class="dmg-map-hit" data-cell data-x="${x}" data-y="${y}" x="${x * CELL}" y="${y * CELL}" width="${CELL}" height="${CELL}"/>`)).join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function standaloneStyle(style: string): string {
|
|
55
|
+
const floor = style === 'scifi' ? '#d9f6f0' : '#ead9b7';
|
|
56
|
+
const door = style === 'cavern' ? '#d7774f' : '#b98142';
|
|
57
|
+
return `<style>.dmg-map-void{fill:#171b22}.dmg-map-floor{fill:${floor};stroke:#6d6253;stroke-width:.7}.dmg-map-door line{stroke:${door};stroke-width:4}.dmg-map-door circle{fill:#fff}.dmg-map-compass circle{fill:#171b22;stroke:#c5a25c;stroke-width:1}.dmg-map-compass path{fill:#c5a25c}</style>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderMap(context: RenderContext): void {
|
|
61
|
+
const { map, root } = context;
|
|
62
|
+
root.dataset.style = map.config.style;
|
|
63
|
+
const mapHost = root.querySelector<HTMLElement>('[data-map-host]');
|
|
64
|
+
if (mapHost) mapHost.innerHTML = buildMapSvg(map);
|
|
65
|
+
setText(root, '[data-map-seed]', map.config.seed);
|
|
66
|
+
setText(root, '[data-room-count]', String(map.rooms.length));
|
|
67
|
+
setText(root, '[data-door-count]', String(map.doorCount));
|
|
68
|
+
renderEvaluation(context);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderEvaluation(context: RenderContext): void {
|
|
72
|
+
const evaluation = evaluateMap(context.map, context.ui);
|
|
73
|
+
setText(context.root, '[data-floor-percent]', `${evaluation.coverage}%`);
|
|
74
|
+
setText(context.root, '[data-evaluation-label]', evaluation.label);
|
|
75
|
+
setText(context.root, '[data-evaluation-hint]', evaluation.hint);
|
|
76
|
+
const badge = context.root.querySelector<HTMLElement>('[data-evaluation]');
|
|
77
|
+
if (badge) badge.dataset.pace = evaluation.pace;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
81
|
+
const element = root.querySelector<HTMLElement>(selector);
|
|
82
|
+
if (element) element.textContent = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
86
|
+
const url = URL.createObjectURL(blob);
|
|
87
|
+
const link = document.createElement('a');
|
|
88
|
+
link.href = url;
|
|
89
|
+
link.download = filename;
|
|
90
|
+
link.click();
|
|
91
|
+
URL.revokeObjectURL(url);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function filename(map: DungeonMap, extension: string): string {
|
|
95
|
+
const seed = map.config.seed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
96
|
+
return `dungeon-${seed || 'map'}.${extension}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function exportSvg(map: DungeonMap): void {
|
|
100
|
+
const blob = new Blob([buildMapSvg(map, true)], { type: 'image/svg+xml;charset=utf-8' });
|
|
101
|
+
downloadBlob(blob, filename(map, 'svg'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function exportJson(map: DungeonMap, edits: DungeonEdit[]): void {
|
|
105
|
+
const data = JSON.stringify({ version: 1, config: map.config, edits }, null, 2);
|
|
106
|
+
downloadBlob(new Blob([data], { type: 'application/json' }), filename(map, 'json'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function exportPng(map: DungeonMap): Promise<void> {
|
|
110
|
+
const svg = buildMapSvg(map, true);
|
|
111
|
+
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
112
|
+
const source = URL.createObjectURL(blob);
|
|
113
|
+
try {
|
|
114
|
+
await drawPng(map, source);
|
|
115
|
+
} finally {
|
|
116
|
+
URL.revokeObjectURL(source);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function drawPng(map: DungeonMap, source: string): Promise<void> {
|
|
121
|
+
const image = await loadImage(source);
|
|
122
|
+
const canvas = document.createElement('canvas');
|
|
123
|
+
canvas.width = map.config.columns * CELL * 2;
|
|
124
|
+
canvas.height = map.config.rows * CELL * 2;
|
|
125
|
+
const context = canvas.getContext('2d');
|
|
126
|
+
if (!context) return;
|
|
127
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
128
|
+
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
129
|
+
if (blob) downloadBlob(blob, filename(map, 'png'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadImage(source: string): Promise<HTMLImageElement> {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const image = new Image();
|
|
135
|
+
image.onload = () => resolve(image);
|
|
136
|
+
image.onerror = () => reject(new Error('Unable to render map image'));
|
|
137
|
+
image.src = source;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Tile } from './logic';
|
|
2
|
+
|
|
3
|
+
type EditHandler = (x: number, y: number, tile: Tile) => void;
|
|
4
|
+
|
|
5
|
+
interface CellPosition {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function bindContextEditor(root: HTMLElement, onEdit: EditHandler): void {
|
|
11
|
+
const host = root.querySelector<HTMLElement>('[data-map-host]');
|
|
12
|
+
const menu = root.querySelector<HTMLElement>('[data-context-menu]');
|
|
13
|
+
if (!host || !menu) return;
|
|
14
|
+
let selected: CellPosition | undefined;
|
|
15
|
+
let holdTimer = 0;
|
|
16
|
+
host.addEventListener('contextmenu', (event) => {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
selected = openCellMenu(root, menu, event);
|
|
19
|
+
});
|
|
20
|
+
host.addEventListener('pointerdown', (event) => {
|
|
21
|
+
holdTimer = window.setTimeout(() => { selected = openCellMenu(root, menu, event); }, 550);
|
|
22
|
+
});
|
|
23
|
+
host.addEventListener('pointerup', () => window.clearTimeout(holdTimer));
|
|
24
|
+
host.addEventListener('pointercancel', () => window.clearTimeout(holdTimer));
|
|
25
|
+
menu.querySelectorAll<HTMLButtonElement>('[data-context-tool]').forEach((button) => {
|
|
26
|
+
button.addEventListener('click', () => {
|
|
27
|
+
if (selected) onEdit(selected.x, selected.y, tileForTool(button.dataset.contextTool));
|
|
28
|
+
closeMenu(menu);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
document.addEventListener('click', (event) => {
|
|
32
|
+
if (!menu.contains(event.target as Node)) closeMenu(menu);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function openCellMenu(root: HTMLElement, menu: HTMLElement, event: MouseEvent | PointerEvent): CellPosition | undefined {
|
|
37
|
+
const cell = (event.target as Element).closest<SVGElement>('[data-cell]');
|
|
38
|
+
if (!cell) return undefined;
|
|
39
|
+
const frame = root.querySelector<HTMLElement>('.dmg-map-frame');
|
|
40
|
+
if (!frame) return undefined;
|
|
41
|
+
const bounds = frame.getBoundingClientRect();
|
|
42
|
+
menu.style.left = `${event.clientX - bounds.left}px`;
|
|
43
|
+
menu.style.top = `${event.clientY - bounds.top}px`;
|
|
44
|
+
menu.hidden = false;
|
|
45
|
+
menu.querySelector<HTMLButtonElement>('button')?.focus();
|
|
46
|
+
return { x: Number(cell.dataset.x), y: Number(cell.dataset.y) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tileForTool(tool: string | undefined): Tile {
|
|
50
|
+
if (tool === 'wall') return 0;
|
|
51
|
+
if (tool === 'door') return 2;
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function closeMenu(menu: HTMLElement): void {
|
|
56
|
+
menu.hidden = true;
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { DungeonMapGeneratorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { DungeonMapGeneratorUI } from './ui';
|
|
5
|
+
export type DungeonMapGeneratorLocaleContent = ToolLocaleContent<DungeonMapGeneratorUI>;
|
|
6
|
+
|
|
7
|
+
export const dungeonMapGenerator: TabletopToolEntry<DungeonMapGeneratorUI> = {
|
|
8
|
+
id: 'dungeon-map-generator',
|
|
9
|
+
icons: { bg: 'mdi:map-outline', fg: 'mdi:castle' },
|
|
10
|
+
i18n: {
|
|
11
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
12
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
13
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
14
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
15
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
16
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
17
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
18
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
19
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
21
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
22
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
23
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
24
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
25
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DungeonMap } from './logic';
|
|
2
|
+
import type { DungeonMapGeneratorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type MapPace = 'compact' | 'balanced' | 'sprawling';
|
|
5
|
+
|
|
6
|
+
export interface MapEvaluation {
|
|
7
|
+
pace: MapPace;
|
|
8
|
+
label: string;
|
|
9
|
+
hint: string;
|
|
10
|
+
coverage: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function evaluateMap(map: DungeonMap, ui: DungeonMapGeneratorUI): MapEvaluation {
|
|
14
|
+
const coverage = Math.round((map.floorCount / (map.config.columns * map.config.rows)) * 100);
|
|
15
|
+
const pace = getPace(coverage);
|
|
16
|
+
return {
|
|
17
|
+
pace,
|
|
18
|
+
coverage,
|
|
19
|
+
label: getLabel(pace, ui),
|
|
20
|
+
hint: getHint(pace, ui),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getPace(coverage: number): MapPace {
|
|
25
|
+
if (coverage < 24) return 'compact';
|
|
26
|
+
if (coverage > 38) return 'sprawling';
|
|
27
|
+
return 'balanced';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getLabel(pace: MapPace, ui: DungeonMapGeneratorUI): string {
|
|
31
|
+
if (pace === 'compact') return ui.compactMap;
|
|
32
|
+
if (pace === 'sprawling') return ui.sprawlingMap;
|
|
33
|
+
return ui.balancedMap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getHint(pace: MapPace, ui: DungeonMapGeneratorUI): string {
|
|
37
|
+
if (pace === 'compact') return ui.compactHint;
|
|
38
|
+
if (pace === 'sprawling') return ui.sprawlingHint;
|
|
39
|
+
return ui.balancedHint;
|
|
40
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { DungeonMapGeneratorLocaleContent, DungeonMapGeneratorUI } from '../entry';
|
|
4
|
+
|
|
5
|
+
const ui: DungeonMapGeneratorUI = {
|
|
6
|
+
intro: 'Wählen Sie einen Seed, stellen Sie das Raster ein und wählen Sie einen Stil. Generieren Sie eine verbundene Karte für Notizen, Druck oder VTT.',
|
|
7
|
+
seedLabel: 'Karten-Seed',
|
|
8
|
+
seedHint: 'Verwenden Sie denselben Seed bei gleichen Einstellungen, um die exakt gleiche Karte zu erstellen.',
|
|
9
|
+
randomSeed: 'Neuer Seed',
|
|
10
|
+
sizeLabel: 'Expeditionsgröße',
|
|
11
|
+
sizeCompact: 'One-Shot',
|
|
12
|
+
sizeSession: 'Sitzung',
|
|
13
|
+
sizeStronghold: 'Festung',
|
|
14
|
+
fineTuneGrid: 'Raster und Dichte feinabstimmen',
|
|
15
|
+
columnsLabel: 'Spalten',
|
|
16
|
+
rowsLabel: 'Zeilen',
|
|
17
|
+
densityLabel: 'Raumdichte',
|
|
18
|
+
densityHint: 'Höhere Dichte erzeugt mehr Räume und nutzbare Bodenfläche.',
|
|
19
|
+
styleLabel: 'Kartenarchitektur',
|
|
20
|
+
dungeonStyle: 'Dungeon',
|
|
21
|
+
dungeonStyleHint: 'Steinräume mit einfachen Gängen.',
|
|
22
|
+
cavernStyle: 'Höhle',
|
|
23
|
+
cavernStyleHint: 'Unregelmäßige Räume mit weichen Kanten.',
|
|
24
|
+
scifiStyle: 'Sci-Fi',
|
|
25
|
+
scifiStyleHint: 'Breite Module mit doppelten Verbindungsgängen.',
|
|
26
|
+
generate: 'Karte zeichnen',
|
|
27
|
+
mapRegionLabel: 'Generierte Dungeon-Karte und Export-Steuerung',
|
|
28
|
+
connectedBadge: 'Alle Räume verbunden',
|
|
29
|
+
roomsLabel: 'Räume',
|
|
30
|
+
doorsLabel: 'Türen',
|
|
31
|
+
floorLabel: 'Bodenabdeckung',
|
|
32
|
+
legendFloor: 'Begehbarer Boden',
|
|
33
|
+
legendWall: 'Feste Wand',
|
|
34
|
+
legendDoor: 'Tür oder Schott',
|
|
35
|
+
editHint: 'Rechtsklick auf eine Zelle zum Bearbeiten',
|
|
36
|
+
editCell: 'Zelle bearbeiten',
|
|
37
|
+
paintFloor: 'Boden',
|
|
38
|
+
paintWall: 'Wand',
|
|
39
|
+
paintDoor: 'Tür',
|
|
40
|
+
mapData: 'Kartendaten',
|
|
41
|
+
copyLink: 'Link kopieren',
|
|
42
|
+
linkCopied: 'Link kopiert',
|
|
43
|
+
exportPng: 'PNG herunterladen',
|
|
44
|
+
exportSvg: 'SVG herunterladen',
|
|
45
|
+
exportJson: 'JSON speichern',
|
|
46
|
+
importJson: 'JSON öffnen',
|
|
47
|
+
importError: 'Diese Datei enthält keine gültige Dungeon-Konfiguration.',
|
|
48
|
+
compactMap: 'Angespannte Erkundung',
|
|
49
|
+
balancedMap: 'Ausgewogene Expedition',
|
|
50
|
+
sprawlingMap: 'Offene Festung',
|
|
51
|
+
compactHint: 'Längere Übergänge lassen mehr unbekannten Raum zwischen Räumen.',
|
|
52
|
+
balancedHint: 'Räume und Gänge teilen sich die Fläche gleichmäßig.',
|
|
53
|
+
sprawlingHint: 'Große Bodenfläche begünstigt Begegnungen und schnelle Bewegung.',
|
|
54
|
+
mapReady: 'Kartenlegende',
|
|
55
|
+
dimensionsUnit: 'Rasterzellen',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const faq = [
|
|
59
|
+
{
|
|
60
|
+
question: 'Erzeugt derselbe Seed immer dieselbe Dungeon-Karte?',
|
|
61
|
+
answer: 'Ja. Seed, Rasterabmessungen, Dichte und Stil bilden eine deterministische Konfiguration. Die Wiederverwendung aller vier Einstellungen baut exakt dieselben Räume und Türen auf.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
question: 'Sind alle generierten Räume erreichbar?',
|
|
65
|
+
answer: 'Ja. Die Räume werden nach der Platzierung über eine Hauptroute verbunden, sodass jeder begehbare Bereich zu einer einzigen verbundenen Karte gehört.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
question: 'Was unterscheidet Dungeon-, Höhlen- und Sci-Fi-Stile?',
|
|
69
|
+
answer: 'Dungeons nutzen regelmäßige Steinräume. Höhlen erzeugen unregelmäßige, abgerundete Kammern. Sci-Fi-Karten bevorzugen breite Module mit doppelten Schotts.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
question: 'Welches Format eignet sich für ein Virtual Tabletop (VTT)?',
|
|
73
|
+
answer: 'PNG ist für die meisten VTTs am einfachsten. SVG bleibt bei jeder Skalierung scharf und lässt sich in Vektorprogrammen leicht bearbeiten.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
question: 'Kann jemand anderes meine Dungeon-Karte regenerieren?',
|
|
77
|
+
answer: 'Ja. Kopieren Sie den Link oder senden Sie die JSON-Datei. Der Empfänger kann dieselbe Konfiguration sofort laden.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
question: 'Fügt dieser Generator Monster, Fallen oder Schätze hinzu?',
|
|
81
|
+
answer: 'Nein. Er erstellt einen neutralen Grundriss, den Sie für jedes Regelwerk anpassen können. Die Ausgestaltung obliegt dem Spielleiter.',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const howTo = [
|
|
86
|
+
{
|
|
87
|
+
name: 'Rasterabmessungen festlegen',
|
|
88
|
+
text: 'Wählen Sie Spalten und Zeilen passend für Ihre Notizen oder VTT-Szene.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'Architektur wählen',
|
|
92
|
+
text: 'Wählen Sie Dungeon, Höhle oder Sci-Fi und passen Sie die Raumdichte an.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Generieren und prüfen',
|
|
96
|
+
text: 'Geben Sie einen Seed ein, zeichnen Sie die Karte und prüfen Sie die Übersicht.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Exportieren oder teilen',
|
|
100
|
+
text: 'Laden Sie PNG oder SVG herunter oder speichern Sie die Konfiguration per Link oder JSON.',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
105
|
+
'@context': 'https://schema.org',
|
|
106
|
+
'@type': 'FAQPage',
|
|
107
|
+
mainEntity: faq.map((item) => ({
|
|
108
|
+
'@type': 'Question',
|
|
109
|
+
name: item.question,
|
|
110
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
115
|
+
'@context': 'https://schema.org',
|
|
116
|
+
'@type': 'SoftwareApplication',
|
|
117
|
+
name: 'Zufalls-Dungeon-Kartengenerator',
|
|
118
|
+
operatingSystem: 'All',
|
|
119
|
+
applicationCategory: 'UtilitiesApplication',
|
|
120
|
+
description: 'Ein deterministischer Generator für Dungeon-Karten mit verbundenen Räumen, Türen und lokalen Exporten.',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const howToSchema: WithContext<HowTo> = {
|
|
124
|
+
'@context': 'https://schema.org',
|
|
125
|
+
'@type': 'HowTo',
|
|
126
|
+
name: 'So generieren Sie eine verbundene Dungeon-Karte',
|
|
127
|
+
step: howTo.map((item) => ({
|
|
128
|
+
'@type': 'HowToStep',
|
|
129
|
+
name: item.name,
|
|
130
|
+
text: item.text,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const content: DungeonMapGeneratorLocaleContent = {
|
|
135
|
+
slug: 'zufalls-dungeon-karten-generator',
|
|
136
|
+
title: 'Dungeon Kartengenerator für Rollenspiele',
|
|
137
|
+
description: 'Erstellen Sie reproduzierbare Dungeon-, Höhlen- und Sci-Fi-Karten mit verbundenen Räumen, Türen und PNG/SVG-Export.',
|
|
138
|
+
ui,
|
|
139
|
+
seo: [
|
|
140
|
+
{ type: 'title', text: 'Erstellen Sie eine spielbare Dungeon-Karte aus einem wiederverwendbaren Seed', level: 2 },
|
|
141
|
+
{ type: 'paragraph', html: 'Eine nützliche Zufalls-Dungeon-Karte erfordert mehr als beliebiges visuelles Rauschen. Jede Kammer muss erreichbar sein, das Raster muss zur Spielfläche passen und das Ergebnis muss jederzeit wiederherstellbar sein. Dieser Generator behandelt Seed und Einstellungen als deterministische Kartenspezifikation. Dieselben Einstellungen erzeugen stets denselben Plan.' },
|
|
142
|
+
{ type: 'title', text: 'Rasterabmessungen für Druck und Virtual Tabletops wählen', level: 2 },
|
|
143
|
+
{ type: 'paragraph', html: 'Die Wahl der Rastergröße ist der erste praktische Schritt. Ein kleineres Raster lässt sich leicht ausdrucken und eignet sich für kurze Abenteuer, während ein großes Raster Raum für Erkundungen bietet. Das exportierte Bild behält quadratische Zellen bei, um sich nahtlos an VTT-Raster anzupassen.' },
|
|
144
|
+
{
|
|
145
|
+
type: 'table',
|
|
146
|
+
headers: ['Rastergröße', 'Typische Nutzung', 'Start-Dichte', 'Planungshinweis'],
|
|
147
|
+
rows: [
|
|
148
|
+
['20 mal 16 Zellen', 'One-Shot Ort', '35 bis 45', 'Schnell zu lesen und einfach zu drucken'],
|
|
149
|
+
['36 mal 26 Zellen', 'Sitzungs-Dungeon', '45 bis 55', 'Ausgewogener Rhythmus zwischen Raum und Gang'],
|
|
150
|
+
['52 mal 38 Zellen', 'Mehrteiliger Komplex', '50 bis 65', 'Viel Platz für Fraktionen und optionale Wege'],
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{ type: 'tip', title: 'Passen Sie die Karte an die Begegnungsgröße an', html: 'Berechnen Sie den benötigten Platz für große Begegnungen vor der Größenwahl. Ein großer Dungeon mit winzigen Räumen wirkt eng, während ein kompakter Plan mit einem großzügigen Hauptraum einprägsame Szenen ermöglicht.' },
|
|
154
|
+
{ type: 'title', text: 'Bodenabdeckung als Tempo-Signal verstehen', level: 2 },
|
|
155
|
+
{ type: 'paragraph', html: 'Die Bodenabdeckung beschreibt den Prozentsatz der begehbaren Fläche. Geringe Abdeckung betont Isolation und unentdeckten Raum. Mittlere Abdeckung sorgt für stetigen Spielfluss. Hohe Abdeckung schafft komplexe Festungen mit dynamischen Kämpfen.' },
|
|
156
|
+
{
|
|
157
|
+
type: 'list',
|
|
158
|
+
items: [
|
|
159
|
+
'<strong>Angespannte Erkundung:</strong> Nutzen Sie leeren Raum für Isolation oder Geheimgänge.',
|
|
160
|
+
'<strong>Ausgewogene Expedition:</strong> Wechseln Sie Räume und Verbindungswege ab.',
|
|
161
|
+
'<strong>Offene Festung:</strong> Planen Sie Sichtlinien und Patrouillenwege ein.',
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{ type: 'title', text: 'Vom neutralen Grundriss zum Abenteuerort', level: 2 },
|
|
165
|
+
{ type: 'paragraph', html: 'Die generierte Karte liefert die reine Architektur. Weisen Sie Räumen erzählerische Funktionen zu und platzieren Sie Hinweise, Fallen und Schätze entsprechend.' },
|
|
166
|
+
{ type: 'tip', title: 'Konfiguration vor dem Ausgestalten sichern', html: 'Kopieren Sie den Link oder speichern Sie das JSON vor dem Hinzufügen von Notizen, um den Spielern eine saubere Karte ohne Spielleiter-Notizen zu geben.' },
|
|
167
|
+
],
|
|
168
|
+
faq,
|
|
169
|
+
bibliography,
|
|
170
|
+
howTo,
|
|
171
|
+
schemas: [faqSchema, appSchema, howToSchema] as unknown as Record<string, unknown>[],
|
|
172
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { DungeonMapGeneratorLocaleContent, DungeonMapGeneratorUI } from '../entry';
|
|
4
|
+
|
|
5
|
+
const ui: DungeonMapGeneratorUI = {
|
|
6
|
+
intro: 'Choose a seed, set the grid, and select an architectural language. Generate a connected map, then take it to your notes, print layout, or virtual tabletop.',
|
|
7
|
+
seedLabel: 'Map seed',
|
|
8
|
+
seedHint: 'Reuse a seed with the same settings to rebuild the exact map.',
|
|
9
|
+
randomSeed: 'New seed',
|
|
10
|
+
sizeLabel: 'Expedition size',
|
|
11
|
+
sizeCompact: 'One Shot',
|
|
12
|
+
sizeSession: 'Session',
|
|
13
|
+
sizeStronghold: 'Stronghold',
|
|
14
|
+
fineTuneGrid: 'Fine tune grid and density',
|
|
15
|
+
columnsLabel: 'Columns',
|
|
16
|
+
rowsLabel: 'Rows',
|
|
17
|
+
densityLabel: 'Room density',
|
|
18
|
+
densityHint: 'Higher density adds more chambers and usable floor.',
|
|
19
|
+
styleLabel: 'Map architecture',
|
|
20
|
+
dungeonStyle: 'Dungeon',
|
|
21
|
+
dungeonStyleHint: 'Stone rooms with single width passages.',
|
|
22
|
+
cavernStyle: 'Cavern',
|
|
23
|
+
cavernStyleHint: 'Irregular chambers with softened edges.',
|
|
24
|
+
scifiStyle: 'Sci Fi',
|
|
25
|
+
scifiStyleHint: 'Wide modules with double width connectors.',
|
|
26
|
+
generate: 'Draw this map',
|
|
27
|
+
mapRegionLabel: 'Generated dungeon map and export controls',
|
|
28
|
+
connectedBadge: 'All rooms connected',
|
|
29
|
+
roomsLabel: 'Rooms',
|
|
30
|
+
doorsLabel: 'Doors',
|
|
31
|
+
floorLabel: 'Floor coverage',
|
|
32
|
+
legendFloor: 'Walkable floor',
|
|
33
|
+
legendWall: 'Solid wall',
|
|
34
|
+
legendDoor: 'Door or bulkhead',
|
|
35
|
+
editHint: 'Right click a cell to edit',
|
|
36
|
+
editCell: 'Edit this cell',
|
|
37
|
+
paintFloor: 'Floor',
|
|
38
|
+
paintWall: 'Wall',
|
|
39
|
+
paintDoor: 'Door',
|
|
40
|
+
mapData: 'Map data',
|
|
41
|
+
copyLink: 'Copy map link',
|
|
42
|
+
linkCopied: 'Link copied',
|
|
43
|
+
exportPng: 'Download PNG',
|
|
44
|
+
exportSvg: 'Download SVG',
|
|
45
|
+
exportJson: 'Save setup JSON',
|
|
46
|
+
importJson: 'Open setup JSON',
|
|
47
|
+
importError: 'This file does not contain a valid dungeon map setup.',
|
|
48
|
+
compactMap: 'Tense exploration',
|
|
49
|
+
balancedMap: 'Balanced expedition',
|
|
50
|
+
sprawlingMap: 'Open stronghold',
|
|
51
|
+
compactHint: 'Longer transitions leave more unknown space between rooms.',
|
|
52
|
+
balancedHint: 'Rooms and passages share the plan without crowding each other.',
|
|
53
|
+
sprawlingHint: 'Generous floor coverage favors encounters and rapid movement.',
|
|
54
|
+
mapReady: 'Map tile legend',
|
|
55
|
+
dimensionsUnit: 'grid cells',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const faq = [
|
|
59
|
+
{
|
|
60
|
+
question: 'Does the same seed always create the same dungeon map?',
|
|
61
|
+
answer: 'Yes. The seed, grid dimensions, density, and architecture style form one deterministic setup. Reusing all four settings rebuilds the same rooms, corridors, and doors.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
question: 'Are all generated rooms reachable?',
|
|
65
|
+
answer: 'Yes. Rooms are connected in a spanning route after placement, so every walkable region belongs to one connected map. Doors mark the transition between rooms and corridors.',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
question: 'What changes between dungeon, cavern, and Sci Fi styles?',
|
|
69
|
+
answer: 'Dungeon maps use regular stone rooms and narrow corridors. Caverns carve irregular rounded chambers. Sci Fi maps favor wider modules and double width connectors for bulkhead style layouts.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
question: 'Which format should I export for a virtual tabletop?',
|
|
73
|
+
answer: 'PNG is the simplest option for most virtual tabletops. SVG stays sharp at any scale and is easier to edit in vector software. Both exports preserve the square grid geometry.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
question: 'Can another person regenerate my random dungeon map?',
|
|
77
|
+
answer: 'Yes. Copy the map link to include the full setup in the URL, or send the JSON setup file. The recipient can load the same deterministic configuration without an account.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
question: 'Does this generator add monsters, traps, or treasure?',
|
|
81
|
+
answer: 'No. It creates a neutral architectural floor plan so you can stock it for any ruleset. Encounter design, secrets, hazards, and narrative meaning remain under the game master\'s control.',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const howTo = [
|
|
86
|
+
{
|
|
87
|
+
name: 'Set the map footprint',
|
|
88
|
+
text: 'Choose the number of columns and rows to match the space available in your notes, print page, or virtual tabletop scene.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'Choose an architecture',
|
|
92
|
+
text: 'Select dungeon, cavern, or Sci Fi, then adjust room density for a tighter or more open expedition.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Generate and inspect',
|
|
96
|
+
text: 'Enter a memorable seed or request a new one, draw the map, and read the room, door, and floor coverage summary.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Export or share',
|
|
100
|
+
text: 'Download PNG or SVG for play, or preserve the exact configuration through a map link or JSON setup file.',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
105
|
+
'@context': 'https://schema.org',
|
|
106
|
+
'@type': 'FAQPage',
|
|
107
|
+
mainEntity: faq.map((item) => ({
|
|
108
|
+
'@type': 'Question',
|
|
109
|
+
name: item.question,
|
|
110
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
111
|
+
})),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
115
|
+
'@context': 'https://schema.org',
|
|
116
|
+
'@type': 'SoftwareApplication',
|
|
117
|
+
name: 'Dungeon Map Generator',
|
|
118
|
+
operatingSystem: 'All',
|
|
119
|
+
applicationCategory: 'UtilitiesApplication',
|
|
120
|
+
description: 'A deterministic random dungeon map generator with connected rooms, corridors, doors, grid sizing, and local exports.',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const howToSchema: WithContext<HowTo> = {
|
|
124
|
+
'@context': 'https://schema.org',
|
|
125
|
+
'@type': 'HowTo',
|
|
126
|
+
name: 'How to generate a connected dungeon map',
|
|
127
|
+
step: howTo.map((item) => ({
|
|
128
|
+
'@type': 'HowToStep',
|
|
129
|
+
name: item.name,
|
|
130
|
+
text: item.text,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const content: DungeonMapGeneratorLocaleContent = {
|
|
135
|
+
slug: 'random-dungeon-map-generator',
|
|
136
|
+
title: 'Random Dungeon Map Generator for Tabletop Games',
|
|
137
|
+
description: 'Create reproducible dungeon, cavern, and Sci Fi maps with connected rooms, doors, grid controls, and PNG or SVG export.',
|
|
138
|
+
ui,
|
|
139
|
+
seo: [
|
|
140
|
+
{ type: 'title', text: 'Build a Playable Dungeon Map from a Reusable Seed', level: 2 },
|
|
141
|
+
{ type: 'paragraph', html: 'A useful random dungeon map needs more than visual noise. Every chamber must be reachable, the grid must fit the intended play surface, and the result must be recoverable when a session moves between devices. This generator treats the seed and controls as a compact map specification. Keep the same seed, columns, rows, density, and architecture to reproduce the exact plan later.' },
|
|
142
|
+
{ type: 'title', text: 'Choose Grid Dimensions for Print and Virtual Tabletops', level: 2 },
|
|
143
|
+
{ type: 'paragraph', html: 'Start with the destination. A smaller grid is easier to print and produces a focused delve, while a larger grid leaves room for exploration, encounter staging, and optional branches. The exported image preserves square cells, so you can align it with the grid controls in a virtual tabletop instead of stretching the map by eye.' },
|
|
144
|
+
{
|
|
145
|
+
type: 'table',
|
|
146
|
+
headers: ['Grid size', 'Typical use', 'Density starting point', 'Planning note'],
|
|
147
|
+
rows: [
|
|
148
|
+
['20 by 16', 'One shot location', '35 to 45', 'Fast to read and easy to print'],
|
|
149
|
+
['36 by 26', 'Session dungeon', '45 to 55', 'Balanced room and corridor rhythm'],
|
|
150
|
+
['52 by 38', 'Multi session complex', '50 to 65', 'More space for factions and optional routes'],
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{ type: 'tip', title: 'Match the Map to the Encounter Scale', html: 'Count the cells required by the largest planned encounter before choosing the full map size. A large dungeon with undersized rooms can still feel cramped, while a compact plan with one generous chamber may support a memorable set piece.' },
|
|
154
|
+
{ type: 'title', text: 'Read Floor Coverage as a Pacing Signal', level: 2 },
|
|
155
|
+
{ type: 'paragraph', html: 'Floor coverage describes how much of the rectangular canvas is walkable. Lower coverage emphasizes uncertain movement and separation between rooms. Midrange coverage supports a steady expedition rhythm. High coverage creates a stronghold or facility where encounters can spill across connected spaces. It is not a difficulty score; it is a prompt for how the location may feel at the table.' },
|
|
156
|
+
{
|
|
157
|
+
type: 'list',
|
|
158
|
+
items: [
|
|
159
|
+
'<strong>Tense exploration:</strong> use the empty space to imply depth, isolation, or concealed areas.',
|
|
160
|
+
'<strong>Balanced expedition:</strong> alternate investigation rooms, connective passages, and encounter spaces.',
|
|
161
|
+
'<strong>Open stronghold:</strong> prepare sight lines, patrol routes, and encounters that can attract nearby groups.',
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{ type: 'title', text: 'Turn a Neutral Layout into an Adventure Location', level: 2 },
|
|
165
|
+
{ type: 'paragraph', html: 'The generated plan intentionally stops at architecture. Assign a purpose to each major room, then place clues, hazards, creatures, and treasure according to that purpose. Use doors as decision points rather than decoration. A locked door can advertise value, a broken door can reveal prior violence, and an open bulkhead can connect two encounters into one larger problem.' },
|
|
166
|
+
{ type: 'tip', title: 'Preserve the Setup Before Stocking Rooms', html: 'Copy the map link or save the JSON setup before adding encounter notes. That gives the group a stable architectural reference and lets you regenerate a clean player version without exposing game master annotations.' },
|
|
167
|
+
],
|
|
168
|
+
faq,
|
|
169
|
+
bibliography,
|
|
170
|
+
howTo,
|
|
171
|
+
schemas: [faqSchema, appSchema, howToSchema] as unknown as Record<string, unknown>[],
|
|
172
|
+
};
|