@jgengine/core 0.19.0 → 0.22.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 +364 -0
- package/dist/ai/decisionGraph.d.ts +2 -0
- package/dist/ai/decisionGraph.js +6 -0
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/anim/animGraph.d.ts +33 -2
- package/dist/anim/animGraph.js +200 -12
- package/dist/anim/locomotionGraph.d.ts +11 -3
- package/dist/anim/locomotionGraph.js +25 -15
- package/dist/audio/audioFalloff.d.ts +4 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +19 -0
- package/dist/combat/abilityKit.js +40 -4
- package/dist/combat/death.d.ts +4 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- package/dist/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +116 -17
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.js +1 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/terrainMaterials.d.ts +10 -0
- package/dist/editor/terrainMaterials.js +13 -0
- package/dist/editor/types.d.ts +11 -0
- package/dist/game/cameraConfig.d.ts +3 -1
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +5 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +52 -20
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- package/dist/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +348 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- package/dist/movement/movementModel.d.ts +24 -1
- package/dist/movement/movementModel.js +74 -7
- package/dist/movement/playerMovement.d.ts +28 -1
- package/dist/movement/playerMovement.js +425 -102
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +3 -1
- package/dist/movement/solidObstacles.js +8 -5
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +3 -3
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/kinematicVehicle.d.ts +8 -3
- package/dist/physics/kinematicVehicle.js +2 -2
- package/dist/physics/physicsWorldBackend.js +19 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleDynamics.d.ts +3 -2
- package/dist/physics/vehicleDynamics.js +2 -2
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/runtime/cameraDirector.d.ts +39 -2
- package/dist/runtime/cameraDirector.js +162 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/context/combat.d.ts +11 -4
- package/dist/runtime/context/combat.js +178 -11
- package/dist/runtime/context/deathLoot.d.ts +9 -8
- package/dist/runtime/context/deathLoot.js +16 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +79 -13
- package/dist/runtime/gameContextTypes.d.ts +14 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +16 -4
- package/dist/runtime/inputSnapshot.js +31 -3
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +26 -5
- package/dist/scene/behaviorRuntime.js +263 -16
- package/dist/scene/behaviors.d.ts +30 -1
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +36 -2
- package/dist/scene/vehicleSeat.js +89 -2
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +9 -2
- package/dist/stats/eventMeter.js +5 -2
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -0
- package/dist/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingKit.d.ts +8 -3
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +3 -3
- package/dist/world/footprintGrid.d.ts +55 -0
- package/dist/world/footprintGrid.js +201 -0
- package/dist/world/grassKind.d.ts +10 -0
- package/dist/world/grassKind.js +22 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +2 -2
- package/dist/world/terraform.js +46 -18
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +11 -0
- package/dist/world/worldSolids.js +112 -1
- package/dist/world.d.ts +5 -5
- package/dist/world.js +4 -4
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-combat/SKILL.md +8 -2
- package/skills/jgengine-combat/api.md +22 -10
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +131 -37
- package/skills/jgengine-gameplay/capabilities.md +68 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +119 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -31,6 +31,14 @@ export interface RegenShieldConfig {
|
|
|
31
31
|
*/
|
|
32
32
|
pool?: RegenShieldPool;
|
|
33
33
|
}
|
|
34
|
+
/** Complete mutable shield state, including pending external damage detection. */
|
|
35
|
+
export interface RegenShieldState {
|
|
36
|
+
max: number;
|
|
37
|
+
current: number;
|
|
38
|
+
sinceDamageMs: number;
|
|
39
|
+
activeDelayMs: number;
|
|
40
|
+
observed: number | null;
|
|
41
|
+
}
|
|
34
42
|
/**
|
|
35
43
|
* A shield pool that stops regenerating for `regenDelayMs` after every hit, then refills at
|
|
36
44
|
* `regenPerSecond` — the delayed-regen primitive that replaces snapshot-comparing stat values per
|
|
@@ -38,6 +46,10 @@ export interface RegenShieldConfig {
|
|
|
38
46
|
* regenerates once it elapses.
|
|
39
47
|
*/
|
|
40
48
|
export interface RegenShield {
|
|
49
|
+
/** Detached pool value, bounds, and regen timing. */
|
|
50
|
+
state(): RegenShieldState;
|
|
51
|
+
/** Restore state; returns false without mutation when an external pool's max differs. */
|
|
52
|
+
restore(next: RegenShieldState): boolean;
|
|
41
53
|
current(): number;
|
|
42
54
|
max(): number;
|
|
43
55
|
setMax(value: number): void;
|
|
@@ -49,6 +61,8 @@ export interface RegenShield {
|
|
|
49
61
|
/** Absorb `amount` and reset the regen grace timer; returns damage that got through (0 while the shield holds). */
|
|
50
62
|
damage(amount: number): number;
|
|
51
63
|
/** Add `amount` without touching the regen timer (a pickup or scripted refill). */
|
|
64
|
+
refill(amount: number): void;
|
|
65
|
+
/** @deprecated Use `refill(amount)` for pickups; `restore(state)` loads persisted state. */
|
|
52
66
|
restore(amount: number): void;
|
|
53
67
|
set(value: number): void;
|
|
54
68
|
/**
|
|
@@ -39,7 +39,30 @@ export function createRegenShield(config) {
|
|
|
39
39
|
sinceDamageMs = 0;
|
|
40
40
|
observed = live;
|
|
41
41
|
}
|
|
42
|
+
function refill(amount) {
|
|
43
|
+
if (amount <= 0)
|
|
44
|
+
return;
|
|
45
|
+
syncExternal();
|
|
46
|
+
write(readCurrent() + amount);
|
|
47
|
+
observed = pool === undefined ? observed : readCurrent();
|
|
48
|
+
}
|
|
49
|
+
function restore(next) {
|
|
50
|
+
if (typeof next === "number")
|
|
51
|
+
return refill(next);
|
|
52
|
+
if (pool !== undefined && readMax() !== next.max)
|
|
53
|
+
return false;
|
|
54
|
+
if (pool === undefined)
|
|
55
|
+
ownMax = Math.max(0, next.max);
|
|
56
|
+
write(next.current);
|
|
57
|
+
sinceDamageMs = Math.max(0, next.sinceDamageMs);
|
|
58
|
+
activeDelayMs = Math.max(0, next.activeDelayMs);
|
|
59
|
+
observed = pool === undefined ? null : next.observed;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
42
62
|
return {
|
|
63
|
+
state: () => ({ max: readMax(), current: readCurrent(), sinceDamageMs, activeDelayMs, observed }),
|
|
64
|
+
restore,
|
|
65
|
+
refill,
|
|
43
66
|
current: readCurrent,
|
|
44
67
|
max: readMax,
|
|
45
68
|
setMax(value) {
|
|
@@ -65,13 +88,6 @@ export function createRegenShield(config) {
|
|
|
65
88
|
observed = pool === undefined ? observed : readCurrent();
|
|
66
89
|
return overflow;
|
|
67
90
|
},
|
|
68
|
-
restore(amount) {
|
|
69
|
-
if (amount <= 0)
|
|
70
|
-
return;
|
|
71
|
-
syncExternal();
|
|
72
|
-
write(readCurrent() + amount);
|
|
73
|
-
observed = pool === undefined ? observed : readCurrent();
|
|
74
|
-
},
|
|
75
91
|
set(value) {
|
|
76
92
|
write(value);
|
|
77
93
|
observed = pool === undefined ? observed : readCurrent();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContentIssue } from "../game/contentValidation.js";
|
|
2
|
+
import type { RecipeDef } from "./recipe.js";
|
|
3
|
+
/** Caller catalogs for native crafting references; omission leaves that namespace unchecked. */
|
|
4
|
+
export interface RecipeCatalogValidationOptions {
|
|
5
|
+
hasReference?(kind: "item" | "station" | "unlock", id: string): boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validate native recipes before registration: ids, counts, timing and caller-catalog references.
|
|
9
|
+
* Empty outputs are valid for game-owned benefits; fractional and zero counts are allowed.
|
|
10
|
+
* Negative/nonfinite counts, durations and ranges reject. Duplicate input rows reject because the
|
|
11
|
+
* crafting input check does not aggregate them; consolidate their counts into one row.
|
|
12
|
+
* No price, scarcity, story or cycle policy is imposed. O(recipes + item rows + requirements).
|
|
13
|
+
*
|
|
14
|
+
* @capability recipe-catalog-validation locate broken crafting item/station/unlock references, duplicate inputs and invalid authored quantities with repair guidance
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateRecipeCatalog(recipes: readonly RecipeDef[], options?: RecipeCatalogValidationOptions): ContentIssue[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate native recipes before registration: ids, counts, timing and caller-catalog references.
|
|
3
|
+
* Empty outputs are valid for game-owned benefits; fractional and zero counts are allowed.
|
|
4
|
+
* Negative/nonfinite counts, durations and ranges reject. Duplicate input rows reject because the
|
|
5
|
+
* crafting input check does not aggregate them; consolidate their counts into one row.
|
|
6
|
+
* No price, scarcity, story or cycle policy is imposed. O(recipes + item rows + requirements).
|
|
7
|
+
*
|
|
8
|
+
* @capability recipe-catalog-validation locate broken crafting item/station/unlock references, duplicate inputs and invalid authored quantities with repair guidance
|
|
9
|
+
*/
|
|
10
|
+
export function validateRecipeCatalog(recipes, options = {}) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const ids = new Set();
|
|
13
|
+
for (let index = 0; index < recipes.length; index++) {
|
|
14
|
+
const recipe = recipes[index];
|
|
15
|
+
const path = `/${index}`;
|
|
16
|
+
const add = (code, field, message, repair, referenceId) => {
|
|
17
|
+
issues.push({ code, severity: "error", path: `${path}/${field}`, contentId: recipe.id,
|
|
18
|
+
...(referenceId === undefined ? {} : { referenceId }), message, repair });
|
|
19
|
+
};
|
|
20
|
+
const reference = (kind, id, field) => {
|
|
21
|
+
if (options.hasReference?.(kind, id) === false) {
|
|
22
|
+
add("unknown-reference", field, `Recipe "${recipe.id}" references unknown ${kind} "${id}".`, `Correct this reference or declare "${id}" in the ${kind} catalog.`, id);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
if (ids.has(recipe.id))
|
|
26
|
+
add("duplicate-recipe", "id", `Recipe id "${recipe.id}" repeats.`, "Give recipes distinct ids and update their references, or remove the duplicate.", recipe.id);
|
|
27
|
+
ids.add(recipe.id);
|
|
28
|
+
for (const direction of ["inputs", "outputs"]) {
|
|
29
|
+
const inputs = new Set();
|
|
30
|
+
for (let row = 0; row < recipe[direction].length; row++) {
|
|
31
|
+
const item = recipe[direction][row];
|
|
32
|
+
const field = `${direction}/${row}`;
|
|
33
|
+
reference("item", item.itemId, `${field}/itemId`);
|
|
34
|
+
if (!Number.isFinite(item.count) || item.count < 0) {
|
|
35
|
+
add("invalid-count", `${field}/count`, "Recipe count must be finite and nonnegative.", "Use a finite nonnegative count; enforce any stricter discrete-unit policy in the game.");
|
|
36
|
+
}
|
|
37
|
+
if (direction === "inputs" && inputs.has(item.itemId)) {
|
|
38
|
+
add("duplicate-input", `${field}/itemId`, `Recipe "${recipe.id}" repeats input "${item.itemId}".`, "Combine this item's input counts into one row so the crafting check sees the total.", item.itemId);
|
|
39
|
+
}
|
|
40
|
+
inputs.add(item.itemId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (recipe.station !== undefined)
|
|
44
|
+
reference("station", recipe.station, "station");
|
|
45
|
+
for (const [row, id] of (recipe.requires ?? []).entries())
|
|
46
|
+
reference("unlock", id, `requires/${row}`);
|
|
47
|
+
for (const field of ["seconds", "stationRange"]) {
|
|
48
|
+
const value = recipe[field];
|
|
49
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
|
|
50
|
+
add("invalid-number", field, `Recipe ${field} must be finite and nonnegative.`, "Use a finite nonnegative value, or omit the optional field.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
@@ -44,7 +44,7 @@ export interface PostAuctionInput {
|
|
|
44
44
|
now: number;
|
|
45
45
|
}
|
|
46
46
|
/** Why {@link AuctionBook.post} refused an auction. */
|
|
47
|
-
export type PostAuctionReason = "invalid-count" | "invalid-price" | "invalid-increment" | "invalid-buyout" | "auction-cap-reached";
|
|
47
|
+
export type PostAuctionReason = "invalid-count" | "invalid-price" | "invalid-increment" | "invalid-buyout" | "auction-cap-reached" | "duplicate-id";
|
|
48
48
|
/** Result of {@link AuctionBook.post}. */
|
|
49
49
|
export type PostAuctionResult = {
|
|
50
50
|
status: "ok";
|
|
@@ -92,7 +92,15 @@ export function createAuctionBook(config) {
|
|
|
92
92
|
if (countOf(input.sellerId) >= config.maxAuctionsPerSeller) {
|
|
93
93
|
return { status: "rejected", reason: "auction-cap-reached" };
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
let id = input.id;
|
|
96
|
+
if (id === undefined) {
|
|
97
|
+
do {
|
|
98
|
+
id = `auction_${(counter += 1)}`;
|
|
99
|
+
} while (auctions.has(id));
|
|
100
|
+
}
|
|
101
|
+
else if (auctions.has(id)) {
|
|
102
|
+
return { status: "rejected", reason: "duplicate-id" };
|
|
103
|
+
}
|
|
96
104
|
const state = {
|
|
97
105
|
id,
|
|
98
106
|
sellerId: input.sellerId,
|
|
@@ -196,6 +204,8 @@ export function createAuctionBook(config) {
|
|
|
196
204
|
return claimed;
|
|
197
205
|
},
|
|
198
206
|
claimItem(playerId, itemId, count) {
|
|
207
|
+
if (!Number.isInteger(count) || count < 1)
|
|
208
|
+
return false;
|
|
199
209
|
const box = boxOf(playerId);
|
|
200
210
|
const index = box.items.findIndex((stack) => stack.itemId === itemId);
|
|
201
211
|
if (index === -1)
|
|
@@ -29,7 +29,7 @@ export interface PostListingInput {
|
|
|
29
29
|
now: number;
|
|
30
30
|
}
|
|
31
31
|
/** Why {@link ListingBook.post} refused a listing. */
|
|
32
|
-
export type PostListingReason = "invalid-count" | "invalid-price" | "price-too-low" | "price-too-high" | "listing-cap-reached";
|
|
32
|
+
export type PostListingReason = "invalid-count" | "invalid-price" | "price-too-low" | "price-too-high" | "listing-cap-reached" | "duplicate-id";
|
|
33
33
|
/** Result of {@link ListingBook.post}. */
|
|
34
34
|
export type PostListingResult = {
|
|
35
35
|
status: "ok";
|
|
@@ -55,7 +55,15 @@ export function createListingBook(config) {
|
|
|
55
55
|
if (countOf(input.sellerId) >= config.maxListingsPerSeller) {
|
|
56
56
|
return { status: "rejected", reason: "listing-cap-reached" };
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
let id = input.id;
|
|
59
|
+
if (id === undefined) {
|
|
60
|
+
do {
|
|
61
|
+
id = `listing_${(counter += 1)}`;
|
|
62
|
+
} while (listings.has(id));
|
|
63
|
+
}
|
|
64
|
+
else if (listings.has(id)) {
|
|
65
|
+
return { status: "rejected", reason: "duplicate-id" };
|
|
66
|
+
}
|
|
59
67
|
const listing = {
|
|
60
68
|
id,
|
|
61
69
|
sellerId: input.sellerId,
|
|
@@ -127,6 +135,8 @@ export function createListingBook(config) {
|
|
|
127
135
|
return claimed;
|
|
128
136
|
},
|
|
129
137
|
claimItem(sellerId, itemId, count) {
|
|
138
|
+
if (!Number.isInteger(count) || count < 0)
|
|
139
|
+
return false;
|
|
130
140
|
const box = boxOf(sellerId);
|
|
131
141
|
const index = box.items.findIndex((stack) => stack.itemId === itemId);
|
|
132
142
|
if (index === -1)
|
|
@@ -116,7 +116,7 @@ export function advanceLedger(ledger, toSeconds, options = {}) {
|
|
|
116
116
|
const catchUp = rule.catchUp ?? "each";
|
|
117
117
|
// Reserve-capped nominal amount per candidate cycle, in chronological order.
|
|
118
118
|
const nominal = reserveCappedAmounts(dueCount, rule.amount, cursor.reserveRemaining);
|
|
119
|
-
const depleted = cursor.reserveRemaining !== undefined && nominal.remaining <=
|
|
119
|
+
const depleted = cursor.reserveRemaining !== undefined && nominal.remaining <= 0;
|
|
120
120
|
const cycles = buildCycles(catchUp, nominal.amounts, firstDue, rule.everySeconds, perRuleLimit, maxCycles);
|
|
121
121
|
if (wasFirstFire && cycles.settled.length > 0) {
|
|
122
122
|
events.push({ kind: "started", ruleId: id, atSeconds: cycles.settled[0].atSeconds, detail: 0 });
|
|
@@ -200,12 +200,12 @@ function reserveCappedAmounts(count, amount, reserveRemaining) {
|
|
|
200
200
|
}
|
|
201
201
|
const amounts = [];
|
|
202
202
|
let remaining = reserveRemaining;
|
|
203
|
-
for (let i = 0; i < count && remaining >
|
|
203
|
+
for (let i = 0; i < count && remaining > 0; i += 1) {
|
|
204
204
|
const take = Math.min(amount, remaining);
|
|
205
205
|
amounts.push(take);
|
|
206
206
|
remaining -= take;
|
|
207
207
|
}
|
|
208
|
-
if (remaining <
|
|
208
|
+
if (remaining < 0)
|
|
209
209
|
remaining = 0;
|
|
210
210
|
return { amounts, remaining, consumed: reserveRemaining - remaining };
|
|
211
211
|
}
|
|
@@ -53,17 +53,17 @@ function validateEntry(entry) {
|
|
|
53
53
|
*/
|
|
54
54
|
export function createShopStock(config = {}) {
|
|
55
55
|
// Insertion-ordered map of live records (Map preserves insertion order for list()/snapshot()).
|
|
56
|
-
|
|
56
|
+
let entries = new Map();
|
|
57
57
|
const listeners = new Set();
|
|
58
58
|
function notify() {
|
|
59
59
|
for (const listener of listeners)
|
|
60
60
|
listener();
|
|
61
61
|
}
|
|
62
|
-
function ingest(entry) {
|
|
62
|
+
function ingest(entry, target = entries) {
|
|
63
63
|
validateEntry(entry);
|
|
64
|
-
if (
|
|
64
|
+
if (target.has(entry.id))
|
|
65
65
|
throw new Error(`duplicate shop entry id: ${entry.id}`);
|
|
66
|
-
|
|
66
|
+
target.set(entry.id, cloneEntry(entry));
|
|
67
67
|
}
|
|
68
68
|
for (const entry of config.entries ?? [])
|
|
69
69
|
ingest(entry);
|
|
@@ -148,9 +148,10 @@ export function createShopStock(config = {}) {
|
|
|
148
148
|
return { entries: Array.from(entries.values(), cloneEntry) };
|
|
149
149
|
},
|
|
150
150
|
restore(snapshot) {
|
|
151
|
-
|
|
151
|
+
const next = new Map();
|
|
152
152
|
for (const entry of snapshot.entries)
|
|
153
|
-
ingest(entry);
|
|
153
|
+
ingest(entry, next);
|
|
154
|
+
entries = next;
|
|
154
155
|
notify();
|
|
155
156
|
},
|
|
156
157
|
};
|
package/dist/economy/techTree.js
CHANGED
|
@@ -50,15 +50,16 @@ export function createTechTree(defs = []) {
|
|
|
50
50
|
const nodes = new Map();
|
|
51
51
|
for (const def of defs)
|
|
52
52
|
nodes.set(def.id, def);
|
|
53
|
-
const
|
|
53
|
+
const definitions = Array.from(nodes.values());
|
|
54
|
+
const unlocks = createUnlocks(definitions.map((d) => ({ id: d.id, category: d.category })));
|
|
54
55
|
function stateOf(userId) {
|
|
55
56
|
return unlocks.list(userId);
|
|
56
57
|
}
|
|
57
58
|
return {
|
|
58
59
|
has: (userId, id) => unlocks.has(userId, id),
|
|
59
|
-
canUnlock: (userId, id) => canUnlockTech(
|
|
60
|
+
canUnlock: (userId, id) => canUnlockTech(definitions, stateOf(userId), id),
|
|
60
61
|
unlock(userId, id) {
|
|
61
|
-
const check = canUnlockTech(
|
|
62
|
+
const check = canUnlockTech(definitions, stateOf(userId), id);
|
|
62
63
|
if (!check.ok)
|
|
63
64
|
return check;
|
|
64
65
|
const node = nodes.get(id);
|
|
@@ -67,10 +68,10 @@ export function createTechTree(defs = []) {
|
|
|
67
68
|
unlocks.grant(userId, extra);
|
|
68
69
|
return { ok: true };
|
|
69
70
|
},
|
|
70
|
-
available: (userId) => availableTech(
|
|
71
|
-
recipes: (userId) => unlockedRecipes(
|
|
71
|
+
available: (userId) => availableTech(definitions, stateOf(userId)),
|
|
72
|
+
recipes: (userId) => unlockedRecipes(definitions, stateOf(userId)),
|
|
72
73
|
list: (userId) => unlocks.list(userId),
|
|
73
|
-
tree: (categoryId) => unlockTree(
|
|
74
|
+
tree: (categoryId) => unlockTree(definitions, categoryId),
|
|
74
75
|
node: (id) => nodes.get(id) ?? null,
|
|
75
76
|
snapshot: (userId) => unlocks.snapshot(userId),
|
|
76
77
|
hydrate: (userId, ids) => unlocks.hydrate(userId, ids),
|
|
@@ -21,8 +21,8 @@ export function createUnlockPoints(config = {}) {
|
|
|
21
21
|
unlocked.clear();
|
|
22
22
|
earned = sanitizeAmount(state.earned);
|
|
23
23
|
for (const [id, cost] of Object.entries(state.unlocked)) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (Number.isFinite(cost) && cost >= 0)
|
|
25
|
+
unlocked.set(id, cost);
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
if (config.state !== undefined) {
|
|
@@ -91,10 +91,7 @@ export function createUnlockPoints(config = {}) {
|
|
|
91
91
|
return refunded;
|
|
92
92
|
},
|
|
93
93
|
snapshot() {
|
|
94
|
-
|
|
95
|
-
for (const [id, cost] of unlocked)
|
|
96
|
-
map[id] = cost;
|
|
97
|
-
return { earned, unlocked: map };
|
|
94
|
+
return { earned, unlocked: Object.fromEntries(unlocked) };
|
|
98
95
|
},
|
|
99
96
|
hydrate(state) {
|
|
100
97
|
load(state);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeEditorSimulation } from "./simulation.js";
|
|
2
|
+
import { parseEditorMaterialAsset, authoredMaterialAssignments, sameMaterialSelector } from "./materialAuthoring.js";
|
|
3
|
+
import { parseMaterialAssignments, validateMaterialAssignments } from "../material/materialAsset.js";
|
|
4
|
+
import { parseStaticPrefabBake } from "./staticPrefab.js";
|
|
5
|
+
import { applyDeltaToSnapshot, applySurfaceDeltaToSnapshot, applyWeightDeltaToSnapshot, editableTerrainFromSnapshot, } from "../world/terraform.js";
|
|
2
6
|
import { patchUiPanel, removeUiPanel } from "../ui/hudDocument.js";
|
|
3
|
-
import { cloneEditorDocument, collectDescendants, createPrefabFragment, editorDocumentExtras, ensureUniqueEditorId, extractEditorFragment, findEditorCollection, findEditorMarker, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectLocked, wouldCreateCycle, } from "./document.js";
|
|
7
|
+
import { cloneEditorDocument, collectDescendants, createEmptyEditorDocument, createPrefabFragment, decodeEditorDocument, editorDocumentExtras, ensureUniqueEditorId, extractEditorFragment, findEditorCollection, findEditorMarker, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectLocked, wouldCreateCycle, } from "./document.js";
|
|
4
8
|
import { fillGridRect, floodFillGrid, migrateGridLayer, paintGridCells, resizeGridLayer, } from "./grid.js";
|
|
5
9
|
import { parseParams } from "../scene/sceneKinds.js";
|
|
6
10
|
/** Pure document mutation helpers + exhaustive EditorCommand apply table. */
|
|
@@ -80,6 +84,11 @@ function insertFragment(state, fragment, offset, renameAll) {
|
|
|
80
84
|
const volumes = clones.volumes.map((volume) => withId({ ...volume, center: shifted(volume.center, offset) }));
|
|
81
85
|
const paths = clones.paths.map((path) => withId({ ...path, points: path.points.map((point) => shifted(point, offset)) }));
|
|
82
86
|
const annotations = clones.annotations.map((note) => withId({ ...note, position: shifted(note.position, offset) }));
|
|
87
|
+
for (const asset of clones.materialAssets ?? []) {
|
|
88
|
+
const existing = doc.materialAssets?.find((item) => item.id === asset.id);
|
|
89
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(asset))
|
|
90
|
+
throw new TypeError(`Material ID conflict: ${asset.id}; rename the imported material before insertion.`);
|
|
91
|
+
}
|
|
83
92
|
if (newIds.length === 0)
|
|
84
93
|
return state;
|
|
85
94
|
return {
|
|
@@ -90,6 +99,7 @@ function insertFragment(state, fragment, offset, renameAll) {
|
|
|
90
99
|
paths: [...doc.paths, ...paths],
|
|
91
100
|
annotations: [...doc.annotations, ...annotations],
|
|
92
101
|
...editorDocumentExtras(doc),
|
|
102
|
+
...(doc.materialAssets === undefined && clones.materialAssets === undefined ? {} : { materialAssets: [...(doc.materialAssets ?? []), ...(clones.materialAssets ?? []).filter((asset) => !doc.materialAssets?.some((item) => item.id === asset.id))] }),
|
|
93
103
|
},
|
|
94
104
|
selection: newIds,
|
|
95
105
|
};
|
|
@@ -97,6 +107,7 @@ function insertFragment(state, fragment, offset, renameAll) {
|
|
|
97
107
|
function fragmentAsDocument(fragment) {
|
|
98
108
|
return {
|
|
99
109
|
version: 1,
|
|
110
|
+
...(fragment.materialAssets === undefined ? {} : { materialAssets: structuredClone(fragment.materialAssets) }),
|
|
100
111
|
markers: [...fragment.markers],
|
|
101
112
|
volumes: [...fragment.volumes],
|
|
102
113
|
paths: [...fragment.paths],
|
|
@@ -448,13 +459,25 @@ const mutationHandlers = {
|
|
|
448
459
|
setTerrain: (state, command) => ({ ...state, document: { ...state.document, terrain: command.terrain } }),
|
|
449
460
|
setMinimapBake: (state, command) => ({ ...state, document: { ...state.document, minimap: command.minimap } }),
|
|
450
461
|
setBake: (state, command) => ({ ...state, document: { ...state.document, bakes: [...(state.document.bakes ?? []).filter((bake) => bake.id !== command.bake.id), command.bake] } }),
|
|
462
|
+
setSimulation: (state, command) => {
|
|
463
|
+
if (command.simulation === undefined) {
|
|
464
|
+
const { simulation: _removed, ...document } = state.document;
|
|
465
|
+
void _removed;
|
|
466
|
+
return { ...state, document };
|
|
467
|
+
}
|
|
468
|
+
return { ...state, document: { ...state.document, simulation: decodeEditorSimulation(command.simulation, state.document) } };
|
|
469
|
+
},
|
|
451
470
|
setEnvironment: (state, command) => {
|
|
452
471
|
if (command.environment === undefined) {
|
|
453
472
|
const { environment: _removed, ...rest } = state.document;
|
|
454
473
|
void _removed;
|
|
455
474
|
return { ...state, document: rest };
|
|
456
475
|
}
|
|
457
|
-
|
|
476
|
+
const decoded = decodeEditorDocument({ ...createEmptyEditorDocument(), environment: command.environment });
|
|
477
|
+
if (!decoded.ok) {
|
|
478
|
+
throw new Error(`invalid editor environment: ${decoded.errors.map((error) => `${error.path} ${error.message}`).join("; ")}`);
|
|
479
|
+
}
|
|
480
|
+
return { ...state, document: { ...state.document, environment: decoded.document.environment } };
|
|
458
481
|
},
|
|
459
482
|
clearTerrain: (state) => {
|
|
460
483
|
const nextDoc = {
|
|
@@ -470,6 +493,7 @@ const mutationHandlers = {
|
|
|
470
493
|
...(state.document.ui === undefined ? {} : { ui: state.document.ui }),
|
|
471
494
|
...(state.document.directives === undefined ? {} : { directives: state.document.directives }),
|
|
472
495
|
...(state.document.minimap === undefined ? {} : { minimap: state.document.minimap }),
|
|
496
|
+
...(state.document.simulation === undefined ? {} : { simulation: structuredClone(state.document.simulation) }),
|
|
473
497
|
...(state.document.environment === undefined ? {} : { environment: state.document.environment }),
|
|
474
498
|
};
|
|
475
499
|
return { ...state, document: nextDoc };
|
|
@@ -522,19 +546,9 @@ const mutationHandlers = {
|
|
|
522
546
|
setTerrainLayers: (state, command) => {
|
|
523
547
|
if (state.document.terrain === undefined)
|
|
524
548
|
return state;
|
|
525
|
-
const terrain = state.document.terrain;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// Keep hand-painted weights only when the layer id sequence is unchanged (a params-only edit).
|
|
529
|
-
const sameSequence = before.length === layers.length && before.every((layer, i) => layer.id === layers[i].id);
|
|
530
|
-
const next = sameSequence
|
|
531
|
-
? { ...terrain, layers }
|
|
532
|
-
: (() => {
|
|
533
|
-
const copy = { ...terrain, layers };
|
|
534
|
-
delete copy.weights;
|
|
535
|
-
return copy;
|
|
536
|
-
})();
|
|
537
|
-
return { ...state, document: { ...state.document, terrain: next } };
|
|
549
|
+
const terrain = editableTerrainFromSnapshot(state.document.terrain);
|
|
550
|
+
terrain.setLayers(command.layers);
|
|
551
|
+
return { ...state, document: { ...state.document, terrain: terrain.snapshot() } };
|
|
538
552
|
},
|
|
539
553
|
convertScatterToObjects: (state, command) => {
|
|
540
554
|
const path = state.document.paths.find((entry) => entry.id === command.pathId);
|
|
@@ -570,6 +584,17 @@ const mutationHandlers = {
|
|
|
570
584
|
},
|
|
571
585
|
};
|
|
572
586
|
},
|
|
587
|
+
setPrefabStaticBake: (state, command) => {
|
|
588
|
+
if (findEditorPrefab(state.document, command.prefabId) === undefined)
|
|
589
|
+
return null;
|
|
590
|
+
const bake = command.bake === null ? undefined : parseStaticPrefabBake(command.bake);
|
|
591
|
+
return { ...state, document: { ...state.document, prefabs: state.document.prefabs.map((prefab) => {
|
|
592
|
+
if (prefab.id !== command.prefabId)
|
|
593
|
+
return prefab;
|
|
594
|
+
const { staticBake: previous, ...source } = prefab;
|
|
595
|
+
return { ...source, ...(bake === undefined ? {} : { staticBake: bake }) };
|
|
596
|
+
}) } };
|
|
597
|
+
},
|
|
573
598
|
deletePrefab: (state, command) => ({
|
|
574
599
|
...state,
|
|
575
600
|
document: {
|
|
@@ -584,6 +609,7 @@ const mutationHandlers = {
|
|
|
584
609
|
const instanceId = command.instanceId ??
|
|
585
610
|
`${prefab.id}_inst_${Date.now().toString(36)}_${Math.floor(Math.random() * 1_296_000).toString(36)}`;
|
|
586
611
|
const tagged = fragmentAsDocument({
|
|
612
|
+
...(prefab.fragment.materialAssets === undefined ? {} : { materialAssets: prefab.fragment.materialAssets }),
|
|
587
613
|
markers: prefab.fragment.markers.map((marker) => tagWithInstance(marker, prefab.id, instanceId)),
|
|
588
614
|
volumes: prefab.fragment.volumes.map((volume) => tagWithInstance(volume, prefab.id, instanceId)),
|
|
589
615
|
paths: prefab.fragment.paths.map((path) => tagWithInstance(path, prefab.id, instanceId)),
|
|
@@ -738,6 +764,53 @@ const mutationHandlers = {
|
|
|
738
764
|
const stamp = (item) => ids.has(item.id) ? { ...item, meta: { ...item.meta, materialId: command.materialId } } : item;
|
|
739
765
|
return { ...state, document: { ...state.document, ...mapPlaceables(state.document, stamp) } };
|
|
740
766
|
},
|
|
767
|
+
upsertMaterialAsset: (state, command) => {
|
|
768
|
+
const asset = parseEditorMaterialAsset(command.asset);
|
|
769
|
+
return { ...state, document: { ...state.document, materialAssets: [...(state.document.materialAssets ?? []).filter((item) => item.id !== asset.id), asset] } };
|
|
770
|
+
},
|
|
771
|
+
removeMaterialAsset: (state, command) => {
|
|
772
|
+
const used = state.document.markers.some((marker) => authoredMaterialAssignments(marker.meta).some((assignment) => assignment.materialId === command.id))
|
|
773
|
+
|| state.document.prefabs.some((prefab) => prefab.fragment.markers.some((marker) => authoredMaterialAssignments(marker.meta).some((assignment) => assignment.materialId === command.id)));
|
|
774
|
+
if (used)
|
|
775
|
+
throw new TypeError(`Material ${command.id} is assigned; clear its references before removal.`);
|
|
776
|
+
if (!state.document.materialAssets?.some((asset) => asset.id === command.id))
|
|
777
|
+
throw new TypeError(`Material not found: ${command.id}`);
|
|
778
|
+
return { ...state, document: { ...state.document, materialAssets: state.document.materialAssets.filter((asset) => asset.id !== command.id) } };
|
|
779
|
+
},
|
|
780
|
+
assignMaterialAsset: (state, command) => {
|
|
781
|
+
const assignment = parseMaterialAssignments([{ materialId: command.materialId, selector: command.selector }])[0];
|
|
782
|
+
const errors = validateMaterialAssignments([assignment], state.document.materialAssets ?? []).filter((item) => item.severity === "error");
|
|
783
|
+
if (errors.length > 0)
|
|
784
|
+
throw new TypeError(errors.map((item) => item.message).join("; "));
|
|
785
|
+
const ids = new Set(command.ids);
|
|
786
|
+
for (const id of ids) {
|
|
787
|
+
if (!state.document.markers.some((marker) => marker.id === id))
|
|
788
|
+
throw new TypeError(`material target is not a marker: ${id}`);
|
|
789
|
+
if (isEditorObjectLocked(state.document, id))
|
|
790
|
+
throw new TypeError(`object is locked: ${id}`);
|
|
791
|
+
}
|
|
792
|
+
const markers = state.document.markers.map((marker) => {
|
|
793
|
+
if (!ids.has(marker.id))
|
|
794
|
+
return marker;
|
|
795
|
+
const assignments = authoredMaterialAssignments(marker.meta).filter((item) => !sameMaterialSelector(item.selector, assignment.selector));
|
|
796
|
+
return { ...marker, meta: { ...marker.meta, materialAssignments: [...assignments, structuredClone(assignment)] } };
|
|
797
|
+
});
|
|
798
|
+
return { ...state, document: { ...state.document, markers } };
|
|
799
|
+
},
|
|
800
|
+
clearMaterialAssets: (state, command) => {
|
|
801
|
+
const ids = new Set(command.ids);
|
|
802
|
+
for (const id of ids)
|
|
803
|
+
if (isEditorObjectLocked(state.document, id))
|
|
804
|
+
throw new TypeError(`object is locked: ${id}`);
|
|
805
|
+
const markers = state.document.markers.map((marker) => {
|
|
806
|
+
if (!ids.has(marker.id) || marker.meta?.materialAssignments === undefined)
|
|
807
|
+
return marker;
|
|
808
|
+
const meta = { ...marker.meta };
|
|
809
|
+
delete meta.materialAssignments;
|
|
810
|
+
return { ...marker, meta };
|
|
811
|
+
});
|
|
812
|
+
return { ...state, document: { ...state.document, markers } };
|
|
813
|
+
},
|
|
741
814
|
undo: () => null,
|
|
742
815
|
redo: () => null,
|
|
743
816
|
};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { EditorSimulation } from "./simulation.js";
|
|
2
|
+
import type { MaterialAsset, MaterialSelector } from "../material/materialAsset.js";
|
|
3
|
+
import { type StaticPrefabBake } from "./staticPrefab.js";
|
|
1
4
|
import { type SurfaceDelta, type TerraformDelta, type TerrainMaterialLayer, type WeightDelta } from "../world/terraform.js";
|
|
2
5
|
import type { EditorUiDocument, EditorUiPanelLayout } from "../ui/hudDocument.js";
|
|
3
6
|
import type { EditorGridCellEdit, EditorGridLayer } from "./grid.js";
|
|
@@ -134,6 +137,9 @@ export type EditorCommand = {
|
|
|
134
137
|
| {
|
|
135
138
|
type: "setEnvironment";
|
|
136
139
|
environment: EditorEnvironment | undefined;
|
|
140
|
+
} | {
|
|
141
|
+
type: "setSimulation";
|
|
142
|
+
simulation: EditorSimulation | undefined;
|
|
137
143
|
} | {
|
|
138
144
|
type: "convertScatterToObjects";
|
|
139
145
|
pathId: string;
|
|
@@ -154,6 +160,10 @@ export type EditorCommand = {
|
|
|
154
160
|
} | {
|
|
155
161
|
type: "deletePrefab";
|
|
156
162
|
prefabId: string;
|
|
163
|
+
} | {
|
|
164
|
+
type: "setPrefabStaticBake";
|
|
165
|
+
prefabId: string;
|
|
166
|
+
bake: StaticPrefabBake | null;
|
|
157
167
|
} | {
|
|
158
168
|
type: "createCollection";
|
|
159
169
|
id: string;
|
|
@@ -208,6 +218,20 @@ export type EditorCommand = {
|
|
|
208
218
|
label?: string;
|
|
209
219
|
meta?: Record<string, unknown>;
|
|
210
220
|
};
|
|
221
|
+
} | {
|
|
222
|
+
type: "upsertMaterialAsset";
|
|
223
|
+
asset: MaterialAsset;
|
|
224
|
+
} | {
|
|
225
|
+
type: "removeMaterialAsset";
|
|
226
|
+
id: string;
|
|
227
|
+
} | {
|
|
228
|
+
type: "assignMaterialAsset";
|
|
229
|
+
ids: readonly string[];
|
|
230
|
+
materialId: string;
|
|
231
|
+
selector: MaterialSelector;
|
|
232
|
+
} | {
|
|
233
|
+
type: "clearMaterialAssets";
|
|
234
|
+
ids: readonly string[];
|
|
211
235
|
} | {
|
|
212
236
|
type: "assignMaterial";
|
|
213
237
|
ids: readonly string[];
|
|
@@ -274,14 +298,27 @@ export interface EditorSession {
|
|
|
274
298
|
getState(): EditorSessionState;
|
|
275
299
|
subscribe(listener: (state: EditorSessionState) => void): () => void;
|
|
276
300
|
dispatch(command: EditorCommand, options?: EditorDispatchOptions): EditorSessionState;
|
|
301
|
+
/** Stage a command batch, then commit one undo step and notification; failures leave all state intact. */
|
|
302
|
+
transaction(commands: readonly EditorCommand[]): EditorTransactionResult;
|
|
277
303
|
exportJson(pretty?: boolean): string;
|
|
278
304
|
canUndo(): boolean;
|
|
279
305
|
canRedo(): boolean;
|
|
280
306
|
}
|
|
307
|
+
/** Atomic command-batch outcome; a failure identifies the zero-based command index. */
|
|
308
|
+
export type EditorTransactionResult = {
|
|
309
|
+
ok: true;
|
|
310
|
+
state: EditorSessionState;
|
|
311
|
+
changed: boolean;
|
|
312
|
+
} | {
|
|
313
|
+
ok: false;
|
|
314
|
+
error: string;
|
|
315
|
+
commandIndex?: number;
|
|
316
|
+
};
|
|
281
317
|
/** Creates an editor session with undo/redo history seeded from an initial document.
|
|
318
|
+
* Environment commands validate and copy their candidate bag before publishing state or history.
|
|
282
319
|
* @internal
|
|
283
320
|
*/
|
|
284
|
-
export declare function createEditorSession(initial: EditorDocument, historyLimit?: number): EditorSession;
|
|
321
|
+
export declare function createEditorSession(initial: EditorDocument, historyLimit?: number, validateDocument?: (document: EditorDocument) => void): EditorSession;
|
|
285
322
|
/** Compact snapshot of a session state — counts, selection, and the selected object.
|
|
286
323
|
* @internal
|
|
287
324
|
*/
|