@jjlmoya/utils-tabletop 1.32.0 → 1.34.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 +2 -2
- package/src/category/i18n/de.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/i18n/id.ts +1 -1
- package/src/category/i18n/it.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/i18n/sv.ts +1 -1
- package/src/category/i18n/tr.ts +1 -1
- package/src/category/index.ts +25 -25
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/entries.ts +43 -43
- package/src/env.d.ts +1 -1
- package/src/index.ts +24 -24
- package/src/layouts/PreviewLayout.astro +93 -93
- package/src/pages/[locale]/[slug].astro +143 -143
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/i18n_coverage.test.ts +36 -36
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_em_dash.test.ts +47 -47
- package/src/tests/no_en_dash.test.ts +70 -70
- package/src/tests/seo_length.test.ts +3 -3
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +18 -12
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/spanish_leakage.test.ts +175 -175
- package/src/tool/dungeon-map-generator/component.astro +135 -135
- package/src/tool/dungeon-map-generator/controller.ts +243 -243
- package/src/tool/dungeon-map-generator/dom-views.ts +144 -144
- package/src/tool/dungeon-map-generator/i18n/de.ts +172 -172
- package/src/tool/dungeon-map-generator/logic.ts +281 -281
- package/src/tool/dungeon-map-generator/random-dungeon-map-generator.css +692 -692
- package/src/tool/dungeon-map-generator/ui.ts +54 -54
- package/src/tool/fantasy-runes-translator/logic.ts +1 -1
- package/src/tool/first-player-selector/i18n/tr.ts +1 -1
- package/src/tool/investigation-board/i18n/ru.ts +209 -209
- package/src/tool/investigation-board/i18n/zh.ts +209 -209
- package/src/tool/lunar-tide-tracker/client.ts +264 -264
- package/src/tool/rpg-initiative-tracker/i18n/de.ts +3 -3
- package/src/tool/rpg-initiative-tracker/i18n/es.ts +1 -1
- package/src/tool/rpg-initiative-tracker/i18n/pl.ts +2 -2
- package/src/tool/rpg-initiative-tracker/i18n/ru.ts +1 -1
- package/src/tool/score-tracker/i18n/pt.ts +1 -1
- package/src/tool/score-tracker/i18n/sv.ts +1 -1
- package/src/tool/score-tracker/i18n/tr.ts +1 -1
- package/src/tool/score-tracker/i18n/zh.ts +1 -1
- package/src/tools.ts +25 -25
|
@@ -1,144 +1,144 @@
|
|
|
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, isPrint = false): string {
|
|
15
|
-
const width = map.config.columns * CELL;
|
|
16
|
-
const height = map.config.rows * CELL;
|
|
17
|
-
const style = isPrint ? 'print' : map.config.style;
|
|
18
|
-
const cells = map.tiles.flatMap((row, y) =>
|
|
19
|
-
row.map((tile, x) => tileSvg(tile, x, y, style))).join('');
|
|
20
|
-
const palette = standalone ? standaloneStyle(style) : '';
|
|
21
|
-
const doors = map.doors.map(doorSvg).join('');
|
|
22
|
-
const hitGrid = standalone ? '' : interactionGridSvg(map);
|
|
23
|
-
return `<svg class="dmg-map dmg-map-${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>`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function tileSvg(tile: Tile, x: number, y: number, style: string): string {
|
|
27
|
-
if (tile === 0) return '';
|
|
28
|
-
const px = x * CELL;
|
|
29
|
-
const py = y * CELL;
|
|
30
|
-
const radius = style === 'cavern' ? 7 : 1;
|
|
31
|
-
return `<rect class="dmg-map-floor" x="${px}" y="${py}" width="${CELL + 0.5}" height="${CELL + 0.5}" rx="${radius}"/>`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function doorSvg(door: Door): string {
|
|
35
|
-
const x = door.x * CELL;
|
|
36
|
-
const y = door.y * CELL;
|
|
37
|
-
if (door.orientation === 'vertical') {
|
|
38
|
-
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>`;
|
|
39
|
-
}
|
|
40
|
-
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>`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function compassSvg(width: number, height: number): string {
|
|
44
|
-
if (width < 400 || height < 300) return '';
|
|
45
|
-
const x = width - 34;
|
|
46
|
-
const y = height - 34;
|
|
47
|
-
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>`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function interactionGridSvg(map: DungeonMap): string {
|
|
51
|
-
return map.tiles.flatMap((row, y) => row.map((_tile, x) =>
|
|
52
|
-
`<rect class="dmg-map-hit" data-cell data-x="${x}" data-y="${y}" x="${x * CELL}" y="${y * CELL}" width="${CELL}" height="${CELL}"/>`)).join('');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function standaloneStyle(style: string): string {
|
|
56
|
-
if (style === 'print') {
|
|
57
|
-
return `<style>.dmg-map-void{fill:#ffffff}.dmg-map-floor{fill:#ffffff;stroke:#333333;stroke-width:.6}.dmg-map-door line{stroke:#000000;stroke-width:4}.dmg-map-door circle{fill:#000000}.dmg-map-compass circle{fill:#ffffff;stroke:#000000;stroke-width:1}.dmg-map-compass path{fill:#000000}</style>`;
|
|
58
|
-
}
|
|
59
|
-
const floor = style === 'scifi' ? '#d9f6f0' : '#ead9b7';
|
|
60
|
-
const door = style === 'cavern' ? '#d7774f' : '#b98142';
|
|
61
|
-
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>`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function renderMap(context: RenderContext): void {
|
|
65
|
-
const { map, root } = context;
|
|
66
|
-
root.dataset.style = map.config.style;
|
|
67
|
-
const mapHost = root.querySelector<HTMLElement>('[data-map-host]');
|
|
68
|
-
if (mapHost) mapHost.innerHTML = buildMapSvg(map);
|
|
69
|
-
setText(root, '[data-map-seed]', map.config.seed);
|
|
70
|
-
setText(root, '[data-room-count]', String(map.rooms.length));
|
|
71
|
-
setText(root, '[data-door-count]', String(map.doorCount));
|
|
72
|
-
renderEvaluation(context);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function renderEvaluation(context: RenderContext): void {
|
|
76
|
-
const evaluation = evaluateMap(context.map, context.ui);
|
|
77
|
-
setText(context.root, '[data-floor-percent]', `${evaluation.coverage}%`);
|
|
78
|
-
setText(context.root, '[data-evaluation-label]', evaluation.label);
|
|
79
|
-
setText(context.root, '[data-evaluation-hint]', evaluation.hint);
|
|
80
|
-
const badge = context.root.querySelector<HTMLElement>('[data-evaluation]');
|
|
81
|
-
if (badge) badge.dataset.pace = evaluation.pace;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
85
|
-
const element = root.querySelector<HTMLElement>(selector);
|
|
86
|
-
if (element) element.textContent = value;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function downloadBlob(blob: Blob, filename: string): void {
|
|
90
|
-
const url = URL.createObjectURL(blob);
|
|
91
|
-
const link = document.createElement('a');
|
|
92
|
-
link.href = url;
|
|
93
|
-
link.download = filename;
|
|
94
|
-
link.click();
|
|
95
|
-
URL.revokeObjectURL(url);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function filename(map: DungeonMap, extension: string, suffix = ''): string {
|
|
99
|
-
const seed = map.config.seed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
100
|
-
return `dungeon-${seed || 'map'}${suffix}.${extension}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function exportSvg(map: DungeonMap, isPrint = false): void {
|
|
104
|
-
const svg = buildMapSvg(map, true, isPrint);
|
|
105
|
-
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
106
|
-
downloadBlob(blob, filename(map, 'svg', isPrint ? '-print' : ''));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function exportJson(map: DungeonMap, edits: DungeonEdit[]): void {
|
|
110
|
-
const data = JSON.stringify({ version: 1, config: map.config, edits }, null, 2);
|
|
111
|
-
downloadBlob(new Blob([data], { type: 'application/json' }), filename(map, 'json'));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function exportPng(map: DungeonMap, isPrint = false): Promise<void> {
|
|
115
|
-
const svg = buildMapSvg(map, true, isPrint);
|
|
116
|
-
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
117
|
-
const source = URL.createObjectURL(blob);
|
|
118
|
-
try {
|
|
119
|
-
await drawPng(map, source, isPrint ? '-print' : '');
|
|
120
|
-
} finally {
|
|
121
|
-
URL.revokeObjectURL(source);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function drawPng(map: DungeonMap, source: string, suffix = ''): Promise<void> {
|
|
126
|
-
const image = await loadImage(source);
|
|
127
|
-
const canvas = document.createElement('canvas');
|
|
128
|
-
canvas.width = map.config.columns * CELL * 2;
|
|
129
|
-
canvas.height = map.config.rows * CELL * 2;
|
|
130
|
-
const context = canvas.getContext('2d');
|
|
131
|
-
if (!context) return;
|
|
132
|
-
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
133
|
-
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
134
|
-
if (blob) downloadBlob(blob, filename(map, 'png', suffix));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function loadImage(source: string): Promise<HTMLImageElement> {
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
const image = new Image();
|
|
140
|
-
image.onload = () => resolve(image);
|
|
141
|
-
image.onerror = () => reject(new Error('Unable to render map image'));
|
|
142
|
-
image.src = source;
|
|
143
|
-
});
|
|
144
|
-
}
|
|
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, isPrint = false): string {
|
|
15
|
+
const width = map.config.columns * CELL;
|
|
16
|
+
const height = map.config.rows * CELL;
|
|
17
|
+
const style = isPrint ? 'print' : map.config.style;
|
|
18
|
+
const cells = map.tiles.flatMap((row, y) =>
|
|
19
|
+
row.map((tile, x) => tileSvg(tile, x, y, style))).join('');
|
|
20
|
+
const palette = standalone ? standaloneStyle(style) : '';
|
|
21
|
+
const doors = map.doors.map(doorSvg).join('');
|
|
22
|
+
const hitGrid = standalone ? '' : interactionGridSvg(map);
|
|
23
|
+
return `<svg class="dmg-map dmg-map-${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>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tileSvg(tile: Tile, x: number, y: number, style: string): string {
|
|
27
|
+
if (tile === 0) return '';
|
|
28
|
+
const px = x * CELL;
|
|
29
|
+
const py = y * CELL;
|
|
30
|
+
const radius = style === 'cavern' ? 7 : 1;
|
|
31
|
+
return `<rect class="dmg-map-floor" x="${px}" y="${py}" width="${CELL + 0.5}" height="${CELL + 0.5}" rx="${radius}"/>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function doorSvg(door: Door): string {
|
|
35
|
+
const x = door.x * CELL;
|
|
36
|
+
const y = door.y * CELL;
|
|
37
|
+
if (door.orientation === 'vertical') {
|
|
38
|
+
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>`;
|
|
39
|
+
}
|
|
40
|
+
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>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compassSvg(width: number, height: number): string {
|
|
44
|
+
if (width < 400 || height < 300) return '';
|
|
45
|
+
const x = width - 34;
|
|
46
|
+
const y = height - 34;
|
|
47
|
+
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>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function interactionGridSvg(map: DungeonMap): string {
|
|
51
|
+
return map.tiles.flatMap((row, y) => row.map((_tile, x) =>
|
|
52
|
+
`<rect class="dmg-map-hit" data-cell data-x="${x}" data-y="${y}" x="${x * CELL}" y="${y * CELL}" width="${CELL}" height="${CELL}"/>`)).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function standaloneStyle(style: string): string {
|
|
56
|
+
if (style === 'print') {
|
|
57
|
+
return `<style>.dmg-map-void{fill:#ffffff}.dmg-map-floor{fill:#ffffff;stroke:#333333;stroke-width:.6}.dmg-map-door line{stroke:#000000;stroke-width:4}.dmg-map-door circle{fill:#000000}.dmg-map-compass circle{fill:#ffffff;stroke:#000000;stroke-width:1}.dmg-map-compass path{fill:#000000}</style>`;
|
|
58
|
+
}
|
|
59
|
+
const floor = style === 'scifi' ? '#d9f6f0' : '#ead9b7';
|
|
60
|
+
const door = style === 'cavern' ? '#d7774f' : '#b98142';
|
|
61
|
+
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>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function renderMap(context: RenderContext): void {
|
|
65
|
+
const { map, root } = context;
|
|
66
|
+
root.dataset.style = map.config.style;
|
|
67
|
+
const mapHost = root.querySelector<HTMLElement>('[data-map-host]');
|
|
68
|
+
if (mapHost) mapHost.innerHTML = buildMapSvg(map);
|
|
69
|
+
setText(root, '[data-map-seed]', map.config.seed);
|
|
70
|
+
setText(root, '[data-room-count]', String(map.rooms.length));
|
|
71
|
+
setText(root, '[data-door-count]', String(map.doorCount));
|
|
72
|
+
renderEvaluation(context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderEvaluation(context: RenderContext): void {
|
|
76
|
+
const evaluation = evaluateMap(context.map, context.ui);
|
|
77
|
+
setText(context.root, '[data-floor-percent]', `${evaluation.coverage}%`);
|
|
78
|
+
setText(context.root, '[data-evaluation-label]', evaluation.label);
|
|
79
|
+
setText(context.root, '[data-evaluation-hint]', evaluation.hint);
|
|
80
|
+
const badge = context.root.querySelector<HTMLElement>('[data-evaluation]');
|
|
81
|
+
if (badge) badge.dataset.pace = evaluation.pace;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setText(root: HTMLElement, selector: string, value: string): void {
|
|
85
|
+
const element = root.querySelector<HTMLElement>(selector);
|
|
86
|
+
if (element) element.textContent = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function downloadBlob(blob: Blob, filename: string): void {
|
|
90
|
+
const url = URL.createObjectURL(blob);
|
|
91
|
+
const link = document.createElement('a');
|
|
92
|
+
link.href = url;
|
|
93
|
+
link.download = filename;
|
|
94
|
+
link.click();
|
|
95
|
+
URL.revokeObjectURL(url);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function filename(map: DungeonMap, extension: string, suffix = ''): string {
|
|
99
|
+
const seed = map.config.seed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
100
|
+
return `dungeon-${seed || 'map'}${suffix}.${extension}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function exportSvg(map: DungeonMap, isPrint = false): void {
|
|
104
|
+
const svg = buildMapSvg(map, true, isPrint);
|
|
105
|
+
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
106
|
+
downloadBlob(blob, filename(map, 'svg', isPrint ? '-print' : ''));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function exportJson(map: DungeonMap, edits: DungeonEdit[]): void {
|
|
110
|
+
const data = JSON.stringify({ version: 1, config: map.config, edits }, null, 2);
|
|
111
|
+
downloadBlob(new Blob([data], { type: 'application/json' }), filename(map, 'json'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function exportPng(map: DungeonMap, isPrint = false): Promise<void> {
|
|
115
|
+
const svg = buildMapSvg(map, true, isPrint);
|
|
116
|
+
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
117
|
+
const source = URL.createObjectURL(blob);
|
|
118
|
+
try {
|
|
119
|
+
await drawPng(map, source, isPrint ? '-print' : '');
|
|
120
|
+
} finally {
|
|
121
|
+
URL.revokeObjectURL(source);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function drawPng(map: DungeonMap, source: string, suffix = ''): Promise<void> {
|
|
126
|
+
const image = await loadImage(source);
|
|
127
|
+
const canvas = document.createElement('canvas');
|
|
128
|
+
canvas.width = map.config.columns * CELL * 2;
|
|
129
|
+
canvas.height = map.config.rows * CELL * 2;
|
|
130
|
+
const context = canvas.getContext('2d');
|
|
131
|
+
if (!context) return;
|
|
132
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
133
|
+
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
134
|
+
if (blob) downloadBlob(blob, filename(map, 'png', suffix));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadImage(source: string): Promise<HTMLImageElement> {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const image = new Image();
|
|
140
|
+
image.onload = () => resolve(image);
|
|
141
|
+
image.onerror = () => reject(new Error('Unable to render map image'));
|
|
142
|
+
image.src = source;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -1,172 +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
|
-
};
|
|
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
|
+
};
|