@jjlmoya/utils-tabletop 1.28.0 → 1.30.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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -0
  4. package/src/tests/locale_completeness.test.ts +2 -9
  5. package/src/tests/tool_validation.test.ts +1 -2
  6. package/src/tool/mus-scoreboard/component.astro +21 -11
  7. package/src/tool/rpg-settlement-exploration-map-generator/.keep +1 -0
  8. package/src/tool/rpg-settlement-exploration-map-generator/bibliography.astro +11 -0
  9. package/src/tool/rpg-settlement-exploration-map-generator/bibliography.ts +7 -0
  10. package/src/tool/rpg-settlement-exploration-map-generator/component.astro +66 -0
  11. package/src/tool/rpg-settlement-exploration-map-generator/controller.ts +231 -0
  12. package/src/tool/rpg-settlement-exploration-map-generator/dom-views.ts +174 -0
  13. package/src/tool/rpg-settlement-exploration-map-generator/editor.ts +85 -0
  14. package/src/tool/rpg-settlement-exploration-map-generator/entry.ts +27 -0
  15. package/src/tool/rpg-settlement-exploration-map-generator/evaluator.ts +8 -0
  16. package/src/tool/rpg-settlement-exploration-map-generator/i18n/de.ts +152 -0
  17. package/src/tool/rpg-settlement-exploration-map-generator/i18n/en.ts +63 -0
  18. package/src/tool/rpg-settlement-exploration-map-generator/i18n/es.ts +233 -0
  19. package/src/tool/rpg-settlement-exploration-map-generator/i18n/fr.ts +152 -0
  20. package/src/tool/rpg-settlement-exploration-map-generator/i18n/id.ts +152 -0
  21. package/src/tool/rpg-settlement-exploration-map-generator/i18n/it.ts +152 -0
  22. package/src/tool/rpg-settlement-exploration-map-generator/i18n/ja.ts +152 -0
  23. package/src/tool/rpg-settlement-exploration-map-generator/i18n/ko.ts +152 -0
  24. package/src/tool/rpg-settlement-exploration-map-generator/i18n/nl.ts +152 -0
  25. package/src/tool/rpg-settlement-exploration-map-generator/i18n/pl.ts +152 -0
  26. package/src/tool/rpg-settlement-exploration-map-generator/i18n/pt.ts +152 -0
  27. package/src/tool/rpg-settlement-exploration-map-generator/i18n/ru.ts +152 -0
  28. package/src/tool/rpg-settlement-exploration-map-generator/i18n/sv.ts +152 -0
  29. package/src/tool/rpg-settlement-exploration-map-generator/i18n/tr.ts +152 -0
  30. package/src/tool/rpg-settlement-exploration-map-generator/i18n/zh.ts +152 -0
  31. package/src/tool/rpg-settlement-exploration-map-generator/index.ts +4 -0
  32. package/src/tool/rpg-settlement-exploration-map-generator/logic.test.ts +65 -0
  33. package/src/tool/rpg-settlement-exploration-map-generator/logic.ts +288 -0
  34. package/src/tool/rpg-settlement-exploration-map-generator/rpg-settlement-exploration-map-generator.css +885 -0
  35. package/src/tool/rpg-settlement-exploration-map-generator/seo.astro +11 -0
  36. package/src/tool/rpg-settlement-exploration-map-generator/storage.test.ts +12 -0
  37. package/src/tool/rpg-settlement-exploration-map-generator/storage.ts +24 -0
  38. package/src/tool/rpg-settlement-exploration-map-generator/ui.ts +81 -0
  39. package/src/tools.ts +2 -0
@@ -0,0 +1,174 @@
1
+ import type { Building, Point, SettlementMap, ServiceType } from './logic';
2
+ import type { SettlementMapUI } from './ui';
3
+
4
+ function serviceLabel(service: ServiceType, ui: SettlementMapUI): string {
5
+ const labels: Record<string, string> = { tavern: ui.serviceTavern, smithy: ui.serviceSmithy, temple: ui.serviceTemple, market: ui.serviceMarket, stable: ui.serviceStable, hall: ui.serviceHall };
6
+ return labels[service] ?? service;
7
+ }
8
+
9
+ function escapeSvgText(value: string): string {
10
+ return value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[character] ?? character);
11
+ }
12
+
13
+ function roofSvg(building: Building, style: string): string {
14
+ const base = building.y + 0.39;
15
+ const left = building.x - 0.15;
16
+ const right = building.x + building.width + 0.15;
17
+ const peak = building.x + building.width / 2;
18
+ if (style === 'edo') return `<path class="rsm-house-roof" d="M${left} ${base}Q${building.x + 0.55} ${building.y - 0.05} ${peak} ${building.y + 0.08}Q${building.x + building.width - 0.55} ${building.y - 0.05} ${right} ${base}Z" />`;
19
+ if (style === 'sahelian') return `<path class="rsm-house-roof" d="M${building.x + 0.05} ${base}H${building.x + building.width - 0.05}V${building.y + 0.68}H${building.x + 0.05}Z" />`;
20
+ return `<polygon class="rsm-house-roof" points="${left},${base} ${peak},${building.y - (style === 'highland' ? 0.62 : 0.48)} ${right},${base}" />`;
21
+ }
22
+
23
+ function buildingDetails(building: Building, style: string): string {
24
+ if (style === 'timber') return `<path class="rsm-house-frame" d="M${building.x + 0.38} ${building.y + 0.5}V${building.y + building.height - 0.25}M${building.x + building.width - 0.38} ${building.y + 0.5}V${building.y + building.height - 0.25}M${building.x + 0.25} ${building.y + 0.72}H${building.x + building.width - 0.25}" />`;
25
+ if (style === 'edo') return `<path class="rsm-house-wood-slats" d="M${building.x + 0.42} ${building.y + 0.52}V${building.y + building.height - 0.25}M${building.x + 0.72} ${building.y + 0.52}V${building.y + building.height - 0.25}M${building.x + building.width - 0.72} ${building.y + 0.52}V${building.y + building.height - 0.25}M${building.x + building.width - 0.42} ${building.y + 0.52}V${building.y + building.height - 0.25}" />`;
26
+ if (style === 'stone' || style === 'medieval') return `<path class="rsm-house-masonry" d="M${building.x + 0.25} ${building.y + 1.05}H${building.x + building.width - 0.25}M${building.x + 0.25} ${building.y + 1.42}H${building.x + building.width - 0.25}M${building.x + 0.65} ${building.y + 0.7}V${building.y + 1.05}M${building.x + building.width - 0.65} ${building.y + 1.42}V${building.y + 1.78}" />`;
27
+ if (style === 'coastal') return `<path class="rsm-house-awning" d="M${building.x + 0.2} ${building.y + 0.42}H${building.x + building.width - 0.2}" />`;
28
+ if (style === 'sahelian') return `<path class="rsm-house-sunband" d="M${building.x + 0.25} ${building.y + 0.82}H${building.x + building.width - 0.25}" />`;
29
+ return `<path class="rsm-house-chimney" d="M${building.x + building.width - 0.8} ${building.y - 0.35}V${building.y + 0.2}H${building.x + building.width - 0.42}V${building.y - 0.25}" />`;
30
+ }
31
+
32
+ function getSignFontSize(size?: string): number {
33
+ if (size === 'town') return 1.05;
34
+ if (size === 'village') return 0.92;
35
+ return 0.84;
36
+ }
37
+
38
+ function serviceSignSvg(building: Building, map: SettlementMap, service: string): string {
39
+ if (!service) return '';
40
+ const signFontSize = getSignFontSize(map.config.size);
41
+ const signWidth = Math.min(8, Math.max(2.1, service.length * signFontSize * 0.58 + 0.72));
42
+ const signX = building.x + building.width / 2 - signWidth / 2;
43
+ const signHeight = signFontSize + 0.34;
44
+ const signY = building.y - signHeight - 0.58;
45
+ return `<g class="rsm-service-sign rsm-service-sign-${map.config.size}"><path class="rsm-service-stem" d="M${building.x + building.width / 2} ${signY + signHeight}V${building.y - 0.49}" /><rect class="rsm-service-plaque" x="${signX}" y="${signY}" width="${signWidth}" height="${signHeight}" rx="0.12" /><text class="rsm-building-glyph" x="${building.x + building.width / 2}" y="${signY + signHeight / 2}" font-size="${signFontSize}" dominant-baseline="middle">${service}</text></g>`;
46
+ }
47
+
48
+ function buildingSvg(building: Building, map: SettlementMap, ui: SettlementMapUI): string {
49
+ const style = map.config.style ?? 'medieval';
50
+ const roof = roofSvg(building, style);
51
+ const windowY = building.y + 0.82;
52
+ const leftWindowX = building.x + 0.48;
53
+ const rightWindowX = building.x + building.width - 0.86;
54
+ const doorX = building.x + building.width / 2 - 0.23;
55
+ const doorY = building.y + building.height - 0.9;
56
+ const windows = `<rect class="rsm-house-window" x="${leftWindowX}" y="${windowY}" width="0.38" height="0.42" rx="0.05" />${building.width > 2.5 ? `<rect class="rsm-house-window" x="${rightWindowX}" y="${windowY}" width="0.38" height="0.42" rx="0.05" />` : ''}`;
57
+ const details = buildingDetails(building, style);
58
+ const service = building.service ? escapeSvgText(serviceLabel(building.service, ui)) : '';
59
+ const title = building.service ? `<title>${serviceLabel(building.service, ui)}</title>` : '<title>Home</title>';
60
+ const serviceMark = serviceSignSvg(building, map, service);
61
+ return `<g class="rsm-building rsm-house-style-${style}" data-building-id="${building.id}" data-cell-x="${building.x}" data-cell-y="${building.y}">${title}<rect class="rsm-house-body" x="${building.x + 0.12}" y="${building.y + 0.35}" width="${building.width - 0.24}" height="${building.height - 0.35}" rx="0.18" />${roof}${details}${windows}<rect class="rsm-house-door" x="${doorX}" y="${doorY}" width="0.46" height="0.8" rx="0.06" />${serviceMark}</g>`;
62
+ }
63
+
64
+ function waterComponents(points: Point[]): Point[][] {
65
+ const remaining = new Set(points.map((point) => `${point.x},${point.y}`));
66
+ const components: Point[][] = [];
67
+ while (remaining.size) {
68
+ const first = [...remaining][0]!;
69
+ const queue = [first.split(',').map(Number) as [number, number]];
70
+ const component: Point[] = [];
71
+ remaining.delete(first);
72
+ while (queue.length) {
73
+ const [x, y] = queue.shift()!;
74
+ component.push({ x, y });
75
+ [{ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 }].forEach((neighbor) => {
76
+ const key = `${neighbor.x},${neighbor.y}`;
77
+ if (remaining.delete(key)) queue.push([neighbor.x, neighbor.y]);
78
+ });
79
+ }
80
+ components.push(component);
81
+ }
82
+ return components;
83
+ }
84
+
85
+ function waterKind(component: Point[]): string {
86
+ if (component.length <= 2) return 'river';
87
+ if (component.length === 3) return 'pond';
88
+ return 'lake';
89
+ }
90
+
91
+ function waterSvg(map: SettlementMap): string {
92
+ return waterComponents(map.water).map((component) => `<g class="rsm-water-component rsm-water-${waterKind(component)}" data-water-size="${component.length}">${component.map((point) => `<rect x="${point.x}" y="${point.y}" width="1" height="1" />`).join('')}</g>`).join('');
93
+ }
94
+
95
+ interface RoadCell { point: Point; neighbors: Point[] }
96
+
97
+ function roadCell(cells: Map<string, RoadCell>, point: Point): RoadCell {
98
+ const key = `${point.x},${point.y}`;
99
+ const existing = cells.get(key);
100
+ if (existing) return existing;
101
+ const created = { point, neighbors: [] };
102
+ cells.set(key, created);
103
+ return created;
104
+ }
105
+
106
+ function roadNetwork(map: SettlementMap): Map<string, RoadCell> {
107
+ const cells = new Map<string, RoadCell>();
108
+ map.paths.forEach((path) => {
109
+ path.points.forEach((point) => roadCell(cells, point));
110
+ path.points.slice(1).forEach((point, index) => {
111
+ const previous = path.points[index]!;
112
+ roadCell(cells, previous).neighbors.push(point);
113
+ roadCell(cells, point).neighbors.push(previous);
114
+ });
115
+ });
116
+ return cells;
117
+ }
118
+
119
+ function roadCellPath(cell: RoadCell): string {
120
+ const center = `${cell.point.x + 0.5},${cell.point.y + 0.5}`;
121
+ if (!cell.neighbors.length) return `M${center}h0`;
122
+ return cell.neighbors.map((neighbor) => `M${center}L${neighbor.x + 0.5},${neighbor.y + 0.5}`).join('');
123
+ }
124
+
125
+ function pathSvg(map: SettlementMap): string {
126
+ return [...roadNetwork(map).values()].map((cell) => `<path class="rsm-road-base" d="${roadCellPath(cell)}" /><path class="rsm-road-line" d="${roadCellPath(cell)}" />`).join('');
127
+ }
128
+
129
+ function getTreeStyle(env: string, style?: string): string {
130
+ if (env === 'coast') return 'coastal';
131
+ if (env === 'mountain') return 'highland';
132
+ return style ?? 'medieval';
133
+ }
134
+
135
+ function treeSvg(point: Point, map: SettlementMap): string {
136
+ const x = point.x + 0.5;
137
+ const y = point.y + 0.5;
138
+ const style = getTreeStyle(map.config.environment, map.config.style);
139
+ const edoBlossom = (point.x * 7 + point.y * 11) % 3 === 0 ? 'green' : 'pink';
140
+ const shapes: Record<string, string> = {
141
+ coastal: '<path d="M0 .65V.05" /><path d="M0 .08C-.16-.2-.58-.27-.62-.52M0 .08C.16-.2.58-.27.62-.52M0 .08C-.05-.22.1-.5.22-.7" />',
142
+ highland: '<path d="M0 .65V.1" /><path d="M-.48 .15L0-.72 .48 .15Z" /><path d="M-.34-.18L0-.88 .34-.18Z" />',
143
+ edo: '<path d="M0 .65V.08" /><path d="M-.52 .08Q0-.48 .52 .08Q0 .27-.52 .08Z" /><path d="M-.34-.22Q0-.68 .34-.22Q0-.08-.34-.22Z" />',
144
+ sahelian: '<path d="M0 .65V.02" /><path d="M-.65 .02Q0-.48 .65 .02Q0 .2-.65 .02Z" />',
145
+ medieval: '<path d="M0 .65V.1" /><path d="M-.55 .14L0-.58 .55 .14Q0 .28-.55 .14Z" /><path d="M-.34-.15L0-.72 .34-.15Z" />',
146
+ timber: '<path d="M0 .65V.1" /><path d="M-.5 .12Q0-.48 .5 .12Q0 .28-.5 .12Z" /><path d="M-.3-.2Q0-.7 .3-.2Q0-.05-.3-.2Z" />',
147
+ stone: '<path d="M0 .65V.1" /><path d="M-.5 .12Q0-.5 .5 .12Q0 .28-.5 .12Z" /><path d="M-.3-.2Q0-.7 .3-.2Q0-.05-.3-.2Z" />',
148
+ };
149
+ const blossomClass = style === 'edo' ? ` rsm-tree-edo-${edoBlossom}` : '';
150
+ return `<g class="rsm-tree rsm-tree-${style}${blossomClass}" transform="translate(${x} ${y})">${shapes[style] ?? shapes.medieval}</g>`;
151
+ }
152
+
153
+ function wildSvg(map: SettlementMap): string { return map.wild.map((point) => treeSvg(point, map)).join(''); }
154
+
155
+ function plazaSvg(map: SettlementMap): string {
156
+ const plaza = map.plaza ?? { x: Math.floor(map.width / 2) - 3, y: Math.floor(map.height / 2) - 2, width: 6, height: 4 };
157
+ return `<g class="rsm-plaza"><rect x="${plaza.x}" y="${plaza.y}" width="${plaza.width}" height="${plaza.height}" rx="0.3" /><path d="M${plaza.x + 1} ${plaza.y + 1}H${plaza.x + plaza.width - 1}M${plaza.x + 1} ${plaza.y + plaza.height - 1}H${plaza.x + plaza.width - 1}" /></g>`;
158
+ }
159
+
160
+ function gridSvg(map: SettlementMap): string {
161
+ const vertical = Array.from({ length: map.width + 1 }, (_, index) => `<path d="M${index} 0V${map.height}" />`).join('');
162
+ const horizontal = Array.from({ length: map.height + 1 }, (_, index) => `<path d="M0 ${index}H${map.width}" />`).join('');
163
+ return `<g class="rsm-grid">${vertical}${horizontal}</g>`;
164
+ }
165
+
166
+ function environmentClass(environment: string): string { return `rsm-environment-${environment}`; }
167
+
168
+ export function buildMapSvg(map: SettlementMap, ui: SettlementMapUI): string {
169
+ return `<svg class="rsm-map-svg ${environmentClass(map.config.environment)}" data-settlement-map viewBox="0 0 ${map.width} ${map.height}" role="img" aria-label="Settlement map"><rect class="rsm-paper" x="0" y="0" width="${map.width}" height="${map.height}" rx="0.5" />${gridSvg(map)}${waterSvg(map)}${plazaSvg(map)}${wildSvg(map)}${pathSvg(map)}${map.buildings.map((building) => buildingSvg(building, map, ui)).join('')}</svg>`;
170
+ }
171
+
172
+ export function updateMapHost(host: HTMLElement, map: SettlementMap, ui: SettlementMapUI): void { host.innerHTML = buildMapSvg(map, ui); }
173
+ export function svgToDataUrl(svg: string): string { return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }
174
+ export function downloadFile(content: string | Blob, filename: string, type?: string): void { const blob = typeof content === 'string' ? new Blob([content], { type: type ?? 'text/plain' }) : content; const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.click(); URL.revokeObjectURL(url); }
@@ -0,0 +1,85 @@
1
+ import type { EditTool } from './logic';
2
+
3
+ interface CellPoint {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ type EditHandler = (point: CellPoint, tool: EditTool) => void;
9
+
10
+ function svgCell(event: MouseEvent, svg: SVGSVGElement): CellPoint | null {
11
+ const point = svg.createSVGPoint();
12
+ point.x = event.clientX;
13
+ point.y = event.clientY;
14
+ const matrix = svg.getScreenCTM();
15
+ if (!matrix) return null;
16
+ const local = point.matrixTransform(matrix.inverse());
17
+ return { x: Math.floor(local.x), y: Math.floor(local.y) };
18
+ }
19
+
20
+ function openContextMenu(root: HTMLElement, menu: HTMLElement, event: MouseEvent): CellPoint | null {
21
+ const svg = (event.target as Element).closest<SVGSVGElement>('[data-settlement-map]');
22
+ if (!svg || !root.querySelector<HTMLElement>('.rsm-map-frame')) return null;
23
+ const cell = svgCell(event, svg);
24
+ if (!cell) return null;
25
+ const edge = 8;
26
+ const gap = 6;
27
+ menu.hidden = false;
28
+ const menuBounds = menu.getBoundingClientRect();
29
+ const left = Math.min(Math.max(edge, event.clientX), window.innerWidth - menuBounds.width - edge);
30
+ const opensBelow = event.clientY + gap + menuBounds.height <= window.innerHeight - edge;
31
+ const preferredTop = opensBelow ? event.clientY + gap : event.clientY - menuBounds.height - gap;
32
+ const top = Math.min(Math.max(edge, preferredTop), window.innerHeight - menuBounds.height - edge);
33
+ menu.style.left = `${left}px`;
34
+ menu.style.top = `${top}px`;
35
+ menu.querySelector<HTMLButtonElement>('button')?.focus();
36
+ return cell;
37
+ }
38
+
39
+ interface PointerOptions {
40
+ host: HTMLElement;
41
+ root: HTMLElement;
42
+ menu: HTMLElement;
43
+ flags: PointerFlags;
44
+ onOpen: (cell: CellPoint | null) => void;
45
+ }
46
+
47
+ function bindPointerEvents(opts: PointerOptions): void {
48
+ let timer: number | undefined;
49
+ opts.host.addEventListener('pointerdown', (event) => {
50
+ if (event.pointerType === 'mouse') return;
51
+ timer = window.setTimeout(() => {
52
+ opts.flags.ignoreNextContext = true;
53
+ opts.flags.ignoreNextClick = true;
54
+ opts.onOpen(openContextMenu(opts.root, opts.menu, event));
55
+ }, 520);
56
+ });
57
+ const cancel = () => { if (timer) window.clearTimeout(timer); timer = undefined; };
58
+ opts.host.addEventListener('pointerup', cancel);
59
+ opts.host.addEventListener('pointercancel', cancel);
60
+ opts.host.addEventListener('pointerleave', cancel);
61
+ }
62
+
63
+ export function bindContextEditor(root: HTMLElement, onEdit: EditHandler): void {
64
+ const host = root.querySelector<HTMLElement>('[data-map-host]');
65
+ const menu = root.querySelector<HTMLElement>('[data-context-menu]');
66
+ if (!host || !menu) return;
67
+ let selected: CellPoint | null = null;
68
+ const flags: PointerFlags = { ignoreNextContext: false, ignoreNextClick: false };
69
+ host.addEventListener('contextmenu', (event) => {
70
+ event.preventDefault();
71
+ if (flags.ignoreNextContext) { flags.ignoreNextContext = false; return; }
72
+ selected = openContextMenu(root, menu, event);
73
+ });
74
+ bindPointerEvents({ host, root, menu, flags, onOpen: (cell) => { selected = cell; } });
75
+ menu.querySelectorAll<HTMLButtonElement>('[data-context-tool]').forEach((btn) => btn.addEventListener('click', () => {
76
+ if (selected) onEdit(selected, btn.dataset.contextTool as EditTool);
77
+ menu.hidden = true;
78
+ }));
79
+ document.addEventListener('click', (event) => {
80
+ if (flags.ignoreNextClick) { flags.ignoreNextClick = false; return; }
81
+ if (!menu.contains(event.target as Node)) menu.hidden = true;
82
+ });
83
+ window.addEventListener('resize', () => { menu.hidden = true; });
84
+ window.addEventListener('scroll', () => { menu.hidden = true; }, true);
85
+ }
@@ -0,0 +1,27 @@
1
+ import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { SettlementMapUI } from './ui';
3
+
4
+ export type { SettlementMapUI } from './ui';
5
+ export type SettlementMapLocaleContent = ToolLocaleContent<SettlementMapUI>;
6
+
7
+ export const rpgSettlementExplorationMapGenerator: TabletopToolEntry<SettlementMapUI> = {
8
+ id: 'rpg-settlement-exploration-map-generator',
9
+ icons: { bg: 'mdi:map-marker-radius-outline', fg: 'mdi:home-city-outline' },
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
+ };
@@ -0,0 +1,8 @@
1
+ import type { SettlementMap } from './logic';
2
+ import type { SettlementMapUI } from './ui';
3
+
4
+ export interface SettlementEvaluation { buildingCount: number; pathCount: number; serviceCount: number; waterCount: number; badge: string; terrainLabel: string }
5
+
6
+ export function evaluateSettlement(map: SettlementMap, ui: SettlementMapUI): SettlementEvaluation {
7
+ return { buildingCount: map.buildings.length, pathCount: map.paths.length, serviceCount: map.buildings.filter((building) => building.service).length, waterCount: map.water.length, badge: ui.readyBadge, terrainLabel: ui[map.config.environment] ?? map.config.environment };
8
+ }
@@ -0,0 +1,152 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { SettlementMapLocaleContent, SettlementMapUI } from '../entry';
4
+
5
+ const ui: SettlementMapUI = {
6
+ intro: 'Gestalten Sie eine erkundbare Siedlungskarte für Ihre Rollenspiele. Wählen Sie Landschaft, Siedlungstyp und Baustil und passen Sie Zellen per Rechtsklick an.',
7
+ seedLabel: 'Startwert und Dorfname',
8
+ seedHint: 'Der Startwert erzeugt ein deterministisches Layout für Straßen und Gebäude.',
9
+ randomSeed: 'Neuer Name',
10
+ environmentLabel: 'Landschaft',
11
+ forest: 'Wald',
12
+ plains: 'Ebene',
13
+ coast: 'Küste',
14
+ river: 'Fluss',
15
+ mountain: 'Gebirge',
16
+ styleLabel: 'Architekturstil',
17
+ styleHint: 'Ändert Dächer, Materialien, Fassaden und regionale Vegetation.',
18
+ styleTimber: 'Fachwerk',
19
+ styleStone: 'Stein',
20
+ styleCoastal: 'Küstenstil',
21
+ styleHighland: 'Hochland',
22
+ styleMedieval: 'Mittelalter',
23
+ styleEdo: 'Edo',
24
+ styleSahelian: 'Sahel',
25
+ sizeLabel: 'Siedlungsgröße',
26
+ hamlet: 'Weiler',
27
+ hamletHint: 'Kleine Raststätte',
28
+ village: 'Dorf',
29
+ villageHint: 'Lokaler Stützpunkt',
30
+ town: 'Stadt',
31
+ townHint: 'Belebter Knotenpunkt',
32
+ homesLabel: 'Wohnhäuser',
33
+ homesHint: 'Passen Sie die Anzahl der Häuser an die gewählte Grundfläche an.',
34
+ servicesLabel: 'Gebäude und Orte',
35
+ serviceListHint: 'Wählen Sie Vorlagen oder fügen Sie eigene Wahrzeichen hinzu.',
36
+ newServicePlaceholder: 'Ort oder Dienst hinzufügen',
37
+ addService: 'Hinzufügen',
38
+ removeService: 'Entfernen',
39
+ serviceTavern: 'Taverne',
40
+ serviceSmithy: 'Schmiede',
41
+ serviceTemple: 'Tempel',
42
+ serviceMarket: 'Markt',
43
+ serviceStable: 'Stallung',
44
+ serviceHall: 'Rathaus',
45
+ generate: 'Karte neu erzeugen',
46
+ mapRegionLabel: 'Bearbeitbare RPG Siedlungskarte',
47
+ mapSummary: 'Siedlungsplan',
48
+ buildings: 'Gebäude',
49
+ paths: 'Wege',
50
+ services: 'Dienste',
51
+ terrain: 'Wasser',
52
+ legendLabel: 'Kartenlegende',
53
+ legendHome: 'Haus',
54
+ legendService: 'Dienst',
55
+ legendPath: 'Weg',
56
+ legendWater: 'Wasser',
57
+ legendWild: 'Wald',
58
+ editLabel: 'Direkte Kartenbearbeitung',
59
+ toolSelect: 'Inspektion',
60
+ toolBuilding: 'Gebäude',
61
+ toolPath: 'Weg',
62
+ toolWater: 'Wasser',
63
+ toolTree: 'Baum',
64
+ toolErase: 'Löschen',
65
+ serviceSelectLabel: 'Neues Gebäudesymbol',
66
+ clickHint: 'Rechtsklick oder langes Drücken zum Bearbeiten einer Zelle.',
67
+ selectedHint: 'Zelle ausgewählt bei',
68
+ shareLink: 'Link kopieren',
69
+ linkCopied: 'Link in Zwischenablage kopiert',
70
+ exportPng: 'PNG',
71
+ exportSvg: 'SVG',
72
+ exportJson: 'JSON speichern',
73
+ importJson: 'JSON öffnen',
74
+ importError: 'Datei ist keine gültige Siedlungskarte.',
75
+ mapData: 'Kartendaten',
76
+ readyBadge: 'Bereit zur Erkundung',
77
+ serviceNone: 'Kein Symbol',
78
+ contextMenuLabel: 'Zellenaktionen',
79
+ contextInspect: 'Zelle prüfen',
80
+ contextBuilding: 'Gebäude bauen',
81
+ contextPath: 'Weg anlegen',
82
+ contextWater: 'Wasser zeichnen',
83
+ contextTree: 'Baum pflanzen',
84
+ contextErase: 'Zelle leeren',
85
+ };
86
+
87
+ const faq = [
88
+ { question: 'Was erzeugt dieser RPG Siedlungskartengenerator?', answer: 'Er erstellt reproduzierbare Übersichtskarten für Weiler, Dörfer und Städte mit Gebäuden, Wegen, Gewässern und Wahrzeichen für Pen and Paper Rollenspiele.' },
89
+ { question: 'Verändert der Startwert das Layout tatsächlich?', answer: 'Ja. Der Startwert steuert die Anordnung von Straßen und Häusern deterministisch. Der gleiche Wert erzeugt exakt dieselbe Startkarte.' },
90
+ { question: 'Welche Siedlungsgrößen stehen zur Auswahl?', answer: 'Weiler, Dorf und Stadt bieten unterschiedliche Maße und Gebäudezahlen.' },
91
+ { question: 'Kann der Baustil angepasst werden?', answer: 'Ja. Zur Wahl stehen Mittelalter, Edo, Sahel, Fachwerk, Stein, Küstenstil und Hochland.' },
92
+ { question: 'Kann die Landschaft gewählt werden?', answer: 'Ja. Sie können Wald, Ebene, Küste, Fluss oder Gebirge festlegen.' },
93
+ { question: 'Wie werden wichtige Gebäude platziert?', answer: 'Dienste wie Taverne, Schmiede oder Rathaus werden mit gewichteter Logik im Zentrum oder am Rand platziert.' },
94
+ { question: 'Können eigene Wahrzeichen hinzugefügt werden?', answer: 'Ja. Eigene Orte können ergänzt und im Browser sowie in Freigabelinks gespeichert werden.' },
95
+ { question: 'Kann die Karte direkt bearbeitet werden?', answer: 'Ja. Per Rechtsklick oder langem Drücken lässt sich jede Zelle anpassen oder löschen.' },
96
+ { question: 'Wie verhalten sich Wege und Wasser beim Bearbeiten?', answer: 'Wege verbinden sich automatisch an Kreuzungen und Wasserzellen fügen sich zu Flüssen oder Seen zusammen.' },
97
+ { question: 'Wie kann eine Karte geteilt werden?', answer: 'Über den Freigabelink oder als JSON Datei bleibt die Karte vollständig bearbeitbar.' },
98
+ { question: 'Welche Exportformate sind am besten geeignet?', answer: 'PNG für schnelle Bilder, SVG für Druck und Vektoren sowie JSON für vollständige Speicherung.' },
99
+ ];
100
+
101
+ const howTo = [
102
+ { name: 'Startwert wählen', text: 'Nutzen Sie den zufälligen Namen oder geben Sie einen eigenen Begriff ein.' },
103
+ { name: 'Umgebung und Stil festlegen', text: 'Wählen Sie Landschaft und Architekturstil passend zur Spielwelt.' },
104
+ { name: 'Größe bestimmen', text: 'Wählen Sie zwischen Weiler, Dorf und Stadt.' },
105
+ { name: 'Orte platzieren', text: 'Aktivieren Sie wichtige Vorlagen oder fügen Sie eigene Wahrzeichen hinzu.' },
106
+ { name: 'Karte anpassen', text: 'Bearbeiten Sie einzelne Zellen direkt über das Kontextmenü.' },
107
+ { name: 'Speichern und teilen', text: 'Nutzen Sie den Freigabelink oder exportieren Sie als PNG, SVG oder JSON.' },
108
+ ];
109
+
110
+ const faqSchema: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
111
+ const appSchema: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'RPG Siedlungskarten Generator', operatingSystem: 'All', applicationCategory: 'GameApplication', description: 'Karten-Generator für Rollenspielsiedlungen mit deterministischen Werten und Bearbeitungsmodus.' };
112
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'Wie man eine RPG Siedlungskarte erstellt', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
113
+
114
+ const schemas: WithContext<SoftwareApplication | FAQPage | HowTo>[] = [faqSchema, appSchema, howToSchema];
115
+
116
+ export const content: SettlementMapLocaleContent = {
117
+ slug: 'rpg-siedlungs-erkundungskarten-generator',
118
+ title: 'RPG Siedlungskarten Generator für Rollenspiele',
119
+ description: 'Erstellen Sie RPG Siedlungskarten für Weiler, Dörfer und Städte mit verschiedenen Baustilen, Umgebungen und Zelleneditor.',
120
+ ui,
121
+ seo: [
122
+ { type: 'title', text: 'Lebendige Siedlungskarten für Rollenspiele Erzeugen', level: 2 },
123
+ { type: 'paragraph', html: 'Erschaffen Sie Siedlungen, die Ihren Spielern echte Orte zum Erkunden bieten und nicht nur eine Ansammlung isolierter Häuser darstellen. Dieser browserbasierte Kartengenerator für Pen-and-Paper-Rollenspiele erstellt Übersichtskarten, auf denen Wohngebäude, Straßennetze, Marktplätze, Gewässer und Baumgruppen harmonisch zusammenwirken. Nutzen Sie das Werkzeug für spontane Zwischenstopps im Dorf, von Fraktionen geprägte Städte, abgelegene Weiler oder als ersten Entwurf für Orte, die Sie im Laufe einer Kampagne weiterentwickeln möchten.' },
124
+ { type: 'title', text: 'Deterministische Startwerte für Wiederholbare Karten', level: 2 },
125
+ { type: 'paragraph', html: 'Jede erzeugte Siedlung basiert auf einem einprägsamen Namens-Startwert. Dieser Wert steuert deterministisch das gesamte Layout, einschließlich Straßenführung, Gebäudeabständen, Dienstzuweisungen und der umgebenden Vegetation. Wenn Sie denselben Startwert und dieselben Optionen erneut eingeben, wird die exakt gleiche Karte rekonstruiert, was sich hervorragend für Spielleiternotizen und spätere Spielrunden eignet.' },
126
+ { type: 'list', items: ['Unterschiedliche Startwerte erzeugen neue Siedlungsmuster innerhalb des gewählten Typs.', 'Das Ergebnis bleibt vollständig reproduzierbar statt unkontrolliert zufällig zu sein.', 'Die Siedlung behält ihre charakteristische Struktur als Weiler, Dorf oder Stadt bei.', 'Der gleiche Startwert kann als stabile Basis für die gemeinsame Kampagnenplanung geteilt werden.'] },
127
+ { type: 'title', text: 'Unterschiedliche Maßstäbe vom Weiler bis zur Stadt', level: 2 },
128
+ { type: 'paragraph', html: 'Der Größenwähler ist keine bloße optische Skalierung. Weiler, Dörfer und Städte nutzen unterschiedliche Bauarten, Dichten und Erwartungen an Dienstleistungen. Ein Weiler bleibt mit unter zehn Häusern überschaubar, während eine Stadt Raum für längere Wege, mehr Gebäude und eine ausgeprägte zentrale Struktur bietet.' },
129
+ { type: 'table', headers: ['Grundfläche', 'Häuserzahl', 'Charakter', 'Bester Einsatz in der Kampagne'], rows: [['Weiler', '3 bis 18', 'Klein, spärlich und auf einen Blick erfassbar', 'Rastort am Wegesrand oder isolierte Gemeinschaft'], ['Dorf', '4 bis 28', 'Lokales Zentrum mit Wegen und Wahrzeichen', 'Wiederkehrende Heimatbasis oder Fraktionsknoten'], ['Stadt', '6 bis 42', 'Breites Netzwerk mit vielen Diensten und Wegen', 'Regionales Handelszentrum oder Ermittlungsort']] },
130
+ { type: 'title', text: 'Sieben Ausprägbare Architekturstile', level: 2 },
131
+ { type: 'paragraph', html: 'Verleihen Sie der Siedlung vor der Platzierung von Details eine eigene Identität. Fachwerk, Stein, Küstenstil, Hochland, Mittelalter, Edo und Sahel verändern Dächer, Wandfarben, Fenster, Türen und Strukturen. Auch die Baumarten passen sich der Region an, von Palmen an der Küste bis hin zu Nadelbäumen im Hochland.' },
132
+ { type: 'list', items: ['Mittelalter und Fachwerk passen zu Märchen dörfern und Waldgemeinschaften.', 'Stein und Hochland eignen sich für rauere, befestigte oder bergelegene Orte.', 'Küstenstil bringt eine maritime Note mit passender Küstenvegetation ein.', 'Edo verleiht Straßen und Häusern eine historische ostasiatische Silhouette.', 'Sahel bietet trockenen Regionen eine authentische Architektur und Farbpalette.'] },
133
+ { type: 'title', text: 'Intelligente Verteilung Wichtiger Gebäude', level: 2 },
134
+ { type: 'paragraph', html: 'Wichtige Orte wie Taverne, Schmiede, Tempel, Markt, Stallung und Rathaus werden mit gewichteter Logik platziert. Öffentliche Gebäude neigen zum Zentrum, während Ställe und Schmieden eher an den Außenwegen liegen. Zudem können Sie eigene Wahrzeichen definieren, die mit vollständigem Namen auf Schildern über den Dächern angezeigt werden.' },
135
+ { type: 'title', text: 'Die Landschaft als Teil der Geschichte', level: 2 },
136
+ { type: 'paragraph', html: 'Wählen Sie Wald, Ebene, Küste, Fluss oder Gebirge und betten Sie die Siedlung in diese Umgebung ein. Das Gelände verleiht dem Ort Kontext: Ein Fluss kann die Anreise prägen, während ein dichter Wald verborgene Pfade verbergen kann.' },
137
+ { type: 'title', text: 'Direkte Bearbeitung Zelle für Zelle', level: 2 },
138
+ { type: 'paragraph', html: 'Die erzeugte Karte ist ein starker Ausgangspunkt, den Sie über das Kontextmenü per Rechtsklick oder langem Drücken individuell anpassen können. Fügen Sie Gebäude, Wege, Wasserstellen oder Bäume hinzu oder löschen Sie unerwünschte Elemente direkt auf der Karte.' },
139
+ { type: 'list', items: ['Fügen Sie fehlende Häuser, Brücken oder Wachtürme hinzu.', 'Passen Sie Wege ohne externe Zeichenprogramme direkt im Browser an.', 'Behalten Sie manuelle Änderungen im gleichen Kartenstatus bei.', 'Funktioniert nahtlos auf PCs sowie Touch-Geräten.'] },
140
+ { type: 'title', text: 'Reaktive Wege und Wasserflächen', level: 2 },
141
+ { type: 'paragraph', html: 'Wege bilden saubere Kreuzungen, wenn mehrere Pfade aufeinandertreffen. Das Zeichnen von Wasser ist ebenfalls reaktiv: Benachbarte Wasserzellen verbinden sich automatisch zu Flüssen, Teichen oder größeren Seen.' },
142
+ { type: 'title', text: 'Teilen und Wiederverwenden von Karten', level: 2 },
143
+ { type: 'paragraph', html: 'Kopieren Sie den Freigabelink, um die vollständige bearbeitbare Karte an Mitspieler oder Spielleiter zu senden. Alle Einstellungen, Gebäude, Wege und manuellen Änderungen bleiben dabei erhalten. Alternativ lässt sich der Zustand als JSON sichern.' },
144
+ { type: 'title', text: 'Exportformate für Jeden Einsatzzweck', level: 2 },
145
+ { type: 'table', headers: ['Format', 'Geeignet für', 'Erhaltene Inhalte'], rows: [['PNG', 'Virtuelle Spieltische und schnelle Handouts', 'Sofort anzeigbares Bild der aktuellen Karte'], ['SVG', 'Druck und Vektorbearbeitung', 'Scharfe skaliersbare Vektorgrafik für große Pläne'], ['JSON', 'Kampagnenarchive und spätere Editiermodi', 'Vollständiger bearbeitbarer Zustand der Siedlung']] },
146
+ { type: 'tip', title: 'Praktischer Ablauf für den Spielabend', html: 'Wählen Sie zuerst Startwert, Grundfläche und Stil. Fügen Sie wichtige Wahrzeichen hinzu und verfeinern Sie die Karte anschließend mit dem Zelleneditor vor Beginn der Spielrunde.' },
147
+ ],
148
+ faq,
149
+ bibliography,
150
+ howTo,
151
+ schemas,
152
+ };
@@ -0,0 +1,63 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { SettlementMapLocaleContent, SettlementMapUI } from '../entry';
4
+
5
+ const ui: SettlementMapUI = {
6
+ intro: 'Lay out a place worth exploring. Pick the landscape, settlement type, and building character, then correct the generated map with a right click or long press.', seedLabel: 'Village name seed', seedHint: 'The seed chooses a different deterministic layout pattern within the selected settlement type.', randomSeed: 'Create a new village name', environmentLabel: 'Landscape', forest: 'Forest', plains: 'Plains', coast: 'Coast', river: 'River', mountain: 'Mountain', styleLabel: 'Village character', styleHint: 'Choose an architectural identity: era, geography, roofs, materials, and details all change.', styleTimber: 'Timber', styleStone: 'Stone', styleCoastal: 'Coastal', styleHighland: 'Highland', styleMedieval: 'Medieval', styleEdo: 'Edo', styleSahelian: 'Sahelian', sizeLabel: 'Settlement footprint', hamlet: 'Hamlet', hamletHint: 'Small stop', village: 'Village', villageHint: 'Local base', town: 'Town', townHint: 'Busy hub', homesLabel: 'Homes', homesHint: 'Each footprint has its own starting size and core services. Fine tune the homes after choosing one.', servicesLabel: 'Services to place', serviceListHint: 'Choose presets or add a named landmark for this settlement. Custom services stay in this browser.', newServicePlaceholder: 'Add a service or landmark', addService: 'Add', removeService: 'Remove', serviceTavern: 'Tavern', serviceSmithy: 'Smithy', serviceTemple: 'Temple', serviceMarket: 'Market', serviceStable: 'Stable', serviceHall: 'Hall', generate: 'Regenerate map', mapRegionLabel: 'Editable RPG settlement map', mapSummary: 'Village plan', buildings: 'Buildings', paths: 'Paths', services: 'Services', terrain: 'Water cells', legendLabel: 'Map legend', legendHome: 'Home', legendService: 'Service mark', legendPath: 'Road', legendWater: 'Water', legendWild: 'Trees', editLabel: 'Edit the map directly', toolSelect: 'Inspect', toolBuilding: 'Building', toolPath: 'Road', toolWater: 'Water', toolTree: 'Tree', toolErase: 'Erase', serviceSelectLabel: 'New building mark', clickHint: 'Right click or long press the plan to edit a cell.', selectedHint: 'Cell selected at', shareLink: 'Copy share link', linkCopied: 'Share link copied', exportPng: 'PNG', exportSvg: 'SVG', exportJson: 'Save JSON', importJson: 'Open JSON', importError: 'That file is not a valid settlement map.', mapData: 'Map data', readyBadge: 'Ready to explore', serviceNone: 'No mark', contextMenuLabel: 'Map cell actions', contextInspect: 'Inspect cell', contextBuilding: 'Add building', contextPath: 'Add road', contextWater: 'Add water', contextTree: 'Add tree', contextErase: 'Erase cell',
7
+ };
8
+
9
+ const faq = [
10
+ { question: 'What does this RPG settlement map generator create?', answer: 'It creates a reproducible overhead exploration map for a hamlet, village, or town. The map combines a settlement footprint with homes, roads, services, plazas, water, trees, and the surrounding landscape, so the place feels like part of a wider region instead of a disconnected pile of buildings. It is designed for RPG worldbuilding, campaign preparation, travel scenes, clues, factions, and points of interest rather than tactical combat grids.' },
11
+ { question: 'Does the seed really change the settlement layout?', answer: 'Yes. The seed is not only a name. It drives a deterministic layout variant inside the selected settlement type, so two seeds can produce different road patterns, house positions, spacing, service assignments, and wild growth while still respecting the urban grammar of a hamlet, village, or town. Reusing the same seed and settings rebuilds the same starting map.' },
12
+ { question: 'Which settlement sizes are available?', answer: 'Hamlet, village, and town each have a different footprint and starting logic. Hamlets support small communities, villages create a more developed local base, and towns make room for denser routes, more services, and broader exploration. The home count reacts to the selected footprint, while still allowing a smaller or larger result within that type range.' },
13
+ { question: 'Can I choose the architectural style of the settlement?', answer: 'Yes. Choose Timber, Stone, Coastal, Highland, Medieval, Edo, or Sahelian. The style changes the visual language of houses, including roof shape, wall treatment, doors, windows, texture, colour accents, and the way nearby trees are drawn. It gives the same settlement generator a different regional or historical identity.' },
14
+ { question: 'Can I choose the landscape around the settlement?', answer: 'Yes. Choose forest, plains, coast, river, or mountain. The landscape remains the visual context around the settlement and influences water, vegetation, and terrain details, so the village does not always become the only thing on the map. Forest and wild growth can occupy the surrounding space while the settlement stays readable at the centre.' },
15
+ { question: 'How are services placed?', answer: 'Built-in services include Tavern, Smithy, Temple, Market, Stable, and Hall, and you can add named custom services or landmarks. The generator assigns them with deterministic weights instead of always attaching them to the first few houses: Hall and similar civic services tend toward the centre, while Stable and Smithy tend toward the edge and outer roads, with controlled variation so every settlement does not follow the exact same pattern. Service labels use the full name and appear on readable signs above the building.' },
16
+ { question: 'Can I add my own services and keep them in a shared map?', answer: 'Yes. Add a custom service or landmark to the dynamic list and it is saved in local storage for later maps. Custom entries can be removed whenever you want, while the built-in service presets remain available. When you copy a share link or save JSON, the custom service catalog and the marks already placed on the map travel with the settlement.' },
17
+ { question: 'Can I edit the generated RPG map directly?', answer: 'Yes. Right-click a map cell on desktop or long-press it on touch devices to open the contextual editor. From there you can inspect the cell, add a building, draw a road, paint water, add a tree, or erase it. The contextual menu repositions itself when it would otherwise be clipped by the map or viewport.' },
18
+ { question: 'How do roads and water behave when I edit them?', answer: 'Roads are built as a connected graph with clean junctions, including places where three or four paths meet. Painting water affects the exact selected cell. Adjacent water cells join visually into a river, a small pond, or a broader lake shape as the component grows, so manual edits respond to their neighbours instead of leaving isolated marks.' },
19
+ { question: 'How can I share or reproduce a settlement for another session?', answer: 'Copy the share link to send a complete editable settlement to another person. It preserves the seed, landscape, footprint, home count, architectural style, selected services, custom service list, generated buildings, roads, water, trees, and manual edits. The same state can also be saved as JSON and reopened later.' },
20
+ { question: 'Which export should I use for a campaign handout?', answer: 'PNG is convenient for notes, virtual tabletops, and quick sharing. SVG stays sharp for printing and further vector editing. JSON is the full editable map state, so it is the best option when you want to preserve every generated and manually painted detail rather than only export a picture.' },
21
+ ];
22
+ const howTo = [
23
+ { name: 'Start with a memorable seed', text: 'Use the generated compound settlement name or type your own seed. The seed controls a repeatable layout variation, so it changes the arrangement without abandoning the selected settlement type.' },
24
+ { name: 'Choose the landscape and character', text: 'Select forest, plains, coast, river, or mountain, then choose an architectural identity such as Medieval, Edo, Sahelian, Timber, Stone, Coastal, or Highland. The surrounding wild growth and the houses use the selected visual language.' },
25
+ { name: 'Set the settlement footprint and homes', text: 'Choose hamlet, village, or town. Each footprint reacts with its own map scale, home range, layout grammar, and core services, then you can fine tune the number of homes to match the location in your campaign.' },
26
+ { name: 'Add meaningful services', text: 'Select built-in services such as Tavern, Smithy, Temple, Market, Stable, and Hall, or add your own landmarks. Placement uses weighted centre and edge preferences with seeded variation, while custom services persist in the browser and in shared map data.' },
27
+ { name: 'Correct the map in context', text: 'Right-click or long-press any cell. Add buildings, roads, water, or trees, inspect what is already there, or erase a cell. Roads repair their junctions and adjacent water cells recompute their river, pond, or lake shape as you paint.' },
28
+ { name: 'Share and export the finished place', text: 'Copy a link for a complete editable version, save JSON as a source file, or export PNG for a handout and SVG for crisp printing. The map stays useful as both a worldbuilding draft and a practical exploration reference.' },
29
+ ];
30
+ const faqSchema: WithContext<FAQPage> = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
31
+ const appSchema: WithContext<SoftwareApplication> = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'RPG Settlement and Exploration Map Generator', operatingSystem: 'All', applicationCategory: 'GameApplication', description: 'A browser based RPG settlement map generator with deterministic layouts, five landscapes, seven architectural styles, weighted services, connected roads, reactive water, contextual editing, share links, local storage, and PNG, SVG, and JSON exports.' };
32
+ const howToSchema: WithContext<HowTo> = { '@context': 'https://schema.org', '@type': 'HowTo', name: 'How to make an RPG settlement exploration map', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
33
+
34
+ export const content: SettlementMapLocaleContent = {
35
+ slug: 'rpg-settlement-exploration-map-generator', title: 'RPG Settlement and Exploration Map Generator', description: 'Generate a deterministic RPG settlement map with landscapes, architectural styles, smart services, connected roads, reactive water, contextual editing, share links, and PNG, SVG, and JSON export.', ui,
36
+ seo: [
37
+ { type: 'title', text: 'Generate a Living RPG Settlement Map', level: 2 },
38
+ { type: 'paragraph', html: 'Build a settlement that gives players somewhere to go, not just a picture of houses. This browser based RPG map generator creates an overhead exploration map where homes, roads, services, plazas, water, trees, and open terrain work together as a place with shape and purpose. Use it for a quick village stop, a faction filled town, a remote hamlet, or the first draft of a location you will keep developing during a campaign.' },
39
+ { type: 'title', text: 'A Seed That Changes the Town Without Losing Its Character', level: 2 },
40
+ { type: 'paragraph', html: 'Every generated settlement starts from a memorable compound name seed. The seed does more than rename the map: it selects a deterministic variation of the layout, including road rhythm, house spacing, service assignment, and surrounding wild growth. Change the seed to explore a new arrangement, then reuse the same seed and settings whenever you need to rebuild the same starting map for your notes, players, or future sessions.' },
41
+ { type: 'list', items: ['Different seeds create different patterns inside the chosen settlement type.', 'The result stays reproducible instead of becoming an untraceable random roll.', 'The settlement remains readable as a hamlet, village, or town while its internal arrangement changes.', 'The same seed can be shared as a stable starting point for collaborative campaign preparation.'] },
42
+ { type: 'title', text: 'Hamlet Village and Town Use Different Urban Grammars', level: 2 },
43
+ { type: 'paragraph', html: 'The footprint selector is not a cosmetic size switch. Hamlets, villages, and towns use different construction patterns, spacing, home ranges, and service expectations. A hamlet can stay under ten houses without looking like a shrunken town, while a town has room for longer routes, more destinations, and a stronger central structure. The home count reacts to the selected footprint so the generated place keeps the right scale.' },
44
+ { type: 'table', headers: ['Footprint', 'Home range', 'Character', 'Best campaign use'], rows: [['Hamlet', '3 to 18', 'Small, sparse, and easy to read at a glance', 'A roadside stop, isolated community, or village edge'], ['Village', '4 to 28', 'A local centre with several routes and landmarks', 'A recurring base, mystery location, or faction hub'], ['Town', '6 to 42', 'A broader network with more services and detours', 'A regional hub, investigation space, or starting settlement']] },
45
+ { type: 'title', text: 'Choose an Architectural Identity', level: 2 },
46
+ { type: 'paragraph', html: 'Give the settlement a visual identity before you start placing details. Timber, Stone, Coastal, Highland, Medieval, Edo, and Sahelian styles alter the houses with different roof profiles, wall colours, doors, windows, textures, and decorative details. The tree language changes with the setting too, from coastal palms and highland conifers to rounded Edo silhouettes, broad medieval canopies, and Sahelian acacia forms. The style makes the map feel like a region, era, or culture rather than a recoloured template.' },
47
+ { type: 'list', items: ['Medieval and Timber support storybook villages, frontier settlements, and woodland communities.', 'Stone and Highland suit rugged, elevated, defensive, or weather exposed places.', 'Coastal introduces a brighter maritime identity with vegetation that belongs near the shore.', 'Edo provides a distinct historical and regional silhouette for streets, houses, and trees.', 'Sahelian gives dryland communities their own architecture, palette, and tree shapes.'] },
48
+ { type: 'title', text: 'Place Services Where They Make Sense', level: 2 },
49
+ { type: 'paragraph', html: 'Services are story anchors with spatial logic. Tavern, Smithy, Temple, Market, Stable, and Hall are not blindly assigned to the first five buildings. Seeded weights choose among suitable candidates, with civic buildings tending toward the centre, stables and smithies favouring outer roads, and other services receiving a useful middle ground with enough variation to avoid a repeated pattern. Add custom services such as a watch post, apothecary, shrine, guildhall, ferry office, or whatever your adventure needs. Full service names appear on large signs above the houses so they stay legible in a town and remain visually attached to the correct building.' },
50
+ { type: 'title', text: 'Let the Landscape Be Part of the Story', level: 2 },
51
+ { type: 'paragraph', html: 'Choose Forest, Plains, Coast, River, or Mountain and let the settlement sit inside that context. The map gives the surrounding landscape room to breathe, with wild growth and terrain framing the populated area instead of turning the village into the only protagonist. A river can define an approach, a forest can hide a path or landmark, and the open ground around a hamlet can communicate isolation before a single encounter is written.' },
52
+ { type: 'title', text: 'Edit the Map Cell by Cell', level: 2 },
53
+ { type: 'paragraph', html: 'The generated plan is a strong starting point, but the contextual editor is where it becomes your location. Right-click on desktop or long-press on a touch screen to open the cell actions exactly where you are working. Inspect a cell, add a building, draw a road, paint water, add a tree, or erase an unwanted mark. The menu repositions itself when the edge of the map would clip it, keeping the editing action available without covering the workspace.' },
54
+ { type: 'list', items: ['Add a missing house, bridge approach, shrine, watch post, field, or other point of interest.', 'Move from generated structure to campaign-specific detail without opening a separate drawing toolbar.', 'Keep manual edits in the same map state as the generated content.', 'Use the same interaction on desktop and touch devices with right click or long press.'] },
55
+ { type: 'title', text: 'Roads and Water Respond to Their Neighbours', level: 2 },
56
+ { type: 'paragraph', html: 'Roads are generated as connected routes rather than decorative disconnected lines. Intersections are drawn cleanly when three or four paths meet, giving the settlement a usable circulation pattern. Water painting is equally reactive: a painted cell remains an exact edit, adjacent cells join into a river or pond, and larger connected groups resolve into a broader lake shape. This makes it practical to sketch a stream bend, extend a shoreline, add a crossing, or reshape a water feature directly on the map.' },
57
+ { type: 'title', text: 'Keep Your Services and Share the Complete Place', level: 2 },
58
+ { type: 'paragraph', html: 'Custom services are saved in local storage so a useful landmark list can grow across maps. Remove custom entries when they no longer belong; built-in presets remain available. When you copy a share link, the recipient gets the complete editable state: seed, footprint, home count, landscape, architectural style, service catalog, service marks, buildings, roads, water, trees, and manual edits. Nothing important is reduced to a screenshot.' },
59
+ { type: 'title', text: 'Export for Notes, Printing, or Continued Editing', level: 2 },
60
+ { type: 'table', headers: ['Format', 'Useful for', 'What it preserves'], rows: [['PNG', 'Virtual tabletops, notes, and quick handouts', 'A ready to view image of the current map'], ['SVG', 'Printing and vector workflows', 'A sharp scalable drawing for larger layouts'], ['JSON', 'Campaign archives and future edits', 'The complete editable settlement state']] },
61
+ { type: 'tip', title: 'A Practical Workflow for Game Night', html: 'Start with a seed, footprint, landscape, and architectural style. Add only the services that can create scenes or clues, then use the contextual editor to place the one bridge, road, tree line, water cell, or outlying house that makes the adventure yours. Copy the share link for the editable master and export a PNG or SVG when you need a clean reference at the table.' },
62
+ ], faq, bibliography, howTo, schemas: [faqSchema, appSchema, howToSchema] as unknown as Record<string, unknown>[],
63
+ };