@jgengine/core 0.6.0 → 0.8.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 +214 -2
- package/README.md +2 -0
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +61 -0
- package/dist/ai/spawnDirector.js +162 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +64 -0
- package/dist/cards/cardPile.js +175 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +47 -0
- package/dist/combat/abilityKit.js +139 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +12 -1
- package/dist/combat/effects.js +31 -19
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +31 -0
- package/dist/combat/telegraph.js +51 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +87 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +516 -8
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +100 -0
- package/dist/game/race.js +236 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +69 -0
- package/dist/game/social.js +138 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +95 -0
- package/dist/game/worldItem.js +138 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +69 -0
- package/dist/input/pointer.js +65 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +115 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +71 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +56 -0
- package/dist/nav/navGrid.js +265 -0
- package/dist/nav/pathFollow.d.ts +31 -0
- package/dist/nav/pathFollow.js +69 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +124 -2
- package/dist/physics/physicsWorld.js +493 -41
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +143 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +99 -0
- package/dist/physics/vehicleBody.js +233 -0
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +107 -3
- package/dist/runtime/gameContext.js +253 -7
- package/dist/runtime/hostPersistence.d.ts +39 -0
- package/dist/runtime/hostPersistence.js +54 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +9 -1
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +79 -0
- package/dist/session/roundState.js +158 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +68 -0
- package/dist/tactics/tacticalGrid.js +261 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +60 -0
- package/dist/turn/turnLoop.js +223 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +69 -0
- package/dist/world/placementController.js +153 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
package/dist/combat/effects.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { applyPoolDelta } from "../scene/entityStats.js";
|
|
2
2
|
import { distanceBetween } from "../scene/spatial.js";
|
|
3
|
+
export function resolveAreaTargets(spatial, input, accept) {
|
|
4
|
+
const falloff = input.falloff ?? "none";
|
|
5
|
+
const los = input.los ?? true;
|
|
6
|
+
const targets = [];
|
|
7
|
+
for (const instanceId of spatial.inRadius(input.at, input.radius)) {
|
|
8
|
+
if (los && !spatial.hasLineOfSight(input.at, instanceId))
|
|
9
|
+
continue;
|
|
10
|
+
let scale = 1;
|
|
11
|
+
if (falloff === "linear") {
|
|
12
|
+
const position = spatial.positionOf(instanceId);
|
|
13
|
+
if (position === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
scale = Math.max(0, 1 - distanceBetween(input.at, position) / input.radius);
|
|
16
|
+
}
|
|
17
|
+
if (accept !== undefined && !accept(instanceId))
|
|
18
|
+
continue;
|
|
19
|
+
targets.push({ instanceId, scale });
|
|
20
|
+
}
|
|
21
|
+
return targets;
|
|
22
|
+
}
|
|
3
23
|
const DEFAULT_DRAIN_STAT = "damage";
|
|
4
24
|
export function createEffectSystem(deps) {
|
|
5
25
|
function resolveRule(instanceId, effect) {
|
|
@@ -20,18 +40,21 @@ export function createEffectSystem(deps) {
|
|
|
20
40
|
}
|
|
21
41
|
return result;
|
|
22
42
|
}
|
|
23
|
-
function canReceive(instanceId, effect) {
|
|
43
|
+
function canReceive(instanceId, effect, magnitude) {
|
|
24
44
|
const rule = resolveRule(instanceId, effect);
|
|
25
45
|
if (rule === null)
|
|
26
46
|
return "not-receivable";
|
|
27
47
|
const stats = deps.resolveStats(instanceId);
|
|
28
48
|
if (stats === undefined)
|
|
29
49
|
return "unknown-instance";
|
|
30
|
-
const
|
|
50
|
+
const restorative = magnitude !== undefined && magnitude < 0;
|
|
51
|
+
const anyPoolHasHeadroom = rule.order.some((statId) => {
|
|
31
52
|
const stat = stats[statId];
|
|
32
|
-
|
|
53
|
+
if (stat === undefined)
|
|
54
|
+
return false;
|
|
55
|
+
return restorative ? stat.current < stat.max : stat.current > stat.min;
|
|
33
56
|
});
|
|
34
|
-
if (!
|
|
57
|
+
if (!anyPoolHasHeadroom)
|
|
35
58
|
return "pools-depleted";
|
|
36
59
|
return null;
|
|
37
60
|
}
|
|
@@ -60,13 +83,13 @@ export function createEffectSystem(deps) {
|
|
|
60
83
|
return { instanceId, effect, applied, lethal };
|
|
61
84
|
}
|
|
62
85
|
function applyTo(instanceId, effect, via, from, scale) {
|
|
63
|
-
if (canReceive(instanceId, effect) !== null)
|
|
64
|
-
return null;
|
|
65
86
|
const rule = resolveRule(instanceId, effect);
|
|
66
87
|
const stats = deps.resolveStats(instanceId);
|
|
67
88
|
if (rule === null || stats === undefined)
|
|
68
89
|
return null;
|
|
69
90
|
const drainMagnitude = modifiedDrainMagnitude(baseDrainMagnitude(effect, via) * scale, rule);
|
|
91
|
+
if (canReceive(instanceId, effect, drainMagnitude) !== null)
|
|
92
|
+
return null;
|
|
70
93
|
const result = drainPools(instanceId, effect, rule, stats, drainMagnitude);
|
|
71
94
|
if (result.lethal)
|
|
72
95
|
deps.onLethal?.(instanceId, { from, via, effect });
|
|
@@ -85,20 +108,9 @@ export function createEffectSystem(deps) {
|
|
|
85
108
|
const result = applyTo(input.to, input.effect, input.via, input.from, 1);
|
|
86
109
|
return result === null ? [] : [result];
|
|
87
110
|
}
|
|
88
|
-
const falloff = input.falloff ?? "none";
|
|
89
|
-
const los = input.los ?? true;
|
|
90
111
|
const results = [];
|
|
91
|
-
for (const
|
|
92
|
-
|
|
93
|
-
continue;
|
|
94
|
-
let scale = 1;
|
|
95
|
-
if (falloff === "linear") {
|
|
96
|
-
const position = deps.spatial.positionOf(instanceId);
|
|
97
|
-
if (position === undefined)
|
|
98
|
-
continue;
|
|
99
|
-
scale = Math.max(0, 1 - distanceBetween(input.at, position) / input.radius);
|
|
100
|
-
}
|
|
101
|
-
const result = applyTo(instanceId, input.effect, input.via, input.from, scale);
|
|
112
|
+
for (const target of resolveAreaTargets(deps.spatial, input)) {
|
|
113
|
+
const result = applyTo(target.instanceId, input.effect, input.via, input.from, target.scale);
|
|
102
114
|
if (result !== null)
|
|
103
115
|
results.push(result);
|
|
104
116
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export interface CameraShake {
|
|
3
|
+
amplitude: number;
|
|
4
|
+
decay: number;
|
|
5
|
+
}
|
|
6
|
+
export interface HitReactionConfig {
|
|
7
|
+
hitstopMs: number;
|
|
8
|
+
knockback: number;
|
|
9
|
+
vertical?: number;
|
|
10
|
+
shake?: CameraShake;
|
|
11
|
+
}
|
|
12
|
+
export interface HitReactionInput {
|
|
13
|
+
attackerPos: EntityPosition;
|
|
14
|
+
targetPos: EntityPosition;
|
|
15
|
+
power?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HitReaction {
|
|
18
|
+
hitstopMs: number;
|
|
19
|
+
impulse: [number, number, number];
|
|
20
|
+
shake: CameraShake | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function resolveHitReaction(config: HitReactionConfig, input: HitReactionInput): HitReaction;
|
|
23
|
+
export declare function applyImpulse(position: EntityPosition, impulse: [number, number, number]): EntityPosition;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function resolveHitReaction(config, input) {
|
|
2
|
+
const power = input.power ?? 1;
|
|
3
|
+
const dx = input.targetPos[0] - input.attackerPos[0];
|
|
4
|
+
const dz = input.targetPos[2] - input.attackerPos[2];
|
|
5
|
+
const planar = Math.hypot(dx, dz);
|
|
6
|
+
const nx = planar === 0 ? 0 : dx / planar;
|
|
7
|
+
const nz = planar === 0 ? 0 : dz / planar;
|
|
8
|
+
const magnitude = config.knockback * power;
|
|
9
|
+
const shake = config.shake === undefined
|
|
10
|
+
? null
|
|
11
|
+
: { amplitude: config.shake.amplitude * power, decay: config.shake.decay };
|
|
12
|
+
return {
|
|
13
|
+
hitstopMs: config.hitstopMs,
|
|
14
|
+
impulse: [nx * magnitude, (config.vertical ?? 0) * power, nz * magnitude],
|
|
15
|
+
shake,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function applyImpulse(position, impulse) {
|
|
19
|
+
return [position[0] + impulse[0], position[1] + impulse[1], position[2] + impulse[2]];
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BallisticSweep } from "../physics/ballisticSweep.js";
|
|
1
2
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
3
|
import type { Aim } from "../scene/spatial.js";
|
|
3
4
|
import type { CombatSpatialDeps, EffectResult, EffectSystem, EffectVia } from "./effects.js";
|
|
@@ -7,11 +8,22 @@ export interface ProjectileShotInput {
|
|
|
7
8
|
aim: Aim;
|
|
8
9
|
effect: string;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
/** A raycast hit against a scene entity — the ray reached `instanceId` at `distance` along its length. */
|
|
12
|
+
export interface EntityRaycastHit {
|
|
13
|
+
kind: "entity";
|
|
11
14
|
instanceId: string;
|
|
12
15
|
distance: number;
|
|
13
16
|
at: EntityPosition;
|
|
14
17
|
}
|
|
18
|
+
/** A raycast hit against a placed scene object's bounding box — see `ProjectileObjectsDeps.halfExtents`. */
|
|
19
|
+
export interface ObjectRaycastHit {
|
|
20
|
+
kind: "object";
|
|
21
|
+
instanceId: string;
|
|
22
|
+
catalogId: string;
|
|
23
|
+
distance: number;
|
|
24
|
+
at: EntityPosition;
|
|
25
|
+
}
|
|
26
|
+
export type RaycastHit = EntityRaycastHit | ObjectRaycastHit;
|
|
15
27
|
export type Raycast = (from: string, aim: Aim, range: number) => RaycastHit[];
|
|
16
28
|
export interface ProjectileSettleReport {
|
|
17
29
|
from: string;
|
|
@@ -20,19 +32,44 @@ export interface ProjectileSettleReport {
|
|
|
20
32
|
effect: string;
|
|
21
33
|
hit: boolean;
|
|
22
34
|
}
|
|
35
|
+
/** Read-only scene object access for the default `raycast`; matches `ObjectStore.list()`/a catalog lookup structurally. */
|
|
36
|
+
export interface ProjectileObjectsDeps {
|
|
37
|
+
list(): readonly {
|
|
38
|
+
instanceId: string;
|
|
39
|
+
catalogId: string;
|
|
40
|
+
position: [number, number, number];
|
|
41
|
+
}[];
|
|
42
|
+
/** Half-extents of the object's axis-aligned bounding box; `null`/omitted falls back to `[0.5, 0.5, 0.5]`. */
|
|
43
|
+
halfExtents?(catalogId: string): [number, number, number] | null;
|
|
44
|
+
}
|
|
23
45
|
export interface ProjectileSystemDeps {
|
|
24
46
|
effects: EffectSystem;
|
|
25
47
|
spatial: CombatSpatialDeps;
|
|
26
48
|
getStat(itemId: string, stat: string): number | null;
|
|
27
49
|
raycast?: Raycast;
|
|
50
|
+
/** Optional object awareness for the default `raycast`; when set, placed objects can block or absorb a shot. */
|
|
51
|
+
objects?: ProjectileObjectsDeps;
|
|
52
|
+
/**
|
|
53
|
+
* Optional collision-aware arc test for ballistic shots (see `createBallisticSweep` in
|
|
54
|
+
* `physics/ballisticSweep`). A hit settles the shot at the impact point; `null` or omission
|
|
55
|
+
* falls back to the closed-form landing.
|
|
56
|
+
*/
|
|
57
|
+
sweepBallistic?: BallisticSweep;
|
|
28
58
|
now?: () => number;
|
|
29
59
|
onSettle?(report: ProjectileSettleReport): void;
|
|
30
60
|
}
|
|
61
|
+
export type ProjectileHit = {
|
|
62
|
+
kind: "entity";
|
|
63
|
+
instanceId: string;
|
|
64
|
+
distance: number;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "object";
|
|
67
|
+
instanceId: string;
|
|
68
|
+
catalogId: string;
|
|
69
|
+
distance: number;
|
|
70
|
+
};
|
|
31
71
|
export interface ProjectilePrediction {
|
|
32
|
-
hits:
|
|
33
|
-
instanceId: string;
|
|
34
|
-
distance: number;
|
|
35
|
-
}[];
|
|
72
|
+
hits: ProjectileHit[];
|
|
36
73
|
blocked?: boolean;
|
|
37
74
|
}
|
|
38
75
|
export type SettleResult = {
|
|
@@ -2,6 +2,38 @@ const DEFAULT_RANGE = 100;
|
|
|
2
2
|
const DEFAULT_PROJECTILE_SPEED = 15;
|
|
3
3
|
const GRAVITY = 9.8;
|
|
4
4
|
const BASE_HIT_RADIUS = 0.5;
|
|
5
|
+
const DEFAULT_OBJECT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
6
|
+
function isEntityHit(hit) {
|
|
7
|
+
return hit.kind === "entity";
|
|
8
|
+
}
|
|
9
|
+
function isObjectHit(hit) {
|
|
10
|
+
return hit.kind === "object";
|
|
11
|
+
}
|
|
12
|
+
function raySegmentAabbDistance(origin, direction, range, min, max) {
|
|
13
|
+
let tMin = 0;
|
|
14
|
+
let tMax = range;
|
|
15
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
16
|
+
const o = origin[axis];
|
|
17
|
+
const d = direction[axis];
|
|
18
|
+
const lo = min[axis];
|
|
19
|
+
const hi = max[axis];
|
|
20
|
+
if (Math.abs(d) < 1e-9) {
|
|
21
|
+
if (o < lo || o > hi)
|
|
22
|
+
return null;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const inv = 1 / d;
|
|
26
|
+
let t1 = (lo - o) * inv;
|
|
27
|
+
let t2 = (hi - o) * inv;
|
|
28
|
+
if (t1 > t2)
|
|
29
|
+
[t1, t2] = [t2, t1];
|
|
30
|
+
tMin = Math.max(tMin, t1);
|
|
31
|
+
tMax = Math.min(tMax, t2);
|
|
32
|
+
if (tMin > tMax)
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return tMin;
|
|
36
|
+
}
|
|
5
37
|
function normalize(vector) {
|
|
6
38
|
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
|
|
7
39
|
if (length === 0)
|
|
@@ -42,7 +74,7 @@ export function createProjectileSystem(deps) {
|
|
|
42
74
|
if (direction === null)
|
|
43
75
|
return [];
|
|
44
76
|
const spreadRad = (aimSpreadDeg(aim) * Math.PI) / 180;
|
|
45
|
-
const
|
|
77
|
+
const entityHits = [];
|
|
46
78
|
for (const instanceId of deps.spatial.inRadius(origin, range)) {
|
|
47
79
|
if (instanceId === from)
|
|
48
80
|
continue;
|
|
@@ -61,8 +93,42 @@ export function createProjectileSystem(deps) {
|
|
|
61
93
|
const perpendicular = Math.sqrt(px * px + py * py + pz * pz);
|
|
62
94
|
if (perpendicular > BASE_HIT_RADIUS + Math.tan(spreadRad) * along)
|
|
63
95
|
continue;
|
|
64
|
-
|
|
96
|
+
entityHits.push({ kind: "entity", instanceId, distance: along, at: position });
|
|
65
97
|
}
|
|
98
|
+
let nearestObject = null;
|
|
99
|
+
for (const object of deps.objects?.list() ?? []) {
|
|
100
|
+
const half = deps.objects?.halfExtents?.(object.catalogId) ?? DEFAULT_OBJECT_HALF_EXTENTS;
|
|
101
|
+
const min = [
|
|
102
|
+
object.position[0] - half[0],
|
|
103
|
+
object.position[1] - half[1],
|
|
104
|
+
object.position[2] - half[2],
|
|
105
|
+
];
|
|
106
|
+
const max = [
|
|
107
|
+
object.position[0] + half[0],
|
|
108
|
+
object.position[1] + half[1],
|
|
109
|
+
object.position[2] + half[2],
|
|
110
|
+
];
|
|
111
|
+
const distance = raySegmentAabbDistance(origin, direction, range, min, max);
|
|
112
|
+
if (distance === null)
|
|
113
|
+
continue;
|
|
114
|
+
if (nearestObject === null || distance < nearestObject.distance) {
|
|
115
|
+
nearestObject = {
|
|
116
|
+
kind: "object",
|
|
117
|
+
instanceId: object.instanceId,
|
|
118
|
+
catalogId: object.catalogId,
|
|
119
|
+
distance,
|
|
120
|
+
at: [
|
|
121
|
+
origin[0] + direction[0] * distance,
|
|
122
|
+
origin[1] + direction[1] * distance,
|
|
123
|
+
origin[2] + direction[2] * distance,
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const blockDistance = nearestObject?.distance ?? Infinity;
|
|
129
|
+
const hits = entityHits.filter((hit) => hit.distance <= blockDistance);
|
|
130
|
+
if (nearestObject !== null)
|
|
131
|
+
hits.push(nearestObject);
|
|
66
132
|
return hits.sort((a, b) => a.distance - b.distance);
|
|
67
133
|
});
|
|
68
134
|
function resolveRange(via) {
|
|
@@ -71,7 +137,7 @@ export function createProjectileSystem(deps) {
|
|
|
71
137
|
function isBallistic(via) {
|
|
72
138
|
return itemStat(via, "projectile.fuseTime") !== null || itemStat(via, "explosion.radius") !== null;
|
|
73
139
|
}
|
|
74
|
-
function
|
|
140
|
+
function ballisticArc(input) {
|
|
75
141
|
const origin = shotOrigin(input.from, input.aim) ?? [0, 0, 0];
|
|
76
142
|
const direction = aimDirection(input.aim) ?? [0, 0, 1];
|
|
77
143
|
const speed = itemStat(input.via, "projectile.speed") ?? DEFAULT_PROJECTILE_SPEED;
|
|
@@ -85,16 +151,32 @@ export function createProjectileSystem(deps) {
|
|
|
85
151
|
: flightCap;
|
|
86
152
|
const flightTime = Math.min(impactTime, flightCap);
|
|
87
153
|
const settledY = Math.max(0, origin[1] + verticalSpeed * flightTime - 0.5 * gravity * flightTime * flightTime);
|
|
88
|
-
return
|
|
89
|
-
origin
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
154
|
+
return {
|
|
155
|
+
origin,
|
|
156
|
+
velocity: [direction[0] * speed, verticalSpeed, direction[2] * speed],
|
|
157
|
+
gravity,
|
|
158
|
+
flightTime,
|
|
159
|
+
landing: [
|
|
160
|
+
origin[0] + direction[0] * speed * flightTime,
|
|
161
|
+
settledY,
|
|
162
|
+
origin[2] + direction[2] * speed * flightTime,
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function ballisticSettlePoint(input) {
|
|
167
|
+
const arc = ballisticArc(input);
|
|
168
|
+
const sweep = deps.sweepBallistic;
|
|
169
|
+
if (sweep !== undefined) {
|
|
170
|
+
const impact = sweep(arc.origin, arc.velocity, arc.gravity, arc.flightTime);
|
|
171
|
+
if (impact !== null)
|
|
172
|
+
return impact.point;
|
|
173
|
+
}
|
|
174
|
+
return arc.landing;
|
|
93
175
|
}
|
|
94
176
|
function predictHits(input) {
|
|
95
177
|
const aim = withWeaponSpread(input.via, input.aim);
|
|
96
178
|
const rawHits = raycast(input.from, aim, resolveRange(input.via));
|
|
97
|
-
const visible = rawHits.filter((hit) => deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
179
|
+
const visible = rawHits.filter((hit) => isObjectHit(hit) || deps.spatial.hasLineOfSight(input.from, hit.instanceId));
|
|
98
180
|
return { rawHits, visible };
|
|
99
181
|
}
|
|
100
182
|
function settledAt(input, hits) {
|
|
@@ -112,7 +194,9 @@ export function createProjectileSystem(deps) {
|
|
|
112
194
|
willHitProjectile(input) {
|
|
113
195
|
const { rawHits, visible } = predictHits(input);
|
|
114
196
|
const prediction = {
|
|
115
|
-
hits: visible.map((
|
|
197
|
+
hits: visible.map((hit) => isObjectHit(hit)
|
|
198
|
+
? { kind: "object", instanceId: hit.instanceId, catalogId: hit.catalogId, distance: hit.distance }
|
|
199
|
+
: { kind: "entity", instanceId: hit.instanceId, distance: hit.distance }),
|
|
116
200
|
};
|
|
117
201
|
if (rawHits.length > 0 && visible.length === 0)
|
|
118
202
|
prediction.blocked = true;
|
|
@@ -139,7 +223,9 @@ export function createProjectileSystem(deps) {
|
|
|
139
223
|
return { status: "settled", shotId, at, hits: [] };
|
|
140
224
|
}
|
|
141
225
|
const { visible } = predictHits(input);
|
|
142
|
-
const
|
|
226
|
+
const entityHits = visible.filter(isEntityHit);
|
|
227
|
+
const objectHits = visible.filter(isObjectHit);
|
|
228
|
+
const receivable = entityHits.filter((hit) => deps.effects.canReceive(hit.instanceId, input.effect) === null);
|
|
143
229
|
const pellets = Math.max(1, Math.round(itemStat(input.via, "pellets") ?? 1));
|
|
144
230
|
const hits = [];
|
|
145
231
|
if (receivable.length > 0) {
|
|
@@ -153,7 +239,7 @@ export function createProjectileSystem(deps) {
|
|
|
153
239
|
}));
|
|
154
240
|
}
|
|
155
241
|
}
|
|
156
|
-
const at = settledAt(input, receivable);
|
|
242
|
+
const at = settledAt(input, receivable.length > 0 ? receivable : objectHits);
|
|
157
243
|
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: receivable.length > 0 });
|
|
158
244
|
return { status: "settled", shotId, at, hits };
|
|
159
245
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ResistVerdict = "immune" | "resist" | "normal" | "vulnerable";
|
|
2
|
+
export interface ResistanceMatrix {
|
|
3
|
+
categories: Record<string, Record<string, ResistVerdict>>;
|
|
4
|
+
multipliers?: Partial<Record<ResistVerdict, number>>;
|
|
5
|
+
default?: ResistVerdict;
|
|
6
|
+
}
|
|
7
|
+
export interface ResistanceResult {
|
|
8
|
+
verdict: ResistVerdict;
|
|
9
|
+
multiplier: number;
|
|
10
|
+
immune: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveResistance(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): ResistanceResult;
|
|
13
|
+
export declare function resistanceScale(matrix: ResistanceMatrix, category: string, targetProperties: readonly string[]): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const DEFAULT_MULTIPLIERS = {
|
|
2
|
+
immune: 0,
|
|
3
|
+
resist: 0.5,
|
|
4
|
+
normal: 1,
|
|
5
|
+
vulnerable: 2,
|
|
6
|
+
};
|
|
7
|
+
function multiplierFor(matrix, verdict) {
|
|
8
|
+
return matrix.multipliers?.[verdict] ?? DEFAULT_MULTIPLIERS[verdict];
|
|
9
|
+
}
|
|
10
|
+
function summaryVerdict(matrix, immune, net) {
|
|
11
|
+
if (immune)
|
|
12
|
+
return "immune";
|
|
13
|
+
const normal = multiplierFor(matrix, "normal");
|
|
14
|
+
if (net > normal)
|
|
15
|
+
return "vulnerable";
|
|
16
|
+
if (net < normal)
|
|
17
|
+
return "resist";
|
|
18
|
+
return "normal";
|
|
19
|
+
}
|
|
20
|
+
export function resolveResistance(matrix, category, targetProperties) {
|
|
21
|
+
const byProperty = matrix.categories[category] ?? {};
|
|
22
|
+
const fallback = matrix.default ?? "normal";
|
|
23
|
+
let net = 1;
|
|
24
|
+
let matched = false;
|
|
25
|
+
let immune = false;
|
|
26
|
+
for (const property of targetProperties) {
|
|
27
|
+
const verdict = byProperty[property];
|
|
28
|
+
if (verdict === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
matched = true;
|
|
31
|
+
if (verdict === "immune")
|
|
32
|
+
immune = true;
|
|
33
|
+
net *= multiplierFor(matrix, verdict);
|
|
34
|
+
}
|
|
35
|
+
if (!matched) {
|
|
36
|
+
if (fallback === "immune")
|
|
37
|
+
return { verdict: "immune", multiplier: 0, immune: true };
|
|
38
|
+
const multiplier = multiplierFor(matrix, fallback);
|
|
39
|
+
return { verdict: fallback, multiplier, immune: false };
|
|
40
|
+
}
|
|
41
|
+
const multiplier = immune ? 0 : net;
|
|
42
|
+
return { verdict: summaryVerdict(matrix, immune, multiplier), multiplier, immune };
|
|
43
|
+
}
|
|
44
|
+
export function resistanceScale(matrix, category, targetProperties) {
|
|
45
|
+
return resolveResistance(matrix, category, targetProperties).multiplier;
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
export type TelegraphShape = {
|
|
3
|
+
kind: "circle";
|
|
4
|
+
radius: number;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "ring";
|
|
7
|
+
radius: number;
|
|
8
|
+
innerRadius: number;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "cone";
|
|
11
|
+
radius: number;
|
|
12
|
+
angle: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "line";
|
|
15
|
+
length: number;
|
|
16
|
+
width: number;
|
|
17
|
+
};
|
|
18
|
+
export interface TelegraphConfig {
|
|
19
|
+
shape: TelegraphShape;
|
|
20
|
+
at: EntityPosition;
|
|
21
|
+
dir?: number;
|
|
22
|
+
windupMs?: number;
|
|
23
|
+
turns?: number;
|
|
24
|
+
kind?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function telegraphProgress(windupMs: number, startedAtMs: number, nowMs: number): number;
|
|
27
|
+
export declare function telegraphFired(windupMs: number, startedAtMs: number, nowMs: number): boolean;
|
|
28
|
+
export declare function telegraphTurnProgress(config: TelegraphConfig, startedTurn: number, currentTurn: number): number;
|
|
29
|
+
export declare function telegraphFiredAtTurn(config: TelegraphConfig, startedTurn: number, currentTurn: number): boolean;
|
|
30
|
+
export declare function telegraphTurnsRemaining(config: TelegraphConfig, startedTurn: number, currentTurn: number): number;
|
|
31
|
+
export declare function pointInTelegraph(config: TelegraphConfig, point: EntityPosition): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function telegraphProgress(windupMs, startedAtMs, nowMs) {
|
|
2
|
+
if (windupMs <= 0)
|
|
3
|
+
return 1;
|
|
4
|
+
return Math.max(0, Math.min(1, (nowMs - startedAtMs) / windupMs));
|
|
5
|
+
}
|
|
6
|
+
export function telegraphFired(windupMs, startedAtMs, nowMs) {
|
|
7
|
+
return nowMs - startedAtMs >= windupMs;
|
|
8
|
+
}
|
|
9
|
+
export function telegraphTurnProgress(config, startedTurn, currentTurn) {
|
|
10
|
+
const turns = config.turns ?? 0;
|
|
11
|
+
if (turns <= 0)
|
|
12
|
+
return 1;
|
|
13
|
+
return Math.max(0, Math.min(1, (currentTurn - startedTurn) / turns));
|
|
14
|
+
}
|
|
15
|
+
export function telegraphFiredAtTurn(config, startedTurn, currentTurn) {
|
|
16
|
+
const turns = config.turns ?? 0;
|
|
17
|
+
return currentTurn - startedTurn >= turns;
|
|
18
|
+
}
|
|
19
|
+
export function telegraphTurnsRemaining(config, startedTurn, currentTurn) {
|
|
20
|
+
const turns = config.turns ?? 0;
|
|
21
|
+
return Math.max(0, turns - (currentTurn - startedTurn));
|
|
22
|
+
}
|
|
23
|
+
function planarDelta(config, point) {
|
|
24
|
+
return { dx: point[0] - config.at[0], dz: point[2] - config.at[2] };
|
|
25
|
+
}
|
|
26
|
+
export function pointInTelegraph(config, point) {
|
|
27
|
+
const { shape } = config;
|
|
28
|
+
const { dx, dz } = planarDelta(config, point);
|
|
29
|
+
const dist = Math.hypot(dx, dz);
|
|
30
|
+
const dir = config.dir ?? 0;
|
|
31
|
+
if (shape.kind === "circle")
|
|
32
|
+
return dist <= shape.radius;
|
|
33
|
+
if (shape.kind === "ring")
|
|
34
|
+
return dist >= shape.innerRadius && dist <= shape.radius;
|
|
35
|
+
if (shape.kind === "cone") {
|
|
36
|
+
if (dist > shape.radius || dist === 0)
|
|
37
|
+
return dist === 0;
|
|
38
|
+
const angleTo = Math.atan2(dx, dz);
|
|
39
|
+
let delta = angleTo - dir;
|
|
40
|
+
while (delta > Math.PI)
|
|
41
|
+
delta -= Math.PI * 2;
|
|
42
|
+
while (delta < -Math.PI)
|
|
43
|
+
delta += Math.PI * 2;
|
|
44
|
+
return Math.abs(delta) <= shape.angle / 2;
|
|
45
|
+
}
|
|
46
|
+
const forwardX = Math.sin(dir);
|
|
47
|
+
const forwardZ = Math.cos(dir);
|
|
48
|
+
const along = dx * forwardX + dz * forwardZ;
|
|
49
|
+
const lateral = Math.abs(dx * forwardZ - dz * forwardX);
|
|
50
|
+
return along >= 0 && along <= shape.length && lateral <= shape.width / 2;
|
|
51
|
+
}
|
|
@@ -3,7 +3,7 @@ export interface CommandRejection {
|
|
|
3
3
|
}
|
|
4
4
|
export interface CommandDefinition<TState, TInput> {
|
|
5
5
|
validate?(state: TState, input: TInput): CommandRejection | null;
|
|
6
|
-
apply(state: TState, input: TInput): TState;
|
|
6
|
+
apply(state: TState, input: TInput): TState | void;
|
|
7
7
|
}
|
|
8
8
|
export type CommandResult<TState> = {
|
|
9
9
|
status: "applied";
|
|
@@ -20,7 +20,8 @@ export function createCommandRegistry() {
|
|
|
20
20
|
const rejection = definition.validate?.(state, input) ?? null;
|
|
21
21
|
if (rejection)
|
|
22
22
|
return { status: "rejected", reason: rejection.reason };
|
|
23
|
-
|
|
23
|
+
const next = definition.apply(state, input);
|
|
24
|
+
return { status: "applied", state: next === undefined ? state : next };
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RecipeItem } from "./recipe.js";
|
|
2
|
+
export type SoilState = "untilled" | "tilled";
|
|
3
|
+
export interface CropDef {
|
|
4
|
+
id: string;
|
|
5
|
+
stages: readonly number[];
|
|
6
|
+
regrowDays?: number;
|
|
7
|
+
needsDailyWater?: boolean;
|
|
8
|
+
harvest?: RecipeItem;
|
|
9
|
+
}
|
|
10
|
+
export interface CropTileState {
|
|
11
|
+
soil: SoilState;
|
|
12
|
+
watered: boolean;
|
|
13
|
+
cropId: string | null;
|
|
14
|
+
stage: number;
|
|
15
|
+
stageProgress: number;
|
|
16
|
+
harvestable: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type CropCatalog = (cropId: string) => CropDef | null;
|
|
19
|
+
export declare function emptyTile(): CropTileState;
|
|
20
|
+
export declare function canTill(state: CropTileState): boolean;
|
|
21
|
+
export declare function tillTile(state: CropTileState): CropTileState;
|
|
22
|
+
export declare function canPlant(state: CropTileState): boolean;
|
|
23
|
+
export declare function plantCrop(state: CropTileState, cropId: string): CropTileState;
|
|
24
|
+
export declare function waterTile(state: CropTileState): CropTileState;
|
|
25
|
+
export declare function maturityDays(def: CropDef): number;
|
|
26
|
+
export declare function advanceCropDay(def: CropDef, state: CropTileState): CropTileState;
|
|
27
|
+
export interface HarvestResult {
|
|
28
|
+
state: CropTileState;
|
|
29
|
+
yield: RecipeItem | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function harvestCrop(def: CropDef, state: CropTileState): HarvestResult;
|
|
32
|
+
export type TileCoord = readonly [number, number];
|
|
33
|
+
export type TilePattern = readonly TileCoord[];
|
|
34
|
+
export declare function tileKey(coord: TileCoord): string;
|
|
35
|
+
export declare function singleTile(): TilePattern;
|
|
36
|
+
export declare function squarePattern(radius: number): TilePattern;
|
|
37
|
+
export declare function diamondPattern(radius: number): TilePattern;
|
|
38
|
+
export declare function rectPattern(width: number, depth: number): TilePattern;
|
|
39
|
+
export declare function patternAt(center: TileCoord, pattern: TilePattern): TileCoord[];
|
|
40
|
+
export interface ApplyToolResult {
|
|
41
|
+
tiles: Map<string, CropTileState>;
|
|
42
|
+
changed: TileCoord[];
|
|
43
|
+
}
|
|
44
|
+
export declare function applyToolToTiles(tiles: ReadonlyMap<string, CropTileState>, center: TileCoord, pattern: TilePattern, apply: (state: CropTileState, coord: TileCoord) => CropTileState, fill?: () => CropTileState): ApplyToolResult;
|
|
45
|
+
export interface DayTicker {
|
|
46
|
+
tick(currentDay: number): number;
|
|
47
|
+
day(): number;
|
|
48
|
+
}
|
|
49
|
+
export declare function createDayTicker(startDay?: number): DayTicker;
|
|
50
|
+
export interface CropField {
|
|
51
|
+
get(coord: TileCoord): CropTileState;
|
|
52
|
+
till(coord: TileCoord, pattern?: TilePattern): TileCoord[];
|
|
53
|
+
plant(coord: TileCoord, cropId: string): boolean;
|
|
54
|
+
water(coord: TileCoord, pattern?: TilePattern): TileCoord[];
|
|
55
|
+
harvest(coord: TileCoord): RecipeItem | null;
|
|
56
|
+
advanceDay(days?: number): void;
|
|
57
|
+
tiles(): ReadonlyMap<string, CropTileState>;
|
|
58
|
+
}
|
|
59
|
+
export declare function createCropField(catalog?: CropCatalog): CropField;
|