@jjlmoya/utils-tabletop 1.29.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -0
- package/src/entries.ts +4 -0
- package/src/tests/locale_completeness.test.ts +2 -9
- package/src/tests/tool_validation.test.ts +1 -2
- package/src/tool/rpg-settlement-exploration-map-generator/.keep +1 -0
- package/src/tool/rpg-settlement-exploration-map-generator/bibliography.astro +11 -0
- package/src/tool/rpg-settlement-exploration-map-generator/bibliography.ts +7 -0
- package/src/tool/rpg-settlement-exploration-map-generator/component.astro +66 -0
- package/src/tool/rpg-settlement-exploration-map-generator/controller.ts +231 -0
- package/src/tool/rpg-settlement-exploration-map-generator/dom-views.ts +174 -0
- package/src/tool/rpg-settlement-exploration-map-generator/editor.ts +85 -0
- package/src/tool/rpg-settlement-exploration-map-generator/entry.ts +27 -0
- package/src/tool/rpg-settlement-exploration-map-generator/evaluator.ts +8 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/de.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/en.ts +63 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/es.ts +233 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/fr.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/id.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/it.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/ja.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/ko.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/nl.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/pl.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/pt.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/ru.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/sv.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/tr.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/i18n/zh.ts +152 -0
- package/src/tool/rpg-settlement-exploration-map-generator/index.ts +4 -0
- package/src/tool/rpg-settlement-exploration-map-generator/logic.test.ts +65 -0
- package/src/tool/rpg-settlement-exploration-map-generator/logic.ts +288 -0
- package/src/tool/rpg-settlement-exploration-map-generator/rpg-settlement-exploration-map-generator.css +885 -0
- package/src/tool/rpg-settlement-exploration-map-generator/seo.astro +11 -0
- package/src/tool/rpg-settlement-exploration-map-generator/storage.test.ts +12 -0
- package/src/tool/rpg-settlement-exploration-map-generator/storage.ts +24 -0
- package/src/tool/rpg-settlement-exploration-map-generator/ui.ts +81 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
export type Environment = 'forest' | 'plains' | 'coast' | 'river' | 'mountain';
|
|
2
|
+
export type SettlementSize = 'hamlet' | 'village' | 'town';
|
|
3
|
+
export type SettlementStyle = 'timber' | 'stone' | 'coastal' | 'highland' | 'medieval' | 'edo' | 'sahelian';
|
|
4
|
+
export type ServiceType = string;
|
|
5
|
+
export type EditTool = 'select' | 'building' | 'path' | 'water' | 'tree' | 'erase';
|
|
6
|
+
|
|
7
|
+
export interface SettlementConfig { seed: string; environment: Environment; size: SettlementSize; style: SettlementStyle; homes: number; services: ServiceType[]; }
|
|
8
|
+
export interface Point { x: number; y: number }
|
|
9
|
+
export interface Building { id: string; x: number; y: number; width: number; height: number; service: ServiceType | null }
|
|
10
|
+
export interface PathRoute { points: Point[] }
|
|
11
|
+
export interface Plaza { x: number; y: number; width: number; height: number }
|
|
12
|
+
|
|
13
|
+
export interface SettlementMap {
|
|
14
|
+
config: SettlementConfig;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
plaza: Plaza;
|
|
18
|
+
buildings: Building[];
|
|
19
|
+
paths: PathRoute[];
|
|
20
|
+
water: Point[];
|
|
21
|
+
wild: Point[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type RandomSource = () => number;
|
|
25
|
+
export interface HomeRange { min: number; max: number }
|
|
26
|
+
export interface SettlementPreset { homes: number; services: ServiceType[] }
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_CONFIG: SettlementConfig = { seed: 'Willow Mere Crossing', environment: 'forest', size: 'village', style: 'medieval', homes: 14, services: ['tavern', 'smithy', 'temple', 'market'] };
|
|
29
|
+
const SIZE_PRESETS: Record<SettlementSize, SettlementPreset> = { hamlet: { homes: 6, services: ['tavern', 'stable'] }, village: { homes: 14, services: ['tavern', 'smithy', 'temple', 'market'] }, town: { homes: 24, services: ['tavern', 'smithy', 'temple', 'market', 'stable', 'hall'] } };
|
|
30
|
+
const DIMENSIONS: Record<SettlementSize, { width: number; height: number }> = { hamlet: { width: 34, height: 24 }, village: { width: 42, height: 28 }, town: { width: 52, height: 34 } };
|
|
31
|
+
const HOME_RANGES: Record<SettlementSize, HomeRange> = { hamlet: { min: 3, max: 18 }, village: { min: 4, max: 28 }, town: { min: 6, max: 42 } };
|
|
32
|
+
|
|
33
|
+
export function homeRange(size: SettlementSize): HomeRange { return HOME_RANGES[size]; }
|
|
34
|
+
export function settlementPreset(size: SettlementSize): SettlementPreset { return { homes: SIZE_PRESETS[size].homes, services: [...SIZE_PRESETS[size].services] }; }
|
|
35
|
+
|
|
36
|
+
const clamp = (val: number, min: number, max: number): number => Math.min(max, Math.max(min, Math.round(val)));
|
|
37
|
+
const isEnv = (val: unknown): val is Environment => ['forest', 'plains', 'coast', 'river', 'mountain'].includes(String(val));
|
|
38
|
+
const isSize = (val: unknown): val is SettlementSize => ['hamlet', 'village', 'town'].includes(String(val));
|
|
39
|
+
const isStyle = (val: unknown): val is SettlementStyle => ['timber', 'stone', 'coastal', 'highland', 'medieval', 'edo', 'sahelian'].includes(String(val));
|
|
40
|
+
|
|
41
|
+
export function normalizeConfig(input: Partial<SettlementConfig>): SettlementConfig {
|
|
42
|
+
const size = isSize(input.size) ? input.size : DEFAULT_CONFIG.size;
|
|
43
|
+
const range = HOME_RANGES[size];
|
|
44
|
+
const services = Array.isArray(input.services) ? input.services.filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map((v) => v.trim()).slice(0, 32) : DEFAULT_CONFIG.services;
|
|
45
|
+
return { seed: String(input.seed || DEFAULT_CONFIG.seed).slice(0, 48), environment: isEnv(input.environment) ? input.environment : DEFAULT_CONFIG.environment, size, style: isStyle(input.style) ? input.style : DEFAULT_CONFIG.style, homes: clamp(input.homes ?? DEFAULT_CONFIG.homes, range.min, range.max), services: [...new Set(services)] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRandom(seed: string): RandomSource {
|
|
49
|
+
let h = 2166136261;
|
|
50
|
+
for (let i = 0; i < seed.length; i += 1) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
51
|
+
let state = h >>> 0;
|
|
52
|
+
return () => { state += 0x6d2b79f5; let v = state; v = Math.imul(v ^ (v >>> 15), v | 1); v ^= v + Math.imul(v ^ (v >>> 7), v | 61); return ((v ^ (v >>> 14)) >>> 0) / 4294967296; };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const randomInt = (rnd: RandomSource, min: number, max: number): number => Math.floor(rnd() * (max - min + 1)) + min;
|
|
56
|
+
|
|
57
|
+
function routeBetween(from: Point, to: Point): Point[] {
|
|
58
|
+
const points: Point[] = [{ ...from }];
|
|
59
|
+
let cur = { ...from };
|
|
60
|
+
while (cur.x !== to.x || cur.y !== to.y) { cur = cur.x !== to.x ? { x: cur.x + Math.sign(to.x - cur.x), y: cur.y } : { x: cur.x, y: cur.y + Math.sign(to.y - cur.y) }; points.push({ ...cur }); }
|
|
61
|
+
return points;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createWater(env: Environment, w: number, h: number, rnd: RandomSource): Point[] {
|
|
65
|
+
const res: Point[] = [];
|
|
66
|
+
if (env === 'coast') {
|
|
67
|
+
const edge = rnd() < 0.5 ? w - 1 : 0;
|
|
68
|
+
for (let y = 0; y < h; y += 1) { const d = 3 + Math.floor(rnd() * 3); for (let o = 0; o < d; o += 1) res.push({ x: edge === 0 ? o : w - 1 - o, y }); }
|
|
69
|
+
}
|
|
70
|
+
if (env === 'river') {
|
|
71
|
+
let x = Math.floor(w * (0.35 + rnd() * 0.3));
|
|
72
|
+
for (let y = 0; y < h; y += 1) { x = clamp(x + randomInt(rnd, -1, 1), 2, w - 3); res.push({ x, y }, { x: x + 1, y }); }
|
|
73
|
+
}
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildingCells(buildings: Building[], plaza: Plaza): Set<string> {
|
|
78
|
+
const cells = new Set<string>();
|
|
79
|
+
buildings.forEach((b) => { for (let y = b.y - 2; y < b.y + b.height + 2; y += 1) for (let x = b.x - 2; x < b.x + b.width + 2; x += 1) cells.add(`${x},${y}`); });
|
|
80
|
+
for (let y = plaza.y - 2; y < plaza.y + plaza.height + 2; y += 1) for (let x = plaza.x - 2; x < plaza.x + plaza.width + 2; x += 1) cells.add(`${x},${y}`);
|
|
81
|
+
return cells;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getWildChance(env: Environment): number {
|
|
85
|
+
if (env === 'forest') return 0.12;
|
|
86
|
+
if (env === 'mountain') return 0.09;
|
|
87
|
+
return 0.07;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface WildContext { config: SettlementConfig; dimensions: { width: number; height: number }; random: RandomSource; water: Set<string>; buildings: Building[]; plaza: Plaza }
|
|
91
|
+
|
|
92
|
+
function createWild(ctx: WildContext): Point[] {
|
|
93
|
+
const res: Point[] = [];
|
|
94
|
+
const chance = getWildChance(ctx.config.environment);
|
|
95
|
+
const blocked = buildingCells(ctx.buildings, ctx.plaza);
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const attempts = Math.floor(ctx.dimensions.width * ctx.dimensions.height * chance * 3);
|
|
98
|
+
for (let i = 0; i < attempts && res.length < Math.floor(ctx.dimensions.width * ctx.dimensions.height * chance); i += 1) {
|
|
99
|
+
const pt = { x: randomInt(ctx.random, 1, ctx.dimensions.width - 2), y: randomInt(ctx.random, 1, ctx.dimensions.height - 2) };
|
|
100
|
+
const key = `${pt.x},${pt.y}`;
|
|
101
|
+
if (!ctx.water.has(key) && !blocked.has(key) && !seen.has(key)) { seen.add(key); res.push(pt); }
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const overlaps = (b: Building, o: Building): boolean => b.x - 1 < o.x + o.width && b.x + b.width + 1 > o.x && b.y - 1 < o.y + o.height && b.y + b.height + 1 > o.y;
|
|
107
|
+
const inPlaza = (b: Building, p: Plaza): boolean => b.x < p.x + p.width && b.x + b.width > p.x && b.y < p.y + p.height && b.y + b.height > p.y;
|
|
108
|
+
const plazaCenter = (p: Plaza): Point => ({ x: p.x + Math.floor(p.width / 2), y: p.y + Math.floor(p.height / 2) });
|
|
109
|
+
const buildingCenter = (b: Building): Point => ({ x: b.x + Math.floor(b.width / 2), y: b.y + Math.floor(b.height / 2) });
|
|
110
|
+
|
|
111
|
+
interface GridContext { spots: Point[]; size: SettlementSize; dimensions: { width: number; height: number }; plaza: Plaza; homes: number; variant: number }
|
|
112
|
+
|
|
113
|
+
function calcStagger(size: SettlementSize, variant: number, row: number, col: number): number {
|
|
114
|
+
if (size === 'town') return variant === 1 ? row % 2 : 0;
|
|
115
|
+
if (variant === 2) return col % 2;
|
|
116
|
+
return row % 2;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface LayoutMetrics { columns: number; xStep: number; yStep: number; startX: number; startY: number; xBias: number; yBias: number }
|
|
120
|
+
|
|
121
|
+
function getLayoutMetrics(ctx: GridContext): LayoutMetrics {
|
|
122
|
+
let columns = 7;
|
|
123
|
+
let xStep = 5;
|
|
124
|
+
let yStep = 4;
|
|
125
|
+
if (ctx.size === 'hamlet') { columns = 4; xStep = 6; yStep = 7; }
|
|
126
|
+
else if (ctx.size === 'village') { columns = 6; xStep = 5; yStep = 5; }
|
|
127
|
+
|
|
128
|
+
const rows = Math.ceil(Math.max(ctx.homes, columns) / columns);
|
|
129
|
+
const startX = Math.max(2, Math.floor(plazaCenter(ctx.plaza).x - ((columns - 1) * xStep + 4) / 2));
|
|
130
|
+
const startY = Math.max(2, Math.floor(plazaCenter(ctx.plaza).y - ((rows - 1) * yStep + 3) / 2));
|
|
131
|
+
const xBias = [0, 1, -1][ctx.variant % 3]!;
|
|
132
|
+
const yBias = [0, -1, 1][(ctx.variant + 1) % 3]!;
|
|
133
|
+
return { columns, xStep, yStep, startX, startY, xBias, yBias };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function addGridSpots(ctx: GridContext): void {
|
|
137
|
+
const m = getLayoutMetrics(ctx);
|
|
138
|
+
const rows = Math.ceil(Math.max(ctx.homes, m.columns) / m.columns);
|
|
139
|
+
for (let r = 0; r < rows; r += 1) {
|
|
140
|
+
for (let c = 0; c < m.columns; c += 1) {
|
|
141
|
+
const stagger = calcStagger(ctx.size, ctx.variant, r, c);
|
|
142
|
+
const extraY = ctx.variant === 2 ? c % 2 : m.yBias;
|
|
143
|
+
const spot = { x: m.startX + c * m.xStep + stagger + m.xBias, y: m.startY + r * m.yStep + extraY };
|
|
144
|
+
if (spot.x + 4 < ctx.dimensions.width - 1 && spot.y + 3 < ctx.dimensions.height - 1) ctx.spots.push(spot);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getHamletAnchors(plaza: Plaza, variant: number): Point[] {
|
|
150
|
+
const raw = [{ x: plaza.x - 5, y: plaza.y - 4 }, { x: plaza.x + plaza.width + 1, y: plaza.y - 4 }, { x: plaza.x - 5, y: plaza.y + plaza.height + 1 }, { x: plaza.x + plaza.width + 1, y: plaza.y + plaza.height + 1 }];
|
|
151
|
+
return raw.map((_, i) => raw[(i + variant) % raw.length]!);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function candidateSpots(ctx: Omit<GridContext, 'spots'>): Point[] {
|
|
155
|
+
const spots: Point[] = [];
|
|
156
|
+
if (ctx.size === 'hamlet') spots.push(...getHamletAnchors(ctx.plaza, ctx.variant));
|
|
157
|
+
addGridSpots({ ...ctx, spots });
|
|
158
|
+
return spots;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildingPattern(style: SettlementStyle, index: number, variant: number): { width: number; height: number } {
|
|
162
|
+
const patterns: Record<SettlementStyle, { width: number; height: number }[]> = {
|
|
163
|
+
timber: [{ width: 3, height: 2 }, { width: 4, height: 2 }, { width: 3, height: 3 }, { width: 2, height: 2 }],
|
|
164
|
+
stone: [{ width: 4, height: 2 }, { width: 3, height: 3 }, { width: 4, height: 3 }, { width: 3, height: 2 }],
|
|
165
|
+
coastal: [{ width: 4, height: 2 }, { width: 3, height: 2 }, { width: 4, height: 3 }, { width: 2, height: 2 }],
|
|
166
|
+
highland: [{ width: 3, height: 3 }, { width: 3, height: 2 }, { width: 2, height: 3 }, { width: 4, height: 2 }],
|
|
167
|
+
medieval: [{ width: 4, height: 2 }, { width: 3, height: 3 }, { width: 4, height: 3 }, { width: 3, height: 2 }],
|
|
168
|
+
edo: [{ width: 4, height: 2 }, { width: 3, height: 2 }, { width: 4, height: 2 }, { width: 3, height: 3 }],
|
|
169
|
+
sahelian: [{ width: 3, height: 2 }, { width: 4, height: 2 }, { width: 3, height: 3 }, { width: 4, height: 2 }],
|
|
170
|
+
};
|
|
171
|
+
return patterns[style][(index + variant) % patterns[style].length]!;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function servicePlacement(service: ServiceType, rnd: RandomSource): 'center' | 'edge' | 'mixed' {
|
|
175
|
+
if (/hall|town|ayunt/i.test(service)) return rnd() < 0.82 ? 'center' : 'mixed';
|
|
176
|
+
if (/stable|smith|forge|establ/i.test(service)) return rnd() < 0.8 ? 'edge' : 'mixed';
|
|
177
|
+
if (/temple|market|tavern/i.test(service)) return rnd() < 0.7 ? 'center' : 'mixed';
|
|
178
|
+
return 'mixed';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function servicePriority(service: ServiceType): number {
|
|
182
|
+
if (/hall|town|ayunt/i.test(service)) return 0;
|
|
183
|
+
if (/temple|market|tavern/i.test(service)) return 1;
|
|
184
|
+
if (/stable|smith|forge|establ/i.test(service)) return 2;
|
|
185
|
+
return 3;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface ScoreContext { building: Building; placement: 'center' | 'edge' | 'mixed'; dim: { width: number; height: number }; plaza: Plaza; rnd: RandomSource }
|
|
189
|
+
|
|
190
|
+
function calcPreference(placement: 'center' | 'edge' | 'mixed', centerDist: number, edgeDist: number): number {
|
|
191
|
+
if (placement === 'edge') return edgeDist;
|
|
192
|
+
if (placement === 'center') return centerDist;
|
|
193
|
+
return centerDist * 0.5 + edgeDist * 0.5;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function serviceScore(ctx: ScoreContext): number {
|
|
197
|
+
const c = buildingCenter(ctx.building);
|
|
198
|
+
const centerDist = Math.hypot(c.x - plazaCenter(ctx.plaza).x, c.y - plazaCenter(ctx.plaza).y) / Math.hypot(ctx.dim.width, ctx.dim.height);
|
|
199
|
+
const edgeDist = Math.min(c.x, c.y, ctx.dim.width - c.x, ctx.dim.height - c.y) / Math.min(ctx.dim.width, ctx.dim.height);
|
|
200
|
+
return calcPreference(ctx.placement, centerDist, edgeDist) + ctx.rnd() * 0.22;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface AssignContext { config: SettlementConfig; buildings: Building[]; dimensions: { width: number; height: number }; plaza: Plaza; variant: number }
|
|
204
|
+
|
|
205
|
+
function assignServices(ctx: AssignContext): Building[] {
|
|
206
|
+
const rnd = createRandom(`${ctx.config.seed}:${ctx.config.size}:${ctx.config.style}:services:${ctx.variant}`);
|
|
207
|
+
const available = [...ctx.buildings];
|
|
208
|
+
const services = [...ctx.config.services].sort((a, b) => servicePriority(a) - servicePriority(b) || rnd() - 0.5);
|
|
209
|
+
services.forEach((service) => {
|
|
210
|
+
if (!available.length) return;
|
|
211
|
+
const placement = servicePlacement(service, rnd);
|
|
212
|
+
const ranked = available.map((building) => ({ building, score: serviceScore({ building, placement, dim: ctx.dimensions, plaza: ctx.plaza, rnd }) })).sort((a, b) => a.score - b.score);
|
|
213
|
+
const selected = ranked[0]!.building;
|
|
214
|
+
selected.service = service;
|
|
215
|
+
available.splice(available.indexOf(selected), 1);
|
|
216
|
+
});
|
|
217
|
+
return ctx.buildings;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function placeBuildings(config: SettlementConfig, placement: { dimensions: { width: number; height: number }; plaza: Plaza; water: Set<string> }, variant: number): Building[] {
|
|
221
|
+
const buildings: Building[] = [];
|
|
222
|
+
const { dimensions, plaza, water } = placement;
|
|
223
|
+
const spots = candidateSpots({ size: config.size, dimensions, plaza, homes: config.homes, variant });
|
|
224
|
+
for (const [i, spot] of spots.entries()) {
|
|
225
|
+
if (buildings.length >= config.homes) break;
|
|
226
|
+
const pat = buildingPattern(config.style, i, variant);
|
|
227
|
+
const b: Building = { id: `building-${buildings.length + 1}`, x: spot.x, y: spot.y, width: pat.width, height: pat.height, service: null };
|
|
228
|
+
const occupied = Array.from({ length: b.height }, (_, r) => Array.from({ length: b.width }, (_, c) => water.has(`${b.x + c},${b.y + r}`))).flat().some(Boolean);
|
|
229
|
+
if (!occupied && !inPlaza(b, plaza) && !buildings.some((o) => overlaps(b, o))) buildings.push(b);
|
|
230
|
+
}
|
|
231
|
+
return assignServices({ config, buildings, dimensions, plaza, variant });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createPaths(buildings: Building[], plaza: Plaza): PathRoute[] {
|
|
235
|
+
const anchor = plazaCenter(plaza);
|
|
236
|
+
return buildings.map((b) => ({ points: routeBetween(buildingCenter(b), anchor) }));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function generateSettlement(input: Partial<SettlementConfig>): SettlementMap {
|
|
240
|
+
const config = normalizeConfig(input);
|
|
241
|
+
const dimensions = DIMENSIONS[config.size];
|
|
242
|
+
const plaza: Plaza = { x: Math.floor(dimensions.width / 2) - 3, y: Math.floor(dimensions.height / 2) - 2, width: 6, height: 4 };
|
|
243
|
+
const random = createRandom(config.seed);
|
|
244
|
+
const layoutVariant = Math.floor(createRandom(`${config.seed}:${config.size}:${config.style}:layout`)() * 3);
|
|
245
|
+
const water = createWater(config.environment, dimensions.width, dimensions.height, random);
|
|
246
|
+
const waterSet = new Set(water.map((p) => `${p.x},${p.y}`));
|
|
247
|
+
const buildings = placeBuildings(config, { dimensions, plaza, water: waterSet }, layoutVariant);
|
|
248
|
+
return { config, width: dimensions.width, height: dimensions.height, plaza, buildings, paths: createPaths(buildings, plaza), water, wild: createWild({ config, dimensions, random, water: waterSet, buildings, plaza }) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getPlaza(map: SettlementMap): Plaza { return map.plaza ?? { x: Math.floor(map.width / 2) - 3, y: Math.floor(map.height / 2) - 2, width: 6, height: 4 }; }
|
|
252
|
+
|
|
253
|
+
export function addBuilding(map: SettlementMap, point: Point, service: ServiceType | null = null): SettlementMap {
|
|
254
|
+
const b: Building = { id: `building-${map.buildings.length + 1}`, x: clamp(point.x, 1, map.width - 4), y: clamp(point.y, 1, map.height - 3), width: 3, height: 2, service };
|
|
255
|
+
if (map.buildings.some((other) => overlaps(b, other)) || inPlaza(b, getPlaza(map))) return map;
|
|
256
|
+
const buildings = [...map.buildings, b];
|
|
257
|
+
return { ...map, buildings, paths: createPaths(buildings, getPlaza(map)) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function addPath(map: SettlementMap, point: Point): SettlementMap {
|
|
261
|
+
const path = { points: [{ x: clamp(point.x, 0, map.width - 1), y: clamp(point.y, 0, map.height - 1) }, { x: clamp(point.x + 2, 0, map.width - 1), y: clamp(point.y, 0, map.height - 1) }] };
|
|
262
|
+
return { ...map, paths: [...map.paths, path] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function addWater(map: SettlementMap, point: Point): SettlementMap {
|
|
266
|
+
const waterPoint = { x: clamp(point.x, 0, map.width - 1), y: clamp(point.y, 0, map.height - 1) };
|
|
267
|
+
const key = `${waterPoint.x},${waterPoint.y}`;
|
|
268
|
+
if (map.water.some((item) => `${item.x},${item.y}` === key)) return map;
|
|
269
|
+
const buildings = map.buildings.filter((b) => waterPoint.x < b.x || waterPoint.x >= b.x + b.width || waterPoint.y < b.y || waterPoint.y >= b.y + b.height);
|
|
270
|
+
const wild = map.wild.filter((item) => item.x !== waterPoint.x || item.y !== waterPoint.y);
|
|
271
|
+
const water = [...map.water, waterPoint];
|
|
272
|
+
return { ...map, buildings, wild, water, paths: createPaths(buildings, getPlaza(map)) };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function addTree(map: SettlementMap, point: Point): SettlementMap {
|
|
276
|
+
const tree = { x: clamp(point.x, 0, map.width - 1), y: clamp(point.y, 0, map.height - 1) };
|
|
277
|
+
if (map.wild.some((item) => item.x === tree.x && item.y === tree.y)) return map;
|
|
278
|
+
return { ...map, wild: [...map.wild, tree] };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function eraseAt(map: SettlementMap, point: Point): SettlementMap {
|
|
282
|
+
const plaza = getPlaza(map);
|
|
283
|
+
const buildings = map.buildings.filter((b) => point.x < b.x || point.x >= b.x + b.width || point.y < b.y || point.y >= b.y + b.height);
|
|
284
|
+
const water = map.water.filter((item) => item.x !== point.x || item.y !== point.y);
|
|
285
|
+
const paths = map.paths.filter((path) => !path.points.some((item) => item.x === point.x && item.y === point.y));
|
|
286
|
+
const wild = map.wild.filter((item) => item.x !== point.x || item.y !== point.y);
|
|
287
|
+
return { ...map, plaza, buildings, water, paths, wild };
|
|
288
|
+
}
|