@jgengine/core 0.12.0 → 0.14.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 +73 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +3 -1
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +2 -2
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +38 -5
- package/dist/game/progression.d.ts +2 -2
- package/dist/game/progression.js +4 -2
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +45 -1
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/scene.d.ts +4 -1
- package/dist/runtime/context/scene.js +86 -1
- package/dist/runtime/gameContext.js +51 -4
- package/dist/runtime/gameContextTypes.d.ts +58 -4
- package/dist/runtime/headlessRunner.d.ts +13 -0
- package/dist/runtime/headlessRunner.js +14 -0
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/colliders.d.ts +41 -0
- package/dist/scene/colliders.js +140 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStats.js +9 -1
- package/dist/scene/sceneKinds.d.ts +19 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.js +5 -2
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityKind.d.ts +2 -2
- package/dist/world/cityKind.js +12 -6
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +35 -2
- package/dist/world/features.js +5 -2
- package/dist/world/grassKind.js +4 -1
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatterCoverage.d.ts +98 -0
- package/dist/world/scatterCoverage.js +101 -0
- package/dist/world/scatterRegion.js +6 -0
- package/dist/world/{pathNetwork.d.ts → streetGenerator.d.ts} +37 -37
- package/dist/world/{pathNetwork.js → streetGenerator.js} +9 -9
- package/dist/world/streets.d.ts +74 -1
- package/dist/world/streets.js +218 -0
- package/dist/world/terrain.d.ts +20 -1
- package/dist/world/terrain.js +41 -2
- package/dist/world.d.ts +1 -0
- package/dist/world.js +1 -0
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +5 -2
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-combat/SKILL.md +15 -1
- package/skills/jgengine-combat/api.md +39 -2
- package/skills/jgengine-combat/capabilities.md +17 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +22 -3
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +35 -5
- package/skills/jgengine-gameplay/capabilities.md +18 -0
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +9 -0
- package/skills/jgengine-world/api.md +82 -25
- package/skills/jgengine-world/capabilities.md +12 -4
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const DEFAULT_FOOTPRINT = { w: 12, d: 10 };
|
|
2
|
+
/** Cumulative arc length of a polyline. */
|
|
3
|
+
function polylineLength(path) {
|
|
4
|
+
let sum = 0;
|
|
5
|
+
for (let i = 0; i + 1 < path.length; i += 1) {
|
|
6
|
+
sum += Math.hypot(path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1]);
|
|
7
|
+
}
|
|
8
|
+
return sum;
|
|
9
|
+
}
|
|
10
|
+
/** Point + unit tangent at arc length `s` along a polyline (clamped to its ends). */
|
|
11
|
+
function stationAt(path, s) {
|
|
12
|
+
let acc = 0;
|
|
13
|
+
for (let i = 0; i + 1 < path.length; i += 1) {
|
|
14
|
+
const a = path[i];
|
|
15
|
+
const b = path[i + 1];
|
|
16
|
+
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
17
|
+
if (len < 1e-9)
|
|
18
|
+
continue;
|
|
19
|
+
if (acc + len >= s || i + 2 === path.length) {
|
|
20
|
+
const t = Math.max(0, Math.min(1, (s - acc) / len));
|
|
21
|
+
return {
|
|
22
|
+
p: [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
|
|
23
|
+
tangent: [(b[0] - a[0]) / len, (b[1] - a[1]) / len],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
acc += len;
|
|
27
|
+
}
|
|
28
|
+
const a = path[0];
|
|
29
|
+
return { p: [a[0], a[1]], tangent: [1, 0] };
|
|
30
|
+
}
|
|
31
|
+
function inArea(area, x, z) {
|
|
32
|
+
if (area === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
const [cx, cz] = area.center ?? [0, 0];
|
|
35
|
+
return Math.abs(x - cx) <= area.halfExtents[0] && Math.abs(z - cz) <= area.halfExtents[1];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Yaw that turns the building FRONT (local +z) to face the road. The lot center sits at
|
|
39
|
+
* `roadPoint + outwardNormal * d`, so the road lies along `-outwardNormal` from the center. In the
|
|
40
|
+
* engine's Y-rotation convention a local +z of `(0, 1)` maps to world `(sin y, cos y)`; setting
|
|
41
|
+
* that equal to `-outwardNormal` gives `y = atan2(-nx, -nz)`.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export function facingRotation(outwardNormal) {
|
|
45
|
+
return Math.atan2(-outwardNormal[0], -outwardNormal[1]);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derive street-aligned building lots from road frontage. Lots are stepped along each road at
|
|
49
|
+
* `footprint.w + spacing`, offset to each side by `width / 2 + setback + footprint.d / 2`, and
|
|
50
|
+
* turned to face the road. Cross-road overlaps are rejected by a bounded nearest-neighbour test, so
|
|
51
|
+
* two roads meeting at a corner don't stack buildings. Deterministic: identical inputs (including
|
|
52
|
+
* seed) always yield the identical list.
|
|
53
|
+
* @capability building-lots derive street-facing building lots from road frontage
|
|
54
|
+
*/
|
|
55
|
+
export function deriveBuildingLots(options) {
|
|
56
|
+
const footprint = options.footprint ?? DEFAULT_FOOTPRINT;
|
|
57
|
+
const spacing = Math.max(0, options.spacing ?? 2);
|
|
58
|
+
const setback = Math.max(0, options.setback ?? 3);
|
|
59
|
+
const bothSides = options.bothSides ?? true;
|
|
60
|
+
const maxLots = Math.max(0, Math.floor(options.maxLots ?? 400));
|
|
61
|
+
const step = Math.max(1, footprint.w + spacing);
|
|
62
|
+
const sides = bothSides ? [1, -1] : [1];
|
|
63
|
+
// Minimum center separation for cross-road de-dup: half the footprint diagonal, a touch under
|
|
64
|
+
// the along-road step so same-road neighbours are never rejected.
|
|
65
|
+
const minSep = Math.min(step * 0.95, Math.hypot(footprint.w, footprint.d) * 0.5);
|
|
66
|
+
const minSep2 = minSep * minSep;
|
|
67
|
+
const lots = [];
|
|
68
|
+
const accepted = [];
|
|
69
|
+
for (let r = 0; r < options.roads.length && lots.length < maxLots; r += 1) {
|
|
70
|
+
const road = options.roads[r];
|
|
71
|
+
if (road.path.length < 2)
|
|
72
|
+
continue;
|
|
73
|
+
const total = polylineLength(road.path);
|
|
74
|
+
if (total < 1e-6)
|
|
75
|
+
continue;
|
|
76
|
+
const front = road.width / 2 + setback;
|
|
77
|
+
const offset = front + footprint.d / 2;
|
|
78
|
+
// Center the run so lots sit symmetrically and endpoints keep a half-step margin.
|
|
79
|
+
const count = Math.max(1, Math.floor(total / step));
|
|
80
|
+
const startS = (total - (count - 1) * step) / 2;
|
|
81
|
+
for (let i = 0; i < count && lots.length < maxLots; i += 1) {
|
|
82
|
+
const s = startS + i * step;
|
|
83
|
+
const station = stationAt(road.path, s);
|
|
84
|
+
const [tx, tz] = station.tangent;
|
|
85
|
+
// Left normal of the tangent (CCW): (-tz, tx). Right side negates it.
|
|
86
|
+
for (const side of sides) {
|
|
87
|
+
if (lots.length >= maxLots)
|
|
88
|
+
break;
|
|
89
|
+
const nx = -tz * side;
|
|
90
|
+
const nz = tx * side;
|
|
91
|
+
const cx = station.p[0] + nx * offset;
|
|
92
|
+
const cz = station.p[1] + nz * offset;
|
|
93
|
+
if (!inArea(options.area, cx, cz))
|
|
94
|
+
continue;
|
|
95
|
+
let clash = false;
|
|
96
|
+
for (const [ax, az] of accepted) {
|
|
97
|
+
const dx = ax - cx;
|
|
98
|
+
const dz = az - cz;
|
|
99
|
+
if (dx * dx + dz * dz < minSep2) {
|
|
100
|
+
clash = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (clash)
|
|
105
|
+
continue;
|
|
106
|
+
lots.push({
|
|
107
|
+
center: [cx, cz],
|
|
108
|
+
rotationY: facingRotation([nx, nz]),
|
|
109
|
+
footprint: { w: footprint.w, d: footprint.d },
|
|
110
|
+
road: r,
|
|
111
|
+
side,
|
|
112
|
+
frontDistance: front,
|
|
113
|
+
});
|
|
114
|
+
accepted.push([cx, cz]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return lots;
|
|
119
|
+
}
|
|
@@ -72,6 +72,12 @@ export interface GeneratedBuilding {
|
|
|
72
72
|
floorHeight: number;
|
|
73
73
|
bounds: Aabb;
|
|
74
74
|
parts: readonly BuildingPartPlacement[];
|
|
75
|
+
/**
|
|
76
|
+
* Optional building yaw (radians) about `center`, applied by the renderer to the whole massing —
|
|
77
|
+
* used by street-aware placement to turn a building's front to face its road. Absent/0 keeps the
|
|
78
|
+
* axis-aligned grid orientation.
|
|
79
|
+
*/
|
|
80
|
+
rotationY?: number;
|
|
75
81
|
}
|
|
76
82
|
export interface BuildingLot {
|
|
77
83
|
id: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seed-driven procedural CITY GENERATOR — the composition seam over the two fabric engines:
|
|
3
|
+
* {@link generateStreets} grows the street network, and {@link deriveBuildingLots} lines its
|
|
4
|
+
* frontage with street-facing building lots. One call, one seed, one deterministic city: same rules
|
|
5
|
+
* ⇒ identical streets and identical lots, with both engines' bounded-work caps intact. Local
|
|
6
|
+
* (volume-centered) coords, same as the street generator; the caller translates into world space.
|
|
7
|
+
*
|
|
8
|
+
* Street dials default to a dense, gently-varied city net (high gridness/connectivity, light
|
|
9
|
+
* winding); pass any subset of {@link StreetNetworkRules} to override, and lot options pass through
|
|
10
|
+
* to the frontage engine untouched. Lanes are skipped as lot frontage by default — buildings line
|
|
11
|
+
* boulevards, avenues, and streets — so alleys stay service alleys; opt lanes in via
|
|
12
|
+
* `lots.laneFrontage`.
|
|
13
|
+
*
|
|
14
|
+
* @capability city-generator compose a deterministic city — street network plus street-facing building lots — from one seed
|
|
15
|
+
*/
|
|
16
|
+
import { type BuildingLotOptions, type PlacedBuildingLot } from "./buildingLots.js";
|
|
17
|
+
import { type StreetNetwork, type StreetNetworkContext, type StreetNetworkRules } from "./streetGenerator.js";
|
|
18
|
+
/** Options for {@link generateCity}: a seed, street-dial overrides, and lot pass-through options. */
|
|
19
|
+
export interface CityGeneratorOptions {
|
|
20
|
+
/** Drives both the street network and downstream lot/floor variation. */
|
|
21
|
+
seed: string;
|
|
22
|
+
/** Street-dial overrides; anything omitted takes the city defaults above. */
|
|
23
|
+
streets?: Partial<Omit<StreetNetworkRules, "seed">>;
|
|
24
|
+
/** Frontage-lot options, minus `roads` (the generated streets are the roads). */
|
|
25
|
+
lots?: Omit<BuildingLotOptions, "roads" | "seed"> & {
|
|
26
|
+
/** Also line `lane`-level streets (alleys) with lots. Default false. */
|
|
27
|
+
laneFrontage?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Ground sampler + bridge/tunnel toggles forwarded to the street generator. */
|
|
30
|
+
context?: StreetNetworkContext;
|
|
31
|
+
}
|
|
32
|
+
/** A generated city: the street network and the building lots lining its frontage. */
|
|
33
|
+
export interface GeneratedCity {
|
|
34
|
+
network: StreetNetwork;
|
|
35
|
+
lots: PlacedBuildingLot[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Grow a street network inside the `hx`/`hz` half-extents and line its frontage with building lots.
|
|
39
|
+
* Deterministic: identical options ⇒ identical city.
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateCity(options: CityGeneratorOptions, hx: number, hz: number): GeneratedCity;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seed-driven procedural CITY GENERATOR — the composition seam over the two fabric engines:
|
|
3
|
+
* {@link generateStreets} grows the street network, and {@link deriveBuildingLots} lines its
|
|
4
|
+
* frontage with street-facing building lots. One call, one seed, one deterministic city: same rules
|
|
5
|
+
* ⇒ identical streets and identical lots, with both engines' bounded-work caps intact. Local
|
|
6
|
+
* (volume-centered) coords, same as the street generator; the caller translates into world space.
|
|
7
|
+
*
|
|
8
|
+
* Street dials default to a dense, gently-varied city net (high gridness/connectivity, light
|
|
9
|
+
* winding); pass any subset of {@link StreetNetworkRules} to override, and lot options pass through
|
|
10
|
+
* to the frontage engine untouched. Lanes are skipped as lot frontage by default — buildings line
|
|
11
|
+
* boulevards, avenues, and streets — so alleys stay service alleys; opt lanes in via
|
|
12
|
+
* `lots.laneFrontage`.
|
|
13
|
+
*
|
|
14
|
+
* @capability city-generator compose a deterministic city — street network plus street-facing building lots — from one seed
|
|
15
|
+
*/
|
|
16
|
+
import { deriveBuildingLots } from "./buildingLots.js";
|
|
17
|
+
import { isOnRoad } from "./roads.js";
|
|
18
|
+
import { generateStreets, } from "./streetGenerator.js";
|
|
19
|
+
/** City-shaped defaults for every street dial except the seed. */
|
|
20
|
+
const CITY_STREET_DEFAULTS = {
|
|
21
|
+
gridness: 0.85,
|
|
22
|
+
loopiness: 0.35,
|
|
23
|
+
connectivity: 0.6,
|
|
24
|
+
branching: 0.25,
|
|
25
|
+
deadEnds: 0.15,
|
|
26
|
+
segmentLength: 90,
|
|
27
|
+
aspect: 1.4,
|
|
28
|
+
winding: 0.15,
|
|
29
|
+
minCurveRadius: 18,
|
|
30
|
+
minTurnAngle: 12,
|
|
31
|
+
maxTurnAngle: 110,
|
|
32
|
+
width: 9,
|
|
33
|
+
boulevards: 0.2,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Grow a street network inside the `hx`/`hz` half-extents and line its frontage with building lots.
|
|
37
|
+
* Deterministic: identical options ⇒ identical city.
|
|
38
|
+
*/
|
|
39
|
+
export function generateCity(options, hx, hz) {
|
|
40
|
+
const { seed, streets: streetOverrides, lots: lotOptions, context } = options;
|
|
41
|
+
const rules = { seed, ...CITY_STREET_DEFAULTS, ...streetOverrides };
|
|
42
|
+
const network = generateStreets(rules, hx, hz, context ?? {});
|
|
43
|
+
const { laneFrontage = false, ...lotRest } = lotOptions ?? {};
|
|
44
|
+
const frontage = network.streets
|
|
45
|
+
.filter((street) => laneFrontage || street.level !== "lane")
|
|
46
|
+
.map((street) => ({ path: street.points, width: street.width }));
|
|
47
|
+
const candidates = deriveBuildingLots({
|
|
48
|
+
...lotRest,
|
|
49
|
+
roads: frontage,
|
|
50
|
+
seed,
|
|
51
|
+
area: lotRest.area ?? { center: [0, 0], halfExtents: [hx, hz] },
|
|
52
|
+
});
|
|
53
|
+
// The frontage engine only avoids the roads it lines; streets excluded from frontage (lanes by
|
|
54
|
+
// default) are still pavement, so drop any lot whose footprint would straddle one.
|
|
55
|
+
const clearance = Math.min(lotRest.footprint?.w ?? 12, lotRest.footprint?.d ?? 10);
|
|
56
|
+
const lots = candidates.filter((lot) => network.streets.every((street) => !isOnRoad(street.points, street.width + clearance, lot.center[0], lot.center[1])));
|
|
57
|
+
return { network, lots };
|
|
58
|
+
}
|
package/dist/world/cityKind.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type ParamSchema, type SceneKindObject, type SceneKindResolveContext, t
|
|
|
2
2
|
import { type BuildingStyle } from "./buildings.js";
|
|
3
3
|
import { type CityLotClass, type CityLotPiece, type CityTreeSpecies, type CityZoneBand, type CityZoneProfile } from "./cityContent.js";
|
|
4
4
|
import type { RoadPoint } from "./roads.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type StreetFeatureKind } from "./streetGenerator.js";
|
|
6
6
|
import { type CityBlockKind } from "./cityBlocks.js";
|
|
7
7
|
/** The editor volume kind marking a box as a procedural city district. */
|
|
8
8
|
export declare const CITY_KIND = "city";
|
|
@@ -130,7 +130,7 @@ export interface CityStreet {
|
|
|
130
130
|
bulb?: RoadPoint;
|
|
131
131
|
/** Bridge/tunnel spans along this street as `[from, to]` index windows into `points`. */
|
|
132
132
|
features?: readonly {
|
|
133
|
-
kind:
|
|
133
|
+
kind: StreetFeatureKind;
|
|
134
134
|
from: number;
|
|
135
135
|
to: number;
|
|
136
136
|
bankHeight: number;
|
package/dist/world/cityKind.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the whole synthesis, from a rigid Manhattan grid (`gridness` 1, `curviness` 0) to winding
|
|
4
4
|
* hillside estates, a two-road crossroads farm town, or — at the loopiness-high / branching-and-
|
|
5
5
|
* connectivity-low corner — a closed RACE CIRCUIT. Streets come from the unified seed-driven
|
|
6
|
-
* {@link
|
|
6
|
+
* {@link generateStreets} engine (city and track are the same generator at opposite slider
|
|
7
7
|
* extremes), carry a visible hierarchy (boulevards with medians, avenues, streets, lanes —
|
|
8
8
|
* optionally gravel), and connect or deliberately dead-end into cul-de-sac bulbs; water gaps become
|
|
9
9
|
* styled bridge decks and ridges become tunnel bores, both path features on continuous streets so a
|
|
@@ -32,7 +32,8 @@ import { parseParams, registerSceneKind, } from "../scene/sceneKinds.js";
|
|
|
32
32
|
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_STYLE } from "./buildings.js";
|
|
33
33
|
import { buildLotPieces, pickClass, pickSpecies, rollClassPlacement, zoneBand, zoneMetric, } from "./cityContent.js";
|
|
34
34
|
import { furnitureSpots } from "./streets.js";
|
|
35
|
-
import {
|
|
35
|
+
import { CITY_BUILDING_BUDGET, budgetWarning, placedCoverage } from "./scatterCoverage.js";
|
|
36
|
+
import { generateStreets, streetNetworkMode, } from "./streetGenerator.js";
|
|
36
37
|
import { buildablePolygon, carveCorridors, conformPolygonToRing, cutParcel, extractBlocks, isSliverBlock, pointsInPolygon, RingWalker, } from "./cityBlocks.js";
|
|
37
38
|
import { fitRectInPolygon, rayDistanceToRing, pointInPolygon, polygonArea, polygonCentroid, polygonMeanWidth, polygonsOverlap, insetRingUniform, ringBounds, } from "./cityGeometry.js";
|
|
38
39
|
/** The editor volume kind marking a box as a procedural city district. */
|
|
@@ -567,7 +568,8 @@ export function readCityRules(meta) {
|
|
|
567
568
|
}
|
|
568
569
|
/** Bounded-work caps so a huge volume can never generate unbounded content. */
|
|
569
570
|
const MAX_STREETS = 320;
|
|
570
|
-
|
|
571
|
+
/** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
|
|
572
|
+
const MAX_LOTS = CITY_BUILDING_BUDGET;
|
|
571
573
|
const MAX_INTERSECTIONS = 600;
|
|
572
574
|
const MAX_TREES = 3200;
|
|
573
575
|
const MAX_LIGHTS = 1200;
|
|
@@ -1215,7 +1217,7 @@ export function resolveCityObject(object, context) {
|
|
|
1215
1217
|
width: rules.streetWidth,
|
|
1216
1218
|
boulevards: rules.boulevards,
|
|
1217
1219
|
};
|
|
1218
|
-
const network =
|
|
1220
|
+
const network = generateStreets(netRules, hx, hz, {
|
|
1219
1221
|
heightAt: heightAt ?? undefined,
|
|
1220
1222
|
minElevation: rules.minElevation,
|
|
1221
1223
|
bridges: rules.bridges,
|
|
@@ -1530,6 +1532,7 @@ export function registerCityKind() {
|
|
|
1530
1532
|
addCategory: "Studios",
|
|
1531
1533
|
accent: "#8fa8c9",
|
|
1532
1534
|
schema: CITY_SCHEMA,
|
|
1535
|
+
coverage: { spec: "city", densityKey: "roadsideOccupancy" },
|
|
1533
1536
|
resolve: (object, _params, context) => resolveCityObject(object, context),
|
|
1534
1537
|
note: (object) => {
|
|
1535
1538
|
const resolved = resolveCityObject(object);
|
|
@@ -1537,7 +1540,7 @@ export function registerCityKind() {
|
|
|
1537
1540
|
return "Give the volume a box footprint.";
|
|
1538
1541
|
if (resolved.streets.length === 0)
|
|
1539
1542
|
return `Footprint too small — needs at least ${Math.ceil(resolved.rules.blockSize * 1.5)} m across.`;
|
|
1540
|
-
const mode =
|
|
1543
|
+
const mode = streetNetworkMode({
|
|
1541
1544
|
seed: resolved.rules.seed,
|
|
1542
1545
|
gridness: resolved.rules.gridness,
|
|
1543
1546
|
loopiness: resolved.rules.loopiness,
|
|
@@ -1558,7 +1561,10 @@ export function registerCityKind() {
|
|
|
1558
1561
|
resolved.tunnels.length > 0 ? `${resolved.tunnels.length} tunnels` : "",
|
|
1559
1562
|
].filter((s) => s.length > 0);
|
|
1560
1563
|
const head = mode === "circuit" ? `circuit · ${resolved.streets.length} streets` : `${resolved.streets.length} streets`;
|
|
1561
|
-
|
|
1564
|
+
// Shared clamp-and-warn: when the block/parcel pipeline hits the building budget, surface it in
|
|
1565
|
+
// the same "capped at N (budget)" wording grass and scatter use.
|
|
1566
|
+
const buildingCap = resolved.rules.fabric ? budgetWarning(placedCoverage("city", 0, resolved.lots.length)) : "";
|
|
1567
|
+
const fabric = resolved.rules.fabric ? ` · ${resolved.lots.length} buildings${buildingCap} · ${resolved.parks.length} parks` : " · no fabric";
|
|
1562
1568
|
return `${head}${fabric}${feats.length > 0 ? ` · ${feats.join(" · ")}` : ""}`;
|
|
1563
1569
|
},
|
|
1564
1570
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { generateBuildingDistrict, resolveBuildingPalette, } from "./buildings.js";
|
|
1
|
+
import { DEFAULT_BUILDING_CONFIG, generateBuilding, generateBuildingDistrict, resolveBuildingPalette, } from "./buildings.js";
|
|
2
|
+
import { deriveBuildingLots } from "./buildingLots.js";
|
|
2
3
|
import { pathLength } from "./roads.js";
|
|
3
4
|
import { resolveTerrainField, resolveTerrainPalette } from "./terrain.js";
|
|
4
5
|
export function resolveStructureBuildings(descriptor) {
|
|
6
|
+
if (descriptor.along !== undefined) {
|
|
7
|
+
return resolveFrontageBuildings(descriptor, descriptor.along);
|
|
8
|
+
}
|
|
5
9
|
const columns = Math.max(1, Math.ceil(Math.sqrt(descriptor.count)));
|
|
6
10
|
const rows = Math.max(1, Math.ceil(descriptor.count / columns));
|
|
7
11
|
const spacing = descriptor.spacing;
|
|
@@ -21,6 +25,58 @@ export function resolveStructureBuildings(descriptor) {
|
|
|
21
25
|
...(descriptor.seed === undefined ? {} : { seed: descriptor.seed }),
|
|
22
26
|
}).slice(0, descriptor.count);
|
|
23
27
|
}
|
|
28
|
+
function hashU32(input) {
|
|
29
|
+
let hash = 2166136261;
|
|
30
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
31
|
+
hash ^= input.charCodeAt(i);
|
|
32
|
+
hash = Math.imul(hash, 16777619);
|
|
33
|
+
}
|
|
34
|
+
hash ^= hash >>> 16;
|
|
35
|
+
hash = Math.imul(hash, 2246822507);
|
|
36
|
+
hash ^= hash >>> 13;
|
|
37
|
+
hash = Math.imul(hash, 3266489909);
|
|
38
|
+
hash ^= hash >>> 16;
|
|
39
|
+
return hash >>> 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build one street-aware building per road-frontage lot: massed at the lot center, its `rotationY`
|
|
43
|
+
* set so the front faces the road (the renderer turns the whole building about `center`), floors
|
|
44
|
+
* rolled deterministically from the descriptor's `stories` and the lot's road/side/index.
|
|
45
|
+
*/
|
|
46
|
+
function resolveFrontageBuildings(descriptor, along) {
|
|
47
|
+
const seed = descriptor.seed ?? "buildings";
|
|
48
|
+
const lots = deriveBuildingLots({
|
|
49
|
+
roads: along.roads,
|
|
50
|
+
footprint: descriptor.footprint,
|
|
51
|
+
spacing: descriptor.spacing,
|
|
52
|
+
maxLots: descriptor.count,
|
|
53
|
+
seed: `${seed}:lots`,
|
|
54
|
+
...(along.setback === undefined ? {} : { setback: along.setback }),
|
|
55
|
+
...(along.bothSides === undefined ? {} : { bothSides: along.bothSides }),
|
|
56
|
+
...(along.area === undefined ? {} : { area: along.area }),
|
|
57
|
+
});
|
|
58
|
+
const [floorsMin, floorsMax] = descriptor.stories;
|
|
59
|
+
const bayWidth = DEFAULT_BUILDING_CONFIG.bayWidth;
|
|
60
|
+
return lots.map((lot, index) => buildLot(lot, index, seed, floorsMin, floorsMax, bayWidth, descriptor.storyHeight));
|
|
61
|
+
}
|
|
62
|
+
function buildLot(lot, index, seed, floorsMin, floorsMax, bayWidth, storyHeight) {
|
|
63
|
+
const lo = Math.min(floorsMin, floorsMax);
|
|
64
|
+
const hi = Math.max(floorsMin, floorsMax);
|
|
65
|
+
const floors = lo + Math.floor((hashU32(`${seed}:floors:${index}`) / 4294967296) * (hi - lo + 1));
|
|
66
|
+
const baysWide = Math.max(1, Math.floor(lot.footprint.w / bayWidth));
|
|
67
|
+
const baysDeep = Math.max(1, Math.floor(lot.footprint.d / bayWidth));
|
|
68
|
+
const building = generateBuilding({
|
|
69
|
+
id: `frontage-${index}`,
|
|
70
|
+
seed: `${seed}:${index}`,
|
|
71
|
+
center: lot.center,
|
|
72
|
+
floors,
|
|
73
|
+
baysWide,
|
|
74
|
+
baysDeep,
|
|
75
|
+
bayWidth,
|
|
76
|
+
floorHeight: storyHeight,
|
|
77
|
+
});
|
|
78
|
+
return { ...building, rotationY: lot.rotationY };
|
|
79
|
+
}
|
|
24
80
|
const TERRAIN_SAMPLE_RESOLUTION = 16;
|
|
25
81
|
function sampleTerrainHeights(descriptor) {
|
|
26
82
|
const field = resolveTerrainField(descriptor);
|
package/dist/world/features.d.ts
CHANGED
|
@@ -291,6 +291,34 @@ export interface SkyEnvironmentConfig {
|
|
|
291
291
|
/** Raymarched volumetric cloud layer over the sky dome. Off by default — omit for no clouds. */
|
|
292
292
|
volumetricClouds?: VolumetricCloudsConfig;
|
|
293
293
|
}
|
|
294
|
+
/** One road a `building({ along })` frontage lines with buildings: centerline + full width. */
|
|
295
|
+
export interface BuildingFrontageRoad {
|
|
296
|
+
/** Road centerline vertices in world XZ; at least two points. */
|
|
297
|
+
path: readonly EnvironmentVec2[];
|
|
298
|
+
/** Road full width in world units; lots set back from `width / 2`. */
|
|
299
|
+
width: number;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Street-aware placement for `building()`: instead of a grid scattered around `position`, lots are
|
|
303
|
+
* stepped along each road's frontage, offset by a consistent setback (a curb + sidewalk strip), and
|
|
304
|
+
* turned so every building FRONT faces its road. `footprint` (`w` = frontage width, `d` = depth) and
|
|
305
|
+
* `spacing` (along-road gap) are shared with the grid mode. Deterministic and bounded by `maxLots`.
|
|
306
|
+
*/
|
|
307
|
+
export interface BuildingFrontageConfig {
|
|
308
|
+
/** Roads whose frontage is lined with lots. */
|
|
309
|
+
roads: readonly BuildingFrontageRoad[];
|
|
310
|
+
/** Sidewalk strip between the curb (`width / 2`) and the building front, world units. Default 3. */
|
|
311
|
+
setback?: number;
|
|
312
|
+
/** Place lots on both sides of each road. Default true. */
|
|
313
|
+
bothSides?: boolean;
|
|
314
|
+
/** Clip region; lots whose center falls outside are dropped. Omit to keep every lot. */
|
|
315
|
+
area?: {
|
|
316
|
+
center?: EnvironmentVec2;
|
|
317
|
+
halfExtents: EnvironmentVec2;
|
|
318
|
+
};
|
|
319
|
+
/** Hard cap on emitted lots so a long network stays bounded. Default 400. */
|
|
320
|
+
maxLots?: number;
|
|
321
|
+
}
|
|
294
322
|
export interface BuildingEnvironmentConfig {
|
|
295
323
|
count?: number;
|
|
296
324
|
position?: EnvironmentVec2;
|
|
@@ -303,6 +331,11 @@ export interface BuildingEnvironmentConfig {
|
|
|
303
331
|
/** Explicit per-part-kind hex colors; any part left unset falls back to the resolved `style` palette. */
|
|
304
332
|
palette?: BuildingPaletteOverrides;
|
|
305
333
|
seed?: string;
|
|
334
|
+
/**
|
|
335
|
+
* Street-aware placement: derive lots along the given roads' frontage (facing each street, set
|
|
336
|
+
* back by a sidewalk strip) instead of the grid around `position`. See {@link BuildingFrontageConfig}.
|
|
337
|
+
*/
|
|
338
|
+
along?: BuildingFrontageConfig;
|
|
306
339
|
}
|
|
307
340
|
export type PadSize = readonly [number, number] | {
|
|
308
341
|
radius: number;
|
|
@@ -344,7 +377,7 @@ export type OceanEnvironmentDescriptor = {
|
|
|
344
377
|
} & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
|
|
345
378
|
export type BuildingEnvironmentDescriptor = {
|
|
346
379
|
kind: "building";
|
|
347
|
-
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette">;
|
|
380
|
+
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along">;
|
|
348
381
|
export type PadEnvironmentDescriptor = {
|
|
349
382
|
kind: "pad";
|
|
350
383
|
} & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY" | "elevation">;
|
|
@@ -509,7 +542,7 @@ export declare function ocean(config?: OceanEnvironmentConfig): OceanEnvironment
|
|
|
509
542
|
/** Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain. */
|
|
510
543
|
export declare function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor;
|
|
511
544
|
export declare function island(config: TerrainIslandConfig): TerrainIslandDescriptor;
|
|
512
|
-
/** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. */
|
|
545
|
+
/** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. Pass `along` to line road frontage instead of gridding around `position`. */
|
|
513
546
|
export declare function building(config?: BuildingEnvironmentConfig): BuildingEnvironmentDescriptor;
|
|
514
547
|
export declare function pad(config: PadEnvironmentConfig): PadEnvironmentDescriptor;
|
|
515
548
|
/** Declares a biome-painted world — the whole-world alternative to a single `environment()` terrain. */
|
package/dist/world/features.js
CHANGED
|
@@ -228,11 +228,13 @@ export function island(config) {
|
|
|
228
228
|
const { kind: _kind, ...descriptor } = terrain(terrainConfig);
|
|
229
229
|
return { ...descriptor, kind: "island", origin };
|
|
230
230
|
}
|
|
231
|
-
/** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. */
|
|
231
|
+
/** Declares a cluster of procedurally-massed buildings for `environment()` — count, footprint, stories, style. Pass `along` to line road frontage instead of gridding around `position`. */
|
|
232
232
|
export function building(config = {}) {
|
|
233
233
|
return withOptional({
|
|
234
234
|
kind: "building",
|
|
235
|
-
|
|
235
|
+
// In frontage mode the road network sizes the district; `count` becomes an upper bound, so a
|
|
236
|
+
// caller that just says `along` gets a filled street rather than a single grid building.
|
|
237
|
+
count: config.count ?? (config.along === undefined ? 1 : config.along.maxLots ?? 400),
|
|
236
238
|
footprint: config.footprint ?? { w: 8, d: 8 },
|
|
237
239
|
stories: config.stories ?? [1, 4],
|
|
238
240
|
storyHeight: config.storyHeight ?? 3,
|
|
@@ -242,6 +244,7 @@ export function building(config = {}) {
|
|
|
242
244
|
...(config.seed === undefined ? {} : { seed: config.seed }),
|
|
243
245
|
...(config.position === undefined ? {} : { position: config.position }),
|
|
244
246
|
...(config.palette === undefined ? {} : { palette: config.palette }),
|
|
247
|
+
...(config.along === undefined ? {} : { along: config.along }),
|
|
245
248
|
});
|
|
246
249
|
}
|
|
247
250
|
export function pad(config) {
|
package/dist/world/grassKind.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @capability grass-field editor-authorable GPU grass patch
|
|
9
9
|
*/
|
|
10
10
|
import { registerSceneKind } from "../scene/sceneKinds.js";
|
|
11
|
+
import { budgetWarning, densityCoverage } from "./scatterCoverage.js";
|
|
11
12
|
/** The editor volume kind marking a box as a GPU grass patch. */
|
|
12
13
|
export const GRASS_FIELD_KIND = "grass_field";
|
|
13
14
|
/** Grass defaults: a lush, lightly-swaying meadow. */
|
|
@@ -89,13 +90,15 @@ export function registerGrassKind() {
|
|
|
89
90
|
addCategory: "Studios",
|
|
90
91
|
accent: "#7fbf4d",
|
|
91
92
|
schema: GRASS_SCHEMA,
|
|
93
|
+
coverage: { spec: "grass_field", densityKey: "density" },
|
|
92
94
|
resolve: (object) => resolveGrassObject(object),
|
|
93
95
|
note: (object) => {
|
|
94
96
|
const resolved = resolveGrassObject(object);
|
|
95
97
|
if (resolved === null)
|
|
96
98
|
return "Give the volume a box footprint.";
|
|
97
99
|
const area = resolved.size[0] * resolved.size[1];
|
|
98
|
-
|
|
100
|
+
const coverage = densityCoverage("grass_field", area, resolved.rules.density);
|
|
101
|
+
return `${Math.round(resolved.size[0])}×${Math.round(resolved.size[1])} m · ≈ ${coverage.count.toLocaleString()} blades${budgetWarning(coverage)}`;
|
|
99
102
|
},
|
|
100
103
|
});
|
|
101
104
|
}
|
package/dist/world/roads.d.ts
CHANGED
|
@@ -29,12 +29,29 @@ export interface RoadRibbonOptions {
|
|
|
29
29
|
* into a mesh, and tests can assert on it directly.
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildRoadRibbon(path: readonly RoadPoint[], width: number, sampleHeight: (x: number, z: number) => number, options?: RoadRibbonOptions): RoadRibbon;
|
|
32
|
+
/** A circular exclusion zone: dashes whose midpoint falls inside are dropped (e.g. junction patches). */
|
|
33
|
+
export interface DashExclusion {
|
|
34
|
+
center: RoadPoint;
|
|
35
|
+
radius: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a flat, ground-draped disc patch centered on `center` — the welded junction surface that
|
|
39
|
+
* covers crossing road ribbons so they read as one intersection instead of stacking/z-fighting.
|
|
40
|
+
* A triangle fan of `segments` sides (default 16), every vertex draped at `sampleHeight + elevation`.
|
|
41
|
+
* Pure geometry; the shell meshes the result with the asphalt material.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildJunctionPatch(center: RoadPoint, radius: number, sampleHeight: (x: number, z: number) => number, options?: {
|
|
44
|
+
elevation?: number;
|
|
45
|
+
segments?: number;
|
|
46
|
+
}): RoadRibbon;
|
|
32
47
|
/**
|
|
33
48
|
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
34
49
|
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
35
|
-
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
50
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles
|
|
51
|
+
* (junction patches) to interrupt the center line through intersections — any dash whose midpoint
|
|
52
|
+
* lands inside an exclusion is dropped.
|
|
36
53
|
*/
|
|
37
|
-
export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number): readonly (readonly RoadPoint[])[];
|
|
54
|
+
export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number, exclude?: readonly DashExclusion[]): readonly (readonly RoadPoint[])[];
|
|
38
55
|
/** Result of {@link nearestOnPath}: closest point on the centerline plus distance and tangent. */
|
|
39
56
|
export interface RoadSample {
|
|
40
57
|
/** Closest point on the centerline. */
|
package/dist/world/roads.js
CHANGED
|
@@ -102,12 +102,48 @@ export function buildRoadRibbon(path, width, sampleHeight, options = {}) {
|
|
|
102
102
|
}
|
|
103
103
|
return { positions, indices };
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a flat, ground-draped disc patch centered on `center` — the welded junction surface that
|
|
107
|
+
* covers crossing road ribbons so they read as one intersection instead of stacking/z-fighting.
|
|
108
|
+
* A triangle fan of `segments` sides (default 16), every vertex draped at `sampleHeight + elevation`.
|
|
109
|
+
* Pure geometry; the shell meshes the result with the asphalt material.
|
|
110
|
+
*/
|
|
111
|
+
export function buildJunctionPatch(center, radius, sampleHeight, options = {}) {
|
|
112
|
+
const elevation = options.elevation ?? 0.08;
|
|
113
|
+
const segments = Math.max(3, Math.floor(options.segments ?? 16));
|
|
114
|
+
if (radius <= 0)
|
|
115
|
+
return { positions: new Float32Array(0), indices: new Uint32Array(0) };
|
|
116
|
+
const [cx, cz] = center;
|
|
117
|
+
// Center vertex + one ring vertex per segment.
|
|
118
|
+
const positions = new Float32Array((segments + 1) * 3);
|
|
119
|
+
positions[0] = cx;
|
|
120
|
+
positions[1] = sampleHeight(cx, cz) + elevation;
|
|
121
|
+
positions[2] = cz;
|
|
122
|
+
for (let i = 0; i < segments; i += 1) {
|
|
123
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
124
|
+
const x = cx + Math.cos(angle) * radius;
|
|
125
|
+
const z = cz + Math.sin(angle) * radius;
|
|
126
|
+
const base = (i + 1) * 3;
|
|
127
|
+
positions[base] = x;
|
|
128
|
+
positions[base + 1] = sampleHeight(x, z) + elevation;
|
|
129
|
+
positions[base + 2] = z;
|
|
130
|
+
}
|
|
131
|
+
const indices = new Uint32Array(segments * 3);
|
|
132
|
+
for (let i = 0; i < segments; i += 1) {
|
|
133
|
+
indices[i * 3] = 0;
|
|
134
|
+
indices[i * 3 + 1] = i + 1;
|
|
135
|
+
indices[i * 3 + 2] = ((i + 1) % segments) + 1;
|
|
136
|
+
}
|
|
137
|
+
return { positions, indices };
|
|
138
|
+
}
|
|
105
139
|
/**
|
|
106
140
|
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
107
141
|
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
108
|
-
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
142
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles
|
|
143
|
+
* (junction patches) to interrupt the center line through intersections — any dash whose midpoint
|
|
144
|
+
* lands inside an exclusion is dropped.
|
|
109
145
|
*/
|
|
110
|
-
export function dashSegments(path, dashLength = 3, gapLength = 3) {
|
|
146
|
+
export function dashSegments(path, dashLength = 3, gapLength = 3, exclude = []) {
|
|
111
147
|
if (path.length < 2 || dashLength <= 0 || gapLength < 0)
|
|
112
148
|
return [];
|
|
113
149
|
const cumulative = [0];
|
|
@@ -136,7 +172,28 @@ export function dashSegments(path, dashLength = 3, gapLength = 3) {
|
|
|
136
172
|
const end = Math.min(total, start + dashLength);
|
|
137
173
|
if (end - start < 1e-6)
|
|
138
174
|
break;
|
|
139
|
-
|
|
175
|
+
const a = pointAt(start);
|
|
176
|
+
const b = pointAt(end);
|
|
177
|
+
if (exclude.length > 0) {
|
|
178
|
+
const mx = (a[0] + b[0]) / 2;
|
|
179
|
+
const mz = (a[1] + b[1]) / 2;
|
|
180
|
+
let excluded = false;
|
|
181
|
+
for (let e = 0; e < exclude.length; e += 1) {
|
|
182
|
+
const zone = exclude[e];
|
|
183
|
+
const dx = mx - zone.center[0];
|
|
184
|
+
const dz = mz - zone.center[1];
|
|
185
|
+
if (dx * dx + dz * dz <= zone.radius * zone.radius) {
|
|
186
|
+
excluded = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (excluded) {
|
|
191
|
+
if (period <= 0)
|
|
192
|
+
break;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
dashes.push([a, b]);
|
|
140
197
|
if (period <= 0)
|
|
141
198
|
break;
|
|
142
199
|
}
|