@jgengine/core 0.4.0 → 0.6.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/CHANGELOG.md +66 -0
- package/dist/game/defineGame.d.ts +3 -0
- package/dist/game/feed.d.ts +2 -0
- package/dist/game/feed.js +10 -8
- package/dist/game/playableGame.d.ts +10 -1
- package/dist/game/progression.d.ts +61 -0
- package/dist/game/progression.js +96 -0
- package/dist/game/quest.d.ts +39 -0
- package/dist/game/quest.js +195 -0
- package/dist/game/trade.d.ts +23 -0
- package/dist/game/trade.js +50 -8
- package/dist/game/unlocks.d.ts +9 -0
- package/dist/game/unlocks.js +19 -1
- package/dist/inventory/slotModel.d.ts +19 -0
- package/dist/inventory/slotModel.js +76 -0
- package/dist/meta/changelog.d.ts +8 -0
- package/dist/meta/changelog.js +43 -0
- package/dist/physics/physicsWorld.d.ts +136 -0
- package/dist/physics/physicsWorld.js +489 -0
- package/dist/runtime/gameContext.d.ts +2 -0
- package/dist/runtime/gameContext.js +3 -0
- package/dist/scene/assetCatalog.d.ts +13 -0
- package/dist/scene/entityStore.d.ts +6 -0
- package/dist/scene/entityStore.js +15 -1
- package/dist/time/simClock.d.ts +62 -0
- package/dist/time/simClock.js +132 -0
- package/dist/world/buildingIndex.d.ts +16 -0
- package/dist/world/buildingIndex.js +63 -0
- package/dist/world/buildings.d.ts +98 -0
- package/dist/world/buildings.js +320 -0
- package/dist/world/features.d.ts +105 -1
- package/dist/world/features.js +95 -0
- package/dist/world/geometry.d.ts +23 -0
- package/dist/world/geometry.js +75 -0
- package/dist/world/interiors.d.ts +35 -0
- package/dist/world/interiors.js +66 -0
- package/dist/world/placement.d.ts +30 -0
- package/dist/world/placement.js +20 -0
- package/dist/world/regions.d.ts +66 -0
- package/dist/world/regions.js +158 -0
- package/dist/world/scatter.d.ts +24 -0
- package/dist/world/scatter.js +126 -0
- package/dist/world/scatterItems.d.ts +38 -0
- package/dist/world/scatterItems.js +85 -0
- package/dist/world/terrain.d.ts +54 -0
- package/dist/world/terrain.js +181 -0
- package/dist/world/water.d.ts +27 -0
- package/dist/world/water.js +125 -0
- package/dist/world/wind.d.ts +17 -0
- package/dist/world/wind.js +40 -0
- package/package.json +5 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SECONDS_PER_GAME_DAY } from "./gameClock.js";
|
|
2
|
+
const MAX_TIMER_FIRES = 10_000;
|
|
3
|
+
export function createSimClock(options = {}) {
|
|
4
|
+
const config = options.config ?? {};
|
|
5
|
+
const onChange = options.onChange ?? (() => { });
|
|
6
|
+
const scale = config.scale !== undefined && config.scale > 0 ? config.scale : 1;
|
|
7
|
+
const configuredSpeeds = config.speeds === undefined ? [1, 2, 3, 4] : config.speeds.filter((value) => value > 0);
|
|
8
|
+
const speeds = configuredSpeeds.length > 0 ? configuredSpeeds : [1];
|
|
9
|
+
const dayLength = config.dayLength !== undefined && config.dayLength > 0 ? config.dayLength : SECONDS_PER_GAME_DAY;
|
|
10
|
+
let now = config.start !== undefined && config.start > 0 ? config.start : 0;
|
|
11
|
+
let paused = config.startPaused ?? false;
|
|
12
|
+
let playSpeed = speeds[0];
|
|
13
|
+
let nextTimerId = 0;
|
|
14
|
+
const timers = new Map();
|
|
15
|
+
let displayMinute = minuteIndex(now, dayLength);
|
|
16
|
+
function calendar() {
|
|
17
|
+
const day = Math.floor(now / dayLength);
|
|
18
|
+
const dayFraction = (now - day * dayLength) / dayLength;
|
|
19
|
+
const secondOfDay = dayFraction * 24 * 3600;
|
|
20
|
+
return {
|
|
21
|
+
totalSeconds: now,
|
|
22
|
+
day,
|
|
23
|
+
hour: Math.floor(secondOfDay / 3600) % 24,
|
|
24
|
+
minute: Math.floor(secondOfDay / 60) % 60,
|
|
25
|
+
second: Math.floor(secondOfDay) % 60,
|
|
26
|
+
dayFraction,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function fireDueTimers() {
|
|
30
|
+
let fires = 0;
|
|
31
|
+
for (;;) {
|
|
32
|
+
let soonest = null;
|
|
33
|
+
for (const [id, timer] of timers) {
|
|
34
|
+
if (timer.due > now)
|
|
35
|
+
continue;
|
|
36
|
+
if (soonest === null || timer.due < soonest.timer.due)
|
|
37
|
+
soonest = { id, timer };
|
|
38
|
+
}
|
|
39
|
+
if (soonest === null)
|
|
40
|
+
return;
|
|
41
|
+
const { id, timer } = soonest;
|
|
42
|
+
if (timer.interval === null) {
|
|
43
|
+
timers.delete(id);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
timer.due += timer.interval;
|
|
47
|
+
}
|
|
48
|
+
timer.callback();
|
|
49
|
+
fires += 1;
|
|
50
|
+
if (fires >= MAX_TIMER_FIRES)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function advance(realDt) {
|
|
55
|
+
if (paused || realDt <= 0)
|
|
56
|
+
return 0;
|
|
57
|
+
const gameDt = realDt * scale * playSpeed;
|
|
58
|
+
now += gameDt;
|
|
59
|
+
fireDueTimers();
|
|
60
|
+
const minute = minuteIndex(now, dayLength);
|
|
61
|
+
if (minute !== displayMinute) {
|
|
62
|
+
displayMinute = minute;
|
|
63
|
+
onChange();
|
|
64
|
+
}
|
|
65
|
+
return gameDt;
|
|
66
|
+
}
|
|
67
|
+
function pause() {
|
|
68
|
+
if (paused)
|
|
69
|
+
return;
|
|
70
|
+
paused = true;
|
|
71
|
+
onChange();
|
|
72
|
+
}
|
|
73
|
+
function play() {
|
|
74
|
+
if (!paused)
|
|
75
|
+
return;
|
|
76
|
+
paused = false;
|
|
77
|
+
onChange();
|
|
78
|
+
}
|
|
79
|
+
function setSpeed(multiplier) {
|
|
80
|
+
if (multiplier <= 0) {
|
|
81
|
+
pause();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const changed = paused || multiplier !== playSpeed;
|
|
85
|
+
playSpeed = multiplier;
|
|
86
|
+
paused = false;
|
|
87
|
+
if (changed)
|
|
88
|
+
onChange();
|
|
89
|
+
}
|
|
90
|
+
function cycleSpeed() {
|
|
91
|
+
const index = speeds.indexOf(playSpeed);
|
|
92
|
+
setSpeed(speeds[(index + 1) % speeds.length]);
|
|
93
|
+
}
|
|
94
|
+
function register(due, interval, callback) {
|
|
95
|
+
const id = nextTimerId;
|
|
96
|
+
nextTimerId += 1;
|
|
97
|
+
timers.set(id, { due, interval, callback });
|
|
98
|
+
return () => {
|
|
99
|
+
timers.delete(id);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
advance,
|
|
104
|
+
now: () => now,
|
|
105
|
+
calendar,
|
|
106
|
+
snapshot: () => ({
|
|
107
|
+
now,
|
|
108
|
+
paused,
|
|
109
|
+
speed: paused ? 0 : playSpeed,
|
|
110
|
+
playSpeed,
|
|
111
|
+
scale,
|
|
112
|
+
speeds,
|
|
113
|
+
calendar: calendar(),
|
|
114
|
+
}),
|
|
115
|
+
isPaused: () => paused,
|
|
116
|
+
speed: () => (paused ? 0 : playSpeed),
|
|
117
|
+
pause,
|
|
118
|
+
play,
|
|
119
|
+
toggle: () => (paused ? play() : pause()),
|
|
120
|
+
setSpeed,
|
|
121
|
+
cycleSpeed,
|
|
122
|
+
after: (seconds, callback) => register(now + Math.max(0, seconds), null, callback),
|
|
123
|
+
every: (seconds, callback) => {
|
|
124
|
+
const interval = Math.max(Number.EPSILON, seconds);
|
|
125
|
+
return register(now + interval, interval, callback);
|
|
126
|
+
},
|
|
127
|
+
at: (time, callback) => register(time, null, callback),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function minuteIndex(seconds, dayLength) {
|
|
131
|
+
return Math.floor((seconds / dayLength) * 24 * 60);
|
|
132
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Aabb, type Vec2 } from "./geometry.js";
|
|
2
|
+
import type { GeneratedBuilding } from "./buildings.js";
|
|
3
|
+
export interface BuildingHit {
|
|
4
|
+
building: GeneratedBuilding;
|
|
5
|
+
distance: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildingIndex {
|
|
8
|
+
readonly buildings: readonly GeneratedBuilding[];
|
|
9
|
+
at(point: Vec2): GeneratedBuilding | undefined;
|
|
10
|
+
within(area: Aabb): GeneratedBuilding[];
|
|
11
|
+
nearest(point: Vec2): BuildingHit | undefined;
|
|
12
|
+
isInside(point: Vec2): boolean;
|
|
13
|
+
blockers(margin?: number): Aabb[];
|
|
14
|
+
readonly bounds: Aabb | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildingIndex(buildings: readonly GeneratedBuilding[]): BuildingIndex;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { aabbOverlap, expandAabb, pointInAabb } from "./geometry.js";
|
|
2
|
+
function centerDistance(point, center) {
|
|
3
|
+
const dx = point[0] - center[0];
|
|
4
|
+
const dz = point[1] - center[1];
|
|
5
|
+
return Math.sqrt(dx * dx + dz * dz);
|
|
6
|
+
}
|
|
7
|
+
function unionBounds(buildings) {
|
|
8
|
+
if (buildings.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
let { minX, minZ, maxX, maxZ } = buildings[0].bounds;
|
|
11
|
+
for (let index = 1; index < buildings.length; index += 1) {
|
|
12
|
+
const b = buildings[index].bounds;
|
|
13
|
+
if (b.minX < minX)
|
|
14
|
+
minX = b.minX;
|
|
15
|
+
if (b.minZ < minZ)
|
|
16
|
+
minZ = b.minZ;
|
|
17
|
+
if (b.maxX > maxX)
|
|
18
|
+
maxX = b.maxX;
|
|
19
|
+
if (b.maxZ > maxZ)
|
|
20
|
+
maxZ = b.maxZ;
|
|
21
|
+
}
|
|
22
|
+
return { minX, minZ, maxX, maxZ };
|
|
23
|
+
}
|
|
24
|
+
export function buildingIndex(buildings) {
|
|
25
|
+
const bounds = unionBounds(buildings);
|
|
26
|
+
function at(point) {
|
|
27
|
+
let best;
|
|
28
|
+
let bestDistance = Infinity;
|
|
29
|
+
for (const building of buildings) {
|
|
30
|
+
if (!pointInAabb(point, building.bounds))
|
|
31
|
+
continue;
|
|
32
|
+
const distance = centerDistance(point, building.center);
|
|
33
|
+
if (distance < bestDistance) {
|
|
34
|
+
bestDistance = distance;
|
|
35
|
+
best = building;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return best;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
buildings,
|
|
42
|
+
bounds,
|
|
43
|
+
at,
|
|
44
|
+
within(area) {
|
|
45
|
+
return buildings.filter((building) => aabbOverlap(building.bounds, area));
|
|
46
|
+
},
|
|
47
|
+
nearest(point) {
|
|
48
|
+
let best;
|
|
49
|
+
for (const building of buildings) {
|
|
50
|
+
const distance = centerDistance(point, building.center);
|
|
51
|
+
if (best === undefined || distance < best.distance)
|
|
52
|
+
best = { building, distance };
|
|
53
|
+
}
|
|
54
|
+
return best;
|
|
55
|
+
},
|
|
56
|
+
isInside(point) {
|
|
57
|
+
return at(point) !== undefined;
|
|
58
|
+
},
|
|
59
|
+
blockers(margin = 0) {
|
|
60
|
+
return buildings.map((building) => margin === 0 ? building.bounds : expandAabb(building.bounds, margin));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Aabb, type Footprint, type Vec2 } from "./geometry.js";
|
|
2
|
+
export type BuildingSeed = number | string;
|
|
3
|
+
export type Vec3 = readonly [number, number, number];
|
|
4
|
+
export type BuildingFacade = "front" | "back" | "left" | "right" | "roof";
|
|
5
|
+
export type BuildingPartKind = "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner";
|
|
6
|
+
export type BuildingPartTag = "ground" | "upper" | "open" | "closed" | "decor" | "structure";
|
|
7
|
+
export interface BuildingProbabilities {
|
|
8
|
+
window: number;
|
|
9
|
+
openStore: number;
|
|
10
|
+
awning: number;
|
|
11
|
+
airConditioner: number;
|
|
12
|
+
clothesline: number;
|
|
13
|
+
storeSign: number;
|
|
14
|
+
roofProp: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BuildingVariantCounts {
|
|
17
|
+
wall: number;
|
|
18
|
+
window: number;
|
|
19
|
+
awning: number;
|
|
20
|
+
airConditioner: number;
|
|
21
|
+
clothesline: number;
|
|
22
|
+
storefront: number;
|
|
23
|
+
shutter: number;
|
|
24
|
+
storeSign: number;
|
|
25
|
+
roofProp: number;
|
|
26
|
+
}
|
|
27
|
+
export interface BuildingConfig {
|
|
28
|
+
id: string;
|
|
29
|
+
seed: BuildingSeed;
|
|
30
|
+
center: Vec2;
|
|
31
|
+
floors: number;
|
|
32
|
+
baysWide: number;
|
|
33
|
+
baysDeep: number;
|
|
34
|
+
bayWidth: number;
|
|
35
|
+
floorHeight: number;
|
|
36
|
+
facadeDepth: number;
|
|
37
|
+
roofOverhang: number;
|
|
38
|
+
probabilities: BuildingProbabilities;
|
|
39
|
+
variants: BuildingVariantCounts;
|
|
40
|
+
}
|
|
41
|
+
export type BuildingConfigInput = Partial<Omit<BuildingConfig, "probabilities" | "variants">> & {
|
|
42
|
+
probabilities?: Partial<BuildingProbabilities>;
|
|
43
|
+
variants?: Partial<BuildingVariantCounts>;
|
|
44
|
+
};
|
|
45
|
+
export interface BuildingKitSlot {
|
|
46
|
+
key: string;
|
|
47
|
+
variant: number;
|
|
48
|
+
}
|
|
49
|
+
export interface BuildingCellRef {
|
|
50
|
+
facade: BuildingFacade;
|
|
51
|
+
level: number;
|
|
52
|
+
bay: number;
|
|
53
|
+
}
|
|
54
|
+
export interface BuildingPartPlacement {
|
|
55
|
+
id: string;
|
|
56
|
+
buildingId: string;
|
|
57
|
+
kind: BuildingPartKind;
|
|
58
|
+
facade: BuildingFacade;
|
|
59
|
+
position: Vec3;
|
|
60
|
+
rotationY: number;
|
|
61
|
+
scale: Vec3;
|
|
62
|
+
kit: BuildingKitSlot;
|
|
63
|
+
cell?: BuildingCellRef;
|
|
64
|
+
tags?: readonly BuildingPartTag[];
|
|
65
|
+
}
|
|
66
|
+
export interface GeneratedBuilding {
|
|
67
|
+
id: string;
|
|
68
|
+
seed: BuildingSeed;
|
|
69
|
+
center: Vec2;
|
|
70
|
+
footprint: Footprint;
|
|
71
|
+
floors: number;
|
|
72
|
+
floorHeight: number;
|
|
73
|
+
bounds: Aabb;
|
|
74
|
+
parts: readonly BuildingPartPlacement[];
|
|
75
|
+
}
|
|
76
|
+
export interface BuildingLot {
|
|
77
|
+
id: string;
|
|
78
|
+
center: Vec2;
|
|
79
|
+
footprint: Footprint;
|
|
80
|
+
quarterTurns: number;
|
|
81
|
+
config: BuildingConfig;
|
|
82
|
+
}
|
|
83
|
+
export interface BuildingGridConfig {
|
|
84
|
+
rows: number;
|
|
85
|
+
columns: number;
|
|
86
|
+
origin?: Vec2;
|
|
87
|
+
lotSize?: Footprint;
|
|
88
|
+
streetWidth?: number;
|
|
89
|
+
seed?: BuildingSeed;
|
|
90
|
+
idPrefix?: string;
|
|
91
|
+
base?: BuildingConfigInput;
|
|
92
|
+
floorRange?: readonly [number, number];
|
|
93
|
+
}
|
|
94
|
+
export declare const DEFAULT_BUILDING_CONFIG: BuildingConfig;
|
|
95
|
+
export declare function createBuildingConfig(input?: BuildingConfigInput): BuildingConfig;
|
|
96
|
+
export declare function generateBuilding(input?: BuildingConfigInput): GeneratedBuilding;
|
|
97
|
+
export declare function createBuildingGrid(config: BuildingGridConfig): BuildingLot[];
|
|
98
|
+
export declare function generateBuildingDistrict(config: BuildingGridConfig): GeneratedBuilding[];
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { footprintAabb } from "./geometry.js";
|
|
2
|
+
const DEFAULT_PROBABILITIES = {
|
|
3
|
+
window: 0.88,
|
|
4
|
+
openStore: 0.58,
|
|
5
|
+
awning: 0.45,
|
|
6
|
+
airConditioner: 0.42,
|
|
7
|
+
clothesline: 0.28,
|
|
8
|
+
storeSign: 0.62,
|
|
9
|
+
roofProp: 0.34,
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_VARIANTS = {
|
|
12
|
+
wall: 6,
|
|
13
|
+
window: 4,
|
|
14
|
+
awning: 3,
|
|
15
|
+
airConditioner: 3,
|
|
16
|
+
clothesline: 3,
|
|
17
|
+
storefront: 4,
|
|
18
|
+
shutter: 3,
|
|
19
|
+
storeSign: 5,
|
|
20
|
+
roofProp: 5,
|
|
21
|
+
};
|
|
22
|
+
export const DEFAULT_BUILDING_CONFIG = {
|
|
23
|
+
id: "building",
|
|
24
|
+
seed: 0,
|
|
25
|
+
center: [0, 0],
|
|
26
|
+
floors: 6,
|
|
27
|
+
baysWide: 6,
|
|
28
|
+
baysDeep: 3,
|
|
29
|
+
bayWidth: 2,
|
|
30
|
+
floorHeight: 2.8,
|
|
31
|
+
facadeDepth: 0.18,
|
|
32
|
+
roofOverhang: 0.35,
|
|
33
|
+
probabilities: DEFAULT_PROBABILITIES,
|
|
34
|
+
variants: DEFAULT_VARIANTS,
|
|
35
|
+
};
|
|
36
|
+
function hashU32(input) {
|
|
37
|
+
let hash = 2166136261;
|
|
38
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
39
|
+
hash ^= input.charCodeAt(index);
|
|
40
|
+
hash = Math.imul(hash, 16777619);
|
|
41
|
+
}
|
|
42
|
+
hash ^= hash >>> 16;
|
|
43
|
+
hash = Math.imul(hash, 2246822507);
|
|
44
|
+
hash ^= hash >>> 13;
|
|
45
|
+
hash = Math.imul(hash, 3266489909);
|
|
46
|
+
hash ^= hash >>> 16;
|
|
47
|
+
return hash >>> 0;
|
|
48
|
+
}
|
|
49
|
+
function hash01(seed, key) {
|
|
50
|
+
return hashU32(`${seed}:${key}`) / 4294967296;
|
|
51
|
+
}
|
|
52
|
+
function probability(value) {
|
|
53
|
+
if (!Number.isFinite(value))
|
|
54
|
+
return 0;
|
|
55
|
+
return Math.min(Math.max(value, 0), 1);
|
|
56
|
+
}
|
|
57
|
+
function positiveInt(value, fallback) {
|
|
58
|
+
if (!Number.isFinite(value))
|
|
59
|
+
return fallback;
|
|
60
|
+
return Math.max(1, Math.floor(value));
|
|
61
|
+
}
|
|
62
|
+
function positiveNumber(value, fallback) {
|
|
63
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
64
|
+
}
|
|
65
|
+
function variantCount(value) {
|
|
66
|
+
return positiveInt(value, 1);
|
|
67
|
+
}
|
|
68
|
+
function variant(seed, key, count) {
|
|
69
|
+
return Math.floor(hash01(seed, key) * variantCount(count));
|
|
70
|
+
}
|
|
71
|
+
function chance(seed, key, p) {
|
|
72
|
+
return hash01(seed, key) < probability(p);
|
|
73
|
+
}
|
|
74
|
+
function rangeInt(seed, key, min, max) {
|
|
75
|
+
const lo = Math.min(min, max);
|
|
76
|
+
const hi = Math.max(min, max);
|
|
77
|
+
return lo + Math.floor(hash01(seed, key) * (hi - lo + 1));
|
|
78
|
+
}
|
|
79
|
+
export function createBuildingConfig(input = {}) {
|
|
80
|
+
return {
|
|
81
|
+
id: input.id ?? DEFAULT_BUILDING_CONFIG.id,
|
|
82
|
+
seed: input.seed ?? DEFAULT_BUILDING_CONFIG.seed,
|
|
83
|
+
center: input.center ?? DEFAULT_BUILDING_CONFIG.center,
|
|
84
|
+
floors: positiveInt(input.floors ?? DEFAULT_BUILDING_CONFIG.floors, DEFAULT_BUILDING_CONFIG.floors),
|
|
85
|
+
baysWide: positiveInt(input.baysWide ?? DEFAULT_BUILDING_CONFIG.baysWide, DEFAULT_BUILDING_CONFIG.baysWide),
|
|
86
|
+
baysDeep: positiveInt(input.baysDeep ?? DEFAULT_BUILDING_CONFIG.baysDeep, DEFAULT_BUILDING_CONFIG.baysDeep),
|
|
87
|
+
bayWidth: positiveNumber(input.bayWidth ?? DEFAULT_BUILDING_CONFIG.bayWidth, DEFAULT_BUILDING_CONFIG.bayWidth),
|
|
88
|
+
floorHeight: positiveNumber(input.floorHeight ?? DEFAULT_BUILDING_CONFIG.floorHeight, DEFAULT_BUILDING_CONFIG.floorHeight),
|
|
89
|
+
facadeDepth: positiveNumber(input.facadeDepth ?? DEFAULT_BUILDING_CONFIG.facadeDepth, DEFAULT_BUILDING_CONFIG.facadeDepth),
|
|
90
|
+
roofOverhang: Math.max(0, input.roofOverhang ?? DEFAULT_BUILDING_CONFIG.roofOverhang),
|
|
91
|
+
probabilities: {
|
|
92
|
+
window: probability(input.probabilities?.window ?? DEFAULT_PROBABILITIES.window),
|
|
93
|
+
openStore: probability(input.probabilities?.openStore ?? DEFAULT_PROBABILITIES.openStore),
|
|
94
|
+
awning: probability(input.probabilities?.awning ?? DEFAULT_PROBABILITIES.awning),
|
|
95
|
+
airConditioner: probability(input.probabilities?.airConditioner ?? DEFAULT_PROBABILITIES.airConditioner),
|
|
96
|
+
clothesline: probability(input.probabilities?.clothesline ?? DEFAULT_PROBABILITIES.clothesline),
|
|
97
|
+
storeSign: probability(input.probabilities?.storeSign ?? DEFAULT_PROBABILITIES.storeSign),
|
|
98
|
+
roofProp: probability(input.probabilities?.roofProp ?? DEFAULT_PROBABILITIES.roofProp),
|
|
99
|
+
},
|
|
100
|
+
variants: {
|
|
101
|
+
wall: variantCount(input.variants?.wall ?? DEFAULT_VARIANTS.wall),
|
|
102
|
+
window: variantCount(input.variants?.window ?? DEFAULT_VARIANTS.window),
|
|
103
|
+
awning: variantCount(input.variants?.awning ?? DEFAULT_VARIANTS.awning),
|
|
104
|
+
airConditioner: variantCount(input.variants?.airConditioner ?? DEFAULT_VARIANTS.airConditioner),
|
|
105
|
+
clothesline: variantCount(input.variants?.clothesline ?? DEFAULT_VARIANTS.clothesline),
|
|
106
|
+
storefront: variantCount(input.variants?.storefront ?? DEFAULT_VARIANTS.storefront),
|
|
107
|
+
shutter: variantCount(input.variants?.shutter ?? DEFAULT_VARIANTS.shutter),
|
|
108
|
+
storeSign: variantCount(input.variants?.storeSign ?? DEFAULT_VARIANTS.storeSign),
|
|
109
|
+
roofProp: variantCount(input.variants?.roofProp ?? DEFAULT_VARIANTS.roofProp),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function faceRotation(facade) {
|
|
114
|
+
if (facade === "front")
|
|
115
|
+
return Math.PI;
|
|
116
|
+
if (facade === "left")
|
|
117
|
+
return Math.PI / 2;
|
|
118
|
+
if (facade === "right")
|
|
119
|
+
return -Math.PI / 2;
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
function faceDepth(facade, width, depth, config) {
|
|
123
|
+
if (facade === "front")
|
|
124
|
+
return config.center[1] + depth / 2 + config.facadeDepth / 2;
|
|
125
|
+
if (facade === "back")
|
|
126
|
+
return config.center[1] - depth / 2 - config.facadeDepth / 2;
|
|
127
|
+
if (facade === "left")
|
|
128
|
+
return config.center[0] - width / 2 - config.facadeDepth / 2;
|
|
129
|
+
return config.center[0] + width / 2 + config.facadeDepth / 2;
|
|
130
|
+
}
|
|
131
|
+
function panelPosition(config, facade, bay, level, width, depth) {
|
|
132
|
+
const y = level * config.floorHeight + config.floorHeight / 2;
|
|
133
|
+
if (facade === "front" || facade === "back") {
|
|
134
|
+
const x = config.center[0] - width / 2 + config.bayWidth * (bay + 0.5);
|
|
135
|
+
return [x, y, faceDepth(facade, width, depth, config)];
|
|
136
|
+
}
|
|
137
|
+
const z = config.center[1] - depth / 2 + config.bayWidth * (bay + 0.5);
|
|
138
|
+
return [faceDepth(facade, width, depth, config), y, z];
|
|
139
|
+
}
|
|
140
|
+
function panelScale(config, _facade, heightRatio, widthRatio = 0.92) {
|
|
141
|
+
const along = config.bayWidth * widthRatio;
|
|
142
|
+
const high = config.floorHeight * heightRatio;
|
|
143
|
+
return [along, high, config.facadeDepth];
|
|
144
|
+
}
|
|
145
|
+
function part(config, kind, facade, position, scale, key, cell, tags) {
|
|
146
|
+
const selectedVariant = kind === "airConditioner"
|
|
147
|
+
? variant(config.seed, `${key}:variant`, config.variants.airConditioner)
|
|
148
|
+
: kind === "roofProp"
|
|
149
|
+
? variant(config.seed, `${key}:variant`, config.variants.roofProp)
|
|
150
|
+
: kind === "storefront"
|
|
151
|
+
? variant(config.seed, `${key}:variant`, config.variants.storefront)
|
|
152
|
+
: kind === "shutter"
|
|
153
|
+
? variant(config.seed, `${key}:variant`, config.variants.shutter)
|
|
154
|
+
: kind === "storeSign"
|
|
155
|
+
? variant(config.seed, `${key}:variant`, config.variants.storeSign)
|
|
156
|
+
: kind === "awning"
|
|
157
|
+
? variant(config.seed, `${key}:variant`, config.variants.awning)
|
|
158
|
+
: kind === "clothesline"
|
|
159
|
+
? variant(config.seed, `${key}:variant`, config.variants.clothesline)
|
|
160
|
+
: kind === "window"
|
|
161
|
+
? variant(config.seed, `${key}:variant`, config.variants.window)
|
|
162
|
+
: kind === "wall"
|
|
163
|
+
? variant(config.seed, `${key}:variant`, config.variants.wall)
|
|
164
|
+
: 0;
|
|
165
|
+
return {
|
|
166
|
+
id: `${config.id}:${key}:${kind}`,
|
|
167
|
+
buildingId: config.id,
|
|
168
|
+
kind,
|
|
169
|
+
facade,
|
|
170
|
+
position,
|
|
171
|
+
rotationY: faceRotation(facade),
|
|
172
|
+
scale,
|
|
173
|
+
kit: { key: `${facade}.${kind}`, variant: selectedVariant },
|
|
174
|
+
...(cell === undefined ? {} : { cell }),
|
|
175
|
+
...(tags === undefined ? {} : { tags }),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function pushUpperFloorParts(parts, config, facade, bay, level, width, depth) {
|
|
179
|
+
const cell = { facade, level, bay };
|
|
180
|
+
const key = `${facade}:${level}:${bay}`;
|
|
181
|
+
const position = panelPosition(config, facade, bay, level, width, depth);
|
|
182
|
+
parts.push(part(config, "wall", facade, position, panelScale(config, facade, 0.92), `${key}:wall`, cell, ["structure", "upper"]));
|
|
183
|
+
if (!chance(config.seed, `${key}:window`, config.probabilities.window))
|
|
184
|
+
return;
|
|
185
|
+
parts.push(part(config, "window", facade, position, panelScale(config, facade, 0.46, 0.58), `${key}:window`, cell, ["upper"]));
|
|
186
|
+
if (chance(config.seed, `${key}:awning`, config.probabilities.awning)) {
|
|
187
|
+
const awningPosition = [position[0], position[1] + config.floorHeight * 0.29, position[2]];
|
|
188
|
+
parts.push(part(config, "awning", facade, awningPosition, panelScale(config, facade, 0.1, 0.72), `${key}:awning`, cell, ["decor", "upper"]));
|
|
189
|
+
}
|
|
190
|
+
if (chance(config.seed, `${key}:ac`, config.probabilities.airConditioner)) {
|
|
191
|
+
const acPosition = [position[0], position[1] - config.floorHeight * 0.25, position[2]];
|
|
192
|
+
parts.push(part(config, "airConditioner", facade, acPosition, panelScale(config, facade, 0.16, 0.24), `${key}:ac`, cell, ["decor", "upper"]));
|
|
193
|
+
}
|
|
194
|
+
if (chance(config.seed, `${key}:clothesline`, config.probabilities.clothesline)) {
|
|
195
|
+
const linePosition = [position[0], position[1] - config.floorHeight * 0.36, position[2]];
|
|
196
|
+
parts.push(part(config, "clothesline", facade, linePosition, panelScale(config, facade, 0.04, 0.72), `${key}:line`, cell, ["decor", "upper"]));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function pushGroundFloorParts(parts, config, facade, bay, width, depth) {
|
|
200
|
+
const cell = { facade, level: 0, bay };
|
|
201
|
+
const key = `${facade}:0:${bay}`;
|
|
202
|
+
const position = panelPosition(config, facade, bay, 0, width, depth);
|
|
203
|
+
if (facade === "front" && chance(config.seed, `${key}:open`, config.probabilities.openStore)) {
|
|
204
|
+
parts.push(part(config, "storefront", facade, position, panelScale(config, facade, 0.86), `${key}:store`, cell, ["ground", "open"]));
|
|
205
|
+
if (chance(config.seed, `${key}:sign`, config.probabilities.storeSign)) {
|
|
206
|
+
const signPosition = [position[0], position[1] + config.floorHeight * 0.34, position[2]];
|
|
207
|
+
parts.push(part(config, "storeSign", facade, signPosition, panelScale(config, facade, 0.14, 0.76), `${key}:sign`, cell, ["decor", "ground"]));
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const kind = facade === "front" ? "shutter" : "wall";
|
|
212
|
+
parts.push(part(config, kind, facade, position, panelScale(config, facade, 0.86), `${key}:${kind}`, cell, ["ground", "closed"]));
|
|
213
|
+
}
|
|
214
|
+
function pushFacade(parts, config, facade, width, depth) {
|
|
215
|
+
const bays = facade === "front" || facade === "back" ? config.baysWide : config.baysDeep;
|
|
216
|
+
for (let bay = 0; bay < bays; bay += 1) {
|
|
217
|
+
pushGroundFloorParts(parts, config, facade, bay, width, depth);
|
|
218
|
+
for (let level = 1; level < config.floors; level += 1) {
|
|
219
|
+
pushUpperFloorParts(parts, config, facade, bay, level, width, depth);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function pushRoof(parts, config, width, depth) {
|
|
224
|
+
const height = config.floors * config.floorHeight;
|
|
225
|
+
parts.push(part(config, "roof", "roof", [config.center[0], height + config.facadeDepth / 2, config.center[1]], [width + config.roofOverhang * 2, config.facadeDepth, depth + config.roofOverhang * 2], "roof:slab", { facade: "roof", level: config.floors, bay: 0 }, ["structure"]));
|
|
226
|
+
const edges = ["front", "back", "left", "right"];
|
|
227
|
+
for (const facade of edges) {
|
|
228
|
+
const position = facade === "front" || facade === "back"
|
|
229
|
+
? [config.center[0], height + config.floorHeight * 0.18, faceDepth(facade, width, depth, config)]
|
|
230
|
+
: [faceDepth(facade, width, depth, config), height + config.floorHeight * 0.18, config.center[1]];
|
|
231
|
+
const scale = facade === "front" || facade === "back"
|
|
232
|
+
? [width + config.roofOverhang * 2, config.floorHeight * 0.28, config.facadeDepth]
|
|
233
|
+
: [depth + config.roofOverhang * 2, config.floorHeight * 0.28, config.facadeDepth];
|
|
234
|
+
parts.push(part(config, "guardrail", facade, position, scale, `roof:${facade}:rail`, undefined, ["structure"]));
|
|
235
|
+
}
|
|
236
|
+
const roofCells = config.baysWide * config.baysDeep;
|
|
237
|
+
for (let index = 0; index < roofCells; index += 1) {
|
|
238
|
+
if (!chance(config.seed, `roof:prop:${index}`, config.probabilities.roofProp))
|
|
239
|
+
continue;
|
|
240
|
+
const x = config.center[0] - width / 2 + config.bayWidth * ((index % config.baysWide) + 0.5);
|
|
241
|
+
const z = config.center[1] - depth / 2 + config.bayWidth * (Math.floor(index / config.baysWide) + 0.5);
|
|
242
|
+
parts.push(part(config, "roofProp", "roof", [x, height + config.facadeDepth + config.floorHeight * 0.16, z], [config.bayWidth * 0.34, config.floorHeight * 0.32, config.bayWidth * 0.34], `roof:prop:${index}`, { facade: "roof", level: config.floors, bay: index }, ["decor"]));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function pushCorners(parts, config, width, depth) {
|
|
246
|
+
const height = config.floors * config.floorHeight;
|
|
247
|
+
const positions = [
|
|
248
|
+
[config.center[0] - width / 2, height / 2, config.center[1] - depth / 2],
|
|
249
|
+
[config.center[0] + width / 2, height / 2, config.center[1] - depth / 2],
|
|
250
|
+
[config.center[0] - width / 2, height / 2, config.center[1] + depth / 2],
|
|
251
|
+
[config.center[0] + width / 2, height / 2, config.center[1] + depth / 2],
|
|
252
|
+
];
|
|
253
|
+
positions.forEach((position, index) => {
|
|
254
|
+
parts.push(part(config, "corner", index < 2 ? "back" : "front", position, [config.facadeDepth * 1.35, height, config.facadeDepth * 1.35], `corner:${index}`, undefined, ["structure"]));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
export function generateBuilding(input = {}) {
|
|
258
|
+
const config = createBuildingConfig(input);
|
|
259
|
+
const width = config.baysWide * config.bayWidth;
|
|
260
|
+
const depth = config.baysDeep * config.bayWidth;
|
|
261
|
+
const footprint = { w: width + config.roofOverhang * 2, d: depth + config.roofOverhang * 2 };
|
|
262
|
+
const parts = [];
|
|
263
|
+
pushFacade(parts, config, "front", width, depth);
|
|
264
|
+
pushFacade(parts, config, "back", width, depth);
|
|
265
|
+
pushFacade(parts, config, "left", width, depth);
|
|
266
|
+
pushFacade(parts, config, "right", width, depth);
|
|
267
|
+
pushCorners(parts, config, width, depth);
|
|
268
|
+
pushRoof(parts, config, width, depth);
|
|
269
|
+
return {
|
|
270
|
+
id: config.id,
|
|
271
|
+
seed: config.seed,
|
|
272
|
+
center: config.center,
|
|
273
|
+
footprint,
|
|
274
|
+
floors: config.floors,
|
|
275
|
+
floorHeight: config.floorHeight,
|
|
276
|
+
bounds: footprintAabb(config.center, footprint),
|
|
277
|
+
parts,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function createBuildingGrid(config) {
|
|
281
|
+
const rows = positiveInt(config.rows, 1);
|
|
282
|
+
const columns = positiveInt(config.columns, 1);
|
|
283
|
+
const lotSize = config.lotSize ?? { w: 14, d: 10 };
|
|
284
|
+
const streetWidth = Math.max(0, config.streetWidth ?? 4);
|
|
285
|
+
const origin = config.origin ?? [0, 0];
|
|
286
|
+
const seed = config.seed ?? 0;
|
|
287
|
+
const idPrefix = config.idPrefix ?? "building";
|
|
288
|
+
const lots = [];
|
|
289
|
+
for (let row = 0; row < rows; row += 1) {
|
|
290
|
+
for (let column = 0; column < columns; column += 1) {
|
|
291
|
+
const index = row * columns + column;
|
|
292
|
+
const id = `${idPrefix}-${row}-${column}`;
|
|
293
|
+
const center = [
|
|
294
|
+
origin[0] + column * (lotSize.w + streetWidth),
|
|
295
|
+
origin[1] + row * (lotSize.d + streetWidth),
|
|
296
|
+
];
|
|
297
|
+
const floorRange = config.floorRange ?? [3, 9];
|
|
298
|
+
const floors = rangeInt(seed, `${id}:floors`, floorRange[0], floorRange[1]);
|
|
299
|
+
const base = config.base ?? {};
|
|
300
|
+
const bayWidth = positiveNumber(base.bayWidth ?? DEFAULT_BUILDING_CONFIG.bayWidth, DEFAULT_BUILDING_CONFIG.bayWidth);
|
|
301
|
+
const baysWide = Math.max(1, Math.floor(lotSize.w / bayWidth));
|
|
302
|
+
const baysDeep = Math.max(1, Math.floor(lotSize.d / bayWidth));
|
|
303
|
+
const buildingConfig = createBuildingConfig({
|
|
304
|
+
...base,
|
|
305
|
+
id,
|
|
306
|
+
seed: `${seed}:${index}`,
|
|
307
|
+
center,
|
|
308
|
+
floors,
|
|
309
|
+
baysWide: base.baysWide ?? baysWide,
|
|
310
|
+
baysDeep: base.baysDeep ?? baysDeep,
|
|
311
|
+
bayWidth,
|
|
312
|
+
});
|
|
313
|
+
lots.push({ id, center, footprint: lotSize, quarterTurns: 0, config: buildingConfig });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return lots;
|
|
317
|
+
}
|
|
318
|
+
export function generateBuildingDistrict(config) {
|
|
319
|
+
return createBuildingGrid(config).map((lot) => generateBuilding(lot.config));
|
|
320
|
+
}
|