@driftengine/script 4.7.0 → 4.7.2
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/capabilities.json +226 -22
- package/dist/bindings/audio.d.ts +2 -37
- package/dist/bindings/audio.js +60 -11
- package/dist/bindings/chemistry.js +2 -0
- package/dist/bindings/core.d.ts +11 -1
- package/dist/bindings/core.js +26 -1
- package/dist/bindings/entities.d.ts +6 -7
- package/dist/bindings/entities.js +28 -23
- package/dist/host.d.ts +6 -1
- package/dist/host.js +2 -4
- package/package.json +13 -13
- package/src/bindings/audio.ts +92 -10
- package/src/bindings/chemistry.ts +8 -0
- package/src/bindings/core.ts +67 -1
- package/src/bindings/entities.ts +45 -42
- package/src/host.ts +15 -8
package/capabilities.json
CHANGED
|
@@ -1034,6 +1034,93 @@
|
|
|
1034
1034
|
"doc": "Where something sits in the stereo field, relative to a listener facing yaw.",
|
|
1035
1035
|
"implementation": "audio.stereoPan"
|
|
1036
1036
|
},
|
|
1037
|
+
{
|
|
1038
|
+
"module": "drift/audio",
|
|
1039
|
+
"name": "duck",
|
|
1040
|
+
"signature": "fn(bus: String, factor: f32, seconds: f32) -> bool",
|
|
1041
|
+
"params": [
|
|
1042
|
+
{
|
|
1043
|
+
"name": "bus",
|
|
1044
|
+
"type": "String"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"name": "factor",
|
|
1048
|
+
"type": "f32"
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
"name": "seconds",
|
|
1052
|
+
"type": "f32"
|
|
1053
|
+
}
|
|
1054
|
+
],
|
|
1055
|
+
"returns": "bool",
|
|
1056
|
+
"effects": [
|
|
1057
|
+
"audio.write"
|
|
1058
|
+
],
|
|
1059
|
+
"deterministic": false,
|
|
1060
|
+
"doc": "Take a bus down to factor of its fader over seconds, 1 to bring it back, leaving the fader where the player set it. False when the mix has no bus of that name.",
|
|
1061
|
+
"implementation": "MixBus.duck"
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
"module": "drift/audio",
|
|
1065
|
+
"name": "fade",
|
|
1066
|
+
"signature": "fn(bus: String, level: f32, seconds: f32) -> bool",
|
|
1067
|
+
"params": [
|
|
1068
|
+
{
|
|
1069
|
+
"name": "bus",
|
|
1070
|
+
"type": "String"
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
"name": "level",
|
|
1074
|
+
"type": "f32"
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
"name": "seconds",
|
|
1078
|
+
"type": "f32"
|
|
1079
|
+
}
|
|
1080
|
+
],
|
|
1081
|
+
"returns": "bool",
|
|
1082
|
+
"effects": [
|
|
1083
|
+
"audio.write"
|
|
1084
|
+
],
|
|
1085
|
+
"deterministic": false,
|
|
1086
|
+
"doc": "Move a bus's fader to level over seconds. For a bus the game made: the music and effects faders are the player's, and duck is how a script goes over them. False when the mix has no bus of that name.",
|
|
1087
|
+
"implementation": "MixBus.fadeLevel"
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
"module": "drift/audio",
|
|
1091
|
+
"name": "recall",
|
|
1092
|
+
"signature": "fn(snapshot: String, seconds: f32) -> bool",
|
|
1093
|
+
"params": [
|
|
1094
|
+
{
|
|
1095
|
+
"name": "snapshot",
|
|
1096
|
+
"type": "String"
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
"name": "seconds",
|
|
1100
|
+
"type": "f32"
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
"returns": "bool",
|
|
1104
|
+
"effects": [
|
|
1105
|
+
"audio.write"
|
|
1106
|
+
],
|
|
1107
|
+
"deterministic": false,
|
|
1108
|
+
"doc": "Crossfade every level, mute and send to a snapshot the host captured, over seconds. False when nothing was captured under that name.",
|
|
1109
|
+
"implementation": "MixConsole.recall"
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
"module": "drift/audio",
|
|
1113
|
+
"name": "pulse",
|
|
1114
|
+
"signature": "fn() -> f32",
|
|
1115
|
+
"params": [],
|
|
1116
|
+
"returns": "f32",
|
|
1117
|
+
"effects": [
|
|
1118
|
+
"nondeterministic"
|
|
1119
|
+
],
|
|
1120
|
+
"deterministic": false,
|
|
1121
|
+
"doc": "How hard the music is kicking, 0 to 1, spiking on each kick and falling back. 0 when the host runs no kick detector.",
|
|
1122
|
+
"implementation": "KickDetector.pulse"
|
|
1123
|
+
},
|
|
1037
1124
|
{
|
|
1038
1125
|
"module": "drift/behavior",
|
|
1039
1126
|
"name": "tick",
|
|
@@ -1552,6 +1639,28 @@
|
|
|
1552
1639
|
"doc": "kg of liquid water on the surface, per square metre of it.",
|
|
1553
1640
|
"implementation": "drift/chemistry.wetness"
|
|
1554
1641
|
},
|
|
1642
|
+
{
|
|
1643
|
+
"module": "drift/chemistry",
|
|
1644
|
+
"name": "wettable",
|
|
1645
|
+
"signature": "fn(chem: Chemistry, parcel: i32) -> bool",
|
|
1646
|
+
"params": [
|
|
1647
|
+
{
|
|
1648
|
+
"name": "chem",
|
|
1649
|
+
"type": "Chemistry"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
"name": "parcel",
|
|
1653
|
+
"type": "i32"
|
|
1654
|
+
}
|
|
1655
|
+
],
|
|
1656
|
+
"returns": "bool",
|
|
1657
|
+
"effects": [
|
|
1658
|
+
"chemistry.read"
|
|
1659
|
+
],
|
|
1660
|
+
"deterministic": true,
|
|
1661
|
+
"doc": "Whether this material can hold liquid water, which is what `wet` asks. Rain over everything asks first, since `wet` refuses an iron nail.",
|
|
1662
|
+
"implementation": "drift/chemistry.wettable"
|
|
1663
|
+
},
|
|
1555
1664
|
{
|
|
1556
1665
|
"module": "drift/chemistry",
|
|
1557
1666
|
"name": "phase",
|
|
@@ -2838,6 +2947,28 @@
|
|
|
2838
2947
|
"doc": "The entity the last `findNearest` on this world found. **Valid only until the next one**, and meaningless when that answered false — the same lifetime a raycast hit has.",
|
|
2839
2948
|
"implementation": "drift/ecs.nearest"
|
|
2840
2949
|
},
|
|
2950
|
+
{
|
|
2951
|
+
"module": "drift/ecs",
|
|
2952
|
+
"name": "instantiate",
|
|
2953
|
+
"signature": "fn(world: World, prefab: String) -> Entity",
|
|
2954
|
+
"params": [
|
|
2955
|
+
{
|
|
2956
|
+
"name": "world",
|
|
2957
|
+
"type": "World"
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
"name": "prefab",
|
|
2961
|
+
"type": "String"
|
|
2962
|
+
}
|
|
2963
|
+
],
|
|
2964
|
+
"returns": "Entity",
|
|
2965
|
+
"effects": [
|
|
2966
|
+
"ecs.write"
|
|
2967
|
+
],
|
|
2968
|
+
"deterministic": false,
|
|
2969
|
+
"doc": "Make an entity from a prefab, with every component it names. Overrides are the host’s to apply; a script that wants a different value writes it after.",
|
|
2970
|
+
"implementation": "drift/ecs.instantiate"
|
|
2971
|
+
},
|
|
2841
2972
|
{
|
|
2842
2973
|
"module": "drift/editor",
|
|
2843
2974
|
"name": "translateMode",
|
|
@@ -4326,6 +4457,96 @@
|
|
|
4326
4457
|
"doc": "Stop whatever that pad is playing. Answers whether the platform took it.",
|
|
4327
4458
|
"implementation": "drift/input.stopRumble"
|
|
4328
4459
|
},
|
|
4460
|
+
{
|
|
4461
|
+
"module": "drift/input",
|
|
4462
|
+
"name": "touchX",
|
|
4463
|
+
"signature": "fn(touch: Touch) -> f32",
|
|
4464
|
+
"params": [
|
|
4465
|
+
{
|
|
4466
|
+
"name": "touch",
|
|
4467
|
+
"type": "Touch"
|
|
4468
|
+
}
|
|
4469
|
+
],
|
|
4470
|
+
"returns": "f32",
|
|
4471
|
+
"effects": [
|
|
4472
|
+
"input.read"
|
|
4473
|
+
],
|
|
4474
|
+
"deterministic": false,
|
|
4475
|
+
"doc": "The touch stick's horizontal, -1 to 1, 0 while no thumb is on it.",
|
|
4476
|
+
"implementation": "drift/input.touchX"
|
|
4477
|
+
},
|
|
4478
|
+
{
|
|
4479
|
+
"module": "drift/input",
|
|
4480
|
+
"name": "touchY",
|
|
4481
|
+
"signature": "fn(touch: Touch) -> f32",
|
|
4482
|
+
"params": [
|
|
4483
|
+
{
|
|
4484
|
+
"name": "touch",
|
|
4485
|
+
"type": "Touch"
|
|
4486
|
+
}
|
|
4487
|
+
],
|
|
4488
|
+
"returns": "f32",
|
|
4489
|
+
"effects": [
|
|
4490
|
+
"input.read"
|
|
4491
|
+
],
|
|
4492
|
+
"deterministic": false,
|
|
4493
|
+
"doc": "The touch stick's vertical, -1 to 1, up negative as a gamepad stick's is.",
|
|
4494
|
+
"implementation": "drift/input.touchY"
|
|
4495
|
+
},
|
|
4496
|
+
{
|
|
4497
|
+
"module": "drift/input",
|
|
4498
|
+
"name": "touchHeld",
|
|
4499
|
+
"signature": "fn(touch: Touch) -> bool",
|
|
4500
|
+
"params": [
|
|
4501
|
+
{
|
|
4502
|
+
"name": "touch",
|
|
4503
|
+
"type": "Touch"
|
|
4504
|
+
}
|
|
4505
|
+
],
|
|
4506
|
+
"returns": "bool",
|
|
4507
|
+
"effects": [
|
|
4508
|
+
"input.read"
|
|
4509
|
+
],
|
|
4510
|
+
"deterministic": false,
|
|
4511
|
+
"doc": "Whether a thumb is held still on the action side of the screen: the held form of the primary.",
|
|
4512
|
+
"implementation": "drift/input.touchHeld"
|
|
4513
|
+
},
|
|
4514
|
+
{
|
|
4515
|
+
"module": "drift/input",
|
|
4516
|
+
"name": "touchTap",
|
|
4517
|
+
"signature": "fn(touch: Touch) -> bool",
|
|
4518
|
+
"params": [
|
|
4519
|
+
{
|
|
4520
|
+
"name": "touch",
|
|
4521
|
+
"type": "Touch"
|
|
4522
|
+
}
|
|
4523
|
+
],
|
|
4524
|
+
"returns": "bool",
|
|
4525
|
+
"effects": [
|
|
4526
|
+
"input.read"
|
|
4527
|
+
],
|
|
4528
|
+
"deterministic": false,
|
|
4529
|
+
"doc": "Whether the action side was tapped since this was last asked. True once per tap, so ask it once a tick.",
|
|
4530
|
+
"implementation": "drift/input.touchTap"
|
|
4531
|
+
},
|
|
4532
|
+
{
|
|
4533
|
+
"module": "drift/input",
|
|
4534
|
+
"name": "touchSlide",
|
|
4535
|
+
"signature": "fn(touch: Touch) -> bool",
|
|
4536
|
+
"params": [
|
|
4537
|
+
{
|
|
4538
|
+
"name": "touch",
|
|
4539
|
+
"type": "Touch"
|
|
4540
|
+
}
|
|
4541
|
+
],
|
|
4542
|
+
"returns": "bool",
|
|
4543
|
+
"effects": [
|
|
4544
|
+
"input.read"
|
|
4545
|
+
],
|
|
4546
|
+
"deterministic": false,
|
|
4547
|
+
"doc": "Whether a downward flick on the action side is being held: the secondary, a slide or a crouch.",
|
|
4548
|
+
"implementation": "drift/input.touchSlide"
|
|
4549
|
+
},
|
|
4329
4550
|
{
|
|
4330
4551
|
"module": "drift/navigation",
|
|
4331
4552
|
"name": "nearest",
|
|
@@ -5777,28 +5998,6 @@
|
|
|
5777
5998
|
"doc": "The world's gravity, in metres per second squared.",
|
|
5778
5999
|
"implementation": "drift/physics.setGravity"
|
|
5779
6000
|
},
|
|
5780
|
-
{
|
|
5781
|
-
"module": "drift/prefab",
|
|
5782
|
-
"name": "spawn",
|
|
5783
|
-
"signature": "fn(world: World, prefab: String) -> Entity",
|
|
5784
|
-
"params": [
|
|
5785
|
-
{
|
|
5786
|
-
"name": "world",
|
|
5787
|
-
"type": "World"
|
|
5788
|
-
},
|
|
5789
|
-
{
|
|
5790
|
-
"name": "prefab",
|
|
5791
|
-
"type": "String"
|
|
5792
|
-
}
|
|
5793
|
-
],
|
|
5794
|
-
"returns": "Entity",
|
|
5795
|
-
"effects": [
|
|
5796
|
-
"ecs.write"
|
|
5797
|
-
],
|
|
5798
|
-
"deterministic": false,
|
|
5799
|
-
"doc": "Make one, with every component the prefab names. Overrides are the host’s to apply; a script that wants a different value writes it after.",
|
|
5800
|
-
"implementation": "drift/prefab.spawn"
|
|
5801
|
-
},
|
|
5802
6001
|
{
|
|
5803
6002
|
"module": "drift/random",
|
|
5804
6003
|
"name": "unit",
|
|
@@ -7637,6 +7836,11 @@
|
|
|
7637
7836
|
"name": "Actions",
|
|
7638
7837
|
"doc": "A bound action map: what the player is pressing."
|
|
7639
7838
|
},
|
|
7839
|
+
{
|
|
7840
|
+
"module": "drift/input",
|
|
7841
|
+
"name": "Touch",
|
|
7842
|
+
"doc": "Touch controls: a stick on one side of the screen, and taps, holds and flicks on the other."
|
|
7843
|
+
},
|
|
7640
7844
|
{
|
|
7641
7845
|
"module": "drift/camera",
|
|
7642
7846
|
"name": "Camera",
|
package/dist/bindings/audio.d.ts
CHANGED
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* `drift/audio` — what this engine lets a script do to sound.
|
|
3
|
-
*
|
|
4
|
-
* **Five capabilities: a slot, two ways to play it, and the two cheap positional helpers.** This
|
|
5
|
-
* header read "playback, placement, the mix, and the rhythm the music is doing" until 2026-08-27,
|
|
6
|
-
* and three of those four were never here — there is no mix verb, no rhythm verb, no HRTF-panned
|
|
7
|
-
* source, no listener and no reverb zone. The sentence was written from *Track G's* deliverables
|
|
8
|
-
* rather than from the list beneath it, which is the same failure `drift/physics` had one file
|
|
9
|
-
* over, and the same one the first `drift/camera` binding had: a comment describing what a
|
|
10
|
-
* subsystem ought to expose rather than what this array does.
|
|
11
|
-
*
|
|
12
|
-
* It is still the right first binding and still the thing that shows what a binding *is* — registry
|
|
13
|
-
* entries describing capabilities the engine already has, plus a map of implementations the host
|
|
14
|
-
* supplies at link time. **No new engine code.**
|
|
15
|
-
*
|
|
16
|
-
* **What is missing, and why it is not guessed at here.** `MixConsole`, `AudioListenerGraph`,
|
|
17
|
-
* `SpatialSource` and `ReverbZone` are four stateful objects with lifetimes, and a script-side
|
|
18
|
-
* surface for them is a design about handles and ownership rather than a table entry — who disposes
|
|
19
|
-
* a source, what happens to its probe slot, whether a zone outlives the script that made it.
|
|
20
|
-
* `docs/CAPABILITIES.md` §4 records it. Taking that badly would put a leak in front of every script
|
|
21
|
-
* author, which is worse than a surface they can see is absent.
|
|
22
|
-
*
|
|
23
|
-
* Three constraints this surface inherits, none of them negotiable:
|
|
24
|
-
*
|
|
25
|
-
* - **`audio.write` is outside the determinism boundary.** `ARCHITECTURE.md` places audio there, so
|
|
26
|
-
* a `@deterministic` function may not call any of it. A simulation decides *that* a door opened
|
|
27
|
-
* and emits an event; something outside the fixed step decides what it sounds like. The compiler
|
|
28
|
-
* is only making a line that already exists visible.
|
|
29
|
-
* - **`audio.play` in a `@hot` function is a diagnostic**, for the reason the engine's own rules
|
|
30
|
-
* forbid allocation there.
|
|
31
|
-
* - **A sound slot may be absent, so `sound` returns an option.** The registry probes several
|
|
32
|
-
* format candidates per slot and the file often is not there. A script that ignores that plays
|
|
33
|
-
* silence and reports success, which is the failure the language's no-implicit-null rule exists
|
|
34
|
-
* to prevent — and this is the first place in the engine where that rule earns its keep.
|
|
35
|
-
*/
|
|
36
|
-
import { AudioGraph, SoundRegistry } from '@driftengine/audio';
|
|
1
|
+
import type { AudioGraph, KickDetector, SoundRegistry } from '@driftengine/audio';
|
|
37
2
|
import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
|
|
38
3
|
export declare const AUDIO_MODULE = "drift/audio";
|
|
39
4
|
/**
|
|
@@ -53,4 +18,4 @@ export declare const AUDIO_CAPABILITIES: readonly CapabilityDefinition[];
|
|
|
53
18
|
* registry's `implementation` strings name these; nothing enforces that mechanically, which is why
|
|
54
19
|
* a test asserts every definition has an implementation here.
|
|
55
20
|
*/
|
|
56
|
-
export declare function audioImplementation(graph: AudioGraph, registry: SoundRegistry): Record<string, unknown>;
|
|
21
|
+
export declare function audioImplementation(graph: AudioGraph, registry: SoundRegistry, kick?: KickDetector | null): Record<string, unknown>;
|
package/dist/bindings/audio.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `drift/audio` — what this engine lets a script do to sound.
|
|
3
3
|
*
|
|
4
|
-
* **
|
|
4
|
+
* **Nine capabilities: a slot, two ways to play it, the two cheap positional helpers, three dials on
|
|
5
|
+
* the mix, and the kick the music is making.** The first five are the original binding. This
|
|
5
6
|
* header read "playback, placement, the mix, and the rhythm the music is doing" until 2026-08-27,
|
|
6
|
-
* and three of those four were
|
|
7
|
-
*
|
|
7
|
+
* and three of those four were not here then — no mix verb, no rhythm verb, no HRTF-panned source,
|
|
8
|
+
* no listener and no reverb zone. The sentence was written from *Track G's* deliverables
|
|
8
9
|
* rather than from the list beneath it, which is the same failure `drift/physics` had one file
|
|
9
10
|
* over, and the same one the first `drift/camera` binding had: a comment describing what a
|
|
10
11
|
* subsystem ought to expose rather than what this array does.
|
|
@@ -13,12 +14,18 @@
|
|
|
13
14
|
* entries describing capabilities the engine already has, plus a map of implementations the host
|
|
14
15
|
* supplies at link time. **No new engine code.**
|
|
15
16
|
*
|
|
16
|
-
* **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* a
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
17
|
+
* **The mix is bound as dials, by name.** `duck`, `fade` and `recall` move a bus or a snapshot the
|
|
18
|
+
* host already made, named by the string it was made under, and `pulse` reads a kick detector the
|
|
19
|
+
* host already runs. None of them hands a script an object, so none of them raises the question of
|
|
20
|
+
* who disposes it; a name nothing answers to is a `false`, never a silent success. The same rule
|
|
21
|
+
* `drift/render` follows: a dial is bound, and anything built from a table waits for a design.
|
|
22
|
+
*
|
|
23
|
+
* **What is missing, and why it is not guessed at here.** `AudioListenerGraph`, `SpatialSource` and
|
|
24
|
+
* `ReverbZone` are stateful objects with lifetimes, and a script-side surface for them is a design
|
|
25
|
+
* about handles and ownership rather than a table entry — who disposes a source, what happens to
|
|
26
|
+
* its probe slot, whether a zone outlives the script that made it. `docs/CAPABILITIES.md` §4
|
|
27
|
+
* records it. Taking that badly would put a leak in front of every script author, which is worse
|
|
28
|
+
* than a surface they can see is absent.
|
|
22
29
|
*
|
|
23
30
|
* Three constraints this surface inherits, none of them negotiable:
|
|
24
31
|
*
|
|
@@ -33,7 +40,7 @@
|
|
|
33
40
|
* silence and reports success, which is the failure the language's no-implicit-null rule exists
|
|
34
41
|
* to prevent — and this is the first place in the engine where that rule earns its keep.
|
|
35
42
|
*/
|
|
36
|
-
import {
|
|
43
|
+
import { distanceGain, stereoPan } from '@driftengine/audio';
|
|
37
44
|
import { defineCapability } from 'driftscript';
|
|
38
45
|
export const AUDIO_MODULE = 'drift/audio';
|
|
39
46
|
/**
|
|
@@ -97,6 +104,31 @@ export const AUDIO_CAPABILITIES = [
|
|
|
97
104
|
{ name: 'dz', type: 'f32' },
|
|
98
105
|
{ name: 'yaw', type: 'f32' },
|
|
99
106
|
], 'f32', ['pure'], true, 'Where something sits in the stereo field, relative to a listener facing yaw.', 'audio.stereoPan'),
|
|
107
|
+
define('duck', [
|
|
108
|
+
{ name: 'bus', type: 'String' },
|
|
109
|
+
{ name: 'factor', type: 'f32' },
|
|
110
|
+
{ name: 'seconds', type: 'f32' },
|
|
111
|
+
], 'bool', ['audio.write'], false, 'Take a bus down to factor of its fader over seconds, 1 to bring it back, leaving the fader ' +
|
|
112
|
+
'where the player set it. False when the mix has no bus of that name.', 'MixBus.duck'),
|
|
113
|
+
define('fade', [
|
|
114
|
+
{ name: 'bus', type: 'String' },
|
|
115
|
+
{ name: 'level', type: 'f32' },
|
|
116
|
+
{ name: 'seconds', type: 'f32' },
|
|
117
|
+
], 'bool', ['audio.write'], false, "Move a bus's fader to level over seconds. For a bus the game made: the music and effects " +
|
|
118
|
+
"faders are the player's, and duck is how a script goes over them. False when the mix has " +
|
|
119
|
+
'no bus of that name.', 'MixBus.fadeLevel'),
|
|
120
|
+
define('recall', [
|
|
121
|
+
{ name: 'snapshot', type: 'String' },
|
|
122
|
+
{ name: 'seconds', type: 'f32' },
|
|
123
|
+
], 'bool', ['audio.write'], false, 'Crossfade every level, mute and send to a snapshot the host captured, over seconds. False ' +
|
|
124
|
+
'when nothing was captured under that name.', 'MixConsole.recall'),
|
|
125
|
+
define('pulse', [], 'f32',
|
|
126
|
+
/*
|
|
127
|
+
* Read from the music as it plays, so a replay hears whatever the speakers did: outside the
|
|
128
|
+
* determinism boundary for the same reason playing a sound is.
|
|
129
|
+
*/
|
|
130
|
+
['nondeterministic'], false, 'How hard the music is kicking, 0 to 1, spiking on each kick and falling back. 0 when the ' +
|
|
131
|
+
'host runs no kick detector.', 'KickDetector.pulse'),
|
|
100
132
|
];
|
|
101
133
|
const some = (value) => ({ tag: 'some', value });
|
|
102
134
|
const none = { tag: 'none' };
|
|
@@ -108,7 +140,8 @@ const none = { tag: 'none' };
|
|
|
108
140
|
* registry's `implementation` strings name these; nothing enforces that mechanically, which is why
|
|
109
141
|
* a test asserts every definition has an implementation here.
|
|
110
142
|
*/
|
|
111
|
-
export function audioImplementation(graph, registry) {
|
|
143
|
+
export function audioImplementation(graph, registry, kick = null) {
|
|
144
|
+
const mix = graph.console;
|
|
112
145
|
return {
|
|
113
146
|
sound(slot) {
|
|
114
147
|
const buffer = registry.get(slot);
|
|
@@ -122,5 +155,21 @@ export function audioImplementation(graph, registry) {
|
|
|
122
155
|
},
|
|
123
156
|
distanceGain,
|
|
124
157
|
stereoPan,
|
|
158
|
+
duck(bus, factor, seconds) {
|
|
159
|
+
const found = mix.find(bus);
|
|
160
|
+
found?.duck(factor, seconds);
|
|
161
|
+
return found !== undefined;
|
|
162
|
+
},
|
|
163
|
+
fade(bus, level, seconds) {
|
|
164
|
+
const found = mix.find(bus);
|
|
165
|
+
found?.fadeLevel(level, seconds);
|
|
166
|
+
return found !== undefined;
|
|
167
|
+
},
|
|
168
|
+
recall(snapshot, seconds) {
|
|
169
|
+
return mix.recall(snapshot, seconds);
|
|
170
|
+
},
|
|
171
|
+
pulse() {
|
|
172
|
+
return kick?.pulse ?? 0;
|
|
173
|
+
},
|
|
125
174
|
};
|
|
126
175
|
}
|
|
@@ -103,6 +103,7 @@ export const CHEMISTRY_CAPABILITIES = [
|
|
|
103
103
|
define('charFraction', [P, PARCEL], 'f32', READ, 'Share of the parcel that is now carbon, 0 to 1.'),
|
|
104
104
|
define('charDepth', [P, PARCEL], 'f32', READ, 'Metres of char measured inward from the surface.'),
|
|
105
105
|
define('wetness', [P, PARCEL], 'f32', READ, 'kg of liquid water on the surface, per square metre of it.'),
|
|
106
|
+
define('wettable', [P, PARCEL], 'bool', READ, 'Whether this material can hold liquid water, which is what `wet` asks. Rain over everything asks first, since `wet` refuses an iron nail.'),
|
|
106
107
|
define('phase', [P, PARCEL], 'i32', READ, '0 solid, 1 liquid, 2 gas, 3 mixed. Mixed is a real answer: wet wood is a solid and a liquid at once.'),
|
|
107
108
|
define('burning', [P, PARCEL], 'bool', READ, 'Whether a flame stands over it.'),
|
|
108
109
|
define('smouldering', [P, PARCEL], 'bool', READ, 'Whether it is glowing with no flame. A smoulder survives air a flame cannot, which is why smothering leaves embers.'),
|
|
@@ -211,6 +212,7 @@ export function chemistryImplementation(services) {
|
|
|
211
212
|
charFraction: (_, parcel) => parcels.charFractionOf(parcel),
|
|
212
213
|
charDepth: (_, parcel) => parcels.charDepthOf(parcel),
|
|
213
214
|
wetness: (_, parcel) => parcels.wetnessOf(parcel),
|
|
215
|
+
wettable: (_, parcel) => parcels.wettable(parcel),
|
|
214
216
|
phase: (_, parcel) => parcels.parcelPhaseOf(parcel),
|
|
215
217
|
burning: (_, parcel) => parcels.burning(parcel),
|
|
216
218
|
smouldering: (_, parcel) => parcels.smouldering(parcel),
|
package/dist/bindings/core.d.ts
CHANGED
|
@@ -127,7 +127,17 @@ export declare const SCENE_CAPABILITIES: readonly CapabilityDefinition[];
|
|
|
127
127
|
* accessor.
|
|
128
128
|
*/
|
|
129
129
|
export declare const PHYSICS_CAPABILITIES: readonly CapabilityDefinition[];
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* `drift/input` — what the player is doing, read through an action map or a touch screen, and the
|
|
132
|
+
* pad's motors.
|
|
133
|
+
*
|
|
134
|
+
* **This header said "action maps, rebinding, gamepad, pointer and touch" over an array that bound
|
|
135
|
+
* only the action reads and rumble**, the failure `docs/CAPABILITIES.md` §4 names for three other
|
|
136
|
+
* binding files. Touch is bound now, as a `Touch` handle beside `Actions`, because a scripted game
|
|
137
|
+
* was otherwise undriveable on a phone: `TouchControls` reports to nothing an action map reads.
|
|
138
|
+
* Rebinding and the pointer stay with the host. A controls screen is the game's own interface, and
|
|
139
|
+
* `ActionMap.rebind` changes what a script's `Actions` answer without the script knowing.
|
|
140
|
+
*/
|
|
131
141
|
export declare const INPUT_CAPABILITIES: readonly CapabilityDefinition[];
|
|
132
142
|
/**
|
|
133
143
|
* `drift/camera` — the cinematic cameras, and **only what they actually have**.
|
package/dist/bindings/core.js
CHANGED
|
@@ -74,6 +74,11 @@ export const CORE_TYPES = [
|
|
|
74
74
|
name: 'Actions',
|
|
75
75
|
doc: 'A bound action map: what the player is pressing.',
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
module: INPUT_MODULE,
|
|
79
|
+
name: 'Touch',
|
|
80
|
+
doc: 'Touch controls: a stick on one side of the screen, and taps, holds and flicks on the other.',
|
|
81
|
+
},
|
|
77
82
|
{ module: CAMERA_MODULE, name: 'Camera', doc: 'A cinematic camera.' },
|
|
78
83
|
{
|
|
79
84
|
module: EVENTS_MODULE,
|
|
@@ -431,7 +436,17 @@ export const PHYSICS_CAPABILITIES = [
|
|
|
431
436
|
{ name: 'z', type: 'f32' },
|
|
432
437
|
], 'void', ['physics.write'], false, "The world's gravity, in metres per second squared."),
|
|
433
438
|
];
|
|
434
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* `drift/input` — what the player is doing, read through an action map or a touch screen, and the
|
|
441
|
+
* pad's motors.
|
|
442
|
+
*
|
|
443
|
+
* **This header said "action maps, rebinding, gamepad, pointer and touch" over an array that bound
|
|
444
|
+
* only the action reads and rumble**, the failure `docs/CAPABILITIES.md` §4 names for three other
|
|
445
|
+
* binding files. Touch is bound now, as a `Touch` handle beside `Actions`, because a scripted game
|
|
446
|
+
* was otherwise undriveable on a phone: `TouchControls` reports to nothing an action map reads.
|
|
447
|
+
* Rebinding and the pointer stay with the host. A controls screen is the game's own interface, and
|
|
448
|
+
* `ActionMap.rebind` changes what a script's `Actions` answer without the script knowing.
|
|
449
|
+
*/
|
|
435
450
|
export const INPUT_CAPABILITIES = [
|
|
436
451
|
define(INPUT_MODULE, 'down', [
|
|
437
452
|
{ name: 'actions', type: 'Actions' },
|
|
@@ -488,6 +503,11 @@ export const INPUT_CAPABILITIES = [
|
|
|
488
503
|
{ name: 'weak', type: 'f32' },
|
|
489
504
|
], 'bool', ['host'], false, 'Rumble for a duration in milliseconds, at two magnitudes in [0, 1]: `strong` is the low-frequency motor and `weak` the high-frequency one. Answers whether the platform took it; false is a real answer, not an error.'),
|
|
490
505
|
define(INPUT_MODULE, 'stopRumble', [{ name: 'actions', type: 'Actions' }], 'bool', ['host'], false, 'Stop whatever that pad is playing. Answers whether the platform took it.'),
|
|
506
|
+
define(INPUT_MODULE, 'touchX', [{ name: 'touch', type: 'Touch' }], 'f32', ['input.read'], false, "The touch stick's horizontal, -1 to 1, 0 while no thumb is on it."),
|
|
507
|
+
define(INPUT_MODULE, 'touchY', [{ name: 'touch', type: 'Touch' }], 'f32', ['input.read'], false, "The touch stick's vertical, -1 to 1, up negative as a gamepad stick's is."),
|
|
508
|
+
define(INPUT_MODULE, 'touchHeld', [{ name: 'touch', type: 'Touch' }], 'bool', ['input.read'], false, 'Whether a thumb is held still on the action side of the screen: the held form of the primary.'),
|
|
509
|
+
define(INPUT_MODULE, 'touchTap', [{ name: 'touch', type: 'Touch' }], 'bool', ['input.read'], false, 'Whether the action side was tapped since this was last asked. True once per tap, so ask it once a tick.'),
|
|
510
|
+
define(INPUT_MODULE, 'touchSlide', [{ name: 'touch', type: 'Touch' }], 'bool', ['input.read'], false, 'Whether a downward flick on the action side is being held: the secondary, a slide or a crouch.'),
|
|
491
511
|
];
|
|
492
512
|
/**
|
|
493
513
|
* `drift/camera` — the cinematic cameras, and **only what they actually have**.
|
|
@@ -795,6 +815,11 @@ export function inputImplementation() {
|
|
|
795
815
|
canRumble: (actions) => actions.canRumble,
|
|
796
816
|
rumble: (actions, durationMs, strong, weak) => actions.rumble(durationMs, strong, weak),
|
|
797
817
|
stopRumble: (actions) => actions.stopRumble(),
|
|
818
|
+
touchX: (touch) => f(touch.moveX),
|
|
819
|
+
touchY: (touch) => f(touch.moveY),
|
|
820
|
+
touchHeld: (touch) => touch.primaryHeld,
|
|
821
|
+
touchTap: (touch) => touch.consumePrimaryPress(),
|
|
822
|
+
touchSlide: (touch) => touch.secondaryHeld,
|
|
798
823
|
};
|
|
799
824
|
}
|
|
800
825
|
export function cameraImplementation() {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `drift/ecs`
|
|
2
|
+
* `drift/ecs` — the entity model as DriftScript reaches it, prefabs included.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* what
|
|
4
|
+
* This and `drift/prefab` were the two surfaces the linker refused by name until Track M shipped,
|
|
5
|
+
* and this file is what killed that refusal. `drift/prefab` is gone again: its one function was
|
|
6
|
+
* `spawn`, a keyword, so no script could call it, and making an entity from a prefab is
|
|
7
|
+
* `ecs.instantiate`. Like every binding here, nothing in it is new engine code: a binding is a
|
|
6
8
|
* description and a lookup.
|
|
7
9
|
*
|
|
8
10
|
* ---
|
|
@@ -26,7 +28,6 @@
|
|
|
26
28
|
import { type ComponentType, type Prefab } from '@driftengine/entities';
|
|
27
29
|
import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
|
|
28
30
|
export declare const ECS_MODULE = "drift/ecs";
|
|
29
|
-
export declare const PREFAB_MODULE = "drift/prefab";
|
|
30
31
|
export declare const ENTITY_TYPES: readonly OpaqueType[];
|
|
31
32
|
/**
|
|
32
33
|
* An entity is `Entity`, and it took the language until DriftScript 1.6.0 to have the word.
|
|
@@ -56,7 +57,6 @@ export declare const ECS_CAPABILITIES: readonly CapabilityDefinition[];
|
|
|
56
57
|
* generated code reaches them exactly as it reaches everything else, through `__bind($host)`.
|
|
57
58
|
*/
|
|
58
59
|
export declare const QUERY_CAPABILITIES: readonly CapabilityDefinition[];
|
|
59
|
-
export declare const PREFAB_CAPABILITIES: readonly CapabilityDefinition[];
|
|
60
60
|
/** What a host supplies so a name can become a component type or a prefab. */
|
|
61
61
|
export interface EntityServices {
|
|
62
62
|
/**
|
|
@@ -66,8 +66,7 @@ export interface EntityServices {
|
|
|
66
66
|
* with two worlds — a simulation and a preview — would otherwise need two hosts.
|
|
67
67
|
*/
|
|
68
68
|
readonly components: ReadonlyMap<string, ComponentType>;
|
|
69
|
-
/** Prefabs by name. Absent when the host has none, which makes `
|
|
69
|
+
/** Prefabs by name. Absent when the host has none, which makes `instantiate` refuse in words. */
|
|
70
70
|
readonly prefabs?: ReadonlyMap<string, Prefab>;
|
|
71
71
|
}
|
|
72
72
|
export declare function entitiesImplementation(services: EntityServices): Record<string, unknown>;
|
|
73
|
-
export declare function prefabImplementation(services: EntityServices): Record<string, unknown>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `drift/ecs`
|
|
2
|
+
* `drift/ecs` — the entity model as DriftScript reaches it, prefabs included.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* what
|
|
4
|
+
* This and `drift/prefab` were the two surfaces the linker refused by name until Track M shipped,
|
|
5
|
+
* and this file is what killed that refusal. `drift/prefab` is gone again: its one function was
|
|
6
|
+
* `spawn`, a keyword, so no script could call it, and making an entity from a prefab is
|
|
7
|
+
* `ecs.instantiate`. Like every binding here, nothing in it is new engine code: a binding is a
|
|
6
8
|
* description and a lookup.
|
|
7
9
|
*
|
|
8
10
|
* ---
|
|
@@ -26,7 +28,6 @@
|
|
|
26
28
|
import { ENTITY_INDEX_CEILING, instantiate, } from '@driftengine/entities';
|
|
27
29
|
import { defineCapability } from 'driftscript';
|
|
28
30
|
export const ECS_MODULE = 'drift/ecs';
|
|
29
|
-
export const PREFAB_MODULE = 'drift/prefab';
|
|
30
31
|
const define = (module, name, params, returns, effects, deterministic, doc) => defineCapability({
|
|
31
32
|
module,
|
|
32
33
|
name,
|
|
@@ -146,6 +147,16 @@ export const ECS_CAPABILITIES = [
|
|
|
146
147
|
{ name: 'radius', type: 'float' },
|
|
147
148
|
], 'bool', ['ecs.read'], true, "Whether any entity carrying a component has position fields putting it within a radius, and the search `nearest` then reads. **A linear scan of that component's column, not an index** — the entity model keeps no spatial structure, so this costs one pass over everything carrying the component. Position is read from three fields you name, so nothing here knows what a transform is called."),
|
|
148
149
|
define(ECS_MODULE, 'nearest', [{ name: 'world', type: 'World' }], 'Entity', ['ecs.read'], true, 'The entity the last `findNearest` on this world found. **Valid only until the next one**, and meaningless when that answered false — the same lifetime a raycast hit has.'),
|
|
150
|
+
/*
|
|
151
|
+
* **Here and not in a `drift/prefab` of its own, because that one could not be called.** It was
|
|
152
|
+
* bound as `prefab.spawn`, and `prefab` and `spawn` are both keywords: `spawn` cannot even appear
|
|
153
|
+
* in an import list, so no script could reach it, and the target never provided the module
|
|
154
|
+
* either. A prefab is made into entities in a world, so the call belongs with the others that do.
|
|
155
|
+
*/
|
|
156
|
+
define(ECS_MODULE, 'instantiate', [
|
|
157
|
+
{ name: 'world', type: 'World' },
|
|
158
|
+
{ name: 'prefab', type: 'String' },
|
|
159
|
+
], 'Entity', ['ecs.write'], false, 'Make an entity from a prefab, with every component it names. Overrides are the host’s to apply; a script that wants a different value writes it after.'),
|
|
149
160
|
];
|
|
150
161
|
/**
|
|
151
162
|
* The four a query loop is built on.
|
|
@@ -174,14 +185,9 @@ export const QUERY_CAPABILITIES = [
|
|
|
174
185
|
{ name: 'forWriting', type: 'bool' },
|
|
175
186
|
], 'View', ['ecs.read'], true, 'A component’s live columns. `forWriting` decides which of a system’s declarations is checked — a loop that only reads takes a readable view, and asking for more than the system declared is refused.'),
|
|
176
187
|
];
|
|
177
|
-
export const PREFAB_CAPABILITIES = [
|
|
178
|
-
define(PREFAB_MODULE, 'spawn', [
|
|
179
|
-
{ name: 'world', type: 'World' },
|
|
180
|
-
{ name: 'prefab', type: 'String' },
|
|
181
|
-
], 'Entity', ['ecs.write'], false, 'Make one, with every component the prefab names. Overrides are the host’s to apply; a script that wants a different value writes it after.'),
|
|
182
|
-
];
|
|
183
188
|
export function entitiesImplementation(services) {
|
|
184
189
|
const { components } = services;
|
|
190
|
+
const prefabs = services.prefabs ?? new Map();
|
|
185
191
|
/**
|
|
186
192
|
* Resolve a component name, or refuse naming what the world has.
|
|
187
193
|
*
|
|
@@ -243,11 +249,15 @@ export function entitiesImplementation(services) {
|
|
|
243
249
|
return step.done ? -1 : step.value;
|
|
244
250
|
},
|
|
245
251
|
view: (w, component, forWriting) => w.view(resolve(component), forWriting),
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
/*
|
|
253
|
+
* **Through the methods a system's view shares with a world, never a store.** Inside a system
|
|
254
|
+
* the value a script passes is the `SystemView` the schedule handed it, which has no `store`;
|
|
255
|
+
* reading `store.size` here threw the moment a system counted anything. The view checks the
|
|
256
|
+
* component against the system's declarations, which is the other thing a store would skip.
|
|
257
|
+
*/
|
|
258
|
+
count: (w, component) => w.count(resolve(component)),
|
|
259
|
+
/* Zero past the end, as this has always answered; the walk's bound is `count`. */
|
|
260
|
+
at: (w, component, index) => Math.max(0, w.at(resolve(component), index)),
|
|
251
261
|
/*
|
|
252
262
|
* **A boolean rather than an option, and a second accessor for the handle.** `Sound?` is an
|
|
253
263
|
* option because a slot is resolved once at load; this is written to be called every frame by
|
|
@@ -322,16 +332,11 @@ export function entitiesImplementation(services) {
|
|
|
322
332
|
* is why the search answers one.
|
|
323
333
|
*/
|
|
324
334
|
nearest: (w) => Math.max(0, found.get(w) ?? -1),
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
export function prefabImplementation(services) {
|
|
328
|
-
const prefabs = services.prefabs ?? new Map();
|
|
329
|
-
return {
|
|
330
|
-
spawn: (w, name) => {
|
|
335
|
+
instantiate: (w, name) => {
|
|
331
336
|
const prefab = prefabs.get(name);
|
|
332
337
|
if (prefab === undefined) {
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
const known = [...prefabs.keys()].map((k) => `\`${k}\``).join(', ');
|
|
339
|
+
throw new Error(`no prefab is registered as \`${name}\`. This host has ${known}.`);
|
|
335
340
|
}
|
|
336
341
|
return instantiate(w, prefab);
|
|
337
342
|
},
|
package/dist/host.d.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* A binding is cheap — a table entry and a lookup — and is the difference between a subsystem
|
|
36
36
|
* a consumer can script and one they can only call from TypeScript.
|
|
37
37
|
*/
|
|
38
|
-
import type { AudioGraph, SoundRegistry } from '@driftengine/audio';
|
|
38
|
+
import type { AudioGraph, KickDetector, SoundRegistry } from '@driftengine/audio';
|
|
39
39
|
import { type BehaviorServices } from './bindings/behavior.ts';
|
|
40
40
|
import { type NavigationServices } from './bindings/navigation.ts';
|
|
41
41
|
import { type CapabilityRegistry, type DriftModule, type TargetManifest } from 'driftscript';
|
|
@@ -87,9 +87,14 @@ export interface HostServices extends CoreServices {
|
|
|
87
87
|
* one of those is a scene that draws and the other is a scene nobody is in.
|
|
88
88
|
*/
|
|
89
89
|
readonly xr?: XrRuntime;
|
|
90
|
+
/**
|
|
91
|
+
* The mix a script plays into, the sounds it can name, and, when the game runs one, the kick
|
|
92
|
+
* detector `pulse` reads. The host calls the detector's `update` each frame; a script only reads.
|
|
93
|
+
*/
|
|
90
94
|
readonly audio?: {
|
|
91
95
|
readonly graph: AudioGraph;
|
|
92
96
|
readonly registry: SoundRegistry;
|
|
97
|
+
readonly kick?: KickDetector;
|
|
93
98
|
};
|
|
94
99
|
/**
|
|
95
100
|
* The component types, and any prefabs, a script may name.
|
package/dist/host.js
CHANGED
|
@@ -13,7 +13,7 @@ import { XR_CAPABILITIES, XR_MODULE, XR_TYPES, xrImplementation, } from './bindi
|
|
|
13
13
|
import { RENDER_CAPABILITIES, RENDER_MODULE, RENDER_TYPES, renderImplementation, } from './bindings/render.js';
|
|
14
14
|
import { ANIMATION_CAPABILITIES, ANIMATION_MODULE, ANIMATION_TYPES, animationImplementation, } from './bindings/animation.js';
|
|
15
15
|
import { CAMERA_CAPABILITIES, CAMERA_MODULE, CORE_TYPES, EVENTS_CAPABILITIES, EVENTS_MODULE, INPUT_CAPABILITIES, INPUT_MODULE, PERSISTENCE_CAPABILITIES, PERSISTENCE_MODULE, PHYSICS_CAPABILITIES, PHYSICS_MODULE, RANDOM_CAPABILITIES, RANDOM_MODULE, SCENE_CAPABILITIES, SCENE_MODULE, TIME_CAPABILITIES, TIME_MODULE, cameraImplementation, eventsImplementation, inputImplementation, persistenceImplementation, physicsImplementation, randomImplementation, sceneImplementation, timeImplementation, } from './bindings/core.js';
|
|
16
|
-
import { ECS_CAPABILITIES, QUERY_CAPABILITIES, ECS_MODULE, ENTITY_TYPES,
|
|
16
|
+
import { ECS_CAPABILITIES, QUERY_CAPABILITIES, ECS_MODULE, ENTITY_TYPES, entitiesImplementation, } from './bindings/entities.js';
|
|
17
17
|
import { CHEMISTRY_CAPABILITIES, CHEMISTRY_MODULE, CHEMISTRY_TYPES, chemistryImplementation, } from './bindings/chemistry.js';
|
|
18
18
|
import { registerStd, stdImplementations } from 'driftscript/std';
|
|
19
19
|
/**
|
|
@@ -97,7 +97,6 @@ export function engineRegistry() {
|
|
|
97
97
|
...NAVIGATION_CAPABILITIES,
|
|
98
98
|
...PERSISTENCE_CAPABILITIES,
|
|
99
99
|
...PHYSICS_CAPABILITIES,
|
|
100
|
-
...PREFAB_CAPABILITIES,
|
|
101
100
|
...RANDOM_CAPABILITIES,
|
|
102
101
|
...RENDER_CAPABILITIES,
|
|
103
102
|
...XR_CAPABILITIES,
|
|
@@ -138,13 +137,12 @@ export function engineImplementations(services) {
|
|
|
138
137
|
script that fails on `math.clamp` with the namespace undefined. */
|
|
139
138
|
const map = { ...stdImplementations() };
|
|
140
139
|
if (services.audio !== undefined) {
|
|
141
|
-
map[AUDIO_MODULE] = audioImplementation(services.audio.graph, services.audio.registry);
|
|
140
|
+
map[AUDIO_MODULE] = audioImplementation(services.audio.graph, services.audio.registry, services.audio.kick ?? null);
|
|
142
141
|
}
|
|
143
142
|
if (services.clocks !== undefined)
|
|
144
143
|
map[TIME_MODULE] = timeImplementation(services.clocks);
|
|
145
144
|
if (services.entities !== undefined) {
|
|
146
145
|
map[ECS_MODULE] = entitiesImplementation(services.entities);
|
|
147
|
-
map[PREFAB_MODULE] = prefabImplementation(services.entities);
|
|
148
146
|
}
|
|
149
147
|
/*
|
|
150
148
|
* These four take their handle as an argument rather than from the host, so they need nothing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/script",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.2",
|
|
4
4
|
"description": "The drift/* capability bindings: what this engine provides to DriftScript, and the only place the two are coupled",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,20 +28,20 @@
|
|
|
28
28
|
],
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"driftscript": "1.13.
|
|
31
|
+
"driftscript": "1.13.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/ai": "4.7.
|
|
35
|
-
"@driftengine/animation": "4.7.
|
|
36
|
-
"@driftengine/audio": "4.7.
|
|
37
|
-
"@driftengine/chemistry": "4.7.
|
|
38
|
-
"@driftengine/core": "4.7.
|
|
39
|
-
"@driftengine/editor": "4.7.
|
|
40
|
-
"@driftengine/entities": "4.7.
|
|
41
|
-
"@driftengine/network": "4.7.
|
|
42
|
-
"@driftengine/terrain": "4.7.
|
|
43
|
-
"@driftengine/ui2d": "4.7.
|
|
44
|
-
"@driftengine/xr": "4.7.
|
|
34
|
+
"@driftengine/ai": "4.7.2",
|
|
35
|
+
"@driftengine/animation": "4.7.2",
|
|
36
|
+
"@driftengine/audio": "4.7.2",
|
|
37
|
+
"@driftengine/chemistry": "4.7.2",
|
|
38
|
+
"@driftengine/core": "4.7.2",
|
|
39
|
+
"@driftengine/editor": "4.7.2",
|
|
40
|
+
"@driftengine/entities": "4.7.2",
|
|
41
|
+
"@driftengine/network": "4.7.2",
|
|
42
|
+
"@driftengine/terrain": "4.7.2",
|
|
43
|
+
"@driftengine/ui2d": "4.7.2",
|
|
44
|
+
"@driftengine/xr": "4.7.2"
|
|
45
45
|
},
|
|
46
46
|
"author": "Drift Technologies",
|
|
47
47
|
"repository": {
|
package/src/bindings/audio.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `drift/audio` — what this engine lets a script do to sound.
|
|
3
3
|
*
|
|
4
|
-
* **
|
|
4
|
+
* **Nine capabilities: a slot, two ways to play it, the two cheap positional helpers, three dials on
|
|
5
|
+
* the mix, and the kick the music is making.** The first five are the original binding. This
|
|
5
6
|
* header read "playback, placement, the mix, and the rhythm the music is doing" until 2026-08-27,
|
|
6
|
-
* and three of those four were
|
|
7
|
-
*
|
|
7
|
+
* and three of those four were not here then — no mix verb, no rhythm verb, no HRTF-panned source,
|
|
8
|
+
* no listener and no reverb zone. The sentence was written from *Track G's* deliverables
|
|
8
9
|
* rather than from the list beneath it, which is the same failure `drift/physics` had one file
|
|
9
10
|
* over, and the same one the first `drift/camera` binding had: a comment describing what a
|
|
10
11
|
* subsystem ought to expose rather than what this array does.
|
|
@@ -13,12 +14,18 @@
|
|
|
13
14
|
* entries describing capabilities the engine already has, plus a map of implementations the host
|
|
14
15
|
* supplies at link time. **No new engine code.**
|
|
15
16
|
*
|
|
16
|
-
* **
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* a
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
17
|
+
* **The mix is bound as dials, by name.** `duck`, `fade` and `recall` move a bus or a snapshot the
|
|
18
|
+
* host already made, named by the string it was made under, and `pulse` reads a kick detector the
|
|
19
|
+
* host already runs. None of them hands a script an object, so none of them raises the question of
|
|
20
|
+
* who disposes it; a name nothing answers to is a `false`, never a silent success. The same rule
|
|
21
|
+
* `drift/render` follows: a dial is bound, and anything built from a table waits for a design.
|
|
22
|
+
*
|
|
23
|
+
* **What is missing, and why it is not guessed at here.** `AudioListenerGraph`, `SpatialSource` and
|
|
24
|
+
* `ReverbZone` are stateful objects with lifetimes, and a script-side surface for them is a design
|
|
25
|
+
* about handles and ownership rather than a table entry — who disposes a source, what happens to
|
|
26
|
+
* its probe slot, whether a zone outlives the script that made it. `docs/CAPABILITIES.md` §4
|
|
27
|
+
* records it. Taking that badly would put a leak in front of every script author, which is worse
|
|
28
|
+
* than a surface they can see is absent.
|
|
22
29
|
*
|
|
23
30
|
* Three constraints this surface inherits, none of them negotiable:
|
|
24
31
|
*
|
|
@@ -33,7 +40,8 @@
|
|
|
33
40
|
* silence and reports success, which is the failure the language's no-implicit-null rule exists
|
|
34
41
|
* to prevent — and this is the first place in the engine where that rule earns its keep.
|
|
35
42
|
*/
|
|
36
|
-
import {
|
|
43
|
+
import { distanceGain, stereoPan } from '@driftengine/audio';
|
|
44
|
+
import type { AudioGraph, KickDetector, SoundRegistry } from '@driftengine/audio';
|
|
37
45
|
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
38
46
|
|
|
39
47
|
export const AUDIO_MODULE = 'drift/audio';
|
|
@@ -148,6 +156,62 @@ export const AUDIO_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
148
156
|
'Where something sits in the stereo field, relative to a listener facing yaw.',
|
|
149
157
|
'audio.stereoPan',
|
|
150
158
|
),
|
|
159
|
+
define(
|
|
160
|
+
'duck',
|
|
161
|
+
[
|
|
162
|
+
{ name: 'bus', type: 'String' },
|
|
163
|
+
{ name: 'factor', type: 'f32' },
|
|
164
|
+
{ name: 'seconds', type: 'f32' },
|
|
165
|
+
],
|
|
166
|
+
'bool',
|
|
167
|
+
['audio.write'],
|
|
168
|
+
false,
|
|
169
|
+
'Take a bus down to factor of its fader over seconds, 1 to bring it back, leaving the fader ' +
|
|
170
|
+
'where the player set it. False when the mix has no bus of that name.',
|
|
171
|
+
'MixBus.duck',
|
|
172
|
+
),
|
|
173
|
+
define(
|
|
174
|
+
'fade',
|
|
175
|
+
[
|
|
176
|
+
{ name: 'bus', type: 'String' },
|
|
177
|
+
{ name: 'level', type: 'f32' },
|
|
178
|
+
{ name: 'seconds', type: 'f32' },
|
|
179
|
+
],
|
|
180
|
+
'bool',
|
|
181
|
+
['audio.write'],
|
|
182
|
+
false,
|
|
183
|
+
"Move a bus's fader to level over seconds. For a bus the game made: the music and effects " +
|
|
184
|
+
"faders are the player's, and duck is how a script goes over them. False when the mix has " +
|
|
185
|
+
'no bus of that name.',
|
|
186
|
+
'MixBus.fadeLevel',
|
|
187
|
+
),
|
|
188
|
+
define(
|
|
189
|
+
'recall',
|
|
190
|
+
[
|
|
191
|
+
{ name: 'snapshot', type: 'String' },
|
|
192
|
+
{ name: 'seconds', type: 'f32' },
|
|
193
|
+
],
|
|
194
|
+
'bool',
|
|
195
|
+
['audio.write'],
|
|
196
|
+
false,
|
|
197
|
+
'Crossfade every level, mute and send to a snapshot the host captured, over seconds. False ' +
|
|
198
|
+
'when nothing was captured under that name.',
|
|
199
|
+
'MixConsole.recall',
|
|
200
|
+
),
|
|
201
|
+
define(
|
|
202
|
+
'pulse',
|
|
203
|
+
[],
|
|
204
|
+
'f32',
|
|
205
|
+
/*
|
|
206
|
+
* Read from the music as it plays, so a replay hears whatever the speakers did: outside the
|
|
207
|
+
* determinism boundary for the same reason playing a sound is.
|
|
208
|
+
*/
|
|
209
|
+
['nondeterministic'],
|
|
210
|
+
false,
|
|
211
|
+
'How hard the music is kicking, 0 to 1, spiking on each kick and falling back. 0 when the ' +
|
|
212
|
+
'host runs no kick detector.',
|
|
213
|
+
'KickDetector.pulse',
|
|
214
|
+
),
|
|
151
215
|
];
|
|
152
216
|
|
|
153
217
|
/** How the runtime represents an option. Matches what the compiler generates for `some`/`none`. */
|
|
@@ -167,7 +231,9 @@ const none: Option<never> = { tag: 'none' };
|
|
|
167
231
|
export function audioImplementation(
|
|
168
232
|
graph: AudioGraph,
|
|
169
233
|
registry: SoundRegistry,
|
|
234
|
+
kick: KickDetector | null = null,
|
|
170
235
|
): Record<string, unknown> {
|
|
236
|
+
const mix = graph.console;
|
|
171
237
|
return {
|
|
172
238
|
sound(slot: string): Option<AudioBuffer> {
|
|
173
239
|
const buffer = registry.get(slot);
|
|
@@ -181,5 +247,21 @@ export function audioImplementation(
|
|
|
181
247
|
},
|
|
182
248
|
distanceGain,
|
|
183
249
|
stereoPan,
|
|
250
|
+
duck(bus: string, factor: number, seconds: number): boolean {
|
|
251
|
+
const found = mix.find(bus);
|
|
252
|
+
found?.duck(factor, seconds);
|
|
253
|
+
return found !== undefined;
|
|
254
|
+
},
|
|
255
|
+
fade(bus: string, level: number, seconds: number): boolean {
|
|
256
|
+
const found = mix.find(bus);
|
|
257
|
+
found?.fadeLevel(level, seconds);
|
|
258
|
+
return found !== undefined;
|
|
259
|
+
},
|
|
260
|
+
recall(snapshot: string, seconds: number): boolean {
|
|
261
|
+
return mix.recall(snapshot, seconds);
|
|
262
|
+
},
|
|
263
|
+
pulse(): number {
|
|
264
|
+
return kick?.pulse ?? 0;
|
|
265
|
+
},
|
|
184
266
|
};
|
|
185
267
|
}
|
|
@@ -189,6 +189,13 @@ export const CHEMISTRY_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
189
189
|
READ,
|
|
190
190
|
'kg of liquid water on the surface, per square metre of it.',
|
|
191
191
|
),
|
|
192
|
+
define(
|
|
193
|
+
'wettable',
|
|
194
|
+
[P, PARCEL],
|
|
195
|
+
'bool',
|
|
196
|
+
READ,
|
|
197
|
+
'Whether this material can hold liquid water, which is what `wet` asks. Rain over everything asks first, since `wet` refuses an iron nail.',
|
|
198
|
+
),
|
|
192
199
|
define(
|
|
193
200
|
'phase',
|
|
194
201
|
[P, PARCEL],
|
|
@@ -488,6 +495,7 @@ export function chemistryImplementation(services: ChemistryServices): Record<str
|
|
|
488
495
|
charFraction: (_: unknown, parcel: number) => parcels.charFractionOf(parcel),
|
|
489
496
|
charDepth: (_: unknown, parcel: number) => parcels.charDepthOf(parcel),
|
|
490
497
|
wetness: (_: unknown, parcel: number) => parcels.wetnessOf(parcel),
|
|
498
|
+
wettable: (_: unknown, parcel: number) => parcels.wettable(parcel),
|
|
491
499
|
phase: (_: unknown, parcel: number) => parcels.parcelPhaseOf(parcel),
|
|
492
500
|
burning: (_: unknown, parcel: number) => parcels.burning(parcel),
|
|
493
501
|
smouldering: (_: unknown, parcel: number) => parcels.smouldering(parcel),
|
package/src/bindings/core.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type QueuedMessage,
|
|
32
32
|
type RayHit,
|
|
33
33
|
SceneNode,
|
|
34
|
+
type TouchControls,
|
|
34
35
|
type WindState,
|
|
35
36
|
angleDelta,
|
|
36
37
|
boxCollider,
|
|
@@ -106,6 +107,11 @@ export const CORE_TYPES: readonly OpaqueType[] = [
|
|
|
106
107
|
name: 'Actions',
|
|
107
108
|
doc: 'A bound action map: what the player is pressing.',
|
|
108
109
|
},
|
|
110
|
+
{
|
|
111
|
+
module: INPUT_MODULE,
|
|
112
|
+
name: 'Touch',
|
|
113
|
+
doc: 'Touch controls: a stick on one side of the screen, and taps, holds and flicks on the other.',
|
|
114
|
+
},
|
|
109
115
|
{ module: CAMERA_MODULE, name: 'Camera', doc: 'A cinematic camera.' },
|
|
110
116
|
{
|
|
111
117
|
module: EVENTS_MODULE,
|
|
@@ -1018,7 +1024,17 @@ export const PHYSICS_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
1018
1024
|
),
|
|
1019
1025
|
];
|
|
1020
1026
|
|
|
1021
|
-
/**
|
|
1027
|
+
/**
|
|
1028
|
+
* `drift/input` — what the player is doing, read through an action map or a touch screen, and the
|
|
1029
|
+
* pad's motors.
|
|
1030
|
+
*
|
|
1031
|
+
* **This header said "action maps, rebinding, gamepad, pointer and touch" over an array that bound
|
|
1032
|
+
* only the action reads and rumble**, the failure `docs/CAPABILITIES.md` §4 names for three other
|
|
1033
|
+
* binding files. Touch is bound now, as a `Touch` handle beside `Actions`, because a scripted game
|
|
1034
|
+
* was otherwise undriveable on a phone: `TouchControls` reports to nothing an action map reads.
|
|
1035
|
+
* Rebinding and the pointer stay with the host. A controls screen is the game's own interface, and
|
|
1036
|
+
* `ActionMap.rebind` changes what a script's `Actions` answer without the script knowing.
|
|
1037
|
+
*/
|
|
1022
1038
|
export const INPUT_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
1023
1039
|
define(
|
|
1024
1040
|
INPUT_MODULE,
|
|
@@ -1147,6 +1163,51 @@ export const INPUT_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
1147
1163
|
false,
|
|
1148
1164
|
'Stop whatever that pad is playing. Answers whether the platform took it.',
|
|
1149
1165
|
),
|
|
1166
|
+
define(
|
|
1167
|
+
INPUT_MODULE,
|
|
1168
|
+
'touchX',
|
|
1169
|
+
[{ name: 'touch', type: 'Touch' }],
|
|
1170
|
+
'f32',
|
|
1171
|
+
['input.read'],
|
|
1172
|
+
false,
|
|
1173
|
+
"The touch stick's horizontal, -1 to 1, 0 while no thumb is on it.",
|
|
1174
|
+
),
|
|
1175
|
+
define(
|
|
1176
|
+
INPUT_MODULE,
|
|
1177
|
+
'touchY',
|
|
1178
|
+
[{ name: 'touch', type: 'Touch' }],
|
|
1179
|
+
'f32',
|
|
1180
|
+
['input.read'],
|
|
1181
|
+
false,
|
|
1182
|
+
"The touch stick's vertical, -1 to 1, up negative as a gamepad stick's is.",
|
|
1183
|
+
),
|
|
1184
|
+
define(
|
|
1185
|
+
INPUT_MODULE,
|
|
1186
|
+
'touchHeld',
|
|
1187
|
+
[{ name: 'touch', type: 'Touch' }],
|
|
1188
|
+
'bool',
|
|
1189
|
+
['input.read'],
|
|
1190
|
+
false,
|
|
1191
|
+
'Whether a thumb is held still on the action side of the screen: the held form of the primary.',
|
|
1192
|
+
),
|
|
1193
|
+
define(
|
|
1194
|
+
INPUT_MODULE,
|
|
1195
|
+
'touchTap',
|
|
1196
|
+
[{ name: 'touch', type: 'Touch' }],
|
|
1197
|
+
'bool',
|
|
1198
|
+
['input.read'],
|
|
1199
|
+
false,
|
|
1200
|
+
'Whether the action side was tapped since this was last asked. True once per tap, so ask it once a tick.',
|
|
1201
|
+
),
|
|
1202
|
+
define(
|
|
1203
|
+
INPUT_MODULE,
|
|
1204
|
+
'touchSlide',
|
|
1205
|
+
[{ name: 'touch', type: 'Touch' }],
|
|
1206
|
+
'bool',
|
|
1207
|
+
['input.read'],
|
|
1208
|
+
false,
|
|
1209
|
+
'Whether a downward flick on the action side is being held: the secondary, a slide or a crouch.',
|
|
1210
|
+
),
|
|
1150
1211
|
];
|
|
1151
1212
|
|
|
1152
1213
|
/**
|
|
@@ -1583,6 +1644,11 @@ export function inputImplementation(): Record<string, unknown> {
|
|
|
1583
1644
|
rumble: (actions: ActionMap, durationMs: number, strong: number, weak: number) =>
|
|
1584
1645
|
actions.rumble(durationMs, strong, weak),
|
|
1585
1646
|
stopRumble: (actions: ActionMap) => actions.stopRumble(),
|
|
1647
|
+
touchX: (touch: TouchControls) => f(touch.moveX),
|
|
1648
|
+
touchY: (touch: TouchControls) => f(touch.moveY),
|
|
1649
|
+
touchHeld: (touch: TouchControls) => touch.primaryHeld,
|
|
1650
|
+
touchTap: (touch: TouchControls) => touch.consumePrimaryPress(),
|
|
1651
|
+
touchSlide: (touch: TouchControls) => touch.secondaryHeld,
|
|
1586
1652
|
};
|
|
1587
1653
|
}
|
|
1588
1654
|
|
package/src/bindings/entities.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `drift/ecs`
|
|
2
|
+
* `drift/ecs` — the entity model as DriftScript reaches it, prefabs included.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* what
|
|
4
|
+
* This and `drift/prefab` were the two surfaces the linker refused by name until Track M shipped,
|
|
5
|
+
* and this file is what killed that refusal. `drift/prefab` is gone again: its one function was
|
|
6
|
+
* `spawn`, a keyword, so no script could call it, and making an entity from a prefab is
|
|
7
|
+
* `ecs.instantiate`. Like every binding here, nothing in it is new engine code: a binding is a
|
|
6
8
|
* description and a lookup.
|
|
7
9
|
*
|
|
8
10
|
* ---
|
|
@@ -30,13 +32,13 @@ import {
|
|
|
30
32
|
type Entity,
|
|
31
33
|
type Prefab,
|
|
32
34
|
type QueryCursor,
|
|
35
|
+
type SystemView,
|
|
33
36
|
type World,
|
|
34
37
|
instantiate,
|
|
35
38
|
} from '@driftengine/entities';
|
|
36
39
|
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
37
40
|
|
|
38
41
|
export const ECS_MODULE = 'drift/ecs';
|
|
39
|
-
export const PREFAB_MODULE = 'drift/prefab';
|
|
40
42
|
|
|
41
43
|
const define = (
|
|
42
44
|
module: string,
|
|
@@ -264,6 +266,24 @@ export const ECS_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
264
266
|
true,
|
|
265
267
|
'The entity the last `findNearest` on this world found. **Valid only until the next one**, and meaningless when that answered false — the same lifetime a raycast hit has.',
|
|
266
268
|
),
|
|
269
|
+
/*
|
|
270
|
+
* **Here and not in a `drift/prefab` of its own, because that one could not be called.** It was
|
|
271
|
+
* bound as `prefab.spawn`, and `prefab` and `spawn` are both keywords: `spawn` cannot even appear
|
|
272
|
+
* in an import list, so no script could reach it, and the target never provided the module
|
|
273
|
+
* either. A prefab is made into entities in a world, so the call belongs with the others that do.
|
|
274
|
+
*/
|
|
275
|
+
define(
|
|
276
|
+
ECS_MODULE,
|
|
277
|
+
'instantiate',
|
|
278
|
+
[
|
|
279
|
+
{ name: 'world', type: 'World' },
|
|
280
|
+
{ name: 'prefab', type: 'String' },
|
|
281
|
+
],
|
|
282
|
+
'Entity',
|
|
283
|
+
['ecs.write'],
|
|
284
|
+
false,
|
|
285
|
+
'Make an entity from a prefab, with every component it names. Overrides are the host’s to apply; a script that wants a different value writes it after.',
|
|
286
|
+
),
|
|
267
287
|
];
|
|
268
288
|
|
|
269
289
|
/**
|
|
@@ -326,21 +346,6 @@ export const QUERY_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
326
346
|
),
|
|
327
347
|
];
|
|
328
348
|
|
|
329
|
-
export const PREFAB_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
330
|
-
define(
|
|
331
|
-
PREFAB_MODULE,
|
|
332
|
-
'spawn',
|
|
333
|
-
[
|
|
334
|
-
{ name: 'world', type: 'World' },
|
|
335
|
-
{ name: 'prefab', type: 'String' },
|
|
336
|
-
],
|
|
337
|
-
'Entity',
|
|
338
|
-
['ecs.write'],
|
|
339
|
-
false,
|
|
340
|
-
'Make one, with every component the prefab names. Overrides are the host’s to apply; a script that wants a different value writes it after.',
|
|
341
|
-
),
|
|
342
|
-
];
|
|
343
|
-
|
|
344
349
|
/** What a host supplies so a name can become a component type or a prefab. */
|
|
345
350
|
export interface EntityServices {
|
|
346
351
|
/**
|
|
@@ -350,12 +355,13 @@ export interface EntityServices {
|
|
|
350
355
|
* with two worlds — a simulation and a preview — would otherwise need two hosts.
|
|
351
356
|
*/
|
|
352
357
|
readonly components: ReadonlyMap<string, ComponentType>;
|
|
353
|
-
/** Prefabs by name. Absent when the host has none, which makes `
|
|
358
|
+
/** Prefabs by name. Absent when the host has none, which makes `instantiate` refuse in words. */
|
|
354
359
|
readonly prefabs?: ReadonlyMap<string, Prefab>;
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
export function entitiesImplementation(services: EntityServices): Record<string, unknown> {
|
|
358
363
|
const { components } = services;
|
|
364
|
+
const prefabs = services.prefabs ?? new Map<string, Prefab>();
|
|
359
365
|
|
|
360
366
|
/**
|
|
361
367
|
* Resolve a component name, or refuse naming what the world has.
|
|
@@ -388,7 +394,7 @@ export function entitiesImplementation(services: EntityServices): Record<string,
|
|
|
388
394
|
return {
|
|
389
395
|
create: (w: World) => w.create(),
|
|
390
396
|
destroy: (w: World, entity: Entity) => w.destroy(entity),
|
|
391
|
-
alive: (w:
|
|
397
|
+
alive: (w: SystemView, entity: Entity) => w.alive(entity),
|
|
392
398
|
has: (w: World, entity: Entity, component: string) => w.has(entity, resolve(component)),
|
|
393
399
|
attach: (w: World, entity: Entity, component: string) => w.add(entity, resolve(component)),
|
|
394
400
|
detach: (w: World, entity: Entity, component: string) => w.remove(entity, resolve(component)),
|
|
@@ -429,11 +435,16 @@ export function entitiesImplementation(services: EntityServices): Record<string,
|
|
|
429
435
|
},
|
|
430
436
|
view: (w: QuerySource, component: string, forWriting: boolean) =>
|
|
431
437
|
w.view(resolve(component), forWriting),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
438
|
+
/*
|
|
439
|
+
* **Through the methods a system's view shares with a world, never a store.** Inside a system
|
|
440
|
+
* the value a script passes is the `SystemView` the schedule handed it, which has no `store`;
|
|
441
|
+
* reading `store.size` here threw the moment a system counted anything. The view checks the
|
|
442
|
+
* component against the system's declarations, which is the other thing a store would skip.
|
|
443
|
+
*/
|
|
444
|
+
count: (w: SystemView, component: string) => w.count(resolve(component)),
|
|
445
|
+
/* Zero past the end, as this has always answered; the walk's bound is `count`. */
|
|
446
|
+
at: (w: SystemView, component: string, index: number) =>
|
|
447
|
+
Math.max(0, w.at(resolve(component), index)),
|
|
437
448
|
/*
|
|
438
449
|
* **A boolean rather than an option, and a second accessor for the handle.** `Sound?` is an
|
|
439
450
|
* option because a slot is resolved once at load; this is written to be called every frame by
|
|
@@ -517,6 +528,14 @@ export function entitiesImplementation(services: EntityServices): Record<string,
|
|
|
517
528
|
* is why the search answers one.
|
|
518
529
|
*/
|
|
519
530
|
nearest: (w: QuerySource) => Math.max(0, found.get(w) ?? -1),
|
|
531
|
+
instantiate: (w: World, name: string) => {
|
|
532
|
+
const prefab = prefabs.get(name);
|
|
533
|
+
if (prefab === undefined) {
|
|
534
|
+
const known = [...prefabs.keys()].map((k) => `\`${k}\``).join(', ');
|
|
535
|
+
throw new Error(`no prefab is registered as \`${name}\`. This host has ${known}.`);
|
|
536
|
+
}
|
|
537
|
+
return instantiate(w, prefab);
|
|
538
|
+
},
|
|
520
539
|
};
|
|
521
540
|
}
|
|
522
541
|
|
|
@@ -532,19 +551,3 @@ interface QuerySource {
|
|
|
532
551
|
query(a: ComponentType, b?: ComponentType, c?: ComponentType, d?: ComponentType): QueryCursor;
|
|
533
552
|
view(type: ComponentType, forWriting: boolean): ComponentView;
|
|
534
553
|
}
|
|
535
|
-
|
|
536
|
-
export function prefabImplementation(services: EntityServices): Record<string, unknown> {
|
|
537
|
-
const prefabs = services.prefabs ?? new Map<string, Prefab>();
|
|
538
|
-
return {
|
|
539
|
-
spawn: (w: World, name: string) => {
|
|
540
|
-
const prefab = prefabs.get(name);
|
|
541
|
-
if (prefab === undefined) {
|
|
542
|
-
throw new Error(
|
|
543
|
-
`no prefab is registered as \`${name}\`. This host has ` +
|
|
544
|
-
`${[...prefabs.keys()].map((k) => `\`${k}\``).join(', ')}.`,
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
return instantiate(w, prefab);
|
|
548
|
-
},
|
|
549
|
-
};
|
|
550
|
-
}
|
package/src/host.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* A binding is cheap — a table entry and a lookup — and is the difference between a subsystem
|
|
36
36
|
* a consumer can script and one they can only call from TypeScript.
|
|
37
37
|
*/
|
|
38
|
-
import type { AudioGraph, SoundRegistry } from '@driftengine/audio';
|
|
38
|
+
import type { AudioGraph, KickDetector, SoundRegistry } from '@driftengine/audio';
|
|
39
39
|
import {
|
|
40
40
|
BEHAVIOR_CAPABILITIES,
|
|
41
41
|
BEHAVIOR_MODULE,
|
|
@@ -161,10 +161,7 @@ import {
|
|
|
161
161
|
ECS_MODULE,
|
|
162
162
|
ENTITY_TYPES,
|
|
163
163
|
type EntityServices,
|
|
164
|
-
PREFAB_CAPABILITIES,
|
|
165
|
-
PREFAB_MODULE,
|
|
166
164
|
entitiesImplementation,
|
|
167
|
-
prefabImplementation,
|
|
168
165
|
} from './bindings/entities.ts';
|
|
169
166
|
import {
|
|
170
167
|
CHEMISTRY_CAPABILITIES,
|
|
@@ -259,7 +256,6 @@ export function engineRegistry(): CapabilityRegistry {
|
|
|
259
256
|
...NAVIGATION_CAPABILITIES,
|
|
260
257
|
...PERSISTENCE_CAPABILITIES,
|
|
261
258
|
...PHYSICS_CAPABILITIES,
|
|
262
|
-
...PREFAB_CAPABILITIES,
|
|
263
259
|
...RANDOM_CAPABILITIES,
|
|
264
260
|
...RENDER_CAPABILITIES,
|
|
265
261
|
...XR_CAPABILITIES,
|
|
@@ -304,7 +300,15 @@ export interface HostServices extends CoreServices {
|
|
|
304
300
|
* one of those is a scene that draws and the other is a scene nobody is in.
|
|
305
301
|
*/
|
|
306
302
|
readonly xr?: XrRuntime;
|
|
307
|
-
|
|
303
|
+
/**
|
|
304
|
+
* The mix a script plays into, the sounds it can name, and, when the game runs one, the kick
|
|
305
|
+
* detector `pulse` reads. The host calls the detector's `update` each frame; a script only reads.
|
|
306
|
+
*/
|
|
307
|
+
readonly audio?: {
|
|
308
|
+
readonly graph: AudioGraph;
|
|
309
|
+
readonly registry: SoundRegistry;
|
|
310
|
+
readonly kick?: KickDetector;
|
|
311
|
+
};
|
|
308
312
|
/**
|
|
309
313
|
* The component types, and any prefabs, a script may name.
|
|
310
314
|
*
|
|
@@ -358,12 +362,15 @@ export function engineImplementations(services: HostServices): Record<string, un
|
|
|
358
362
|
const map: Record<string, unknown> = { ...stdImplementations() };
|
|
359
363
|
|
|
360
364
|
if (services.audio !== undefined) {
|
|
361
|
-
map[AUDIO_MODULE] = audioImplementation(
|
|
365
|
+
map[AUDIO_MODULE] = audioImplementation(
|
|
366
|
+
services.audio.graph,
|
|
367
|
+
services.audio.registry,
|
|
368
|
+
services.audio.kick ?? null,
|
|
369
|
+
);
|
|
362
370
|
}
|
|
363
371
|
if (services.clocks !== undefined) map[TIME_MODULE] = timeImplementation(services.clocks);
|
|
364
372
|
if (services.entities !== undefined) {
|
|
365
373
|
map[ECS_MODULE] = entitiesImplementation(services.entities);
|
|
366
|
-
map[PREFAB_MODULE] = prefabImplementation(services.entities);
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
/*
|