cubeforge 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ import { AnimationClip } from '@cubeforge/gameplay';
13
13
  export { AISteering, AnimationClip, AnimationControllerResult, BindingControls, CharacterControls, CutsceneControls, CutsceneStep, DialogueControls, DialogueLine, DialogueScript, ForceControls, GameState as GameStateDefinition, GameStateMachineResult, HealthControls, HealthOptions, KinematicBodyControls, LevelTransitionControls, ObjectPool, PathfindingControls, PlatformerControllerOptions, RestartControls, SaveControls, SaveOptions, TopDownMovementOptions, TransitionOptions, TransitionType, TweenControls, useAISteering, useAnimationController, useCharacterController, useCutscene, useDamageZone, useDialogue, useDropThrough, useForces, useGameStateMachine, useGameStore, useHealth, useKinematicBody, useLevelTransition, useObjectPool, usePathfinding, usePersistedBindings, usePlatformerController, useRestart, useSave, useTopDownMovement, useTween } from '@cubeforge/gameplay';
14
14
  import { EngineState } from '@cubeforge/context';
15
15
  export { EngineState, useCircleEnter, useCircleExit, useCircleStay, useCollidingWith, useCollisionEnter, useCollisionExit, useCollisionStay, useTriggerEnter, useTriggerExit, useTriggerStay } from '@cubeforge/context';
16
- export { AudioGroup, SoundControls, SpatialSoundControls, SpatialSoundOptions, duck, getGroupVolume, getListenerPosition, setGroupMute, setGroupVolume, setListenerPosition, setMasterVolume, stopGroup, useSound, useSpatialSound } from '@cubeforge/audio';
16
+ export { AudioGroup, MusicControls, MusicOptions, SoundControls, SoundOptions, SpatialSoundControls, SpatialSoundOptions, duck, getGroupVolume, getListenerPosition, getMasterVolume, setGroupMute, setGroupVolume, setGroupVolumeFaded, setListenerPosition, setMasterVolume, stopGroup, useMusic, useSound, useSpatialSound } from '@cubeforge/audio';
17
17
  export { DevToolsHandle } from '@cubeforge/devtools';
18
18
 
19
19
  interface GameControls {
package/dist/index.js CHANGED
@@ -16723,12 +16723,24 @@ function getAudioCtx() {
16723
16723
  return _audioCtx;
16724
16724
  }
16725
16725
  var groupGainNodes = /* @__PURE__ */ new Map();
16726
+ var groupVolumes = /* @__PURE__ */ new Map();
16727
+ var groupMuted = /* @__PURE__ */ new Map();
16728
+ var groupSources = /* @__PURE__ */ new Map();
16729
+ function registerGroupSource(group, stopFn) {
16730
+ let set = groupSources.get(group);
16731
+ if (!set) {
16732
+ set = /* @__PURE__ */ new Set();
16733
+ groupSources.set(group, set);
16734
+ }
16735
+ set.add(stopFn);
16736
+ return () => set.delete(stopFn);
16737
+ }
16726
16738
  function getGroupGainNode(group) {
16727
16739
  const existing = groupGainNodes.get(group);
16728
16740
  if (existing) return existing;
16729
16741
  const ctx = getAudioCtx();
16730
16742
  const gain = ctx.createGain();
16731
- gain.gain.value = 1;
16743
+ gain.gain.value = groupVolumes.get(group) ?? 1;
16732
16744
  if (group === "master") {
16733
16745
  gain.connect(ctx.destination);
16734
16746
  } else {
@@ -16738,38 +16750,56 @@ function getGroupGainNode(group) {
16738
16750
  return gain;
16739
16751
  }
16740
16752
  function setGroupVolume(group, volume) {
16753
+ const clamped = Math.max(0, Math.min(1, volume));
16754
+ groupVolumes.set(group, clamped);
16755
+ if (groupMuted.get(group)) return;
16741
16756
  const node = groupGainNodes.get(group);
16742
- if (node) node.gain.value = Math.max(0, Math.min(1, volume));
16743
- else {
16744
- getGroupGainNode(group).gain.value = Math.max(0, Math.min(1, volume));
16745
- }
16757
+ if (node) node.gain.value = clamped;
16758
+ else getGroupGainNode(group).gain.value = clamped;
16746
16759
  }
16747
16760
  function setMasterVolume(volume) {
16748
- getGroupGainNode("master").gain.value = Math.max(0, Math.min(1, volume));
16761
+ const clamped = Math.max(0, Math.min(1, volume));
16762
+ groupVolumes.set("master", clamped);
16763
+ if (groupMuted.get("master")) return;
16764
+ getGroupGainNode("master").gain.value = clamped;
16749
16765
  }
16750
16766
  function getGroupVolume(group) {
16751
- return groupGainNodes.get(group)?.gain.value ?? 1;
16767
+ return groupVolumes.get(group) ?? 1;
16768
+ }
16769
+ function getMasterVolume() {
16770
+ return getGroupVolume("master");
16752
16771
  }
16753
16772
  function setGroupMute(group, muted) {
16773
+ groupMuted.set(group, muted);
16754
16774
  const node = getGroupGainNode(group);
16755
- node.gain.value = muted ? 0 : 1;
16775
+ node.gain.value = muted ? 0 : groupVolumes.get(group) ?? 1;
16756
16776
  }
16757
16777
  function stopGroup(group) {
16758
- setGroupMute(group, true);
16759
- setTimeout(() => {
16760
- const node = groupGainNodes.get(group);
16761
- if (node) node.gain.value = 1;
16762
- }, 16);
16778
+ const sources = groupSources.get(group);
16779
+ if (sources) {
16780
+ for (const stop of [...sources]) stop();
16781
+ sources.clear();
16782
+ }
16783
+ }
16784
+ function setGroupVolumeFaded(group, volume, duration) {
16785
+ const clamped = Math.max(0, Math.min(1, volume));
16786
+ groupVolumes.set(group, clamped);
16787
+ const node = getGroupGainNode(group);
16788
+ const ctx = getAudioCtx();
16789
+ const now = ctx.currentTime;
16790
+ node.gain.cancelScheduledValues(now);
16791
+ node.gain.setValueAtTime(node.gain.value, now);
16792
+ node.gain.linearRampToValueAtTime(clamped, now + Math.max(0, duration));
16763
16793
  }
16764
16794
  function duck(group, amount, duration) {
16765
16795
  const node = getGroupGainNode(group);
16766
16796
  const ctx = getAudioCtx();
16767
16797
  const now = ctx.currentTime;
16768
- const prev = node.gain.value;
16798
+ const prev = groupVolumes.get(group) ?? 1;
16769
16799
  node.gain.cancelScheduledValues(now);
16770
- node.gain.setValueAtTime(prev, now);
16771
- node.gain.linearRampToValueAtTime(amount, now + 0.05);
16772
- node.gain.setValueAtTime(amount, now + duration);
16800
+ node.gain.setValueAtTime(node.gain.value, now);
16801
+ node.gain.linearRampToValueAtTime(Math.max(0, Math.min(1, amount)), now + 0.05);
16802
+ node.gain.setValueAtTime(Math.max(0, Math.min(1, amount)), now + duration);
16773
16803
  node.gain.linearRampToValueAtTime(prev, now + duration + 0.2);
16774
16804
  }
16775
16805
 
@@ -16817,7 +16847,10 @@ function useSound(src, opts = {}) {
16817
16847
  const volRef = useRef26(opts.volume ?? 1);
16818
16848
  const loopRef = useRef26(opts.loop ?? false);
16819
16849
  const groupRef = useRef26(opts.group);
16850
+ const rateRef = useRef26(opts.playbackRate ?? 1);
16851
+ const onEndedRef = useRef26(opts.onEnded);
16820
16852
  const maxInstances = opts.maxInstances ?? 4;
16853
+ onEndedRef.current = opts.onEnded;
16821
16854
  useEffect58(() => {
16822
16855
  let cancelled = false;
16823
16856
  loadBuffer(src).then((buf) => {
@@ -16831,6 +16864,7 @@ function useSound(src, opts = {}) {
16831
16864
  } catch {
16832
16865
  }
16833
16866
  entry.gain.disconnect();
16867
+ entry.unregister?.();
16834
16868
  }
16835
16869
  activeInstances.current = [];
16836
16870
  bufferRef.current = null;
@@ -16839,6 +16873,7 @@ function useSound(src, opts = {}) {
16839
16873
  }, [src]);
16840
16874
  const getDestination = () => groupRef.current ? getGroupGainNode(groupRef.current) : getGroupGainNode("master");
16841
16875
  const removeInstance = (entry) => {
16876
+ entry.unregister?.();
16842
16877
  const idx = activeInstances.current.indexOf(entry);
16843
16878
  if (idx !== -1) activeInstances.current.splice(idx, 1);
16844
16879
  };
@@ -16846,39 +16881,56 @@ function useSound(src, opts = {}) {
16846
16881
  while (activeInstances.current.length >= maxInstances) {
16847
16882
  const oldest = activeInstances.current.shift();
16848
16883
  if (oldest) {
16884
+ oldest.source.onended = null;
16849
16885
  try {
16850
16886
  oldest.source.stop();
16851
16887
  } catch {
16852
16888
  }
16853
16889
  oldest.gain.disconnect();
16890
+ oldest.unregister?.();
16854
16891
  }
16855
16892
  }
16856
16893
  };
16857
- const play = (playOpts) => {
16858
- if (!bufferRef.current) return;
16894
+ const spawnEntry = (buf, startGain) => {
16859
16895
  const ctx = getAudioCtx();
16860
- if (ctx.state === "suspended") void ctx.resume();
16861
- stopOldestIfNeeded();
16862
- const entry = createPoolEntry(ctx, bufferRef.current, loopRef.current, volRef.current, getDestination());
16896
+ const dest = getDestination();
16897
+ const entry = createPoolEntry(ctx, buf, loopRef.current, startGain, dest);
16898
+ entry.source.playbackRate.value = rateRef.current;
16899
+ const group = groupRef.current ?? "master";
16900
+ entry.unregister = registerGroupSource(group, () => {
16901
+ try {
16902
+ entry.source.stop();
16903
+ } catch {
16904
+ }
16905
+ removeInstance(entry);
16906
+ entry.gain.disconnect();
16907
+ });
16863
16908
  entry.source.onended = () => {
16864
16909
  removeInstance(entry);
16865
16910
  entry.gain.disconnect();
16911
+ onEndedRef.current?.();
16866
16912
  };
16913
+ return entry;
16914
+ };
16915
+ const play = (playOpts) => {
16916
+ if (!bufferRef.current) return;
16917
+ const ctx = getAudioCtx();
16918
+ if (ctx.state === "suspended") void ctx.resume();
16919
+ stopOldestIfNeeded();
16920
+ const entry = spawnEntry(bufferRef.current, volRef.current);
16867
16921
  const delay = playOpts?.delay;
16868
- if (delay && delay > 0) {
16869
- entry.source.start(ctx.currentTime + delay);
16870
- } else {
16871
- entry.source.start();
16872
- }
16922
+ entry.source.start(delay && delay > 0 ? ctx.currentTime + delay : void 0);
16873
16923
  activeInstances.current.push(entry);
16874
16924
  };
16875
16925
  const stop = () => {
16876
- for (const entry of activeInstances.current) {
16926
+ for (const entry of [...activeInstances.current]) {
16927
+ entry.source.onended = null;
16877
16928
  try {
16878
16929
  entry.source.stop();
16879
16930
  } catch {
16880
16931
  }
16881
16932
  entry.gain.disconnect();
16933
+ entry.unregister?.();
16882
16934
  }
16883
16935
  activeInstances.current = [];
16884
16936
  };
@@ -16888,18 +16940,20 @@ function useSound(src, opts = {}) {
16888
16940
  entry.gain.gain.value = v;
16889
16941
  }
16890
16942
  };
16943
+ const setPlaybackRate = (rate) => {
16944
+ rateRef.current = rate;
16945
+ for (const entry of activeInstances.current) {
16946
+ entry.source.playbackRate.value = rate;
16947
+ }
16948
+ };
16891
16949
  const fadeIn = (duration) => {
16892
16950
  if (!bufferRef.current) return;
16893
16951
  const ctx = getAudioCtx();
16894
16952
  if (ctx.state === "suspended") void ctx.resume();
16895
16953
  stop();
16896
- const entry = createPoolEntry(ctx, bufferRef.current, loopRef.current, 0, getDestination());
16954
+ const entry = spawnEntry(bufferRef.current, 0);
16897
16955
  entry.gain.gain.setValueAtTime(0, ctx.currentTime);
16898
16956
  entry.gain.gain.linearRampToValueAtTime(volRef.current, ctx.currentTime + duration);
16899
- entry.source.onended = () => {
16900
- removeInstance(entry);
16901
- entry.gain.disconnect();
16902
- };
16903
16957
  entry.source.start();
16904
16958
  activeInstances.current.push(entry);
16905
16959
  };
@@ -16930,19 +16984,26 @@ function useSound(src, opts = {}) {
16930
16984
  if (!buf) return;
16931
16985
  const ctx = getAudioCtx();
16932
16986
  if (ctx.state === "suspended") void ctx.resume();
16933
- const entry = createPoolEntry(ctx, buf, loopRef.current, 0, getDestination());
16987
+ const entry = spawnEntry(buf, 0);
16934
16988
  entry.gain.gain.setValueAtTime(0, ctx.currentTime);
16935
16989
  entry.gain.gain.linearRampToValueAtTime(volRef.current, ctx.currentTime + duration);
16936
- entry.source.onended = () => {
16937
- removeInstance(entry);
16938
- entry.gain.disconnect();
16939
- };
16940
16990
  entry.source.start();
16941
16991
  activeInstances.current.push(entry);
16942
16992
  bufferRef.current = buf;
16943
16993
  }).catch(console.error);
16944
16994
  };
16945
- return { play, stop, setVolume, fadeIn, fadeOut, crossfadeTo };
16995
+ return {
16996
+ play,
16997
+ stop,
16998
+ setVolume,
16999
+ setPlaybackRate,
17000
+ fadeIn,
17001
+ fadeOut,
17002
+ crossfadeTo,
17003
+ get isPlaying() {
17004
+ return activeInstances.current.length > 0;
17005
+ }
17006
+ };
16946
17007
  }
16947
17008
 
16948
17009
  // ../../packages/audio/src/useSpatialSound.ts
@@ -17076,6 +17137,136 @@ function useSpatialSound(src, opts = {}) {
17076
17137
  return { play, stop, setPosition, setVolume };
17077
17138
  }
17078
17139
 
17140
+ // ../../packages/audio/src/useMusic.ts
17141
+ import { useEffect as useEffect60, useRef as useRef28 } from "react";
17142
+ var currentTrack = null;
17143
+ function stopTrack(track, fadeDuration) {
17144
+ const ctx = getAudioCtx();
17145
+ const now = ctx.currentTime;
17146
+ track.gain.gain.cancelScheduledValues(now);
17147
+ track.gain.gain.setValueAtTime(track.gain.gain.value, now);
17148
+ if (fadeDuration > 0) {
17149
+ track.gain.gain.linearRampToValueAtTime(0, now + fadeDuration);
17150
+ setTimeout(
17151
+ () => {
17152
+ try {
17153
+ track.source.stop();
17154
+ } catch {
17155
+ }
17156
+ track.gain.disconnect();
17157
+ track.unregister();
17158
+ },
17159
+ fadeDuration * 1e3 + 50
17160
+ );
17161
+ } else {
17162
+ try {
17163
+ track.source.stop();
17164
+ } catch {
17165
+ }
17166
+ track.gain.disconnect();
17167
+ track.unregister();
17168
+ }
17169
+ }
17170
+ var musicBufferCache = /* @__PURE__ */ new Map();
17171
+ async function loadMusicBuffer(src) {
17172
+ const cached = musicBufferCache.get(src);
17173
+ if (cached) return cached;
17174
+ const res = await fetch(src);
17175
+ const data = await res.arrayBuffer();
17176
+ const buf = await getAudioCtx().decodeAudioData(data);
17177
+ musicBufferCache.set(src, buf);
17178
+ return buf;
17179
+ }
17180
+ function useMusic(src, opts = {}) {
17181
+ const volRef = useRef28(opts.volume ?? 1);
17182
+ const loopRef = useRef28(opts.loop ?? true);
17183
+ const isPlayingRef = useRef28(false);
17184
+ const srcRef = useRef28(src);
17185
+ useEffect60(() => {
17186
+ srcRef.current = src;
17187
+ }, [src]);
17188
+ useEffect60(() => {
17189
+ return () => {
17190
+ if (currentTrack) {
17191
+ stopTrack(currentTrack, 1);
17192
+ currentTrack = null;
17193
+ isPlayingRef.current = false;
17194
+ }
17195
+ };
17196
+ }, []);
17197
+ const startTrack = (buf, fadeDuration) => {
17198
+ const ctx = getAudioCtx();
17199
+ if (ctx.state === "suspended") void ctx.resume();
17200
+ if (currentTrack) {
17201
+ stopTrack(currentTrack, fadeDuration);
17202
+ currentTrack = null;
17203
+ }
17204
+ const dest = getGroupGainNode("music");
17205
+ const gain = ctx.createGain();
17206
+ gain.gain.value = 0;
17207
+ gain.connect(dest);
17208
+ const source = ctx.createBufferSource();
17209
+ source.buffer = buf;
17210
+ source.loop = loopRef.current;
17211
+ source.connect(gain);
17212
+ const unregister = registerGroupSource("music", () => {
17213
+ try {
17214
+ source.stop();
17215
+ } catch {
17216
+ }
17217
+ gain.disconnect();
17218
+ if (currentTrack?.source === source) {
17219
+ currentTrack = null;
17220
+ isPlayingRef.current = false;
17221
+ }
17222
+ });
17223
+ source.onended = () => {
17224
+ gain.disconnect();
17225
+ unregister();
17226
+ if (currentTrack?.source === source) {
17227
+ currentTrack = null;
17228
+ isPlayingRef.current = false;
17229
+ }
17230
+ };
17231
+ if (fadeDuration > 0) {
17232
+ gain.gain.setValueAtTime(0, ctx.currentTime);
17233
+ gain.gain.linearRampToValueAtTime(volRef.current, ctx.currentTime + fadeDuration);
17234
+ } else {
17235
+ gain.gain.value = volRef.current;
17236
+ }
17237
+ source.start();
17238
+ currentTrack = { src: srcRef.current, source, gain, unregister };
17239
+ isPlayingRef.current = true;
17240
+ };
17241
+ const play = (fadeDuration = 1) => {
17242
+ loadMusicBuffer(srcRef.current).then((buf) => startTrack(buf, fadeDuration)).catch(console.error);
17243
+ };
17244
+ const stop = (fadeDuration = 1) => {
17245
+ if (currentTrack) {
17246
+ stopTrack(currentTrack, fadeDuration);
17247
+ currentTrack = null;
17248
+ isPlayingRef.current = false;
17249
+ }
17250
+ };
17251
+ const crossfadeTo = (newSrc, fadeDuration = 1) => {
17252
+ srcRef.current = newSrc;
17253
+ loadMusicBuffer(newSrc).then((buf) => startTrack(buf, fadeDuration)).catch(console.error);
17254
+ };
17255
+ const setVolume = (v) => {
17256
+ volRef.current = v;
17257
+ if (currentTrack) currentTrack.gain.gain.value = v;
17258
+ };
17259
+ return {
17260
+ play,
17261
+ stop,
17262
+ crossfadeTo,
17263
+ setVolume,
17264
+ get isPlaying() {
17265
+ return isPlayingRef.current;
17266
+ }
17267
+ };
17268
+ }
17269
+
17079
17270
  // ../../packages/audio/src/listener.ts
17080
17271
  function setListenerPosition(x, y) {
17081
17272
  const ctx = getAudioCtx();
@@ -17264,6 +17455,7 @@ export {
17264
17455
  getDescendants,
17265
17456
  getGroupVolume,
17266
17457
  getListenerPosition,
17458
+ getMasterVolume,
17267
17459
  gjk,
17268
17460
  gjkEpaQuery,
17269
17461
  globalInputContext,
@@ -17311,6 +17503,7 @@ export {
17311
17503
  setDeterministicMode,
17312
17504
  setGroupMute,
17313
17505
  setGroupVolume,
17506
+ setGroupVolumeFaded,
17314
17507
  setListenerPosition,
17315
17508
  setMassProperties,
17316
17509
  setMasterVolume,
@@ -17369,6 +17562,7 @@ export {
17369
17562
  useKinematicBody,
17370
17563
  useLevelTransition,
17371
17564
  useLocalMultiplayer,
17565
+ useMusic,
17372
17566
  useObjectPool,
17373
17567
  useParent,
17374
17568
  usePathfinding,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",