@adatechnology/game-vegetation-three 0.2.7 → 0.3.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/dist/chunk-4RHCXAUC.js +1226 -0
- package/dist/chunk-4RHCXAUC.js.map +1 -0
- package/dist/index.d.ts +5 -237
- package/dist/index.global.js +59 -10
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +20 -1204
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +237 -0
- package/dist/testing.js +41 -0
- package/dist/testing.js.map +1 -0
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -1,1207 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
var PLANTINGS = [
|
|
22
|
-
{ context: "front_yard", areas: AREAS.front_yard, options: { n: 12, noLarge: true, maxCount: { trees: 1, hedges: 10, mid: 14, ground: 40 } }, tag: "f" },
|
|
23
|
-
{ context: "backyard", areas: AREAS.backyard, options: { n: 14, maxCount: { trees: 3, hedges: 4, mid: 12, ground: 50 } }, tag: "b" },
|
|
24
|
-
{ context: "front_yard", areas: AREAS.sides, options: { n: 8, noLarge: true, maxCount: { trees: 0, hedges: 4, mid: 6, ground: 14 } }, tag: "s" },
|
|
25
|
-
{ context: "sidewalk_tree", areas: AREAS.sidewalk_tree, options: { n: 4, treeDensity: 0.3, maxCount: { trees: 2, hedges: 0, mid: 0, ground: 0 } }, tag: "c" }
|
|
26
|
-
];
|
|
27
|
-
function scatterLot(sdk, species, seedNumber, plantings = PLANTINGS) {
|
|
28
|
-
const seed = `quintal-${seedNumber}`;
|
|
29
|
-
const placements = [];
|
|
30
|
-
const used = /* @__PURE__ */ new Set();
|
|
31
|
-
for (const planting of plantings) {
|
|
32
|
-
const { rules, picks } = species.scatterRulesFor(species.regions.br, planting.context, seed, planting.options);
|
|
33
|
-
for (const p of picks) used.add(p.id);
|
|
34
|
-
planting.areas.forEach((area, i) => {
|
|
35
|
-
const exclude = [EX_HOUSE, EX_PATH];
|
|
36
|
-
const perArea = rules.map((rule) => ({ ...rule, maxCount: Math.max(1, Math.ceil(rule.maxCount / planting.areas.length)) }));
|
|
37
|
-
placements.push(...sdk.scatter(area, perArea, { seed: `${seed}:${planting.tag}:${i}`, exclude }));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return { placements, used };
|
|
41
|
-
}
|
|
42
|
-
function buildScenery(THREE) {
|
|
43
|
-
const g = new THREE.Group();
|
|
44
|
-
g.name = "nursery-scenery";
|
|
45
|
-
const material = (color, roughness = 0.9) => new THREE.MeshStandardMaterial({ color, roughness, metalness: 0 });
|
|
46
|
-
const mats = { ground: material(6257989), asphalt: material(3816770), concrete: material(12039080), house: material(14931645), roof: material(10112312), door: material(7031343), glass: material(10471389, 0.2), wall: material(13222578) };
|
|
47
|
-
const box = (w, h, d, m, x, y, z) => {
|
|
48
|
-
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
|
|
49
|
-
mesh.position.set(x, y, z);
|
|
50
|
-
mesh.castShadow = mesh.receiveShadow = true;
|
|
51
|
-
g.add(mesh);
|
|
52
|
-
return mesh;
|
|
53
|
-
};
|
|
54
|
-
const yard = new THREE.Mesh(new THREE.PlaneGeometry(LOT.W, LOT.D), mats.ground);
|
|
55
|
-
yard.rotation.x = -Math.PI / 2;
|
|
56
|
-
yard.position.set(6, 0.01, 10);
|
|
57
|
-
yard.receiveShadow = true;
|
|
58
|
-
g.add(yard);
|
|
59
|
-
box(30, 0.04, 4.6, mats.asphalt, 6, 0, -4.6);
|
|
60
|
-
box(30, 0.1, 2.2, mats.concrete, 6, 0.04, -1.1);
|
|
61
|
-
box(1.6, 0.03, 6.6, mats.concrete, 6, 0.02, 3.2);
|
|
62
|
-
const hw = HOUSE.x1 - HOUSE.x0;
|
|
63
|
-
const hd = HOUSE.z1 - HOUSE.z0;
|
|
64
|
-
const hcx = (HOUSE.x0 + HOUSE.x1) / 2;
|
|
65
|
-
const hcz = (HOUSE.z0 + HOUSE.z1) / 2;
|
|
66
|
-
box(hw, 2.8, hd, mats.house, hcx, 1.4, hcz);
|
|
67
|
-
const roof = new THREE.Mesh(new THREE.ConeGeometry(1, 1.5, 4), mats.roof);
|
|
68
|
-
roof.rotation.y = Math.PI / 4;
|
|
69
|
-
roof.scale.set((hw + 1.2) / Math.SQRT2, 1, (hd + 1.2) / Math.SQRT2);
|
|
70
|
-
roof.position.set(hcx, 2.8 + 0.75, hcz);
|
|
71
|
-
roof.castShadow = roof.receiveShadow = true;
|
|
72
|
-
g.add(roof);
|
|
73
|
-
box(1.1, 2.1, 0.08, mats.door, 6, 1.05, HOUSE.z0 - 0.04);
|
|
74
|
-
box(1.6, 1.1, 0.08, mats.glass, 3.8, 1.5, HOUSE.z0 - 0.04);
|
|
75
|
-
box(1.6, 1.1, 0.08, mats.glass, 8.2, 1.5, HOUSE.z0 - 0.04);
|
|
76
|
-
box(5, 1.4, 0.2, mats.wall, 2.5, 0.7, 0);
|
|
77
|
-
box(5, 1.4, 0.2, mats.wall, 9.5, 0.7, 0);
|
|
78
|
-
box(0.2, 1.8, LOT.D, mats.wall, 0, 0.9, 10);
|
|
79
|
-
box(0.2, 1.8, LOT.D, mats.wall, LOT.W, 0.9, 10);
|
|
80
|
-
box(LOT.W, 1.8, 0.2, mats.wall, 6, 0.9, LOT.D);
|
|
81
|
-
return g;
|
|
82
|
-
}
|
|
83
|
-
function createNursery(sdk, species, THREE, materials, parent, origin) {
|
|
84
|
-
const T = shapeGeometries(THREE);
|
|
85
|
-
const group = new THREE.Group();
|
|
86
|
-
group.name = "nursery";
|
|
87
|
-
group.position.set(origin[0], 0, origin[1]);
|
|
88
|
-
const dynamic = new THREE.Group();
|
|
89
|
-
const colliders = new THREE.Group();
|
|
90
|
-
colliders.visible = false;
|
|
91
|
-
let scenery = null;
|
|
92
|
-
let lotObject = null;
|
|
93
|
-
let data = null;
|
|
94
|
-
let stats = null;
|
|
95
|
-
let actor = null;
|
|
96
|
-
const walker = { x: WAY[0][0], z: WAY[0][1], waypoint: 0, on: false };
|
|
97
|
-
const actors = [{ x: 0, y: 0.8, z: 0, radius: 1.5 }];
|
|
98
|
-
let attached = false;
|
|
99
|
-
const disposeObject = (object) => {
|
|
100
|
-
object.traverse((child) => {
|
|
101
|
-
child.geometry?.dispose?.();
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
const clearGroup = (g) => {
|
|
105
|
-
for (const child of [...g.children]) {
|
|
106
|
-
g.remove(child);
|
|
107
|
-
disposeObject(child);
|
|
108
|
-
child.material?.dispose?.();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const ensure = () => {
|
|
112
|
-
if (attached) return;
|
|
113
|
-
attached = true;
|
|
114
|
-
scenery = buildScenery(T);
|
|
115
|
-
const body = T.CapsuleGeometry ? new T.CapsuleGeometry(0.28, 1, 4, 10) : new T.CylinderGeometry(0.28, 0.28, 1.56, 10);
|
|
116
|
-
actor = new THREE.Mesh(body, new THREE.MeshStandardMaterial({ color: 14254635, roughness: 0.7 }));
|
|
117
|
-
actor.castShadow = true;
|
|
118
|
-
actor.position.set(walker.x, 0.78, walker.z);
|
|
119
|
-
actor.visible = false;
|
|
120
|
-
group.add(scenery, dynamic, colliders, actor);
|
|
121
|
-
parent.add(group);
|
|
122
|
-
};
|
|
123
|
-
const blocked = (x, z) => {
|
|
124
|
-
if (x < 0.55 || x > LOT.W - 0.55 || z < 0.55 || z > LOT.D - 0.55) return true;
|
|
125
|
-
if (x > HOUSE.x0 - 0.4 && x < HOUSE.x1 + 0.4 && z > HOUSE.z0 - 0.4 && z < HOUSE.z1 + 0.4) return true;
|
|
126
|
-
if (data) {
|
|
127
|
-
for (const c of data.colliders) {
|
|
128
|
-
const dx = x - c.x;
|
|
129
|
-
const dz = z - c.z;
|
|
130
|
-
const r = c.r + 0.3;
|
|
131
|
-
if (dx * dx + dz * dz < r * r) return true;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return false;
|
|
135
|
-
};
|
|
136
|
-
return {
|
|
137
|
-
group,
|
|
138
|
-
get stats() {
|
|
139
|
-
return stats;
|
|
140
|
-
},
|
|
141
|
-
get planted() {
|
|
142
|
-
return lotObject !== null;
|
|
143
|
-
},
|
|
144
|
-
get walking() {
|
|
145
|
-
return walker.on;
|
|
146
|
-
},
|
|
147
|
-
get position() {
|
|
148
|
-
return { x: walker.x, z: walker.z };
|
|
149
|
-
},
|
|
150
|
-
/** Plants the lot with the settings (replaces the one standing). */
|
|
151
|
-
plant(settings) {
|
|
152
|
-
ensure();
|
|
153
|
-
if (lotObject) {
|
|
154
|
-
dynamic.remove(lotObject);
|
|
155
|
-
disposeObject(lotObject);
|
|
156
|
-
lotObject = null;
|
|
157
|
-
}
|
|
158
|
-
clearGroup(colliders);
|
|
159
|
-
const { placements, used } = scatterLot(sdk, species, settings.seed);
|
|
160
|
-
const t0 = Date.now();
|
|
161
|
-
data = sdk.buildLot(placements, (id) => species.toSpec(id), { seed: `quintal-${settings.seed}`, quality: settings.quality, lod: settings.lod, condition: settings.condition, season: settings.season });
|
|
162
|
-
const buildMs = Date.now() - t0;
|
|
163
|
-
lotObject = sdk.createLotObject(THREE, data, materials);
|
|
164
|
-
lotObject.traverse((c) => {
|
|
165
|
-
if (c.isMesh) {
|
|
166
|
-
c.castShadow = true;
|
|
167
|
-
c.receiveShadow = true;
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
dynamic.add(lotObject);
|
|
171
|
-
for (const c of data.colliders) {
|
|
172
|
-
const m = new THREE.Mesh(new T.CylinderGeometry(c.r, c.r, c.h, 14, 1, true), new THREE.MeshBasicMaterial({ color: c.damage ? 16742938 : 15017245, wireframe: true }));
|
|
173
|
-
m.position.set(c.x, c.h / 2, c.z);
|
|
174
|
-
colliders.add(m);
|
|
175
|
-
}
|
|
176
|
-
stats = { triangles: data.triangleCount, plants: data.plantCount, colliders: data.colliders.length, meshes: Object.keys(data.meshes).length, lod: data.lodCounts, buildMs, species: used.size };
|
|
177
|
-
return stats;
|
|
178
|
-
},
|
|
179
|
-
/** The colliders of the lot as wire cylinders. */
|
|
180
|
-
showColliders(on) {
|
|
181
|
-
colliders.visible = on;
|
|
182
|
-
},
|
|
183
|
-
/** Starts or stops the walker that crosses the lot along `WAY`, bending the plants. */
|
|
184
|
-
setWalker(on) {
|
|
185
|
-
ensure();
|
|
186
|
-
walker.on = on;
|
|
187
|
-
actor.visible = on;
|
|
188
|
-
if (!on) {
|
|
189
|
-
actors[0].radius = 0;
|
|
190
|
-
materials.setActors(actors);
|
|
191
|
-
} else actors[0].radius = 1.5;
|
|
192
|
-
},
|
|
193
|
-
step(dt) {
|
|
194
|
-
if (!attached || !walker.on) return;
|
|
195
|
-
const target = WAY[walker.waypoint];
|
|
196
|
-
const ddx = target[0] - walker.x;
|
|
197
|
-
const ddz = target[1] - walker.z;
|
|
198
|
-
const d = Math.hypot(ddx, ddz);
|
|
199
|
-
let dx = 0;
|
|
200
|
-
let dz = 0;
|
|
201
|
-
if (d < 0.25) walker.waypoint = (walker.waypoint + 1) % WAY.length;
|
|
202
|
-
else {
|
|
203
|
-
dx = ddx / d;
|
|
204
|
-
dz = ddz / d;
|
|
205
|
-
}
|
|
206
|
-
if (dx || dz) {
|
|
207
|
-
const sp = 2 * dt;
|
|
208
|
-
const nx = walker.x + dx * sp;
|
|
209
|
-
const nz = walker.z + dz * sp;
|
|
210
|
-
if (!blocked(nx, walker.z)) walker.x = nx;
|
|
211
|
-
if (!blocked(walker.x, nz)) walker.z = nz;
|
|
212
|
-
if (blocked(nx, nz)) walker.waypoint = (walker.waypoint + 1) % WAY.length;
|
|
213
|
-
actor.rotation.y = Math.atan2(dx, dz);
|
|
214
|
-
}
|
|
215
|
-
actor.position.set(walker.x, 0.78, walker.z);
|
|
216
|
-
const a = actors[0];
|
|
217
|
-
a.x = origin[0] + walker.x;
|
|
218
|
-
a.z = origin[1] + walker.z;
|
|
219
|
-
a.y = 0.8;
|
|
220
|
-
materials.setActors(actors);
|
|
221
|
-
},
|
|
222
|
-
/** Takes the lot, the walker and the scenery away. */
|
|
223
|
-
clear() {
|
|
224
|
-
if (walker.on) this.setWalker(false);
|
|
225
|
-
if (lotObject) {
|
|
226
|
-
dynamic.remove(lotObject);
|
|
227
|
-
disposeObject(lotObject);
|
|
228
|
-
lotObject = null;
|
|
229
|
-
}
|
|
230
|
-
clearGroup(colliders);
|
|
231
|
-
if (attached) {
|
|
232
|
-
parent.remove(group);
|
|
233
|
-
clearGroup(group);
|
|
234
|
-
attached = false;
|
|
235
|
-
scenery = null;
|
|
236
|
-
actor = null;
|
|
237
|
-
}
|
|
238
|
-
data = null;
|
|
239
|
-
stats = null;
|
|
240
|
-
walker.x = WAY[0][0];
|
|
241
|
-
walker.z = WAY[0][1];
|
|
242
|
-
walker.waypoint = 0;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// src/test-area/plants.ts
|
|
248
|
-
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
249
|
-
var PLANT_SEED = 3;
|
|
250
|
-
var PLANT_DEFS = [
|
|
251
|
-
{ id: "mango_tree", kind: "tree", sway: true },
|
|
252
|
-
// young mango trees (same species, thin trunk): the 15 and 30 cm trunks on which the SDK documents the number of axe swings
|
|
253
|
-
{ id: "mango_young_15", base: "mango_tree", kind: "tree", sway: true, over: { height: 1.7, trunkRadius: 0.1, crownRadius: 1 }, name: ["Mangueira jovem (tronco de 15 cm)", "Young mango tree (15 cm trunk)"] },
|
|
254
|
-
{ id: "mango_young_30", base: "mango_tree", kind: "tree", sway: true, over: { height: 1.7, trunkRadius: 0.17, crownRadius: 1.2 }, name: ["Mangueira jovem (tronco de 30 cm)", "Young mango tree (30 cm trunk)"] },
|
|
255
|
-
{ id: "yellow_ipe", kind: "tree", sway: true },
|
|
256
|
-
{ id: "coconut_palm", kind: "tree", sway: true },
|
|
257
|
-
{ id: "banana_with_bunch", kind: "tree", sway: true },
|
|
258
|
-
{ id: "araucaria", kind: "tree", sway: true },
|
|
259
|
-
{ id: "dead_tree", kind: "tree", sway: true, condition: "dead" },
|
|
260
|
-
{ id: "hibiscus_shrub", kind: "shrub", sway: true },
|
|
261
|
-
{ id: "boxwood_hedge", kind: "hedge", sway: true, segments: 4 },
|
|
262
|
-
{ id: "rose_bush_red", kind: "shrub", sway: true },
|
|
263
|
-
{ id: "sunflower", kind: "flower", sway: true },
|
|
264
|
-
{ id: "mandacaru", kind: "cactus", sway: false },
|
|
265
|
-
{ id: "prickly_pear", kind: "cactus", sway: false },
|
|
266
|
-
{ id: "crown_cactus", kind: "cactus", sway: false },
|
|
267
|
-
{ id: "potted_fern", kind: "pot", sway: true, movable: true },
|
|
268
|
-
{ id: "potted_cactus", kind: "pot", sway: false, movable: true },
|
|
269
|
-
{ id: "lawn_tuft", kind: "tuft", sway: true }
|
|
270
|
-
];
|
|
271
|
-
var sanitize = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
272
|
-
var FILL = { green_wood: 0.5, wood_soft: 0.5, foliage: 0.035, flowers: 0.03, flesh_fruit: 0.8, ceramic: 0.2, soil: 0.7, cactus: 0.5 };
|
|
273
|
-
var SWAY_AMPLITUDE = { trunk: 4e-3, branch: 0.012, crown: 0.03, fruit: 0.04, stem: 0.025, arm: 0, pad: 0, pot: 0, soil: 0 };
|
|
274
|
-
function createPlants(sdk, species, materials, seed = PLANT_SEED) {
|
|
275
|
-
const defs = new Map(PLANT_DEFS.map((def) => [def.id, def]));
|
|
276
|
-
const specFor = (def) => {
|
|
277
|
-
const spec = species.toSpec(def.base ?? def.id);
|
|
278
|
-
return def.over ? { ...spec, ...def.over } : spec;
|
|
279
|
-
};
|
|
280
|
-
const cache = /* @__PURE__ */ new Map();
|
|
281
|
-
const plantFor = (def, segment) => {
|
|
282
|
-
const key = `${def.id}:${segment}`;
|
|
283
|
-
let plant = cache.get(key);
|
|
284
|
-
if (!plant) {
|
|
285
|
-
plant = sdk.generate(specFor(def), { seed: seed + segment * 7, ...def.condition ? { condition: def.condition } : {} });
|
|
286
|
-
cache.set(key, plant);
|
|
287
|
-
}
|
|
288
|
-
return plant;
|
|
289
|
-
};
|
|
290
|
-
function buildObject(THREE, def) {
|
|
291
|
-
const root = markKitOrigin(new THREE.Group(), { owner: "game-vegetation-three", kind: "plant", id: def.base ?? def.id });
|
|
292
|
-
root.name = def.id;
|
|
293
|
-
const segments = def.segments ?? 1;
|
|
294
|
-
let spacing = 0;
|
|
295
|
-
for (let s = 0; s < segments; s++) {
|
|
296
|
-
const plant = plantFor(def, s);
|
|
297
|
-
const object = sdk.createPlantObject(THREE, plant, materials(THREE), { separable: true });
|
|
298
|
-
const prefix = segments > 1 ? `s${s}_` : "";
|
|
299
|
-
const children = object.children.slice();
|
|
300
|
-
if (s === 0 && segments > 1) {
|
|
301
|
-
const g0 = children[0].geometry;
|
|
302
|
-
g0.computeBoundingBox();
|
|
303
|
-
spacing = (g0.boundingBox.max.x - g0.boundingBox.min.x) * 0.9;
|
|
304
|
-
}
|
|
305
|
-
for (const mesh of children) {
|
|
306
|
-
const piece = mesh.userData.destructionPart;
|
|
307
|
-
const bits = String(piece.id).split(":");
|
|
308
|
-
const id = prefix + sanitize(piece.id);
|
|
309
|
-
mesh.name = id;
|
|
310
|
-
const dx = (s - (segments - 1) / 2) * spacing;
|
|
311
|
-
const meta = {
|
|
312
|
-
id,
|
|
313
|
-
piece: piece.id,
|
|
314
|
-
kind: piece.kind,
|
|
315
|
-
sub: bits[1] ?? null,
|
|
316
|
-
base: prefix + sanitize(bits[0]),
|
|
317
|
-
parent: piece.parent ? prefix + sanitize(piece.parent) : null,
|
|
318
|
-
seg: s,
|
|
319
|
-
materialKey: piece.materialKey,
|
|
320
|
-
radius: piece.radius,
|
|
321
|
-
length: piece.length,
|
|
322
|
-
axis: piece.axis,
|
|
323
|
-
dx
|
|
324
|
-
};
|
|
325
|
-
mesh.userData.veg = meta;
|
|
326
|
-
mesh.position.x += dx;
|
|
327
|
-
mesh.castShadow = true;
|
|
328
|
-
root.add(mesh);
|
|
329
|
-
}
|
|
330
|
-
if (s === 0) {
|
|
331
|
-
root.userData.collider = plant.collider || null;
|
|
332
|
-
root.userData.thorny = !!plant.thorny;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
root.userData.speciesId = def.id;
|
|
336
|
-
root.userData.separable = true;
|
|
337
|
-
root.userData.segments = segments;
|
|
338
|
-
return root;
|
|
339
|
-
}
|
|
340
|
-
function buildSpec(root, def) {
|
|
341
|
-
const meshes = root.children.filter((c) => c.isMesh && c.userData.veg);
|
|
342
|
-
const byId = {};
|
|
343
|
-
for (const m of meshes) byId[m.userData.veg.id] = m;
|
|
344
|
-
const parts = [];
|
|
345
|
-
for (const m of meshes) {
|
|
346
|
-
const v = m.userData.veg;
|
|
347
|
-
const g = m.geometry;
|
|
348
|
-
if (!g.boundingBox) g.computeBoundingBox();
|
|
349
|
-
const b = g.boundingBox;
|
|
350
|
-
const mn = [b.min.x + m.position.x, b.min.y + m.position.y, b.min.z + m.position.z];
|
|
351
|
-
const mx = [b.max.x + m.position.x, b.max.y + m.position.y, b.max.z + m.position.z];
|
|
352
|
-
const size = [Math.max(mx[0] - mn[0], 0.02), Math.max(mx[1] - mn[1], 0.02), Math.max(mx[2] - mn[2], 0.02)];
|
|
353
|
-
const c = [(mn[0] + mx[0]) / 2, (mn[1] + mx[1]) / 2, (mn[2] + mx[2]) / 2];
|
|
354
|
-
const material = m.userData.destructionMaterial;
|
|
355
|
-
const round = v.kind === "trunk" || v.kind === "pot";
|
|
356
|
-
let shape = round ? { kind: "cylinder", radius: Math.max(size[0], size[2]) / 2, height: size[1] } : { kind: "box", size };
|
|
357
|
-
let rotation = null;
|
|
358
|
-
if (!v.sub && (v.radius ?? 0) > 0 && (v.length ?? 0) > 0 && (v.kind === "trunk" || v.kind === "branch" || v.kind === "arm" || v.kind === "stem")) {
|
|
359
|
-
const rr = Math.max(v.radius, 0.01);
|
|
360
|
-
const ll = Math.max(v.length, 0.02);
|
|
361
|
-
shape = v.kind === "trunk" ? { kind: "cylinder", radius: rr, height: ll } : { kind: "box", size: [rr * 2, ll, rr * 2] };
|
|
362
|
-
if (v.axis) {
|
|
363
|
-
const ax = v.axis;
|
|
364
|
-
const tilt = Math.hypot(ax[0], ax[2]);
|
|
365
|
-
if (tilt > 0.02 && ax[1] > -0.99) rotation = [Math.atan2(ax[2], ax[1]), 0, -Math.asin(Math.max(-1, Math.min(1, ax[0])))];
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const tags = [];
|
|
369
|
-
const p = { id: v.id, material, shape, position: c, tags };
|
|
370
|
-
if (rotation) p.rotation = rotation;
|
|
371
|
-
p.fill = v.sub === "spines" ? 0.02 : v.sub === "flowers" ? 0.01 : v.kind === "trunk" ? 0.8 : v.kind === "branch" ? 0.1 : v.kind === "arm" ? 0.12 : v.kind === "pad" ? 0.3 : FILL[material] ?? 0.5;
|
|
372
|
-
if (v.kind === "fruit" || v.sub === "wood") p.mass = 15;
|
|
373
|
-
if (v.kind === "trunk") {
|
|
374
|
-
p.grain = "y";
|
|
375
|
-
tags.push("trunk");
|
|
376
|
-
}
|
|
377
|
-
if (v.kind === "branch") tags.push("branch");
|
|
378
|
-
if (material === "foliage" || v.sub === "foliage") tags.push("leaf");
|
|
379
|
-
if (v.sub === "flowers") tags.push("flower");
|
|
380
|
-
if (v.sub === "spines") tags.push("thorns");
|
|
381
|
-
if (v.kind === "fruit") tags.push("fruit");
|
|
382
|
-
if (v.kind === "arm") tags.push("arm");
|
|
383
|
-
if (v.kind === "pad") tags.push("pad");
|
|
384
|
-
if (v.kind === "pot") {
|
|
385
|
-
p.mass = 2.4;
|
|
386
|
-
p.hp = 25;
|
|
387
|
-
tags.push("pot");
|
|
388
|
-
}
|
|
389
|
-
if (v.kind === "soil") {
|
|
390
|
-
p.mass = 3.2;
|
|
391
|
-
tags.push("soil");
|
|
392
|
-
}
|
|
393
|
-
if (def.kind === "pot" && v.kind === "stem" && !v.sub) p.mass = 0.4;
|
|
394
|
-
if (def.kind === "pot" && v.sub) p.mass = 0.05;
|
|
395
|
-
const isRoot = !v.parent && !v.sub && (v.kind === "trunk" || v.kind === "pad" || v.kind === "stem" && def.kind !== "pot");
|
|
396
|
-
if (isRoot && !def.movable) p.anchored = true;
|
|
397
|
-
parts.push(p);
|
|
398
|
-
}
|
|
399
|
-
const joints = [];
|
|
400
|
-
const seen = /* @__PURE__ */ new Set();
|
|
401
|
-
const add = (a, b, kind, extra) => {
|
|
402
|
-
if (!a || !b || a === b || !byId[a] || !byId[b]) return;
|
|
403
|
-
const id = `${a}~${b}`;
|
|
404
|
-
if (seen.has(id)) return;
|
|
405
|
-
seen.add(id);
|
|
406
|
-
joints.push({ id, a, b, kind, ...extra });
|
|
407
|
-
};
|
|
408
|
-
const firstOf = (seg, kind) => {
|
|
409
|
-
for (const m of meshes) {
|
|
410
|
-
const v = m.userData.veg;
|
|
411
|
-
if (v.seg === seg && v.kind === kind && !v.sub) return v.id;
|
|
412
|
-
}
|
|
413
|
-
return null;
|
|
414
|
-
};
|
|
415
|
-
for (const m of meshes) {
|
|
416
|
-
const v = m.userData.veg;
|
|
417
|
-
if (v.sub) {
|
|
418
|
-
add(v.base, v.id, "tie");
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (v.kind === "soil") {
|
|
422
|
-
add(v.parent, v.id, "resting");
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
if (v.parent) {
|
|
426
|
-
add(v.parent, v.id, "stem", v.kind === "arm" ? { strength: 60 } : void 0);
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
if (v.kind === "fruit") {
|
|
430
|
-
add(firstOf(v.seg, "crown") || firstOf(v.seg, "pad") || firstOf(v.seg, "stem") || firstOf(v.seg, "trunk"), v.id, "stem");
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (def.kind === "pot" && v.kind === "stem") add(firstOf(v.seg, "soil"), v.id, "tie");
|
|
434
|
-
}
|
|
435
|
-
const segments = def.segments ?? 1;
|
|
436
|
-
if (segments > 1) for (let s = 0; s + 1 < segments; s++) add(`s${s}_stem`, `s${s + 1}_stem`, "stem");
|
|
437
|
-
return { name: def.id, parts, joints, tags: ["plant"] };
|
|
438
|
-
}
|
|
439
|
-
function swayClip(def) {
|
|
440
|
-
const plant = plantFor(def, 0);
|
|
441
|
-
const segments = def.segments ?? 1;
|
|
442
|
-
const keys = [];
|
|
443
|
-
const N = 8;
|
|
444
|
-
const D = 4;
|
|
445
|
-
for (let k = 0; k <= N; k++) {
|
|
446
|
-
const ph = k / N * Math.PI * 2;
|
|
447
|
-
const parts = {};
|
|
448
|
-
for (let s = 0; s < segments; s++) {
|
|
449
|
-
const prefix = segments > 1 ? `s${s}_` : "";
|
|
450
|
-
for (const pc of plant.pieces) {
|
|
451
|
-
const a = SWAY_AMPLITUDE[pc.kind] || 0;
|
|
452
|
-
if (!a) continue;
|
|
453
|
-
if (pc.kind === "stem" && /(^|:)wood$/.test(pc.id)) continue;
|
|
454
|
-
if (def.kind === "pot" && pc.kind === "stem" && pc.id.indexOf(":") < 0 && def.id === "potted_cactus") continue;
|
|
455
|
-
const f = pc.id.indexOf(":flowers") > 0 ? 1.6 : 1;
|
|
456
|
-
parts[prefix + sanitize(pc.id)] = { rotation: [Math.sin(ph + s) * a * f * 0.6, 0, Math.sin(ph * 1 + 0.7 + s) * a * f] };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
keys.push({ t: k / N * D, parts: k === 0 || k === N ? {} : parts });
|
|
460
|
-
}
|
|
461
|
-
return { id: "sway", duration: D, keys, events: [{ t: 0.2, type: "sound", data: { key: "leaves_rustle" } }] };
|
|
462
|
-
}
|
|
463
|
-
function items(locale, defsToUse = PLANT_DEFS) {
|
|
464
|
-
const loc = String(locale || "pt-BR").toLowerCase().indexOf("en") === 0 ? "en" : "pt-BR";
|
|
465
|
-
return defsToUse.map((def) => {
|
|
466
|
-
const item = {
|
|
467
|
-
id: def.id,
|
|
468
|
-
name: def.name ? def.name[loc === "en" ? 1 : 0] : species.name(def.base ?? def.id, loc),
|
|
469
|
-
build: (THREE) => buildObject(THREE, def),
|
|
470
|
-
spec: (root) => {
|
|
471
|
-
const spec = buildSpec(root, def);
|
|
472
|
-
root.userData.vegSpec = spec;
|
|
473
|
-
return spec;
|
|
474
|
-
},
|
|
475
|
-
movable: !!def.movable
|
|
476
|
-
};
|
|
477
|
-
if (def.sway) item.animations = [swayClip(def)];
|
|
478
|
-
return item;
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
return { defs, specFor, plantFor, buildObject, buildSpec, swayClip, items };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// src/test-area/scenarios.ts
|
|
485
|
-
function createScenarios(locale) {
|
|
486
|
-
const en = String(locale || "pt-BR").toLowerCase().indexOf("en") === 0;
|
|
487
|
-
const L = (pt, e) => en ? e : pt;
|
|
488
|
-
const yn = (b) => b ? L("sim", "yes") : L("n\xE3o", "no");
|
|
489
|
-
const specOf = (so) => so.root.userData.vegSpec;
|
|
490
|
-
const partSpec = (so, pid) => specOf(so).parts.filter((p) => p.id === pid)[0];
|
|
491
|
-
const world = (so, local) => {
|
|
492
|
-
const r = so.root;
|
|
493
|
-
return [r.position.x + local[0], r.position.y + local[1], r.position.z + local[2]];
|
|
494
|
-
};
|
|
495
|
-
const norm = (v) => {
|
|
496
|
-
const l = Math.hypot(v[0], v[1], v[2]) || 1;
|
|
497
|
-
return [v[0] / l, v[1] / l, v[2] / l];
|
|
498
|
-
};
|
|
499
|
-
const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
500
|
-
const DIRS = [[0, 0, -1], [1, 0, 0], [-1, 0, 0], [0, 0, 1], [0.7, 0, -0.7], [-0.7, 0, -0.7], [0.7, 0, 0.7], [-0.7, 0, 0.7], [0, -1, 0]];
|
|
501
|
-
const center = (so, pid) => world(so, partSpec(so, pid).position);
|
|
502
|
-
const aim = (room, so, target, standoff, opt = {}) => {
|
|
503
|
-
let best = null;
|
|
504
|
-
for (const dir of DIRS) {
|
|
505
|
-
const d = norm(dir);
|
|
506
|
-
const origin = [target[0] - d[0] * standoff, target[1] - d[1] * standoff, target[2] - d[2] * standoff];
|
|
507
|
-
if (origin[1] < 0.25) continue;
|
|
508
|
-
const hit = room.world.raycast(origin, d, standoff + (opt.beyond == null ? 0.6 : opt.beyond));
|
|
509
|
-
if (!hit || hit.object !== so.objectId) continue;
|
|
510
|
-
let score = dist(hit.point, target);
|
|
511
|
-
if (opt.joint) {
|
|
512
|
-
const nj = room.world.nearestJoint(so.objectId, hit.point, opt.jointRange || 0.45);
|
|
513
|
-
if (!nj || nj.id !== opt.joint) score += 20;
|
|
514
|
-
}
|
|
515
|
-
if (!best || score < best.score) best = { score, origin, target, hit };
|
|
516
|
-
}
|
|
517
|
-
if (!best) best = { score: 99, origin: [target[0], target[1], target[2] + standoff], target, hit: null };
|
|
518
|
-
room.look(best.origin, best.target);
|
|
519
|
-
return best;
|
|
520
|
-
};
|
|
521
|
-
const AIMDIRS = [[0, 0, -1], [0.7, 0, -0.7], [-0.7, 0, -0.7], [1, 0, 0], [-1, 0, 0], [0, -1, 0], [0.7, 0, 0.7], [-0.7, 0, 0.7], [0, 0, 1]];
|
|
522
|
-
const aimPart = (room, so, pidsIn, gap, at) => {
|
|
523
|
-
const pids = typeof pidsIn === "string" ? [pidsIn] : pidsIn;
|
|
524
|
-
let best = null;
|
|
525
|
-
for (const pid of pids) {
|
|
526
|
-
const c = at ? world(so, at) : center(so, pid);
|
|
527
|
-
for (const dir of AIMDIRS) {
|
|
528
|
-
const d = norm(dir);
|
|
529
|
-
const far = 8;
|
|
530
|
-
const origin = [c[0] - d[0] * far, c[1] - d[1] * far, c[2] - d[2] * far];
|
|
531
|
-
if (origin[1] < 0.3) continue;
|
|
532
|
-
const hit = room.world.raycast(origin, d, far + 3);
|
|
533
|
-
if (!hit || hit.object !== so.objectId || hit.part !== pid) continue;
|
|
534
|
-
const score = dist(hit.point, c);
|
|
535
|
-
if (!best || score < best.score) best = { score, d, hit, pid };
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
if (!best) best = { score: 99, d: [0, 0, -1], hit: { point: center(so, pids[0]) }, pid: pids[0] };
|
|
539
|
-
const H = best.hit.point;
|
|
540
|
-
const from = [H[0] - best.d[0] * gap, H[1] - best.d[1] * gap, H[2] - best.d[2] * gap];
|
|
541
|
-
if (from[1] < 0.3) from[1] = 0.3;
|
|
542
|
-
room.look(from, [H[0] + best.d[0] * 0.3, H[1] + best.d[1] * 0.3, H[2] + best.d[2] * 0.3]);
|
|
543
|
-
return { origin: from, point: H, found: best.score < 99, pid: best.pid };
|
|
544
|
-
};
|
|
545
|
-
const crownsOf = (so) => specOf(so).parts.filter((p) => /^(s\d+_)?crown\d*$/.test(p.id)).map((p) => p.id);
|
|
546
|
-
const jointPos = (so, jid) => {
|
|
547
|
-
const j = specOf(so).joints.filter((x) => x.id === jid)[0];
|
|
548
|
-
const a = partSpec(so, j.a);
|
|
549
|
-
const b = partSpec(so, j.b);
|
|
550
|
-
const box = (p) => {
|
|
551
|
-
const h = p.shape.kind === "box" ? p.shape.size.map((s) => s / 2) : [p.shape.radius, p.shape.height / 2, p.shape.radius];
|
|
552
|
-
const r = p.rotation || [0, 0, 0];
|
|
553
|
-
const cx = Math.cos(r[0]), sx = Math.sin(r[0]), cz = Math.cos(r[2]), sz = Math.sin(r[2]);
|
|
554
|
-
const mn = [Infinity, Infinity, Infinity];
|
|
555
|
-
const mx = [-Infinity, -Infinity, -Infinity];
|
|
556
|
-
for (let i = 0; i < 8; i++) {
|
|
557
|
-
const x = (i & 1 ? 1 : -1) * h[0], y = (i & 2 ? 1 : -1) * h[1], z = (i & 4 ? 1 : -1) * h[2];
|
|
558
|
-
const x1 = x * cz - y * sz, y1 = x * sz + y * cz;
|
|
559
|
-
const w = [x1, y1 * cx - z * sx, y1 * sx + z * cx];
|
|
560
|
-
for (let k = 0; k < 3; k++) {
|
|
561
|
-
mn[k] = Math.min(mn[k], p.position[k] + w[k]);
|
|
562
|
-
mx[k] = Math.max(mx[k], p.position[k] + w[k]);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return { min: mn, max: mx };
|
|
566
|
-
};
|
|
567
|
-
const A_ = box(a);
|
|
568
|
-
const B_ = box(b);
|
|
569
|
-
return world(so, [0, 1, 2].map((k) => (Math.max(A_.min[k], B_.min[k]) + Math.min(A_.max[k], B_.max[k])) / 2));
|
|
570
|
-
};
|
|
571
|
-
const lost = (p) => !p || p.status === "broken" || p.status === "ashes" || p.status === "melted";
|
|
572
|
-
const snap = (room, so, pid) => {
|
|
573
|
-
const st = room.world.snapshot(so.objectId);
|
|
574
|
-
const p = st && st.parts.filter((x) => x.id === pid)[0];
|
|
575
|
-
if (!p) return null;
|
|
576
|
-
return { status: p.status, hp: p.hp, maxHp: p.maxHp, temperature: p.temperature, burning: p.burning, char: p.char || 0, fuel: p.fuel, holes: (p.holes || []).length, material: p.material };
|
|
577
|
-
};
|
|
578
|
-
const logCount = (room, so, type) => room.log.entries({ type, objectId: so.objectId }).length;
|
|
579
|
-
const jointsOpened = (room, so) => {
|
|
580
|
-
let n = 0;
|
|
581
|
-
for (const e of room.log.entries({ type: "detach", objectId: so.objectId })) n += e.data && e.data.joints ? e.data.joints.length : 1;
|
|
582
|
-
for (const e of room.log.entries({ type: "fracture", objectId: so.objectId })) n += e.data && e.data.jointsOpened || 0;
|
|
583
|
-
return n;
|
|
584
|
-
};
|
|
585
|
-
const detachedJoint = (room, so, jid) => room.log.entries({ type: "detach", objectId: so.objectId }).some((e) => e.data && (e.data.joint === jid || (e.data.joints || []).indexOf(jid) >= 0));
|
|
586
|
-
const spawn = (room, id, x, z) => room.spawn(id, [x, 0, z], 0);
|
|
587
|
-
const split = (room, so) => {
|
|
588
|
-
const home = room.world.snapshot(so.objectId);
|
|
589
|
-
const loose = room.world.ids().filter((i) => /^obj_/.test(i)).map((i) => ({ id: i, st: room.world.snapshot(i) }));
|
|
590
|
-
const anchored = {};
|
|
591
|
-
for (const p of specOf(so).parts) if (p.anchored) anchored[p.id] = true;
|
|
592
|
-
const rooted = [];
|
|
593
|
-
for (const g of home.groups) if (g.some((pid) => anchored[pid])) for (const pid of g) rooted.push(pid);
|
|
594
|
-
const inLoose = (pid) => loose.some((o) => o.st.parts.some((p) => p.id === pid && !lost(p)));
|
|
595
|
-
const inHome = (pid) => home.parts.some((p) => p.id === pid && !lost(p));
|
|
596
|
-
return { home, loose, rooted, inLoose, inHome, sections: home.groups.length + loose.length };
|
|
597
|
-
};
|
|
598
|
-
const fell = (room, ctx, so, max) => {
|
|
599
|
-
take(room, "axe");
|
|
600
|
-
let swings = 0;
|
|
601
|
-
const h = partSpec(so, "trunk").shape.height;
|
|
602
|
-
for (let i = 0; i < max; i++) {
|
|
603
|
-
aimPart(room, so, "trunk", 0.5, [0, Math.min(0.9, h / 3), 0]);
|
|
604
|
-
room.use();
|
|
605
|
-
ctx.step(1.5);
|
|
606
|
-
swings++;
|
|
607
|
-
if (lost(room.world.part(so.objectId, "trunk"))) break;
|
|
608
|
-
}
|
|
609
|
-
ctx.step(5);
|
|
610
|
-
return { swings, trunk: snap(room, so, "trunk") };
|
|
611
|
-
};
|
|
612
|
-
const sc = (id, ptName, enName, run) => ({ id: `veg_${id}`, name: L(ptName, enName), run });
|
|
613
|
-
const pct = (x) => `${Math.round((x || 0) * 100)}%`;
|
|
614
|
-
const take = (room, id) => {
|
|
615
|
-
room.setTool(id);
|
|
616
|
-
room.waitUntilReady();
|
|
617
|
-
};
|
|
618
|
-
return [
|
|
619
|
-
// 1 ---------------------------------------------------------------------------------------------------------------
|
|
620
|
-
sc("axe_fells_crown", "Machado no tronco de 15 cm derruba a copa (8 a 15 golpes)", "Axe on a 15 cm trunk fells the crown (8 to 15 swings)", (ctx) => {
|
|
621
|
-
const room = ctx.room;
|
|
622
|
-
const so = spawn(room, "mango_young_15", 0, -6);
|
|
623
|
-
const r = fell(room, ctx, so, 60);
|
|
624
|
-
const sp = split(room, so);
|
|
625
|
-
const crownGone = lost(room.world.part(so.objectId, "crown")) || !sp.inHome("crown") || sp.rooted.indexOf("crown") < 0;
|
|
626
|
-
const branches = specOf(so).parts.filter((p) => p.tags.indexOf("branch") >= 0).map((p) => p.id);
|
|
627
|
-
const branchesFree = branches.every((b) => sp.rooted.indexOf(b) < 0);
|
|
628
|
-
const detach = jointsOpened(room, so);
|
|
629
|
-
ctx.expect(r.trunk.status === "broken" && r.swings >= 8 && r.swings <= 15, L(`Machado no tronco de 15 cm (hp ${Math.round(r.trunk.maxHp)}): tronco ${r.trunk.status} depois de ${r.swings} golpes (esperado: broken em 8 a 15 golpes)`, `Axe on a 15 cm trunk (hp ${Math.round(r.trunk.maxHp)}): trunk ${r.trunk.status} after ${r.swings} swings (expected: broken in 8 to 15 swings)`), { swings: r.swings, status: r.trunk.status, maxHp: r.trunk.maxHp });
|
|
630
|
-
ctx.expect(sp.rooted.length === 0 && crownGone && branchesFree, L(`Nada da copa continua preso ao ch\xE3o: pe\xE7as ainda enraizadas ${sp.rooted.length}, copa solta ${yn(crownGone)}, ${branches.length} galhos soltos ${yn(branchesFree)} (esperado: 0, sim, sim)`, `Nothing of the crown stays rooted: rooted pieces ${sp.rooted.length}, crown free ${yn(crownGone)}, ${branches.length} branches free ${yn(branchesFree)} (expected: 0, yes, yes)`), { rooted: sp.rooted.length });
|
|
631
|
-
ctx.expect(detach > 0 && sp.loose.length > 0, L(`Registro: ${detach} soltura(s) de junta (as juntas se abrem quando o tronco se perde) e ${sp.loose.length} peda\xE7o(s) caindo como objeto pr\xF3prio (esperado: 1 ou mais de cada)`, `Log: ${detach} joint detach(es) (joints open when the trunk is lost) and ${sp.loose.length} piece(s) falling as their own object (expected: 1+ each)`), { detach, loose: sp.loose.length });
|
|
632
|
-
}),
|
|
633
|
-
sc("axe_fells_30cm", "Machado no tronco de 30 cm (25 a 40 golpes)", "Axe on a 30 cm trunk (25 to 40 swings)", (ctx) => {
|
|
634
|
-
const room = ctx.room;
|
|
635
|
-
const so = spawn(room, "mango_young_30", 0, -6);
|
|
636
|
-
const r = fell(room, ctx, so, 90);
|
|
637
|
-
const sp = split(room, so);
|
|
638
|
-
ctx.expect(r.trunk.status === "broken" && r.swings >= 25 && r.swings <= 40, L(`Machado no tronco de 30 cm (hp ${Math.round(r.trunk.maxHp)}): tronco ${r.trunk.status} depois de ${r.swings} golpes (esperado: broken em 25 a 40 golpes)`, `Axe on a 30 cm trunk (hp ${Math.round(r.trunk.maxHp)}): trunk ${r.trunk.status} after ${r.swings} swings (expected: broken in 25 to 40 swings)`), { swings: r.swings, status: r.trunk.status, maxHp: r.trunk.maxHp });
|
|
639
|
-
ctx.expect(sp.rooted.length === 0, L(`\xC1rvore ca\xEDda: pe\xE7as ainda presas ao ch\xE3o ${sp.rooted.length} (esperado: 0)`, `Fallen tree: pieces still rooted ${sp.rooted.length} (expected: 0)`), { rooted: sp.rooted.length });
|
|
640
|
-
}),
|
|
641
|
-
sc("axe_cuts_branch_joint", "Machado passa pela junta tronco-galho", "Axe works through the trunk-branch joint", (ctx) => {
|
|
642
|
-
const room = ctx.room;
|
|
643
|
-
const so = spawn(room, "mango_tree", 0, -6);
|
|
644
|
-
const jid = "trunk~branch1";
|
|
645
|
-
take(room, "axe");
|
|
646
|
-
let swings = 0;
|
|
647
|
-
let detached = false;
|
|
648
|
-
for (let i = 0; i < 60 && !detached; i++) {
|
|
649
|
-
aim(room, so, jointPos(so, jid), 0.5, { joint: jid, beyond: 0.3 });
|
|
650
|
-
room.use();
|
|
651
|
-
ctx.step(1.5);
|
|
652
|
-
swings++;
|
|
653
|
-
detached = detachedJoint(room, so, jid);
|
|
654
|
-
}
|
|
655
|
-
ctx.step(3);
|
|
656
|
-
const trunk = snap(room, so, "trunk");
|
|
657
|
-
const sp = split(room, so);
|
|
658
|
-
const branchOff = sp.inLoose("branch1") && !sp.inHome("branch1");
|
|
659
|
-
const restStays = sp.inHome("trunk") && sp.rooted.indexOf("trunk") >= 0;
|
|
660
|
-
ctx.expect(detached && branchOff, L(`Machado na junta ${jid}: galho solto ${yn(branchOff)} depois de ${swings} golpe(s), com o tronco ${trunk.status} (esperado: sim)`, `Axe on joint ${jid}: branch detached ${yn(branchOff)} after ${swings} swing(s), trunk ${trunk.status} (expected: yes)`), { swings });
|
|
661
|
-
ctx.expect(trunk.status !== "broken" && restStays, L(`A junta cedeu antes do tronco: tronco ${trunk.status}, tronco de p\xE9 e preso ao ch\xE3o ${yn(restStays)} (esperado: n\xE3o quebrado, sim)`, `The joint gave way before the trunk: trunk ${trunk.status}, trunk standing and rooted ${yn(restStays)} (expected: not broken, yes)`), {});
|
|
662
|
-
}),
|
|
663
|
-
sc("shot_cannot_fell", "Tiro de 9 mm n\xE3o derruba a \xE1rvore", "A 9 mm shot never fells a tree", (ctx) => {
|
|
664
|
-
const room = ctx.room;
|
|
665
|
-
const so = spawn(room, "mango_young_15", 0, -6);
|
|
666
|
-
take(room, "pistol_9mm");
|
|
667
|
-
let shots = 0;
|
|
668
|
-
for (let i = 0; i < 30; i++) {
|
|
669
|
-
aimPart(room, so, "trunk", 2.5, [0, 0.5, 0]);
|
|
670
|
-
room.use();
|
|
671
|
-
ctx.step(0.4);
|
|
672
|
-
shots++;
|
|
673
|
-
}
|
|
674
|
-
const trunk = snap(room, so, "trunk");
|
|
675
|
-
const sp = split(room, so);
|
|
676
|
-
ctx.expect(trunk.status !== "broken" && trunk.hp > 0, L(`${shots} tiros de 9 mm no tronco de 15 cm: tronco ${trunk.status}, hp ${Math.round(trunk.hp)}/${Math.round(trunk.maxHp)} (esperado: n\xE3o quebrado)`, `${shots} 9 mm shots on the 15 cm trunk: trunk ${trunk.status}, hp ${Math.round(trunk.hp)}/${Math.round(trunk.maxHp)} (expected: not broken)`), { shots, hp: trunk.hp });
|
|
677
|
-
ctx.expect(sp.loose.length === 0 && sp.inHome("trunk") && sp.rooted.indexOf("crown") >= 0, L(`A \xE1rvore continua de p\xE9: peda\xE7os soltos ${sp.loose.length}, copa presa ao ch\xE3o ${yn(sp.rooted.indexOf("crown") >= 0)} (esperado: 0, sim)`, `The tree still stands: loose pieces ${sp.loose.length}, crown rooted ${yn(sp.rooted.indexOf("crown") >= 0)} (expected: 0, yes)`), {});
|
|
678
|
-
}),
|
|
679
|
-
// 2 ---------------------------------------------------------------------------------------------------------------
|
|
680
|
-
sc("saw_cuts_branch", "Serra no galho", "Saw on a branch", (ctx) => {
|
|
681
|
-
const room = ctx.room;
|
|
682
|
-
const so = spawn(room, "yellow_ipe", 0, -6);
|
|
683
|
-
const jid = "trunk~branch1";
|
|
684
|
-
take(room, "hacksaw");
|
|
685
|
-
aim(room, so, jointPos(so, jid), 0.45, { joint: jid });
|
|
686
|
-
room.use(4);
|
|
687
|
-
ctx.step(1);
|
|
688
|
-
const sp = split(room, so);
|
|
689
|
-
const movedBranch = sp.inLoose("branch1") && !sp.inHome("branch1");
|
|
690
|
-
const movedCrown = sp.inLoose("crown1");
|
|
691
|
-
const restStays = ["trunk", "branch0", "branch2", "branch3", "crown"].every((pid) => sp.inHome(pid) && sp.rooted.indexOf(pid) >= 0);
|
|
692
|
-
const detach = logCount(room, so, "detach");
|
|
693
|
-
ctx.expect(movedBranch && detach > 0, L(`Serra na junta tronco-galho 1: galho solto do tronco ${yn(movedBranch)}, ${detach} linha(s) de soltura no log (esperado: sim, 1)`, `Saw on the trunk-branch 1 joint: branch detached ${yn(movedBranch)}, ${detach} detach line(s) in the log (expected: yes, 1)`), { detach });
|
|
694
|
-
ctx.expect(movedCrown, L(`A folhagem do galho cortado foi junto: ${yn(movedCrown)} (esperado: sim)`, `The foliage of the cut branch went with it: ${yn(movedCrown)} (expected: yes)`), {});
|
|
695
|
-
ctx.expect(restStays, L(`O resto da \xE1rvore (tronco, outros galhos e copa) continua inteiro e preso ao ch\xE3o: ${yn(restStays)} (esperado: sim)`, `The rest of the tree (trunk, other branches, crown) stays whole and rooted: ${yn(restStays)} (expected: yes)`), {});
|
|
696
|
-
}),
|
|
697
|
-
// 3 ---------------------------------------------------------------------------------------------------------------
|
|
698
|
-
sc("lighter_ignites_foliage", "Isqueiro na folhagem pega fogo", "Lighter on the foliage catches fire", (ctx) => {
|
|
699
|
-
const room = ctx.room;
|
|
700
|
-
const so = spawn(room, "mango_tree", 0, -6);
|
|
701
|
-
take(room, "lighter");
|
|
702
|
-
const a = aimPart(room, so, crownsOf(so), 0.08);
|
|
703
|
-
room.use(6);
|
|
704
|
-
const crown = snap(room, so, a.pid);
|
|
705
|
-
const lit = crown.burning || crown.temperature > 300;
|
|
706
|
-
let peak = 0;
|
|
707
|
-
let charMax = 0;
|
|
708
|
-
for (let t = 0; t < 18; t++) {
|
|
709
|
-
ctx.step(5);
|
|
710
|
-
for (const p of room.world.state(so.objectId).parts) {
|
|
711
|
-
if (p.material === "green_wood") {
|
|
712
|
-
peak = Math.max(peak, p.temperature);
|
|
713
|
-
charMax = Math.max(charMax, p.char || 0);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
const st = room.world.state(so.objectId);
|
|
718
|
-
const burnt = st.parts.filter((p) => p.material === "foliage" && (p.burning || p.char > 0 || p.status === "ashes")).length;
|
|
719
|
-
ctx.expect(lit, L(`Isqueiro na copa: folhagem acesa ou acima de 300 \xB0C = ${yn(lit)} (${Math.round(crown.temperature)} \xB0C; esperado: sim)`, `Lighter on the crown: foliage lit or above 300 C = ${yn(lit)} (${Math.round(crown.temperature)} C; expected: yes)`), { temperature: crown.temperature });
|
|
720
|
-
ctx.expect(burnt > 0, L(`Depois de 90 s: ${burnt} pe\xE7a(s) de folhagem queimando, carbonizada(s) ou em cinza (esperado: 1 ou mais)`, `After 90 s: ${burnt} foliage piece(s) burning, charred or ash (expected: 1+)`), { burnt });
|
|
721
|
-
ctx.expect(peak > 150 || charMax > 0, L(`O fogo da folhagem passa para a madeira verde (tronco e galhos): pico de ${Math.round(peak)} \xB0C, carboniza\xE7\xE3o m\xE1xima ${pct(charMax)} (esperado: pico acima de 150 \xB0C ou carboniza\xE7\xE3o acima de 0; a madeira verde s\xF3 pega fogo acima de 400 \xB0C)`, `Foliage fire reaches the green wood (trunk and branches): peak ${Math.round(peak)} C, max char ${pct(charMax)} (expected: peak above 150 C or char above 0; green wood only ignites above 400 C)`), { peak, charMax });
|
|
722
|
-
}),
|
|
723
|
-
// 4 ---------------------------------------------------------------------------------------------------------------
|
|
724
|
-
sc("shot_strips_leaves", "Tiro na copa arranca folhas", "A shot into the crown strips leaves", (ctx) => {
|
|
725
|
-
const room = ctx.room;
|
|
726
|
-
const so = spawn(room, "mango_tree", 0, -6);
|
|
727
|
-
take(room, "pistol_9mm");
|
|
728
|
-
const tgt = aimPart(room, so, crownsOf(so), 2.5).pid;
|
|
729
|
-
const before = snap(room, so, tgt);
|
|
730
|
-
for (let i = 0; i < 3; i++) {
|
|
731
|
-
aimPart(room, so, tgt, 2.5);
|
|
732
|
-
room.use();
|
|
733
|
-
ctx.step(0.6);
|
|
734
|
-
}
|
|
735
|
-
const crown = snap(room, so, tgt);
|
|
736
|
-
const leaves = room.log.entries({ type: "debris", objectId: so.objectId }).filter((e) => e.data && e.data.kind === "leaves").length;
|
|
737
|
-
const hurt = crown.hp < before.hp || crown.status === "broken";
|
|
738
|
-
ctx.expect(hurt && crown.status !== "intact", L(`Tr\xEAs tiros na copa: hp ${Math.round(before.hp)} para ${Math.round(crown.hp)}, estado ${crown.status} (esperado: hp menor, n\xE3o intacta)`, `Three shots into the crown: hp ${Math.round(before.hp)} to ${Math.round(crown.hp)}, status ${crown.status} (expected: lower hp, not intact)`), { hp: crown.hp });
|
|
739
|
-
ctx.expect(leaves > 0 || crown.holes > 0, L(`Folhas arrancadas: ${leaves} linha(s) de folhas voando, ${crown.holes} furo(s) na copa (esperado: 1 ou mais)`, `Leaves torn off: ${leaves} flying-leaf line(s), ${crown.holes} hole(s) in the crown (expected: 1+)`), { leaves, holes: crown.holes });
|
|
740
|
-
}),
|
|
741
|
-
// 5 ---------------------------------------------------------------------------------------------------------------
|
|
742
|
-
sc("shot_breaks_pot", "Tiros quebram o vaso e a terra se espalha", "Shots break the pot and the soil spills", (ctx) => {
|
|
743
|
-
const room = ctx.room;
|
|
744
|
-
const so = room.spawn("potted_fern", [0, 0, -4], 0);
|
|
745
|
-
take(room, "pistol_9mm");
|
|
746
|
-
let shots = 0;
|
|
747
|
-
for (let i = 0; i < 12; i++) {
|
|
748
|
-
const cur = snap(room, so, "pot");
|
|
749
|
-
if (!cur) break;
|
|
750
|
-
aimPart(room, so, "pot", 1.5);
|
|
751
|
-
room.use();
|
|
752
|
-
ctx.step(0.5);
|
|
753
|
-
shots++;
|
|
754
|
-
const after = snap(room, so, "pot");
|
|
755
|
-
if (after && (after.status === "broken" || after.hp <= 0)) break;
|
|
756
|
-
}
|
|
757
|
-
ctx.step(3);
|
|
758
|
-
const sp = split(room, so);
|
|
759
|
-
let potNow = snap(room, so, "pot");
|
|
760
|
-
for (const o of sp.loose) {
|
|
761
|
-
const q = o.st.parts.filter((x) => x.id === "pot")[0];
|
|
762
|
-
if (q) potNow = q;
|
|
763
|
-
}
|
|
764
|
-
const soilOut = sp.inLoose("soil") || !sp.inHome("soil");
|
|
765
|
-
const soilApart = !sp.home.groups.some((g) => g.indexOf("soil") >= 0 && g.indexOf("pot") >= 0);
|
|
766
|
-
const cracks = room.log.entries({ type: "crack", objectId: so.objectId }).length;
|
|
767
|
-
const dt = jointsOpened(room, so);
|
|
768
|
-
ctx.expect(!!potNow && (potNow.status === "broken" || potNow.hp <= 0) && shots >= 1 && shots <= 4, L(`Tiros no vaso: o vaso quebra: cer\xE2mica ${potNow && potNow.status} com hp ${potNow ? `${Math.round(potNow.hp)}/${Math.round(potNow.maxHp)}` : "?"} e ${cracks} trinca(s) depois de ${shots} tiro(s) de 9 mm (esperado: vaso quebrado em 1 a 4 tiros)`, `Shots into the pot: the pot breaks: ceramic ${potNow && potNow.status} with hp ${potNow ? `${Math.round(potNow.hp)}/${Math.round(potNow.maxHp)}` : "?"} and ${cracks} crack(s) after ${shots} 9 mm shot(s) (expected: pot broken in 1 to 4 shots)`), { status: potNow && potNow.status, shots, cracks });
|
|
769
|
-
ctx.expect(soilApart || soilOut, L(`A terra saiu do vaso e caiu junto com a planta: ${yn(soilApart || soilOut)} (esperado: sim)`, `The soil left the pot and fell with the plant: ${yn(soilApart || soilOut)} (expected: yes)`), {});
|
|
770
|
-
ctx.expect(dt > 0, L(`Registro: ${dt} soltura(s) de junta (esperado: 1 ou mais)`, `Log: ${dt} joint detach(es) (expected: 1+)`), { detach: dt });
|
|
771
|
-
}),
|
|
772
|
-
// 6 ---------------------------------------------------------------------------------------------------------------
|
|
773
|
-
sc("machete_arm", "Machete na junta do bra\xE7o do mandacaru", "Machete on the mandacaru arm joint", (ctx) => {
|
|
774
|
-
const room = ctx.room;
|
|
775
|
-
const so = spawn(room, "mandacaru", 0, -5);
|
|
776
|
-
take(room, "machete");
|
|
777
|
-
const arm = "arm0";
|
|
778
|
-
const jid = `stem~${arm}`;
|
|
779
|
-
let swings = 0;
|
|
780
|
-
let detached = false;
|
|
781
|
-
for (let i = 0; i < 40 && !detached; i++) {
|
|
782
|
-
aim(room, so, jointPos(so, jid), 0.55, { joint: jid, beyond: 0.3 });
|
|
783
|
-
room.use();
|
|
784
|
-
ctx.step(1.2);
|
|
785
|
-
swings++;
|
|
786
|
-
detached = detachedJoint(room, so, jid) || lost(room.world.part(so.objectId, arm));
|
|
787
|
-
}
|
|
788
|
-
ctx.step(3);
|
|
789
|
-
const a = snap(room, so, arm);
|
|
790
|
-
const sp = split(room, so);
|
|
791
|
-
const armOff = !a || a.status === "broken" || sp.inLoose(arm) && !sp.inHome(arm);
|
|
792
|
-
const stemOk = sp.inHome("stem") && sp.rooted.indexOf("stem") >= 0 && room.world.part(so.objectId, "stem").status !== "broken";
|
|
793
|
-
const spines = specOf(so).parts.filter((p) => p.tags.indexOf("thorns") >= 0);
|
|
794
|
-
const thorny = so.root.userData.thorny === true && spines.length > 0;
|
|
795
|
-
const spinesOff = !sp.rooted.some((pid) => pid.indexOf(`${arm}_spines`) === 0);
|
|
796
|
-
ctx.expect(armOff, L(`Machete na junta ${jid}: bra\xE7o ${a ? a.status : "solto como objeto pr\xF3prio"} depois de ${swings} golpe(s) (esperado: solto do caule pela junta)`, `Machete on joint ${jid}: arm ${a ? a.status : "loose as its own object"} after ${swings} swing(s) (expected: off the stem through the joint)`), { swings });
|
|
797
|
-
ctx.expect(stemOk, L(`O caule principal continua de p\xE9 e preso ao ch\xE3o: ${yn(stemOk)} (esperado: sim)`, `The main stem stays standing and rooted: ${yn(stemOk)} (expected: yes)`), {});
|
|
798
|
-
ctx.expect(thorny && spinesOff, L(`Espinhos: cacto marcado como espinhoso (userData.thorny, ${spines.length} pe\xE7as de espinho) ${yn(thorny)}, espinhos do bra\xE7o sa\xEDram junto ${yn(spinesOff)} (esperado: sim, sim)`, `Thorns: cactus flagged thorny (userData.thorny, ${spines.length} spine pieces) ${yn(thorny)}, the arm spines went with it ${yn(spinesOff)} (expected: yes, yes)`), { spines: spines.length });
|
|
799
|
-
}),
|
|
800
|
-
// 7 ---------------------------------------------------------------------------------------------------------------
|
|
801
|
-
sc("pad_pulled", "Palma arrancada", "Prickly pear pad pulled off", (ctx) => {
|
|
802
|
-
const room = ctx.room;
|
|
803
|
-
const so = spawn(room, "prickly_pear", 0, -4);
|
|
804
|
-
const jid = "pad0~pad1";
|
|
805
|
-
take(room, "utility_knife");
|
|
806
|
-
aim(room, so, jointPos(so, jid), 0.25, { joint: jid, beyond: 0.3 });
|
|
807
|
-
room.use(1.7);
|
|
808
|
-
ctx.step(1);
|
|
809
|
-
const sp = split(room, so);
|
|
810
|
-
const padOff = sp.inLoose("pad1") && !sp.inHome("pad1");
|
|
811
|
-
const childWith = sp.inLoose("pad3");
|
|
812
|
-
const spinesWith = sp.inLoose("pad1_spines");
|
|
813
|
-
const motherOk = sp.inHome("pad0") && sp.rooted.indexOf("pad0") >= 0 && sp.inHome("pad2");
|
|
814
|
-
ctx.expect(padOff, L(`Raquete 1 cortada da planta-m\xE3e na junta ${jid}: ${yn(padOff)} (esperado: sim)`, `Pad 1 cut off the mother plant at joint ${jid}: ${yn(padOff)} (expected: yes)`), {});
|
|
815
|
-
ctx.expect(childWith && spinesWith, L(`A raquete filha (3) e os espinhos da raquete 1 v\xE3o junto: ${yn(childWith && spinesWith)} (esperado: sim)`, `The child pad (3) and the spines of pad 1 go with it: ${yn(childWith && spinesWith)} (expected: yes)`), {});
|
|
816
|
-
ctx.expect(motherOk, L(`A planta-m\xE3e (raquetes 0 e 2) fica presa ao ch\xE3o: ${yn(motherOk)} (esperado: sim)`, `The mother plant (pads 0 and 2) stays rooted: ${yn(motherOk)} (expected: yes)`), {});
|
|
817
|
-
}),
|
|
818
|
-
// 8 ---------------------------------------------------------------------------------------------------------------
|
|
819
|
-
sc("dead_burns_faster", "\xC1rvore morta queima mais r\xE1pido que viva", "A dead tree burns faster than a living one", (ctx) => {
|
|
820
|
-
const room = ctx.room;
|
|
821
|
-
const dead = spawn(room, "dead_tree", -4, -6);
|
|
822
|
-
const live = spawn(room, "mango_tree", 4, -6);
|
|
823
|
-
take(room, "lighter");
|
|
824
|
-
for (const so of [dead, live]) {
|
|
825
|
-
aimPart(room, so, "trunk", 0.08, [0, 0.8, 0]);
|
|
826
|
-
room.use(8);
|
|
827
|
-
}
|
|
828
|
-
const d0 = snap(room, dead, "trunk");
|
|
829
|
-
const l0 = snap(room, live, "trunk");
|
|
830
|
-
ctx.step(120);
|
|
831
|
-
const d = snap(room, dead, "trunk");
|
|
832
|
-
const l = snap(room, live, "trunk");
|
|
833
|
-
const score = (p) => p ? Math.max(p.char, p.burning ? 0.01 : 0, (p.temperature - 20) / 2e3) : 0;
|
|
834
|
-
ctx.expect(d0.material === "wood_soft" && l0.material === "green_wood", L(`Materiais do tronco: \xE1rvore morta ${d0.material}, viva ${l0.material} (esperado: wood_soft, green_wood)`, `Trunk materials: dead tree ${d0.material}, living ${l0.material} (expected: wood_soft, green_wood)`), { dead: d0.material, live: l0.material });
|
|
835
|
-
ctx.expect(score(d) > score(l), L(`Depois de 8 s de isqueiro e 120 s de espera: morta queimando ${yn(d.burning)}, ${Math.round(d.temperature)} \xB0C, carboniza\xE7\xE3o ${pct(d.char)}; viva queimando ${yn(l.burning)}, ${Math.round(l.temperature)} \xB0C, carboniza\xE7\xE3o ${pct(l.char)} (esperado: a morta mais queimada)`, `After 8 s of lighter and 120 s of waiting: dead burning ${yn(d.burning)}, ${Math.round(d.temperature)} C, char ${pct(d.char)}; living burning ${yn(l.burning)}, ${Math.round(l.temperature)} C, char ${pct(l.char)} (expected: dead burns more)`), { dead: score(d), live: score(l) });
|
|
836
|
-
}),
|
|
837
|
-
// 9 ---------------------------------------------------------------------------------------------------------------
|
|
838
|
-
sc("hedge_cut_pieces", "Cerca viva cortada em partes", "Hedge cut into sections", (ctx) => {
|
|
839
|
-
const room = ctx.room;
|
|
840
|
-
const so = spawn(room, "boxwood_hedge", 0, -5);
|
|
841
|
-
take(room, "hacksaw");
|
|
842
|
-
for (const jid of ["s0_stem~s1_stem", "s2_stem~s3_stem"]) {
|
|
843
|
-
aim(room, so, jointPos(so, jid), 0.45, { joint: jid });
|
|
844
|
-
room.use(1.7);
|
|
845
|
-
ctx.step(0.3);
|
|
846
|
-
}
|
|
847
|
-
ctx.step(1);
|
|
848
|
-
const sp = split(room, so);
|
|
849
|
-
const all = [...sp.home.parts];
|
|
850
|
-
for (const o of sp.loose) all.push(...o.st.parts);
|
|
851
|
-
const alive = all.filter((p) => !lost(p)).length;
|
|
852
|
-
const total = specOf(so).parts.length;
|
|
853
|
-
ctx.expect(sp.sections === 3, L(`Duas serradas na cerca viva: ${sp.sections} parte(s) separada(s) (esperado: 3)`, `Two saw cuts in the hedge: ${sp.sections} separate section(s) (expected: 3)`), { sections: sp.sections });
|
|
854
|
-
ctx.expect(alive === total, L(`Nenhuma se\xE7\xE3o foi destru\xEDda, s\xF3 separada: ${alive} de ${total} pe\xE7as inteiras (esperado: todas)`, `No section was destroyed, only separated: ${alive} of ${total} pieces intact (expected: all)`), { alive, total });
|
|
855
|
-
}),
|
|
856
|
-
// 10 --------------------------------------------------------------------------------------------------------------
|
|
857
|
-
sc("walk_on_grass", "Pisar na grama n\xE3o colide", "Walking on grass does not collide", (ctx) => {
|
|
858
|
-
const room = ctx.room;
|
|
859
|
-
const tuft = spawn(room, "lawn_tuft", 0, -3);
|
|
860
|
-
const tree = spawn(room, "mango_tree", 4, -3);
|
|
861
|
-
const noBlock = !tuft.root.userData.collider;
|
|
862
|
-
const treeBlock = !!(tree.root.userData.collider && tree.root.userData.collider.kind);
|
|
863
|
-
ctx.expect(noBlock && treeBlock, L(`Colisor do SDK: touceira ${noBlock ? "nenhum" : "tem"}, tronco da mangueira ${treeBlock ? "tem" : "nenhum"} (esperado: nenhum, tem)`, `SDK collider: tuft ${noBlock ? "none" : "has one"}, mango trunk ${treeBlock ? "has one" : "none"} (expected: none, has one)`), {});
|
|
864
|
-
room.look([0, 1.7, -0.5], [0, 1.7, -6]);
|
|
865
|
-
const before = room.camera.position.z;
|
|
866
|
-
room.walk([0, 1.7, -6], 3);
|
|
867
|
-
ctx.step(3.2);
|
|
868
|
-
const after = room.camera.position.z;
|
|
869
|
-
const tp = snap(room, tuft, "stem");
|
|
870
|
-
ctx.expect(after < -3.3 && !!tp && tp.status === "intact", L(`Andando para a frente (room.walk): z foi de ${before.toFixed(1)} a ${after.toFixed(1)}, atravessou a touceira ${yn(after < -3.3)}, touceira ${tp && tp.status} (esperado: atravessou, intacta)`, `Walking forward (room.walk): z went from ${before.toFixed(1)} to ${after.toFixed(1)}, crossed the tuft ${yn(after < -3.3)}, tuft ${tp && tp.status} (expected: crossed, intact)`), { before, after });
|
|
871
|
-
})
|
|
872
|
-
];
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// src/test-area/text.ts
|
|
876
|
-
var TEXT = {
|
|
877
|
-
en: {
|
|
878
|
-
group: "Trees, plants and flowers",
|
|
879
|
-
name: "Vegetation",
|
|
880
|
-
controls: { seed: "Lot seed", quality: "Lot quality", lod: "Lot detail (LOD)", condition: "Condition", season: "Season", wind: "Wind", gust: "Gusts" },
|
|
881
|
-
hints: {
|
|
882
|
-
seed: "The seed of the nursery lot: the same seed plants the same yard",
|
|
883
|
-
quality: "High: every plant at the detail below. Game: one level for all plants and small ones cheaper, with a triangle budget",
|
|
884
|
-
lod: "Level of detail of the plants of the lot (0 the most detailed)",
|
|
885
|
-
condition: "How the lot plants look: healthy, dry, dead or overgrown (decides green or dry wood for the destruction)",
|
|
886
|
-
season: "Season of the lot (flowers and leaf colours)",
|
|
887
|
-
wind: "Strength of the wind that sways the leaves, flowers and grass",
|
|
888
|
-
gust: "Slowly varying extra push on top of the wind"
|
|
889
|
-
},
|
|
890
|
-
options: {
|
|
891
|
-
quality: { high: "High", game: "Game" },
|
|
892
|
-
lod: { "0": "LOD 0", "1": "LOD 1", "2": "LOD 2" },
|
|
893
|
-
condition: { healthy: "Healthy", dry: "Dry", dead: "Dead", overgrown: "Overgrown" },
|
|
894
|
-
season: { spring: "Spring", summer: "Summer", autumn: "Autumn", winter: "Winter" }
|
|
895
|
-
},
|
|
896
|
-
actions: { plant_lot: "Plant the nursery lot", clear_lot: "Remove the nursery lot", toggle_walker: "Start or stop the walker" },
|
|
897
|
-
layers: { colliders: "Colliders (trunks and cactus bodies)", piece_boxes: "Boxes of the pieces", wood_axes: "Wood axes (radius, length, axis)" },
|
|
898
|
-
metrics: { plants: "Plants placed", pieces: "Pieces", triangles: "Triangles", draw_calls: "Draw calls", lot_triangles: "Lot triangles", lot_plants: "Lot plants", lot_colliders: "Lot colliders", lot_lod0: "Lot plants at LOD 0", lot_lod1: "Lot plants at LOD 1", lot_lod2: "Lot plants at LOD 2", lot_ms: "Lot build time" },
|
|
899
|
-
lot: { planted: "Nursery lot planted: {0} plants, {1} triangles, {2} colliders", cleared: "Nursery lot removed", walker_on: "The walker crosses the lot and bends the plants", walker_off: "The walker stopped" }
|
|
900
|
-
},
|
|
901
|
-
"pt-BR": {
|
|
902
|
-
group: "\xC1rvores, plantas e flores",
|
|
903
|
-
name: "Vegeta\xE7\xE3o",
|
|
904
|
-
controls: { seed: "Semente do canteiro", quality: "Qualidade do canteiro", lod: "Detalhe do canteiro (LOD)", condition: "Condi\xE7\xE3o", season: "Esta\xE7\xE3o", wind: "Vento", gust: "Rajadas" },
|
|
905
|
-
hints: {
|
|
906
|
-
seed: "A semente do canteiro: a mesma semente planta o mesmo quintal",
|
|
907
|
-
quality: "Alta: todas as plantas no detalhe abaixo. Jogo: um n\xEDvel para todas e as pequenas mais baratas, com or\xE7amento de tri\xE2ngulos",
|
|
908
|
-
lod: "N\xEDvel de detalhe das plantas do canteiro (0 o mais detalhado)",
|
|
909
|
-
condition: "Como ficam as plantas do canteiro: saud\xE1vel, seca, morta ou malcuidada (decide madeira verde ou seca na destrui\xE7\xE3o)",
|
|
910
|
-
season: "Esta\xE7\xE3o do canteiro (flores e cor das folhas)",
|
|
911
|
-
wind: "For\xE7a do vento que balan\xE7a folhas, flores e grama",
|
|
912
|
-
gust: "Empurr\xE3o extra que varia devagar, somado ao vento"
|
|
913
|
-
},
|
|
914
|
-
options: {
|
|
915
|
-
quality: { high: "Alta", game: "Jogo" },
|
|
916
|
-
lod: { "0": "LOD 0", "1": "LOD 1", "2": "LOD 2" },
|
|
917
|
-
condition: { healthy: "Saud\xE1vel", dry: "Seca", dead: "Morta", overgrown: "Malcuidada" },
|
|
918
|
-
season: { spring: "Primavera", summer: "Ver\xE3o", autumn: "Outono", winter: "Inverno" }
|
|
919
|
-
},
|
|
920
|
-
actions: { plant_lot: "Plantar o canteiro", clear_lot: "Tirar o canteiro", toggle_walker: "Ligar ou desligar o caminhante" },
|
|
921
|
-
layers: { colliders: "Colisores (troncos e corpos de cacto)", piece_boxes: "Caixas das pe\xE7as", wood_axes: "Eixos da madeira (raio, comprimento, eixo)" },
|
|
922
|
-
metrics: { plants: "Plantas postas", pieces: "Pe\xE7as", triangles: "Tri\xE2ngulos", draw_calls: "Chamadas de desenho", lot_triangles: "Tri\xE2ngulos do canteiro", lot_plants: "Plantas do canteiro", lot_colliders: "Colisores do canteiro", lot_lod0: "Plantas do canteiro em LOD 0", lot_lod1: "Plantas do canteiro em LOD 1", lot_lod2: "Plantas do canteiro em LOD 2", lot_ms: "Tempo de montagem do canteiro" },
|
|
923
|
-
lot: { planted: "Canteiro plantado: {0} plantas, {1} tri\xE2ngulos, {2} colisores", cleared: "Canteiro retirado", walker_on: "O caminhante cruza o canteiro e dobra as plantas", walker_off: "O caminhante parou" }
|
|
924
|
-
}
|
|
925
|
-
};
|
|
926
|
-
var fill = (template, ...values) => template.replace(/\{(\d+)\}/g, (_m, i) => String(values[Number(i)]));
|
|
927
|
-
|
|
928
|
-
// src/test-area/module.ts
|
|
929
|
-
var VEGETATION_AREA_ID = "vegetation";
|
|
930
|
-
var VEGETATION_ITEM_IDS = PLANT_DEFS.map((def) => def.id);
|
|
931
|
-
var VEGETATION_SCENARIO_IDS = [
|
|
932
|
-
"veg_axe_fells_crown",
|
|
933
|
-
"veg_axe_fells_30cm",
|
|
934
|
-
"veg_axe_cuts_branch_joint",
|
|
935
|
-
"veg_shot_cannot_fell",
|
|
936
|
-
"veg_saw_cuts_branch",
|
|
937
|
-
"veg_lighter_ignites_foliage",
|
|
938
|
-
"veg_shot_strips_leaves",
|
|
939
|
-
"veg_shot_breaks_pot",
|
|
940
|
-
"veg_machete_arm",
|
|
941
|
-
"veg_pad_pulled",
|
|
942
|
-
"veg_dead_burns_faster",
|
|
943
|
-
"veg_hedge_cut_pieces",
|
|
944
|
-
"veg_walk_on_grass"
|
|
945
|
-
];
|
|
946
|
-
var defaultSdk = () => ({ ...GameVegetationMain, ...GameVegetationAdapter });
|
|
947
|
-
var AXIS_COLOURS = { trunk: 14254635, branch: 15057019, arm: 7327599, stem: 7327639, pad: 7321551 };
|
|
948
|
-
function createVegetationTestModule(options = {}) {
|
|
949
|
-
const locale = options.locale ?? "pt-BR";
|
|
950
|
-
const text = TEXT[locale.toLowerCase().startsWith("en") ? "en" : "pt-BR"];
|
|
951
|
-
const sdk = options.GameVegetation ?? defaultSdk();
|
|
952
|
-
const species = options.GameVegetationSpecies ?? GameVegetationSpeciesMain;
|
|
953
|
-
let mats = options.materials ?? null;
|
|
954
|
-
let three = null;
|
|
955
|
-
let host = null;
|
|
956
|
-
const materials = (THREE) => {
|
|
957
|
-
if (!mats) mats = sdk.createVegetationMaterials(THREE ?? three);
|
|
958
|
-
return mats;
|
|
959
|
-
};
|
|
960
|
-
const plants = createPlants(sdk, species, (THREE) => materials(THREE), options.seed ?? PLANT_SEED);
|
|
961
|
-
const placed = /* @__PURE__ */ new Map();
|
|
962
|
-
const settings = { seed: 1, quality: "high", lod: 0, condition: "healthy", season: "spring", wind: 0.35, gust: 0.15 };
|
|
963
|
-
let nursery = null;
|
|
964
|
-
let clock = 0;
|
|
965
|
-
let helpersShown = "";
|
|
966
|
-
const item = (base) => ({
|
|
967
|
-
...base,
|
|
968
|
-
build: (THREE) => {
|
|
969
|
-
three = THREE;
|
|
970
|
-
const root = base.build(THREE);
|
|
971
|
-
placed.set(root.uuid, { root, helpers: [] });
|
|
972
|
-
return root;
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
const clearHelpers = () => {
|
|
976
|
-
for (const entry of placed.values()) {
|
|
977
|
-
discardObject3D(entry.helpers);
|
|
978
|
-
entry.helpers = [];
|
|
979
|
-
}
|
|
980
|
-
helpersShown = "";
|
|
981
|
-
};
|
|
982
|
-
const boxKey = () => `${host?.bench?.layer("colliders")}|${host?.bench?.layer("piece_boxes")}|${host?.bench?.layer("wood_axes")}|${placed.size}`;
|
|
983
|
-
const refreshLayers = () => {
|
|
984
|
-
if (!host?.bench || !three) return;
|
|
985
|
-
clearHelpers();
|
|
986
|
-
nursery?.showColliders(host.bench.layer("colliders"));
|
|
987
|
-
for (const entry of placed.values()) {
|
|
988
|
-
const root = entry.root;
|
|
989
|
-
if (!root.parent) continue;
|
|
990
|
-
const mark = (helper) => {
|
|
991
|
-
helper.userData.helper = true;
|
|
992
|
-
root.add(helper);
|
|
993
|
-
entry.helpers.push(helper);
|
|
994
|
-
};
|
|
995
|
-
if (host.bench.layer("colliders") && root.userData.collider && root.userData.collider.kind) {
|
|
996
|
-
const c = root.userData.collider;
|
|
997
|
-
const wire = new three.Mesh(new (shapeGeometries2(three)).CylinderGeometry(c.radius, c.radius, c.height, 14, 1, true), new three.MeshBasicMaterial({ color: root.userData.thorny ? 16742938 : 15017245, wireframe: true }));
|
|
998
|
-
wire.position.set(0, c.height / 2, 0);
|
|
999
|
-
mark(wire);
|
|
1000
|
-
}
|
|
1001
|
-
for (const mesh of root.children) {
|
|
1002
|
-
const v = mesh.userData?.veg;
|
|
1003
|
-
if (!v || !mesh.geometry) continue;
|
|
1004
|
-
if (host.bench.layer("piece_boxes")) {
|
|
1005
|
-
mesh.geometry.computeBoundingBox();
|
|
1006
|
-
const box = mesh.geometry.boundingBox.clone().translate(mesh.position);
|
|
1007
|
-
mark(new three.Box3Helper(box, 6737151));
|
|
1008
|
-
}
|
|
1009
|
-
if (host.bench.layer("wood_axes") && v.radius !== void 0 && v.length !== void 0 && v.axis && !v.sub) {
|
|
1010
|
-
const base = pieceBase(root, v);
|
|
1011
|
-
if (base) {
|
|
1012
|
-
const from = new three.Vector3(base[0] + v.dx, base[1], base[2]);
|
|
1013
|
-
const to = from.clone().add(new three.Vector3(v.axis[0], v.axis[1], v.axis[2]).multiplyScalar(v.length));
|
|
1014
|
-
const line = new three.Line(new three.BufferGeometry().setFromPoints([from, to]), new three.LineBasicMaterial({ color: AXIS_COLOURS[v.kind] ?? 16777215 }));
|
|
1015
|
-
mark(line);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
helpersShown = boxKey();
|
|
1021
|
-
};
|
|
1022
|
-
const pieceBase = (root, v) => {
|
|
1023
|
-
const mesh = root.children.find((c) => c.userData?.veg?.id === v.id);
|
|
1024
|
-
const base = mesh?.userData?.destructionPart?.base;
|
|
1025
|
-
return Array.isArray(base) ? base : null;
|
|
1026
|
-
};
|
|
1027
|
-
const nurseryOf = () => {
|
|
1028
|
-
if (!host || !three) return null;
|
|
1029
|
-
if (!nursery) nursery = createNursery(sdk, species, three, materials(three), host.scene, options.nurseryOrigin ?? [-6, 14]);
|
|
1030
|
-
return nursery;
|
|
1031
|
-
};
|
|
1032
|
-
const plantLot = () => {
|
|
1033
|
-
const n = nurseryOf();
|
|
1034
|
-
if (!n || !host) return;
|
|
1035
|
-
const stats = n.plant(settings);
|
|
1036
|
-
n.showColliders(Boolean(host.bench?.layer("colliders")));
|
|
1037
|
-
host.log(fill(text.lot.planted, stats.plants, stats.triangles, stats.colliders), { plants: stats.plants, triangles: stats.triangles });
|
|
1038
|
-
};
|
|
1039
|
-
const replant = () => {
|
|
1040
|
-
if (nursery?.planted) plantLot();
|
|
1041
|
-
};
|
|
1042
|
-
const applyWind = () => {
|
|
1043
|
-
if (mats) mats.setWind({ strength: settings.wind, gust: settings.gust });
|
|
1044
|
-
};
|
|
1045
|
-
const onChange = (change) => {
|
|
1046
|
-
if (change.kind === "layer") return refreshLayers();
|
|
1047
|
-
if (change.kind === "action") {
|
|
1048
|
-
if (change.id === "plant_lot") plantLot();
|
|
1049
|
-
else if (change.id === "clear_lot") {
|
|
1050
|
-
nursery?.clear();
|
|
1051
|
-
host?.log(text.lot.cleared);
|
|
1052
|
-
} else if (change.id === "toggle_walker") {
|
|
1053
|
-
const n = nurseryOf();
|
|
1054
|
-
if (!n) return;
|
|
1055
|
-
if (!n.planted) plantLot();
|
|
1056
|
-
n.setWalker(!n.walking);
|
|
1057
|
-
host?.log(n.walking ? text.lot.walker_on : text.lot.walker_off);
|
|
1058
|
-
}
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
switch (change.id) {
|
|
1062
|
-
case "seed":
|
|
1063
|
-
settings.seed = Number(change.value);
|
|
1064
|
-
replant();
|
|
1065
|
-
break;
|
|
1066
|
-
case "quality":
|
|
1067
|
-
settings.quality = String(change.value);
|
|
1068
|
-
replant();
|
|
1069
|
-
break;
|
|
1070
|
-
case "lod":
|
|
1071
|
-
settings.lod = Number(change.value);
|
|
1072
|
-
replant();
|
|
1073
|
-
break;
|
|
1074
|
-
case "condition":
|
|
1075
|
-
settings.condition = String(change.value);
|
|
1076
|
-
replant();
|
|
1077
|
-
break;
|
|
1078
|
-
case "season":
|
|
1079
|
-
settings.season = String(change.value);
|
|
1080
|
-
replant();
|
|
1081
|
-
break;
|
|
1082
|
-
case "wind":
|
|
1083
|
-
settings.wind = Number(change.value);
|
|
1084
|
-
applyWind();
|
|
1085
|
-
break;
|
|
1086
|
-
case "gust":
|
|
1087
|
-
settings.gust = Number(change.value);
|
|
1088
|
-
applyWind();
|
|
1089
|
-
break;
|
|
1090
|
-
default:
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
};
|
|
1094
|
-
const frame = (dt) => {
|
|
1095
|
-
clock += dt;
|
|
1096
|
-
if (mats) mats.update(clock);
|
|
1097
|
-
for (const [id, entry] of placed) if (!entry.root.parent) placed.delete(id);
|
|
1098
|
-
if (host?.bench && boxKey() !== helpersShown && (host.bench.layer("colliders") || host.bench.layer("piece_boxes") || host.bench.layer("wood_axes") || helpersShown !== "")) refreshLayers();
|
|
1099
|
-
nursery?.step(dt);
|
|
1100
|
-
};
|
|
1101
|
-
const count = () => {
|
|
1102
|
-
let drawCalls = 0;
|
|
1103
|
-
let triangles = 0;
|
|
1104
|
-
let pieces = 0;
|
|
1105
|
-
for (const entry of placed.values()) {
|
|
1106
|
-
entry.root.traverse((mesh) => {
|
|
1107
|
-
if (!mesh.isMesh || mesh.userData.helper) return;
|
|
1108
|
-
for (let p = mesh; p; p = p.parent) if (p.visible === false) return;
|
|
1109
|
-
drawCalls += 1;
|
|
1110
|
-
if (mesh.userData.veg) pieces += 1;
|
|
1111
|
-
const g = mesh.geometry;
|
|
1112
|
-
triangles += (g.index ? g.index.count : g.attributes.position.count) / 3;
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
return { drawCalls, triangles: Math.round(triangles), pieces };
|
|
1116
|
-
};
|
|
1117
|
-
const lotCalls = () => {
|
|
1118
|
-
let calls = 0;
|
|
1119
|
-
nursery?.group.traverse((o) => {
|
|
1120
|
-
if (o.isMesh && o.visible) calls += 1;
|
|
1121
|
-
});
|
|
1122
|
-
return calls;
|
|
1123
|
-
};
|
|
1124
|
-
return {
|
|
1125
|
-
id: VEGETATION_AREA_ID,
|
|
1126
|
-
name: text.name,
|
|
1127
|
-
ground: { surface: "lawn" },
|
|
1128
|
-
group: text.group,
|
|
1129
|
-
get placed() {
|
|
1130
|
-
return placed;
|
|
1131
|
-
},
|
|
1132
|
-
itemIds: VEGETATION_ITEM_IDS,
|
|
1133
|
-
materials,
|
|
1134
|
-
tick: (t) => {
|
|
1135
|
-
if (mats) mats.update(t);
|
|
1136
|
-
},
|
|
1137
|
-
items: (loc) => plants.items(loc || locale).map(item),
|
|
1138
|
-
scenarios: (loc) => createScenarios(loc || locale),
|
|
1139
|
-
area: () => {
|
|
1140
|
-
const rows = [
|
|
1141
|
-
{ z: -4, ids: ["hibiscus_shrub", "rose_bush_red", "sunflower", "mandacaru", "prickly_pear", "crown_cactus"] },
|
|
1142
|
-
{ z: -9, ids: ["banana_with_bunch", "dead_tree", "boxwood_hedge", "potted_fern", "potted_cactus", "lawn_tuft"] },
|
|
1143
|
-
{ z: -16, ids: ["mango_tree", "yellow_ipe", "coconut_palm", "araucaria", "mango_young_30", "mango_young_15"] }
|
|
1144
|
-
];
|
|
1145
|
-
const widths = { mango_tree: 5, yellow_ipe: 5, coconut_palm: 4.5, araucaria: 4, mango_young_30: 3, mango_young_15: 2.6, banana_with_bunch: 3.5, dead_tree: 3.5, boxwood_hedge: 5.5, hibiscus_shrub: 2, rose_bush_red: 1.6, sunflower: 1.4, mandacaru: 2.2, prickly_pear: 2, crown_cactus: 1.8, potted_fern: 1.4, potted_cactus: 1.2, lawn_tuft: 1.2 };
|
|
1146
|
-
const gap = 1.2;
|
|
1147
|
-
const placements = rows.flatMap((row) => {
|
|
1148
|
-
const total = row.ids.reduce((sum, id) => sum + (widths[id] ?? 2) + gap, -gap);
|
|
1149
|
-
let cursor = -total / 2;
|
|
1150
|
-
return row.ids.map((id) => {
|
|
1151
|
-
const width = widths[id] ?? 2;
|
|
1152
|
-
const x = cursor + width / 2;
|
|
1153
|
-
cursor += width + gap;
|
|
1154
|
-
return { item: id, position: [x, 0, row.z], rotationY: 0 };
|
|
1155
|
-
});
|
|
1156
|
-
});
|
|
1157
|
-
return { placements, view: { eye: [0, 6, 8], target: [0, 1.8, -10] } };
|
|
1158
|
-
},
|
|
1159
|
-
controls: () => [
|
|
1160
|
-
{ kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 0, max: 99999, step: 1, default: 1 },
|
|
1161
|
-
{ kind: "choice", id: "quality", name: text.controls.quality, hint: text.hints.quality, options: ["high", "game"].map((id) => ({ id, name: text.options.quality[id] })), default: "high" },
|
|
1162
|
-
{ kind: "choice", id: "lod", name: text.controls.lod, hint: text.hints.lod, options: ["0", "1", "2"].map((id) => ({ id, name: text.options.lod[id] })), default: "0" },
|
|
1163
|
-
{ kind: "choice", id: "condition", name: text.controls.condition, hint: text.hints.condition, options: ["healthy", "dry", "dead", "overgrown"].map((id) => ({ id, name: text.options.condition[id] })), default: "healthy" },
|
|
1164
|
-
{ kind: "choice", id: "season", name: text.controls.season, hint: text.hints.season, options: ["spring", "summer", "autumn", "winter"].map((id) => ({ id, name: text.options.season[id] })), default: "spring" },
|
|
1165
|
-
{ kind: "number", id: "wind", name: text.controls.wind, hint: text.hints.wind, min: 0, max: 1, step: 0.05, default: 0.35 },
|
|
1166
|
-
{ kind: "number", id: "gust", name: text.controls.gust, hint: text.hints.gust, min: 0, max: 1, step: 0.05, default: 0.15 },
|
|
1167
|
-
{ kind: "action", id: "plant_lot", name: text.actions.plant_lot, slow: true },
|
|
1168
|
-
{ kind: "action", id: "clear_lot", name: text.actions.clear_lot },
|
|
1169
|
-
{ kind: "action", id: "toggle_walker", name: text.actions.toggle_walker, slow: true }
|
|
1170
|
-
],
|
|
1171
|
-
layers: () => ["colliders", "piece_boxes", "wood_axes"].map((id) => ({ id, name: text.layers[id], default: false })),
|
|
1172
|
-
metrics: () => [
|
|
1173
|
-
{ id: "plants", name: text.metrics.plants, read: () => placed.size },
|
|
1174
|
-
{ id: "pieces", name: text.metrics.pieces, read: () => count().pieces },
|
|
1175
|
-
{ id: "triangles", name: text.metrics.triangles, read: () => count().triangles },
|
|
1176
|
-
{ id: "draw_calls", name: text.metrics.draw_calls, read: () => count().drawCalls + lotCalls() },
|
|
1177
|
-
{ id: "lot_triangles", name: text.metrics.lot_triangles, read: () => nursery?.stats?.triangles ?? 0 },
|
|
1178
|
-
{ id: "lot_plants", name: text.metrics.lot_plants, read: () => nursery?.stats?.plants ?? 0 },
|
|
1179
|
-
{ id: "lot_colliders", name: text.metrics.lot_colliders, read: () => nursery?.stats?.colliders ?? 0 },
|
|
1180
|
-
{ id: "lot_lod0", name: text.metrics.lot_lod0, read: () => nursery?.stats?.lod[0] ?? 0 },
|
|
1181
|
-
{ id: "lot_lod1", name: text.metrics.lot_lod1, read: () => nursery?.stats?.lod[1] ?? 0 },
|
|
1182
|
-
{ id: "lot_lod2", name: text.metrics.lot_lod2, read: () => nursery?.stats?.lod[2] ?? 0 },
|
|
1183
|
-
{ id: "lot_ms", name: text.metrics.lot_ms, unit: "ms", read: () => nursery?.stats?.buildMs ?? 0 }
|
|
1184
|
-
],
|
|
1185
|
-
attach(lent) {
|
|
1186
|
-
host = lent;
|
|
1187
|
-
three = lent.THREE;
|
|
1188
|
-
const stopChange = lent.bench?.onChange(onChange);
|
|
1189
|
-
const stopFrame = lent.onFrame(frame);
|
|
1190
|
-
applyWind();
|
|
1191
|
-
return () => {
|
|
1192
|
-
stopChange?.();
|
|
1193
|
-
stopFrame();
|
|
1194
|
-
clearHelpers();
|
|
1195
|
-
nursery?.clear();
|
|
1196
|
-
nursery = null;
|
|
1197
|
-
host = null;
|
|
1198
|
-
};
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
function loadVegetationTestModule(room, options = {}) {
|
|
1203
|
-
return room.loadModule(createVegetationTestModule(options));
|
|
1204
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
AREAS,
|
|
3
|
+
EX_HOUSE,
|
|
4
|
+
EX_PATH,
|
|
5
|
+
HOUSE,
|
|
6
|
+
LOT,
|
|
7
|
+
PLANTINGS,
|
|
8
|
+
PLANT_DEFS,
|
|
9
|
+
PLANT_SEED,
|
|
10
|
+
VEGETATION_AREA_ID,
|
|
11
|
+
VEGETATION_ITEM_IDS,
|
|
12
|
+
VEGETATION_SCENARIO_IDS,
|
|
13
|
+
WAY,
|
|
14
|
+
createNursery,
|
|
15
|
+
createPlants,
|
|
16
|
+
createVegetationTestModule,
|
|
17
|
+
loadVegetationTestModule,
|
|
18
|
+
sanitize,
|
|
19
|
+
scatterLot
|
|
20
|
+
} from "./chunk-4RHCXAUC.js";
|
|
1205
21
|
export {
|
|
1206
22
|
AREAS,
|
|
1207
23
|
EX_HOUSE,
|