@remix-gg/three 0.1.1

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.
Files changed (67) hide show
  1. package/dist/assets.d.ts +36 -0
  2. package/dist/assets.d.ts.map +1 -0
  3. package/dist/assets.js +100 -0
  4. package/dist/audio.d.ts +42 -0
  5. package/dist/audio.d.ts.map +1 -0
  6. package/dist/audio.js +150 -0
  7. package/dist/camera.d.ts +72 -0
  8. package/dist/camera.d.ts.map +1 -0
  9. package/dist/camera.js +120 -0
  10. package/dist/collide.d.ts +111 -0
  11. package/dist/collide.d.ts.map +1 -0
  12. package/dist/collide.js +321 -0
  13. package/dist/forgiveness.d.ts +71 -0
  14. package/dist/forgiveness.d.ts.map +1 -0
  15. package/dist/forgiveness.js +85 -0
  16. package/dist/game.d.ts +69 -0
  17. package/dist/game.d.ts.map +1 -0
  18. package/dist/game.js +209 -0
  19. package/dist/hud/index.d.ts +72 -0
  20. package/dist/hud/index.d.ts.map +1 -0
  21. package/dist/hud/index.js +142 -0
  22. package/dist/hud/styles.d.ts +14 -0
  23. package/dist/hud/styles.d.ts.map +1 -0
  24. package/dist/hud/styles.js +142 -0
  25. package/dist/index.d.ts +30 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +27 -0
  28. package/dist/input/gestures.d.ts +124 -0
  29. package/dist/input/gestures.d.ts.map +1 -0
  30. package/dist/input/gestures.js +171 -0
  31. package/dist/input/index.d.ts +30 -0
  32. package/dist/input/index.d.ts.map +1 -0
  33. package/dist/input/index.js +121 -0
  34. package/dist/juice.d.ts +151 -0
  35. package/dist/juice.d.ts.map +1 -0
  36. package/dist/juice.js +237 -0
  37. package/dist/loop.d.ts +27 -0
  38. package/dist/loop.d.ts.map +1 -0
  39. package/dist/loop.js +30 -0
  40. package/dist/platform/index.d.ts +50 -0
  41. package/dist/platform/index.d.ts.map +1 -0
  42. package/dist/platform/index.js +177 -0
  43. package/dist/platform/sdk-contract.d.ts +113 -0
  44. package/dist/platform/sdk-contract.d.ts.map +1 -0
  45. package/dist/platform/sdk-contract.js +18 -0
  46. package/dist/ramp.d.ts +39 -0
  47. package/dist/ramp.d.ts.map +1 -0
  48. package/dist/ramp.js +24 -0
  49. package/dist/random.d.ts +128 -0
  50. package/dist/random.d.ts.map +1 -0
  51. package/dist/random.js +160 -0
  52. package/dist/scene/dispose.d.ts +46 -0
  53. package/dist/scene/dispose.d.ts.map +1 -0
  54. package/dist/scene/dispose.js +108 -0
  55. package/dist/scene/lighting.d.ts +42 -0
  56. package/dist/scene/lighting.d.ts.map +1 -0
  57. package/dist/scene/lighting.js +118 -0
  58. package/dist/scene/pool.d.ts +36 -0
  59. package/dist/scene/pool.d.ts.map +1 -0
  60. package/dist/scene/pool.js +70 -0
  61. package/dist/three.d.ts +2 -0
  62. package/dist/three.d.ts.map +1 -0
  63. package/dist/three.js +11 -0
  64. package/dist/viewport.d.ts +86 -0
  65. package/dist/viewport.d.ts.map +1 -0
  66. package/dist/viewport.js +174 -0
  67. package/package.json +43 -0
package/dist/ramp.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { type Easing, type EasingName } from './juice.js';
2
+ /**
3
+ * Difficulty over time, as a clamped curve.
4
+ *
5
+ * The hand-rolled version is `speed += 0.001` in update — linear, uncapped,
6
+ * and unplayable by the sixty-second mark, which is exactly when a good run is
7
+ * getting interesting. A ramp is a *shape*: where it starts, where it tops
8
+ * out, how long it takes to get there, and how the difficulty is distributed
9
+ * along the way. The cap is built in because every endless game needs one and
10
+ * the hand-rolled version never has one.
11
+ */
12
+ export type RampOptions = {
13
+ /** The value at 0 — the first ten seconds, which should be easy. */
14
+ from: number;
15
+ /** The cap. Reached at `over` and held forever after. */
16
+ to: number;
17
+ /** Seconds (or metres, or points — whatever drives it) to reach the cap. */
18
+ over: number;
19
+ /**
20
+ * How difficulty is distributed. 'quadIn' (the default) rises gently early
21
+ * and steepens late: a forgiving opening while the player finds the verb,
22
+ * with the pressure arriving once they have it. 'quadOut' front-loads it.
23
+ */
24
+ curve?: EasingName | Easing;
25
+ };
26
+ /**
27
+ * A pure function of progress: `speed(game.time.elapsed)`.
28
+ *
29
+ * const speed = createRamp({ from: 8, to: 22, over: 75 })
30
+ * update(game, step) {
31
+ * player.position.z -= speed(game.time.elapsed) * step
32
+ * }
33
+ *
34
+ * Drive it by elapsed time, distance, or score — anything monotonic. Several
35
+ * ramps can share one input: speed on 75s, spawn density on 90s, gap width
36
+ * shrinking on 60s, so the pressure arrives in layers instead of all at once.
37
+ */
38
+ export declare function createRamp(options: RampOptions): (progress: number) => number;
39
+ //# sourceMappingURL=ramp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ramp.d.ts","sourceRoot":"","sources":["../src/ramp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,UAAU,EAAW,MAAM,YAAY,CAAA;AAElE;;;;;;;;;GASG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAA;IACV,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAU7E"}
package/dist/ramp.js ADDED
@@ -0,0 +1,24 @@
1
+ import { easings } from './juice.js';
2
+ /**
3
+ * A pure function of progress: `speed(game.time.elapsed)`.
4
+ *
5
+ * const speed = createRamp({ from: 8, to: 22, over: 75 })
6
+ * update(game, step) {
7
+ * player.position.z -= speed(game.time.elapsed) * step
8
+ * }
9
+ *
10
+ * Drive it by elapsed time, distance, or score — anything monotonic. Several
11
+ * ramps can share one input: speed on 75s, spawn density on 90s, gap width
12
+ * shrinking on 60s, so the pressure arrives in layers instead of all at once.
13
+ */
14
+ export function createRamp(options) {
15
+ if (!(options.over > 0))
16
+ throw new Error(`createRamp: over must be > 0, got ${options.over}`);
17
+ const curve = typeof options.curve === 'function' ? options.curve : easings[options.curve ?? 'quadIn'];
18
+ if (!curve)
19
+ throw new Error(`createRamp: unknown easing '${String(options.curve)}'`);
20
+ return (progress) => {
21
+ const t = Math.min(Math.max(progress / options.over, 0), 1);
22
+ return options.from + (options.to - options.from) * curve(t);
23
+ };
24
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Randomness that feels fair.
3
+ *
4
+ * `Math.random()` is stateless, and stateless rolls streak: a 25% crit chance
5
+ * misses eight times in a row for one player in ten thousand attacks, and that
6
+ * player experiences a broken game, not an unlucky one. Players expect random
7
+ * events to self-correct over a handful of trials — real independence does not —
8
+ * so a repeated player-facing roll wants a distribution that is *guaranteed*
9
+ * over a short cycle and merely *shuffled* within it.
10
+ *
11
+ * That is the shuffle bag (the "marble bag"): fill a bag with the outcomes in
12
+ * their intended ratio, draw without replacement, refill and reshuffle when it
13
+ * empties. A 1-in-4 crit is then exactly one crit in every four attacks, the
14
+ * worst possible drought is bounded by construction (2 × (cycle − 1) misses,
15
+ * when a crit opens one cycle and closes the next), and the ratio is still
16
+ * honest over any long run.
17
+ *
18
+ * This lives in the SDK rather than in prompt prose because every game that
19
+ * hand-rolls it gets a different one — usually with the `sort(() => r - 0.5)`
20
+ * shuffle, which is biased on every engine — and because an injectable RNG is
21
+ * what makes game logic deterministic under test and daily-challenge seeds
22
+ * reproducible across players.
23
+ */
24
+ export type ShuffleBagEntry<T> = {
25
+ /** What `draw()` returns when this marble comes up. */
26
+ value: T;
27
+ /** How many of this marble go into each cycle. A positive integer. */
28
+ count: number;
29
+ };
30
+ export type ShuffleBagOptions = {
31
+ /**
32
+ * The random source, returning [0, 1) like `Math.random` — which is the
33
+ * default. Pass `seededRandom(seed)` when the sequence has to reproduce:
34
+ * unit tests, replays, a daily challenge every player should experience
35
+ * identically.
36
+ */
37
+ rng?: () => number;
38
+ };
39
+ export type ShuffleBag<T> = {
40
+ /** The next outcome. The bag refills and reshuffles itself when it empties. */
41
+ draw(): T;
42
+ /** Draws left before the next refill. */
43
+ readonly remaining: number;
44
+ /** Draws per full cycle: the sum of every entry's count. */
45
+ readonly size: number;
46
+ /**
47
+ * Discard the part-drawn cycle so the next draw starts a fresh, full bag.
48
+ * Call it from the run reset — a run that inherits the tail of the last
49
+ * run's bag starts with odds the player never saw the front of.
50
+ */
51
+ reset(): void;
52
+ };
53
+ /**
54
+ * A pool of outcomes drawn without replacement, refilled when empty.
55
+ *
56
+ * Values are generic, so bags nest: a bag whose values are themselves bags
57
+ * first decides the category, then the category's own bag decides the item —
58
+ * which is how "rare letters are not only rare but spaced out" is built.
59
+ *
60
+ * Use it for every repeated player-facing roll: crits, drops, obstacle types.
61
+ * One-off cosmetic jitter keeps using the plain RNG; nobody counts particles.
62
+ */
63
+ export declare function createShuffleBag<T>(entries: readonly ShuffleBagEntry<T>[], options?: ShuffleBagOptions): ShuffleBag<T>;
64
+ export type PityTimerOptions = {
65
+ /** The per-roll probability, 0..1. */
66
+ chance: number;
67
+ /** The ceiling: roll number `guaranteedBy` hits if none before it did. */
68
+ guaranteedBy: number;
69
+ /** The random source, returning [0, 1). Defaults to `Math.random`. */
70
+ rng?: () => number;
71
+ };
72
+ export type PityTimer = {
73
+ /** One attempt. A hit — rolled or guaranteed — resets the counter. */
74
+ roll(): boolean;
75
+ /** Misses since the last hit. */
76
+ readonly misses: number;
77
+ /** Forget the streak. Call from the run reset if pity is per-run. */
78
+ reset(): void;
79
+ };
80
+ /**
81
+ * A rare event with a guaranteed ceiling: independent rolls, but the Nth
82
+ * attempt after a hit always hits.
83
+ *
84
+ * The shuffle bag is the wrong tool for the genuinely rare — a 1-in-50 drop
85
+ * means a 50-item bag, and the guarantee it offers ("exactly one per 50") is
86
+ * more rigid than a drop wants to feel. The pity timer keeps every roll
87
+ * independent and honest, and only intervenes at the tail: the player who
88
+ * would have gone 80 drops dry is the one player it touches, and that player
89
+ * was about to quit.
90
+ */
91
+ export declare function createPityTimer(options: PityTimerOptions): PityTimer;
92
+ export type EscalatingChanceOptions = {
93
+ /** The probability of the first attempt after a hit, 0..1. */
94
+ start: number;
95
+ /** Added to the probability after each miss. Clamped at 1. */
96
+ step: number;
97
+ /** The random source, returning [0, 1). Defaults to `Math.random`. */
98
+ rng?: () => number;
99
+ };
100
+ export type EscalatingChance = {
101
+ /** One attempt. A hit resets the probability to `start`. */
102
+ roll(): boolean;
103
+ /** The probability the NEXT roll will be made at. */
104
+ readonly chance: number;
105
+ /** Back to `start`. Call from the run reset. */
106
+ reset(): void;
107
+ };
108
+ /**
109
+ * A probability that climbs with every miss and resets on a hit — the
110
+ * pseudo-random distribution behind "25%" crits in Dota and Warcraft 3.
111
+ *
112
+ * Softer than the pity timer: there is no hard ceiling, just pressure toward
113
+ * the mean from both sides. Streaks of misses get rapidly less likely, and
114
+ * back-to-back hits do too, because a hit drops the odds back to `start`.
115
+ * `start: 0.05, step: 0.1` feels like roughly a fifth and never insults the
116
+ * player with an eight-miss run.
117
+ */
118
+ export declare function createEscalatingChance(options: EscalatingChanceOptions): EscalatingChance;
119
+ /**
120
+ * mulberry32: a seeded drop-in for `Math.random`, returning [0, 1).
121
+ *
122
+ * Not cryptographic and not meant to be — it exists so a sequence can be
123
+ * replayed. Same seed, same sequence, on every engine, which is what makes a
124
+ * shuffle bag assertable in a unit test and a daily challenge identical for
125
+ * every player who types the same date.
126
+ */
127
+ export declare function seededRandom(seed: number): () => number;
128
+ //# sourceMappingURL=random.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"random.d.ts","sourceRoot":"","sources":["../src/random.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI;IAC/B,uDAAuD;IACvD,KAAK,EAAE,CAAC,CAAA;IACR,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B,+EAA+E;IAC/E,IAAI,IAAI,CAAC,CAAA;IACT,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,OAAO,EAAE,SAAS,eAAe,CAAC,CAAC,CAAC,EAAE,EACtC,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CAAC,CAAC,CAAC,CAwCf;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,sEAAsE;IACtE,IAAI,IAAI,OAAO,CAAA;IACf,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAuBpE;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4DAA4D;IAC5D,IAAI,IAAI,OAAO,CAAA;IACf,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,gDAAgD;IAChD,KAAK,IAAI,IAAI,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAqBzF;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CASvD"}
package/dist/random.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Randomness that feels fair.
3
+ *
4
+ * `Math.random()` is stateless, and stateless rolls streak: a 25% crit chance
5
+ * misses eight times in a row for one player in ten thousand attacks, and that
6
+ * player experiences a broken game, not an unlucky one. Players expect random
7
+ * events to self-correct over a handful of trials — real independence does not —
8
+ * so a repeated player-facing roll wants a distribution that is *guaranteed*
9
+ * over a short cycle and merely *shuffled* within it.
10
+ *
11
+ * That is the shuffle bag (the "marble bag"): fill a bag with the outcomes in
12
+ * their intended ratio, draw without replacement, refill and reshuffle when it
13
+ * empties. A 1-in-4 crit is then exactly one crit in every four attacks, the
14
+ * worst possible drought is bounded by construction (2 × (cycle − 1) misses,
15
+ * when a crit opens one cycle and closes the next), and the ratio is still
16
+ * honest over any long run.
17
+ *
18
+ * This lives in the SDK rather than in prompt prose because every game that
19
+ * hand-rolls it gets a different one — usually with the `sort(() => r - 0.5)`
20
+ * shuffle, which is biased on every engine — and because an injectable RNG is
21
+ * what makes game logic deterministic under test and daily-challenge seeds
22
+ * reproducible across players.
23
+ */
24
+ /**
25
+ * A pool of outcomes drawn without replacement, refilled when empty.
26
+ *
27
+ * Values are generic, so bags nest: a bag whose values are themselves bags
28
+ * first decides the category, then the category's own bag decides the item —
29
+ * which is how "rare letters are not only rare but spaced out" is built.
30
+ *
31
+ * Use it for every repeated player-facing roll: crits, drops, obstacle types.
32
+ * One-off cosmetic jitter keeps using the plain RNG; nobody counts particles.
33
+ */
34
+ export function createShuffleBag(entries, options = {}) {
35
+ const rng = options.rng ?? Math.random;
36
+ const cycle = [];
37
+ for (const entry of entries) {
38
+ if (!Number.isInteger(entry.count) || entry.count <= 0) {
39
+ throw new Error(`createShuffleBag: count must be a positive integer, got ${entry.count}`);
40
+ }
41
+ for (let i = 0; i < entry.count; i++)
42
+ cycle.push(entry.value);
43
+ }
44
+ if (cycle.length === 0)
45
+ throw new Error('createShuffleBag: entries must not be empty');
46
+ const bag = [];
47
+ const refill = () => {
48
+ bag.length = 0;
49
+ bag.push(...cycle);
50
+ // Fisher–Yates. `sort(() => rng() - 0.5)` — the shuffle an LLM reaches for
51
+ // first — is measurably biased under every engine's sort, which quietly
52
+ // un-randomises exactly the distribution the bag exists to guarantee.
53
+ for (let i = bag.length - 1; i > 0; i--) {
54
+ const j = Math.floor(rng() * (i + 1));
55
+ const swap = bag[i];
56
+ bag[i] = bag[j];
57
+ bag[j] = swap;
58
+ }
59
+ };
60
+ return {
61
+ draw() {
62
+ if (bag.length === 0)
63
+ refill();
64
+ return bag.pop();
65
+ },
66
+ get remaining() {
67
+ return bag.length;
68
+ },
69
+ size: cycle.length,
70
+ reset() {
71
+ bag.length = 0;
72
+ },
73
+ };
74
+ }
75
+ /**
76
+ * A rare event with a guaranteed ceiling: independent rolls, but the Nth
77
+ * attempt after a hit always hits.
78
+ *
79
+ * The shuffle bag is the wrong tool for the genuinely rare — a 1-in-50 drop
80
+ * means a 50-item bag, and the guarantee it offers ("exactly one per 50") is
81
+ * more rigid than a drop wants to feel. The pity timer keeps every roll
82
+ * independent and honest, and only intervenes at the tail: the player who
83
+ * would have gone 80 drops dry is the one player it touches, and that player
84
+ * was about to quit.
85
+ */
86
+ export function createPityTimer(options) {
87
+ const { chance, guaranteedBy } = options;
88
+ const rng = options.rng ?? Math.random;
89
+ if (!(chance >= 0 && chance <= 1)) {
90
+ throw new Error(`createPityTimer: chance must be in 0..1, got ${chance}`);
91
+ }
92
+ if (!Number.isInteger(guaranteedBy) || guaranteedBy <= 0) {
93
+ throw new Error(`createPityTimer: guaranteedBy must be a positive integer, got ${guaranteedBy}`);
94
+ }
95
+ let misses = 0;
96
+ return {
97
+ roll() {
98
+ const hit = misses + 1 >= guaranteedBy || rng() < chance;
99
+ misses = hit ? 0 : misses + 1;
100
+ return hit;
101
+ },
102
+ get misses() {
103
+ return misses;
104
+ },
105
+ reset() {
106
+ misses = 0;
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * A probability that climbs with every miss and resets on a hit — the
112
+ * pseudo-random distribution behind "25%" crits in Dota and Warcraft 3.
113
+ *
114
+ * Softer than the pity timer: there is no hard ceiling, just pressure toward
115
+ * the mean from both sides. Streaks of misses get rapidly less likely, and
116
+ * back-to-back hits do too, because a hit drops the odds back to `start`.
117
+ * `start: 0.05, step: 0.1` feels like roughly a fifth and never insults the
118
+ * player with an eight-miss run.
119
+ */
120
+ export function createEscalatingChance(options) {
121
+ const { start, step } = options;
122
+ const rng = options.rng ?? Math.random;
123
+ if (!(start >= 0 && start <= 1)) {
124
+ throw new Error(`createEscalatingChance: start must be in 0..1, got ${start}`);
125
+ }
126
+ if (!(step > 0))
127
+ throw new Error(`createEscalatingChance: step must be > 0, got ${step}`);
128
+ let chance = start;
129
+ return {
130
+ roll() {
131
+ const hit = rng() < chance;
132
+ chance = hit ? start : Math.min(chance + step, 1);
133
+ return hit;
134
+ },
135
+ get chance() {
136
+ return chance;
137
+ },
138
+ reset() {
139
+ chance = start;
140
+ },
141
+ };
142
+ }
143
+ /**
144
+ * mulberry32: a seeded drop-in for `Math.random`, returning [0, 1).
145
+ *
146
+ * Not cryptographic and not meant to be — it exists so a sequence can be
147
+ * replayed. Same seed, same sequence, on every engine, which is what makes a
148
+ * shuffle bag assertable in a unit test and a daily challenge identical for
149
+ * every player who types the same date.
150
+ */
151
+ export function seededRandom(seed) {
152
+ let state = seed >>> 0;
153
+ return () => {
154
+ state = (state + 0x6d2b79f5) >>> 0;
155
+ let t = state;
156
+ t = Math.imul(t ^ (t >>> 15), t | 1);
157
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
158
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
159
+ };
160
+ }
@@ -0,0 +1,46 @@
1
+ import type { Object3D } from 'three';
2
+ /**
3
+ * The Three.js leak, solved once.
4
+ *
5
+ * `scene.remove(mesh)` frees nothing: geometries, materials and textures are
6
+ * GPU resources that only `renderer.info.memory` ever sees, and they survive
7
+ * until someone calls `.dispose()`. A game that rebuilds its level on every
8
+ * restart and never disposes will climb until the WebView is killed — and
9
+ * because it climbs on the GPU, JS heap profiling shows nothing.
10
+ */
11
+ export type Disposable = {
12
+ dispose(): void;
13
+ };
14
+ /** Anything that must outlive a restart: cached assets, shared materials. */
15
+ export type DisposeOptions = {
16
+ skip?: ReadonlySet<unknown>;
17
+ };
18
+ /**
19
+ * Recursively disposes every geometry, material and texture under `object`.
20
+ *
21
+ * Shared resources are visited once, and anything in `skip` (pass
22
+ * `assets.owned`) is left alone — cached assets live for the whole session and
23
+ * disposing one would blank every other object that clones it.
24
+ */
25
+ export declare function disposeObject(object: Object3D, options?: DisposeOptions): void;
26
+ /** Detach from the scene graph and dispose. The normal way to remove a thing. */
27
+ export declare function destroy(object: Object3D, options?: DisposeOptions): void;
28
+ /**
29
+ * A bag of things to free together.
30
+ *
31
+ * `game.transient` is a scope cleared on every `restart()`, which is why a game
32
+ * only has to remember rule 8 — register what you create after setup — instead
33
+ * of tracking individual resources.
34
+ */
35
+ export declare class DisposalScope {
36
+ private readonly items;
37
+ private skip;
38
+ /** @param skip resources this scope must never dispose (`assets.owned`). */
39
+ constructor(skip?: ReadonlySet<unknown>);
40
+ add<T extends Object3D | Disposable>(item: T): T;
41
+ /** Removes without disposing — for an object that was handed off elsewhere. */
42
+ forget(item: unknown): void;
43
+ get size(): number;
44
+ dispose(): void;
45
+ }
46
+ //# sourceMappingURL=dispose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispose.d.ts","sourceRoot":"","sources":["../../src/scene/dispose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,QAAQ,EAAW,MAAM,OAAO,CAAA;AAExE;;;;;;;;GAQG;AAEH,MAAM,MAAM,UAAU,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,CAAA;AAE5C,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;CAAE,CAAA;AA4C5D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,IAAI,CAelF;AAED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,IAAI,CAG5E;AAED;;;;;;GAMG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,IAAI,CAA8B;IAE1C,4EAA4E;gBAChE,IAAI,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC;IAIvC,GAAG,CAAC,CAAC,SAAS,QAAQ,GAAG,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC;IAKhD,+EAA+E;IAC/E,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAK3B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,IAAI,IAAI;CAUhB"}
@@ -0,0 +1,108 @@
1
+ const EMPTY = new Set();
2
+ function isTexture(value) {
3
+ return (typeof value === 'object' &&
4
+ value !== null &&
5
+ value.isTexture === true);
6
+ }
7
+ function disposeMaterial(material, skip, seen) {
8
+ if (skip.has(material) || seen.has(material))
9
+ return;
10
+ seen.add(material);
11
+ // Enumerate rather than list the slots: three has ~20 map properties and
12
+ // MeshPhysicalMaterial keeps adding them, so a hardcoded list is a slow leak
13
+ // waiting for the next release.
14
+ for (const value of Object.values(material)) {
15
+ if (!isTexture(value) || skip.has(value) || seen.has(value))
16
+ continue;
17
+ seen.add(value);
18
+ value.dispose();
19
+ }
20
+ material.dispose();
21
+ }
22
+ function disposeGeometry(geometry, skip, seen) {
23
+ if (skip.has(geometry) || seen.has(geometry))
24
+ return;
25
+ seen.add(geometry);
26
+ geometry.dispose();
27
+ }
28
+ /**
29
+ * Recursively disposes every geometry, material and texture under `object`.
30
+ *
31
+ * Shared resources are visited once, and anything in `skip` (pass
32
+ * `assets.owned`) is left alone — cached assets live for the whole session and
33
+ * disposing one would blank every other object that clones it.
34
+ */
35
+ export function disposeObject(object, options = {}) {
36
+ const skip = options.skip ?? EMPTY;
37
+ const seen = new Set();
38
+ object.traverse((node) => {
39
+ const renderable = node;
40
+ if (renderable.geometry)
41
+ disposeGeometry(renderable.geometry, skip, seen);
42
+ const material = renderable.material;
43
+ if (!material)
44
+ return;
45
+ if (Array.isArray(material)) {
46
+ for (const entry of material)
47
+ disposeMaterial(entry, skip, seen);
48
+ }
49
+ else {
50
+ disposeMaterial(material, skip, seen);
51
+ }
52
+ });
53
+ }
54
+ /** Detach from the scene graph and dispose. The normal way to remove a thing. */
55
+ export function destroy(object, options = {}) {
56
+ object.removeFromParent();
57
+ disposeObject(object, options);
58
+ }
59
+ /**
60
+ * A bag of things to free together.
61
+ *
62
+ * `game.transient` is a scope cleared on every `restart()`, which is why a game
63
+ * only has to remember rule 8 — register what you create after setup — instead
64
+ * of tracking individual resources.
65
+ */
66
+ export class DisposalScope {
67
+ items = [];
68
+ skip = EMPTY;
69
+ /** @param skip resources this scope must never dispose (`assets.owned`). */
70
+ constructor(skip) {
71
+ if (skip)
72
+ this.skip = skip;
73
+ }
74
+ add(item) {
75
+ this.items.push(item);
76
+ return item;
77
+ }
78
+ /** Removes without disposing — for an object that was handed off elsewhere. */
79
+ forget(item) {
80
+ const index = this.items.indexOf(item);
81
+ if (index >= 0)
82
+ this.items.splice(index, 1);
83
+ }
84
+ get size() {
85
+ return this.items.length;
86
+ }
87
+ dispose() {
88
+ // Reverse order so a child registered after its parent is freed first.
89
+ for (let i = this.items.length - 1; i >= 0; i--) {
90
+ const item = this.items[i];
91
+ if (this.skip.has(item))
92
+ continue;
93
+ if (isObject3D(item))
94
+ destroy(item, { skip: this.skip });
95
+ else if (isDisposable(item))
96
+ item.dispose();
97
+ }
98
+ this.items.length = 0;
99
+ }
100
+ }
101
+ function isObject3D(value) {
102
+ return (typeof value === 'object' &&
103
+ value !== null &&
104
+ value.isObject3D === true);
105
+ }
106
+ function isDisposable(value) {
107
+ return typeof value?.dispose === 'function';
108
+ }
@@ -0,0 +1,42 @@
1
+ import { type Box3, DirectionalLight, HemisphereLight, Mesh, type Object3D, type Scene, type WebGLRenderer } from 'three';
2
+ /**
3
+ * Three phone-tuned presets. There is deliberately no EffectComposer here: at
4
+ * 720x1080 with DPR capped at 2 the real framebuffer is 1440x2160, and a bloom
5
+ * chain costs 4-6 ms of a 16.6 ms frame on a mid-range Android WebView. ACES
6
+ * tone mapping (set by `createGame`) plus emissive materials gets most of the
7
+ * look for free.
8
+ */
9
+ export type LightingPreset = 'studio' | 'sunset' | 'night';
10
+ export type LightingOptions = {
11
+ /** Real shadow maps are opt-in — they cost a second scene pass. */
12
+ shadows?: boolean;
13
+ /** Ortho frustum for the shadow camera. Without it shadows look chunky. */
14
+ shadowBounds?: Box3;
15
+ renderer?: WebGLRenderer;
16
+ };
17
+ export type Lighting = {
18
+ key: DirectionalLight;
19
+ fill: HemisphereLight;
20
+ dispose(): void;
21
+ };
22
+ export declare function applyLighting(scene: Scene, preset: LightingPreset, options?: LightingOptions): Lighting;
23
+ export type BlobShadowOptions = {
24
+ radius?: number;
25
+ opacity?: number;
26
+ /** World Y the blob sits on. */
27
+ groundY?: number;
28
+ };
29
+ export type BlobShadow = {
30
+ mesh: Mesh;
31
+ update(): void;
32
+ dispose(): void;
33
+ };
34
+ /**
35
+ * A flat translucent disc that tracks an object's ground position.
36
+ *
37
+ * On a small screen this reads better than a real shadow map and costs one
38
+ * draw call instead of a depth pass — the same trick the Remix avatar already
39
+ * uses in `packages/ui/src/lib/avatar-three.ts`.
40
+ */
41
+ export declare function blobShadow(target: Object3D, options?: BlobShadowOptions): BlobShadow;
42
+ //# sourceMappingURL=lighting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lighting.d.ts","sourceRoot":"","sources":["../../src/scene/lighting.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,IAAI,EAGT,gBAAgB,EAEhB,eAAe,EACf,IAAI,EAEJ,KAAK,QAAQ,EAEb,KAAK,KAAK,EAEV,KAAK,aAAa,EACnB,MAAM,OAAO,CAAA;AAEd;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AA8C1D,MAAM,MAAM,eAAe,GAAG;IAC5B,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,aAAa,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,gBAAgB,CAAA;IACrB,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,eAAoB,GAC5B,QAAQ,CAiDV;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAiCxF"}