rn-gamekit 0.1.0 → 0.2.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.
- package/CHANGELOG.md +75 -0
- package/README.md +97 -16
- package/lib/module/assets/defineAssets.js +28 -3
- package/lib/module/assets/defineAssets.js.map +1 -1
- package/lib/module/assets.js +11 -0
- package/lib/module/assets.js.map +1 -0
- package/lib/module/audio/createGameAudio.js +784 -0
- package/lib/module/audio/createGameAudio.js.map +1 -0
- package/lib/module/audio/errors.js +12 -0
- package/lib/module/audio/errors.js.map +1 -0
- package/lib/module/audio/index.js +5 -0
- package/lib/module/audio/index.js.map +1 -0
- package/lib/module/audio/resolver.js +37 -0
- package/lib/module/audio/resolver.js.map +1 -0
- package/lib/module/audio/types.js +4 -0
- package/lib/module/audio/types.js.map +1 -0
- package/lib/module/audio.js +9 -0
- package/lib/module/audio.js.map +1 -0
- package/lib/module/camera2d/bounds.js +59 -0
- package/lib/module/camera2d/bounds.js.map +1 -0
- package/lib/module/camera2d/extents.js +25 -0
- package/lib/module/camera2d/extents.js.map +1 -0
- package/lib/module/camera2d/follow.js +138 -0
- package/lib/module/camera2d/follow.js.map +1 -0
- package/lib/module/camera2d/index.js +20 -0
- package/lib/module/camera2d/index.js.map +1 -0
- package/lib/module/camera2d/interpolation.js +98 -0
- package/lib/module/camera2d/interpolation.js.map +1 -0
- package/lib/module/camera2d/shake.js +85 -0
- package/lib/module/camera2d/shake.js.map +1 -0
- package/lib/module/camera2d/transform.js +151 -0
- package/lib/module/camera2d/transform.js.map +1 -0
- package/lib/module/camera2d/types.js +4 -0
- package/lib/module/camera2d/types.js.map +1 -0
- package/lib/module/camera2d/validation.js +98 -0
- package/lib/module/camera2d/validation.js.map +1 -0
- package/lib/module/camera2d/visibility.js +138 -0
- package/lib/module/camera2d/visibility.js.map +1 -0
- package/lib/module/camera2d.js +9 -0
- package/lib/module/camera2d.js.map +1 -0
- package/lib/module/collision2d/colliders.js +266 -0
- package/lib/module/collision2d/colliders.js.map +1 -0
- package/lib/module/collision2d/debug.js +105 -0
- package/lib/module/collision2d/debug.js.map +1 -0
- package/lib/module/collision2d/filters.js +40 -0
- package/lib/module/collision2d/filters.js.map +1 -0
- package/lib/module/collision2d/index.js +12 -0
- package/lib/module/collision2d/index.js.map +1 -0
- package/lib/module/collision2d/intersections.js +67 -0
- package/lib/module/collision2d/intersections.js.map +1 -0
- package/lib/module/collision2d/manifolds.js +218 -0
- package/lib/module/collision2d/manifolds.js.map +1 -0
- package/lib/module/collision2d/segments.js +187 -0
- package/lib/module/collision2d/segments.js.map +1 -0
- package/lib/module/collision2d/spatialHash.js +167 -0
- package/lib/module/collision2d/spatialHash.js.map +1 -0
- package/lib/module/collision2d/sweeps.js +255 -0
- package/lib/module/collision2d/sweeps.js.map +1 -0
- package/lib/module/collision2d.js +9 -0
- package/lib/module/collision2d.js.map +1 -0
- package/lib/module/core/input/buttonPad.js +214 -0
- package/lib/module/core/input/buttonPad.js.map +1 -0
- package/lib/module/core/input/createInputBuffer.js +66 -14
- package/lib/module/core/input/createInputBuffer.js.map +1 -1
- package/lib/module/core/session/createGameSession.js +454 -50
- package/lib/module/core/session/createGameSession.js.map +1 -1
- package/lib/module/core/session/deepFreeze.js +92 -2
- package/lib/module/core/session/deepFreeze.js.map +1 -1
- package/lib/module/core/session/types.js +12 -0
- package/lib/module/core/session/types.js.map +1 -1
- package/lib/module/definition/defineGame.js +23 -0
- package/lib/module/definition/defineGame.js.map +1 -1
- package/lib/module/events/defineGameEvents.js +46 -0
- package/lib/module/events/defineGameEvents.js.map +1 -0
- package/lib/module/events/errors.js +16 -0
- package/lib/module/events/errors.js.map +1 -0
- package/lib/module/events/index.js +7 -0
- package/lib/module/events/index.js.map +1 -0
- package/lib/module/events/payload.js +219 -0
- package/lib/module/events/payload.js.map +1 -0
- package/lib/module/events/seed.js +33 -0
- package/lib/module/events/seed.js.map +1 -0
- package/lib/module/events/types.js +4 -0
- package/lib/module/events/types.js.map +1 -0
- package/lib/module/events.js +13 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/geometry/helpers.js +140 -0
- package/lib/module/geometry/helpers.js.map +1 -0
- package/lib/module/geometry/index.js +5 -0
- package/lib/module/geometry/index.js.map +1 -0
- package/lib/module/geometry/validation.js +104 -0
- package/lib/module/geometry/validation.js.map +1 -0
- package/lib/module/geometry.js +9 -0
- package/lib/module/geometry.js.map +1 -0
- package/lib/module/haptics/createGameHaptics.js +214 -0
- package/lib/module/haptics/createGameHaptics.js.map +1 -0
- package/lib/module/haptics/errors.js +12 -0
- package/lib/module/haptics/errors.js.map +1 -0
- package/lib/module/haptics/index.js +5 -0
- package/lib/module/haptics/index.js.map +1 -0
- package/lib/module/haptics/resolver.js +37 -0
- package/lib/module/haptics/resolver.js.map +1 -0
- package/lib/module/haptics/types.js +4 -0
- package/lib/module/haptics/types.js.map +1 -0
- package/lib/module/haptics.js +9 -0
- package/lib/module/haptics.js.map +1 -0
- package/lib/module/index.js +8 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/particles/createParticleSystem.js +481 -0
- package/lib/module/particles/createParticleSystem.js.map +1 -0
- package/lib/module/particles/defineParticleEffect.js +178 -0
- package/lib/module/particles/defineParticleEffect.js.map +1 -0
- package/lib/module/particles/errors.js +9 -0
- package/lib/module/particles/errors.js.map +1 -0
- package/lib/module/particles/index.js +8 -0
- package/lib/module/particles/index.js.map +1 -0
- package/lib/module/particles/sampling.js +121 -0
- package/lib/module/particles/sampling.js.map +1 -0
- package/lib/module/particles/types.js +4 -0
- package/lib/module/particles/types.js.map +1 -0
- package/lib/module/particles.js +4 -0
- package/lib/module/particles.js.map +1 -0
- package/lib/module/react/GameButtonPad.js +344 -0
- package/lib/module/react/GameButtonPad.js.map +1 -0
- package/lib/module/react/GamePointerInput.js +74 -16
- package/lib/module/react/GamePointerInput.js.map +1 -1
- package/lib/module/react/GameView.js +139 -60
- package/lib/module/react/GameView.js.map +1 -1
- package/lib/module/react/assets/createGameAssetStore.js +145 -95
- package/lib/module/react/assets/createGameAssetStore.js.map +1 -1
- package/lib/module/react/assets/useGameAssets.js +60 -35
- package/lib/module/react/assets/useGameAssets.js.map +1 -1
- package/lib/module/react/bindAppLifecycle.js +46 -17
- package/lib/module/react/bindAppLifecycle.js.map +1 -1
- package/lib/module/react/bindGameSession.js +13 -2
- package/lib/module/react/bindGameSession.js.map +1 -1
- package/lib/module/react/camera2d/GameLayer2D.js +50 -0
- package/lib/module/react/camera2d/GameLayer2D.js.map +1 -0
- package/lib/module/react/camera2d/defineGameCamera2D.js +30 -0
- package/lib/module/react/camera2d/defineGameCamera2D.js.map +1 -0
- package/lib/module/react/camera2d/usePresentedCameraBinding.js +177 -0
- package/lib/module/react/camera2d/usePresentedCameraBinding.js.map +1 -0
- package/lib/module/react/cleanupList.js +38 -0
- package/lib/module/react/cleanupList.js.map +1 -0
- package/lib/module/react/lifecycleSource.js +49 -0
- package/lib/module/react/lifecycleSource.js.map +1 -0
- package/lib/module/react/particles/ParticleView.js +308 -0
- package/lib/module/react/particles/ParticleView.js.map +1 -0
- package/lib/module/react/particles/culling.js +115 -0
- package/lib/module/react/particles/culling.js.map +1 -0
- package/lib/module/react/particles/index.js +7 -0
- package/lib/module/react/particles/index.js.map +1 -0
- package/lib/module/react/particles/spriteXForm.js +45 -0
- package/lib/module/react/particles/spriteXForm.js.map +1 -0
- package/lib/module/react/particles/useParticlePresentation.js +204 -0
- package/lib/module/react/particles/useParticlePresentation.js.map +1 -0
- package/lib/module/react/pointerBinding.js +55 -17
- package/lib/module/react/pointerBinding.js.map +1 -1
- package/lib/module/react/pointerCamera.js +28 -0
- package/lib/module/react/pointerCamera.js.map +1 -0
- package/lib/module/react/pointerCoalescer.js +16 -9
- package/lib/module/react/pointerCoalescer.js.map +1 -1
- package/lib/module/react/sprites/GameSprite.js +5 -17
- package/lib/module/react/sprites/GameSprite.js.map +1 -1
- package/lib/module/react/sprites/GameWorld2D.js +112 -9
- package/lib/module/react/sprites/GameWorld2D.js.map +1 -1
- package/lib/module/react/sprites/Sprite.js +48 -63
- package/lib/module/react/sprites/Sprite.js.map +1 -1
- package/lib/module/react/sprites/SpriteBatch.js +53 -28
- package/lib/module/react/sprites/SpriteBatch.js.map +1 -1
- package/lib/module/react/sprites/batchVisibility.js +58 -0
- package/lib/module/react/sprites/batchVisibility.js.map +1 -0
- package/lib/module/react/sprites/spriteSelection.js +103 -0
- package/lib/module/react/sprites/spriteSelection.js.map +1 -0
- package/lib/module/react/sprites/spriteTransform.js +46 -25
- package/lib/module/react/sprites/spriteTransform.js.map +1 -1
- package/lib/module/react/tilemap/TileMapLayer2D.js +217 -0
- package/lib/module/react/tilemap/TileMapLayer2D.js.map +1 -0
- package/lib/module/react/tilemap/tilePresentation.js +374 -0
- package/lib/module/react/tilemap/tilePresentation.js.map +1 -0
- package/lib/module/react/useGameSession.js +70 -0
- package/lib/module/react/useGameSession.js.map +1 -0
- package/lib/module/react/useGameSessionStatus.js +46 -0
- package/lib/module/react/useGameSessionStatus.js.map +1 -0
- package/lib/module/react/viewportBinding.js +5 -1
- package/lib/module/react/viewportBinding.js.map +1 -1
- package/lib/module/react.js +12 -1
- package/lib/module/react.js.map +1 -1
- package/lib/module/scene/defineScene.js +17 -0
- package/lib/module/scene/defineScene.js.map +1 -1
- package/lib/module/sprites/sampleSpriteClip.js +66 -6
- package/lib/module/sprites/sampleSpriteClip.js.map +1 -1
- package/lib/module/sprites/spriteAnimationState.js +10 -20
- package/lib/module/sprites/spriteAnimationState.js.map +1 -1
- package/lib/module/sprites.js +11 -0
- package/lib/module/sprites.js.map +1 -0
- package/lib/module/storage/adapters/asyncStorage.js +57 -0
- package/lib/module/storage/adapters/asyncStorage.js.map +1 -0
- package/lib/module/storage/adapters/memory.js +50 -0
- package/lib/module/storage/adapters/memory.js.map +1 -0
- package/lib/module/storage/errors.js +34 -0
- package/lib/module/storage/errors.js.map +1 -0
- package/lib/module/storage/index.js +11 -0
- package/lib/module/storage/index.js.map +1 -0
- package/lib/module/storage/schema.js +151 -0
- package/lib/module/storage/schema.js.map +1 -0
- package/lib/module/storage/serialization.js +249 -0
- package/lib/module/storage/serialization.js.map +1 -0
- package/lib/module/storage/store.js +387 -0
- package/lib/module/storage/store.js.map +1 -0
- package/lib/module/storage/types.js +16 -0
- package/lib/module/storage/types.js.map +1 -0
- package/lib/module/storage/validation.js +111 -0
- package/lib/module/storage/validation.js.map +1 -0
- package/lib/module/storage.js +4 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/testing.js +4 -0
- package/lib/module/testing.js.map +1 -1
- package/lib/module/tilemap/chunkStats.js +27 -0
- package/lib/module/tilemap/chunkStats.js.map +1 -0
- package/lib/module/tilemap/definitions.js +308 -0
- package/lib/module/tilemap/definitions.js.map +1 -0
- package/lib/module/tilemap/errors.js +13 -0
- package/lib/module/tilemap/errors.js.map +1 -0
- package/lib/module/tilemap/index.js +10 -0
- package/lib/module/tilemap/index.js.map +1 -0
- package/lib/module/tilemap/movement.js +379 -0
- package/lib/module/tilemap/movement.js.map +1 -0
- package/lib/module/tilemap/queries.js +114 -0
- package/lib/module/tilemap/queries.js.map +1 -0
- package/lib/module/tilemap/tiledAdapter.js +141 -0
- package/lib/module/tilemap/tiledAdapter.js.map +1 -0
- package/lib/module/tilemap/types.js +4 -0
- package/lib/module/tilemap/types.js.map +1 -0
- package/lib/module/tilemap.js +4 -0
- package/lib/module/tilemap.js.map +1 -0
- package/lib/typescript/src/assets/defineAssets.d.ts +1 -1
- package/lib/typescript/src/assets/defineAssets.d.ts.map +1 -1
- package/lib/typescript/src/assets.d.ts +11 -0
- package/lib/typescript/src/assets.d.ts.map +1 -0
- package/lib/typescript/src/audio/createGameAudio.d.ts +5 -0
- package/lib/typescript/src/audio/createGameAudio.d.ts.map +1 -0
- package/lib/typescript/src/audio/errors.d.ts +6 -0
- package/lib/typescript/src/audio/errors.d.ts.map +1 -0
- package/lib/typescript/src/audio/index.d.ts +4 -0
- package/lib/typescript/src/audio/index.d.ts.map +1 -0
- package/lib/typescript/src/audio/resolver.d.ts +44 -0
- package/lib/typescript/src/audio/resolver.d.ts.map +1 -0
- package/lib/typescript/src/audio/types.d.ts +42 -0
- package/lib/typescript/src/audio/types.d.ts.map +1 -0
- package/lib/typescript/src/audio.d.ts +8 -0
- package/lib/typescript/src/audio.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/bounds.d.ts +25 -0
- package/lib/typescript/src/camera2d/bounds.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/extents.d.ts +17 -0
- package/lib/typescript/src/camera2d/extents.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/follow.d.ts +24 -0
- package/lib/typescript/src/camera2d/follow.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/index.d.ts +19 -0
- package/lib/typescript/src/camera2d/index.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/interpolation.d.ts +17 -0
- package/lib/typescript/src/camera2d/interpolation.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/shake.d.ts +20 -0
- package/lib/typescript/src/camera2d/shake.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/transform.d.ts +44 -0
- package/lib/typescript/src/camera2d/transform.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/types.d.ts +74 -0
- package/lib/typescript/src/camera2d/types.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/validation.d.ts +30 -0
- package/lib/typescript/src/camera2d/validation.d.ts.map +1 -0
- package/lib/typescript/src/camera2d/visibility.d.ts +47 -0
- package/lib/typescript/src/camera2d/visibility.d.ts.map +1 -0
- package/lib/typescript/src/camera2d.d.ts +7 -0
- package/lib/typescript/src/camera2d.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/colliders.d.ts +120 -0
- package/lib/typescript/src/collision2d/colliders.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/debug.d.ts +67 -0
- package/lib/typescript/src/collision2d/debug.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/filters.d.ts +28 -0
- package/lib/typescript/src/collision2d/filters.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/index.d.ts +17 -0
- package/lib/typescript/src/collision2d/index.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/intersections.d.ts +24 -0
- package/lib/typescript/src/collision2d/intersections.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/manifolds.d.ts +39 -0
- package/lib/typescript/src/collision2d/manifolds.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/segments.d.ts +24 -0
- package/lib/typescript/src/collision2d/segments.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/spatialHash.d.ts +50 -0
- package/lib/typescript/src/collision2d/spatialHash.d.ts.map +1 -0
- package/lib/typescript/src/collision2d/sweeps.d.ts +60 -0
- package/lib/typescript/src/collision2d/sweeps.d.ts.map +1 -0
- package/lib/typescript/src/collision2d.d.ts +7 -0
- package/lib/typescript/src/collision2d.d.ts.map +1 -0
- package/lib/typescript/src/core/input/buttonPad.d.ts +59 -0
- package/lib/typescript/src/core/input/buttonPad.d.ts.map +1 -0
- package/lib/typescript/src/core/input/createInputBuffer.d.ts +1 -1
- package/lib/typescript/src/core/input/createInputBuffer.d.ts.map +1 -1
- package/lib/typescript/src/core/input/types.d.ts +7 -0
- package/lib/typescript/src/core/input/types.d.ts.map +1 -1
- package/lib/typescript/src/core/session/createGameSession.d.ts +4 -2
- package/lib/typescript/src/core/session/createGameSession.d.ts.map +1 -1
- package/lib/typescript/src/core/session/deepFreeze.d.ts +21 -1
- package/lib/typescript/src/core/session/deepFreeze.d.ts.map +1 -1
- package/lib/typescript/src/core/session/diagnostics.d.ts +2 -0
- package/lib/typescript/src/core/session/diagnostics.d.ts.map +1 -1
- package/lib/typescript/src/core/session/types.d.ts +69 -4
- package/lib/typescript/src/core/session/types.d.ts.map +1 -1
- package/lib/typescript/src/definition/defineGame.d.ts +12 -1
- package/lib/typescript/src/definition/defineGame.d.ts.map +1 -1
- package/lib/typescript/src/definition/types.d.ts +10 -1
- package/lib/typescript/src/definition/types.d.ts.map +1 -1
- package/lib/typescript/src/events/defineGameEvents.d.ts +26 -0
- package/lib/typescript/src/events/defineGameEvents.d.ts.map +1 -0
- package/lib/typescript/src/events/errors.d.ts +12 -0
- package/lib/typescript/src/events/errors.d.ts.map +1 -0
- package/lib/typescript/src/events/index.d.ts +6 -0
- package/lib/typescript/src/events/index.d.ts.map +1 -0
- package/lib/typescript/src/events/payload.d.ts +21 -0
- package/lib/typescript/src/events/payload.d.ts.map +1 -0
- package/lib/typescript/src/events/seed.d.ts +3 -0
- package/lib/typescript/src/events/seed.d.ts.map +1 -0
- package/lib/typescript/src/events/types.d.ts +42 -0
- package/lib/typescript/src/events/types.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +12 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/geometry/helpers.d.ts +35 -0
- package/lib/typescript/src/geometry/helpers.d.ts.map +1 -0
- package/lib/typescript/src/geometry/index.d.ts +4 -0
- package/lib/typescript/src/geometry/index.d.ts.map +1 -0
- package/lib/typescript/src/geometry/types.d.ts +49 -0
- package/lib/typescript/src/geometry/types.d.ts.map +1 -1
- package/lib/typescript/src/geometry/validation.d.ts +44 -0
- package/lib/typescript/src/geometry/validation.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +7 -0
- package/lib/typescript/src/geometry.d.ts.map +1 -0
- package/lib/typescript/src/haptics/createGameHaptics.d.ts +3 -0
- package/lib/typescript/src/haptics/createGameHaptics.d.ts.map +1 -0
- package/lib/typescript/src/haptics/errors.d.ts +6 -0
- package/lib/typescript/src/haptics/errors.d.ts.map +1 -0
- package/lib/typescript/src/haptics/index.d.ts +4 -0
- package/lib/typescript/src/haptics/index.d.ts.map +1 -0
- package/lib/typescript/src/haptics/resolver.d.ts +22 -0
- package/lib/typescript/src/haptics/resolver.d.ts.map +1 -0
- package/lib/typescript/src/haptics/types.d.ts +37 -0
- package/lib/typescript/src/haptics/types.d.ts.map +1 -0
- package/lib/typescript/src/haptics.d.ts +8 -0
- package/lib/typescript/src/haptics.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/particles/createParticleSystem.d.ts +5 -0
- package/lib/typescript/src/particles/createParticleSystem.d.ts.map +1 -0
- package/lib/typescript/src/particles/defineParticleEffect.d.ts +3 -0
- package/lib/typescript/src/particles/defineParticleEffect.d.ts.map +1 -0
- package/lib/typescript/src/particles/errors.d.ts +5 -0
- package/lib/typescript/src/particles/errors.d.ts.map +1 -0
- package/lib/typescript/src/particles/index.d.ts +6 -0
- package/lib/typescript/src/particles/index.d.ts.map +1 -0
- package/lib/typescript/src/particles/sampling.d.ts +54 -0
- package/lib/typescript/src/particles/sampling.d.ts.map +1 -0
- package/lib/typescript/src/particles/types.d.ts +292 -0
- package/lib/typescript/src/particles/types.d.ts.map +1 -0
- package/lib/typescript/src/particles.d.ts +2 -0
- package/lib/typescript/src/particles.d.ts.map +1 -0
- package/lib/typescript/src/react/GameButtonPad.d.ts +84 -0
- package/lib/typescript/src/react/GameButtonPad.d.ts.map +1 -0
- package/lib/typescript/src/react/GamePointerInput.d.ts.map +1 -1
- package/lib/typescript/src/react/GameView.d.ts +27 -1
- package/lib/typescript/src/react/GameView.d.ts.map +1 -1
- package/lib/typescript/src/react/assets/createGameAssetStore.d.ts.map +1 -1
- package/lib/typescript/src/react/assets/useGameAssets.d.ts.map +1 -1
- package/lib/typescript/src/react/bindAppLifecycle.d.ts +7 -1
- package/lib/typescript/src/react/bindAppLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/react/bindGameSession.d.ts.map +1 -1
- package/lib/typescript/src/react/camera2d/GameLayer2D.d.ts +24 -0
- package/lib/typescript/src/react/camera2d/GameLayer2D.d.ts.map +1 -0
- package/lib/typescript/src/react/camera2d/defineGameCamera2D.d.ts +44 -0
- package/lib/typescript/src/react/camera2d/defineGameCamera2D.d.ts.map +1 -0
- package/lib/typescript/src/react/camera2d/usePresentedCameraBinding.d.ts +22 -0
- package/lib/typescript/src/react/camera2d/usePresentedCameraBinding.d.ts.map +1 -0
- package/lib/typescript/src/react/cleanupList.d.ts +21 -0
- package/lib/typescript/src/react/cleanupList.d.ts.map +1 -0
- package/lib/typescript/src/react/instrumentation.d.ts +33 -3
- package/lib/typescript/src/react/instrumentation.d.ts.map +1 -1
- package/lib/typescript/src/react/lifecycleSource.d.ts +21 -0
- package/lib/typescript/src/react/lifecycleSource.d.ts.map +1 -0
- package/lib/typescript/src/react/particles/ParticleView.d.ts +65 -0
- package/lib/typescript/src/react/particles/ParticleView.d.ts.map +1 -0
- package/lib/typescript/src/react/particles/culling.d.ts +70 -0
- package/lib/typescript/src/react/particles/culling.d.ts.map +1 -0
- package/lib/typescript/src/react/particles/index.d.ts +7 -0
- package/lib/typescript/src/react/particles/index.d.ts.map +1 -0
- package/lib/typescript/src/react/particles/spriteXForm.d.ts +28 -0
- package/lib/typescript/src/react/particles/spriteXForm.d.ts.map +1 -0
- package/lib/typescript/src/react/particles/useParticlePresentation.d.ts +48 -0
- package/lib/typescript/src/react/particles/useParticlePresentation.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerBinding.d.ts +38 -8
- package/lib/typescript/src/react/pointerBinding.d.ts.map +1 -1
- package/lib/typescript/src/react/pointerCamera.d.ts +20 -0
- package/lib/typescript/src/react/pointerCamera.d.ts.map +1 -0
- package/lib/typescript/src/react/pointerCoalescer.d.ts +17 -1
- package/lib/typescript/src/react/pointerCoalescer.d.ts.map +1 -1
- package/lib/typescript/src/react/sprites/GameSprite.d.ts +3 -10
- package/lib/typescript/src/react/sprites/GameSprite.d.ts.map +1 -1
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts +51 -6
- package/lib/typescript/src/react/sprites/GameWorld2D.d.ts.map +1 -1
- package/lib/typescript/src/react/sprites/Sprite.d.ts.map +1 -1
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts +22 -1
- package/lib/typescript/src/react/sprites/SpriteBatch.d.ts.map +1 -1
- package/lib/typescript/src/react/sprites/batchVisibility.d.ts +28 -0
- package/lib/typescript/src/react/sprites/batchVisibility.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteSelection.d.ts +58 -0
- package/lib/typescript/src/react/sprites/spriteSelection.d.ts.map +1 -0
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts +27 -12
- package/lib/typescript/src/react/sprites/spriteTransform.d.ts.map +1 -1
- package/lib/typescript/src/react/tilemap/TileMapLayer2D.d.ts +69 -0
- package/lib/typescript/src/react/tilemap/TileMapLayer2D.d.ts.map +1 -0
- package/lib/typescript/src/react/tilemap/tilePresentation.d.ts +217 -0
- package/lib/typescript/src/react/tilemap/tilePresentation.d.ts.map +1 -0
- package/lib/typescript/src/react/useGameSession.d.ts +48 -0
- package/lib/typescript/src/react/useGameSession.d.ts.map +1 -0
- package/lib/typescript/src/react/useGameSessionStatus.d.ts +23 -0
- package/lib/typescript/src/react/useGameSessionStatus.d.ts.map +1 -0
- package/lib/typescript/src/react/viewportBinding.d.ts +5 -1
- package/lib/typescript/src/react/viewportBinding.d.ts.map +1 -1
- package/lib/typescript/src/react.d.ts +18 -1
- package/lib/typescript/src/react.d.ts.map +1 -1
- package/lib/typescript/src/scene/defineScene.d.ts +22 -2
- package/lib/typescript/src/scene/defineScene.d.ts.map +1 -1
- package/lib/typescript/src/scene/types.d.ts +24 -4
- package/lib/typescript/src/scene/types.d.ts.map +1 -1
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts +26 -2
- package/lib/typescript/src/sprites/sampleSpriteClip.d.ts.map +1 -1
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts +6 -7
- package/lib/typescript/src/sprites/spriteAnimationState.d.ts.map +1 -1
- package/lib/typescript/src/sprites.d.ts +10 -0
- package/lib/typescript/src/sprites.d.ts.map +1 -0
- package/lib/typescript/src/storage/adapters/asyncStorage.d.ts +18 -0
- package/lib/typescript/src/storage/adapters/asyncStorage.d.ts.map +1 -0
- package/lib/typescript/src/storage/adapters/memory.d.ts +16 -0
- package/lib/typescript/src/storage/adapters/memory.d.ts.map +1 -0
- package/lib/typescript/src/storage/errors.d.ts +37 -0
- package/lib/typescript/src/storage/errors.d.ts.map +1 -0
- package/lib/typescript/src/storage/index.d.ts +11 -0
- package/lib/typescript/src/storage/index.d.ts.map +1 -0
- package/lib/typescript/src/storage/schema.d.ts +22 -0
- package/lib/typescript/src/storage/schema.d.ts.map +1 -0
- package/lib/typescript/src/storage/serialization.d.ts +23 -0
- package/lib/typescript/src/storage/serialization.d.ts.map +1 -0
- package/lib/typescript/src/storage/store.d.ts +3 -0
- package/lib/typescript/src/storage/store.d.ts.map +1 -0
- package/lib/typescript/src/storage/types.d.ts +69 -0
- package/lib/typescript/src/storage/types.d.ts.map +1 -0
- package/lib/typescript/src/storage/validation.d.ts +24 -0
- package/lib/typescript/src/storage/validation.d.ts.map +1 -0
- package/lib/typescript/src/storage.d.ts +2 -0
- package/lib/typescript/src/storage.d.ts.map +1 -0
- package/lib/typescript/src/testing.d.ts +1 -0
- package/lib/typescript/src/testing.d.ts.map +1 -1
- package/lib/typescript/src/tilemap/chunkStats.d.ts +14 -0
- package/lib/typescript/src/tilemap/chunkStats.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/definitions.d.ts +38 -0
- package/lib/typescript/src/tilemap/definitions.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/errors.d.ts +7 -0
- package/lib/typescript/src/tilemap/errors.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/index.d.ts +8 -0
- package/lib/typescript/src/tilemap/index.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/movement.d.ts +52 -0
- package/lib/typescript/src/tilemap/movement.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/queries.d.ts +21 -0
- package/lib/typescript/src/tilemap/queries.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/tiledAdapter.d.ts +32 -0
- package/lib/typescript/src/tilemap/tiledAdapter.d.ts.map +1 -0
- package/lib/typescript/src/tilemap/types.d.ts +127 -0
- package/lib/typescript/src/tilemap/types.d.ts.map +1 -0
- package/lib/typescript/src/tilemap.d.ts +2 -0
- package/lib/typescript/src/tilemap.d.ts.map +1 -0
- package/package.json +100 -10
- package/src/assets/defineAssets.ts +31 -5
- package/src/assets.ts +27 -0
- package/src/audio/createGameAudio.ts +749 -0
- package/src/audio/errors.ts +12 -0
- package/src/audio/index.ts +9 -0
- package/src/audio/resolver.ts +74 -0
- package/src/audio/types.ts +47 -0
- package/src/audio.ts +13 -0
- package/src/camera2d/bounds.ts +69 -0
- package/src/camera2d/extents.ts +25 -0
- package/src/camera2d/follow.ts +165 -0
- package/src/camera2d/index.ts +29 -0
- package/src/camera2d/interpolation.ts +113 -0
- package/src/camera2d/shake.ts +90 -0
- package/src/camera2d/transform.ts +148 -0
- package/src/camera2d/types.ts +73 -0
- package/src/camera2d/validation.ts +126 -0
- package/src/camera2d/visibility.ts +181 -0
- package/src/camera2d.ts +6 -0
- package/src/collision2d/colliders.ts +292 -0
- package/src/collision2d/debug.ts +133 -0
- package/src/collision2d/filters.ts +48 -0
- package/src/collision2d/index.ts +74 -0
- package/src/collision2d/intersections.ts +82 -0
- package/src/collision2d/manifolds.ts +191 -0
- package/src/collision2d/segments.ts +176 -0
- package/src/collision2d/spatialHash.ts +211 -0
- package/src/collision2d/sweeps.ts +269 -0
- package/src/collision2d.ts +6 -0
- package/src/core/input/buttonPad.ts +235 -0
- package/src/core/input/createInputBuffer.ts +95 -14
- package/src/core/input/types.ts +7 -0
- package/src/core/session/createGameSession.ts +480 -53
- package/src/core/session/deepFreeze.ts +104 -3
- package/src/core/session/diagnostics.ts +2 -0
- package/src/core/session/types.ts +77 -4
- package/src/definition/defineGame.ts +61 -3
- package/src/definition/types.ts +10 -0
- package/src/events/defineGameEvents.ts +46 -0
- package/src/events/errors.ts +14 -0
- package/src/events/index.ts +14 -0
- package/src/events/payload.ts +234 -0
- package/src/events/seed.ts +33 -0
- package/src/events/types.ts +53 -0
- package/src/events.ts +20 -0
- package/src/geometry/helpers.ts +136 -0
- package/src/geometry/index.ts +14 -0
- package/src/geometry/types.ts +54 -0
- package/src/geometry/validation.ts +131 -0
- package/src/geometry.ts +6 -0
- package/src/haptics/createGameHaptics.ts +214 -0
- package/src/haptics/errors.ts +12 -0
- package/src/haptics/index.ts +3 -0
- package/src/haptics/resolver.ts +58 -0
- package/src/haptics/types.ts +48 -0
- package/src/haptics.ts +7 -0
- package/src/index.ts +37 -1
- package/src/particles/createParticleSystem.ts +505 -0
- package/src/particles/defineParticleEffect.ts +164 -0
- package/src/particles/errors.ts +6 -0
- package/src/particles/index.ts +5 -0
- package/src/particles/sampling.ts +141 -0
- package/src/particles/types.ts +327 -0
- package/src/particles.ts +1 -0
- package/src/react/GameButtonPad.tsx +436 -0
- package/src/react/GamePointerInput.tsx +73 -8
- package/src/react/GameView.tsx +160 -61
- package/src/react/assets/createGameAssetStore.ts +140 -107
- package/src/react/assets/useGameAssets.ts +63 -31
- package/src/react/bindAppLifecycle.ts +55 -19
- package/src/react/bindGameSession.ts +12 -2
- package/src/react/camera2d/GameLayer2D.tsx +43 -0
- package/src/react/camera2d/defineGameCamera2D.ts +59 -0
- package/src/react/camera2d/usePresentedCameraBinding.ts +185 -0
- package/src/react/cleanupList.ts +48 -0
- package/src/react/instrumentation.ts +31 -3
- package/src/react/lifecycleSource.ts +58 -0
- package/src/react/particles/ParticleView.tsx +358 -0
- package/src/react/particles/culling.ts +145 -0
- package/src/react/particles/index.ts +6 -0
- package/src/react/particles/spriteXForm.ts +72 -0
- package/src/react/particles/useParticlePresentation.ts +261 -0
- package/src/react/pointerBinding.ts +74 -16
- package/src/react/pointerCamera.ts +33 -0
- package/src/react/pointerCoalescer.ts +36 -10
- package/src/react/sprites/GameSprite.tsx +4 -33
- package/src/react/sprites/GameWorld2D.tsx +113 -7
- package/src/react/sprites/Sprite.tsx +51 -72
- package/src/react/sprites/SpriteBatch.tsx +74 -22
- package/src/react/sprites/batchVisibility.ts +76 -0
- package/src/react/sprites/spriteSelection.ts +133 -0
- package/src/react/sprites/spriteTransform.ts +46 -26
- package/src/react/tilemap/TileMapLayer2D.tsx +274 -0
- package/src/react/tilemap/tilePresentation.ts +491 -0
- package/src/react/useGameSession.ts +90 -0
- package/src/react/useGameSessionStatus.ts +53 -0
- package/src/react/viewportBinding.ts +5 -1
- package/src/react.ts +18 -1
- package/src/scene/defineScene.ts +47 -5
- package/src/scene/types.ts +37 -3
- package/src/sprites/sampleSpriteClip.ts +82 -14
- package/src/sprites/spriteAnimationState.ts +29 -25
- package/src/sprites.ts +22 -0
- package/src/storage/adapters/asyncStorage.ts +77 -0
- package/src/storage/adapters/memory.ts +58 -0
- package/src/storage/errors.ts +88 -0
- package/src/storage/index.ts +18 -0
- package/src/storage/schema.ts +147 -0
- package/src/storage/serialization.ts +248 -0
- package/src/storage/store.ts +380 -0
- package/src/storage/types.ts +76 -0
- package/src/storage/validation.ts +102 -0
- package/src/storage.ts +1 -0
- package/src/testing.ts +4 -0
- package/src/tilemap/chunkStats.ts +24 -0
- package/src/tilemap/definitions.ts +327 -0
- package/src/tilemap/errors.ts +11 -0
- package/src/tilemap/index.ts +10 -0
- package/src/tilemap/movement.ts +368 -0
- package/src/tilemap/queries.ts +136 -0
- package/src/tilemap/tiledAdapter.ts +165 -0
- package/src/tilemap/types.ts +119 -0
- package/src/tilemap.ts +1 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { createAudioInstallationError, GameAudioError } from './errors';
|
|
2
|
+
import { loadAudioApi } from './resolver';
|
|
3
|
+
import type { AudioCategory, AudioSoundRecord, CreateGameAudioOptions, GameAudio, GameAudioPlayOptions } from './types';
|
|
4
|
+
import type { GameLifecycleSource } from '../core/session/types';
|
|
5
|
+
|
|
6
|
+
const CATEGORIES: readonly AudioCategory[] = ['master', 'music', 'sfx', 'ui'] as const;
|
|
7
|
+
|
|
8
|
+
function isValidVolume(volume: number): boolean {
|
|
9
|
+
return Number.isFinite(volume) && volume >= 0 && volume <= 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assertCategory(category: AudioCategory): void {
|
|
13
|
+
if (!CATEGORIES.includes(category)) {
|
|
14
|
+
throw new GameAudioError(`Unknown audio category "${String(category)}"`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertVolume(volume: number): void {
|
|
19
|
+
if (!isValidVolume(volume)) {
|
|
20
|
+
throw new GameAudioError(`Volume must be a finite number in [0, 1], got ${String(volume)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// GS-AUDIO-04: backend-global interruption observation is reference
|
|
25
|
+
// counted — the first live instance enables it, the last dispose disables
|
|
26
|
+
// it, so two instances can never disable each other's observation.
|
|
27
|
+
let interruptionObserverCount = 0;
|
|
28
|
+
|
|
29
|
+
let assetInputLoader: ((assetId: number) => Promise<number | string | ArrayBuffer>) | null = null;
|
|
30
|
+
|
|
31
|
+
export function __setAssetInputLoader(
|
|
32
|
+
fn: ((assetId: number) => Promise<number | string | ArrayBuffer>) | null,
|
|
33
|
+
): void {
|
|
34
|
+
assetInputLoader = fn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function __getAssetInputLoader(): ((assetId: number) => Promise<number | string | ArrayBuffer>) | null {
|
|
38
|
+
return assetInputLoader;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createGameAudio<T extends AudioSoundRecord>(
|
|
42
|
+
options: CreateGameAudioOptions<T>,
|
|
43
|
+
): Promise<GameAudio<T>> {
|
|
44
|
+
let api: Awaited<ReturnType<typeof loadAudioApi>>;
|
|
45
|
+
try {
|
|
46
|
+
api = await loadAudioApi();
|
|
47
|
+
} catch {
|
|
48
|
+
throw createAudioInstallationError();
|
|
49
|
+
}
|
|
50
|
+
if (!api || !api.AudioContext) {
|
|
51
|
+
throw createAudioInstallationError();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!options || typeof options.sounds !== 'object' || options.sounds === null) {
|
|
55
|
+
throw new GameAudioError('createGameAudio requires { sounds: Record<string, number> }');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const soundIds = Object.keys(options.sounds) as (keyof T & string)[];
|
|
59
|
+
if (soundIds.length === 0) {
|
|
60
|
+
throw new GameAudioError('createGameAudio sounds must not be empty');
|
|
61
|
+
}
|
|
62
|
+
for (const id of soundIds) {
|
|
63
|
+
const v = (options.sounds as Record<string, unknown>)[id];
|
|
64
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
65
|
+
throw new GameAudioError(`Sound "${id}" must be a finite asset ID (require result)`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { AudioContext, AudioManager } = api;
|
|
70
|
+
let context: InstanceType<typeof AudioContext> | null = null;
|
|
71
|
+
type GainLike = { gain: { value: number; setValueAtTime?: (v:number,t:number)=>unknown; linearRampToValueAtTime?: (v:number,t:number)=>unknown; cancelScheduledValues?: (t:number)=>unknown }; connect: (dest: unknown)=>unknown };
|
|
72
|
+
let masterGain: GainLike | null = null;
|
|
73
|
+
const categoryGains: Record<AudioCategory, GainLike | null> = { master: null, music: null, sfx: null, ui: null };
|
|
74
|
+
let interruptionSub: { remove(): void } | null = null;
|
|
75
|
+
let interruptionEnabled = false;
|
|
76
|
+
let appStateSub: { remove(): void } | null = null;
|
|
77
|
+
let idleSuspendTimer: ReturnType<typeof setTimeout> | null = null;
|
|
78
|
+
|
|
79
|
+
// Create context in a transaction so failure cleans up
|
|
80
|
+
try {
|
|
81
|
+
context = new AudioContext();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const volumes: Record<AudioCategory, number> = { master: 1, music: 1, sfx: 1, ui: 1 };
|
|
87
|
+
|
|
88
|
+
let muted = false;
|
|
89
|
+
let disposed = false;
|
|
90
|
+
let userPaused = false;
|
|
91
|
+
let sessionPaused = false;
|
|
92
|
+
let appPaused = false;
|
|
93
|
+
let interruptionPaused = false;
|
|
94
|
+
let interruptionRequiresExplicitResume = false;
|
|
95
|
+
|
|
96
|
+
const bufferCache = new Map<string, unknown>();
|
|
97
|
+
const pendingDecodes = new Map<string, Promise<unknown>>();
|
|
98
|
+
|
|
99
|
+
async function getBuffer(id: keyof T & string): Promise<unknown> {
|
|
100
|
+
if (disposed) throw new GameAudioError('GameAudio is disposed');
|
|
101
|
+
if (bufferCache.has(id)) return bufferCache.get(id)!;
|
|
102
|
+
if (pendingDecodes.has(id)) return pendingDecodes.get(id)!;
|
|
103
|
+
const assetId = (options.sounds as Record<string, number>)[id] as number;
|
|
104
|
+
const promise = (async () => {
|
|
105
|
+
let input: number | string | ArrayBuffer;
|
|
106
|
+
if (assetInputLoader) {
|
|
107
|
+
input = await assetInputLoader(assetId);
|
|
108
|
+
} else {
|
|
109
|
+
let assetUri: string | null = null;
|
|
110
|
+
let expoAssetFailed = false;
|
|
111
|
+
try {
|
|
112
|
+
const { Asset: ExpoAsset } = (await import('expo-asset')) as unknown as { Asset: { fromModule(id: number): { downloadAsync(): Promise<void>; localUri: string | null; uri: string } } };
|
|
113
|
+
const asset = ExpoAsset.fromModule(assetId);
|
|
114
|
+
await asset.downloadAsync();
|
|
115
|
+
assetUri = asset.localUri ?? asset.uri;
|
|
116
|
+
} catch {
|
|
117
|
+
expoAssetFailed = true;
|
|
118
|
+
}
|
|
119
|
+
if (assetUri) input = assetUri;
|
|
120
|
+
else if (expoAssetFailed) input = assetId;
|
|
121
|
+
else throw new GameAudioError(`Failed to resolve asset for sound "${String(id)}" — expo-asset returned no URI`);
|
|
122
|
+
}
|
|
123
|
+
let buffer: unknown;
|
|
124
|
+
try {
|
|
125
|
+
buffer = await (context as InstanceType<typeof AudioContext>).decodeAudioData(input as unknown as ArrayBuffer);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw new GameAudioError(`Failed to decode audio asset "${String(id)}": ${(e as Error).message}`);
|
|
128
|
+
}
|
|
129
|
+
bufferCache.set(id, buffer);
|
|
130
|
+
pendingDecodes.delete(id);
|
|
131
|
+
return buffer;
|
|
132
|
+
})();
|
|
133
|
+
pendingDecodes.set(id, promise);
|
|
134
|
+
try { return await promise; } catch (e) { pendingDecodes.delete(id); throw e; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function applyGain(category: AudioCategory): void {
|
|
138
|
+
const gain = categoryGains[category];
|
|
139
|
+
if (!gain) return;
|
|
140
|
+
// F1: write only own volume; composition happens in graph (master * category)
|
|
141
|
+
const value = volumes[category];
|
|
142
|
+
try {
|
|
143
|
+
const param = gain.gain as unknown as { value: number; cancelScheduledValues?: (t:number)=>void; setValueAtTime?: (v:number,t:number)=>void; linearRampToValueAtTime?: (v:number,t:number)=>void };
|
|
144
|
+
const now = (context as unknown as { currentTime: number }).currentTime ?? 0;
|
|
145
|
+
if (param.cancelScheduledValues) param.cancelScheduledValues(now);
|
|
146
|
+
if (param.setValueAtTime) param.setValueAtTime(param.value, now);
|
|
147
|
+
if (param.linearRampToValueAtTime) param.linearRampToValueAtTime(value, now + 0.02);
|
|
148
|
+
else param.value = value;
|
|
149
|
+
} catch {
|
|
150
|
+
try { (gain.gain as unknown as { value: number }).value = value; } catch {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function applyAllGains(): void {
|
|
155
|
+
for (const c of CATEGORIES) applyGain(c);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Transaction: gain graph plus eager decode, with cleanup on failure
|
|
159
|
+
// (F3, GS-AUDIO-03). Any failure closes the half-built context before
|
|
160
|
+
// rethrowing — init never returns a silent partial object.
|
|
161
|
+
try {
|
|
162
|
+
// Gain setup — master -> destination, categories -> master (F1).
|
|
163
|
+
// GS-AUDIO-03: the gain graph is REQUIRED setup. A failure throws
|
|
164
|
+
// transactionally (the init boundary closes the context and rethrows)
|
|
165
|
+
// instead of returning an apparently usable but silent object.
|
|
166
|
+
const mg = (context as unknown as { createGain?: () => GainLike }).createGain?.();
|
|
167
|
+
if (!mg) {
|
|
168
|
+
throw new GameAudioError('createGameAudio requires a working channel gain graph (master createGain failed)');
|
|
169
|
+
}
|
|
170
|
+
masterGain = mg;
|
|
171
|
+
try {
|
|
172
|
+
(mg as unknown as { connect: (d: unknown) => void }).connect(
|
|
173
|
+
(context as unknown as { destination: unknown }).destination,
|
|
174
|
+
);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
throw new GameAudioError(`createGameAudio requires a working channel gain graph (master connect failed: ${(e as Error).message})`);
|
|
177
|
+
}
|
|
178
|
+
for (const cat of CATEGORIES) {
|
|
179
|
+
if (cat === 'master') {
|
|
180
|
+
categoryGains[cat] = mg;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const cg = (context as unknown as { createGain?: () => GainLike }).createGain?.();
|
|
184
|
+
if (!cg) {
|
|
185
|
+
throw new GameAudioError(`createGameAudio requires a working channel gain graph (category ${cat} createGain failed)`);
|
|
186
|
+
}
|
|
187
|
+
categoryGains[cat] = cg;
|
|
188
|
+
try {
|
|
189
|
+
(cg as unknown as { connect: (d: unknown) => void }).connect(mg);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
throw new GameAudioError(`createGameAudio requires a working channel gain graph (category ${cat} connect failed: ${(e as Error).message})`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await Promise.all(soundIds.map((id) => getBuffer(id)));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Close context exactly once before rethrowing
|
|
198
|
+
try { await (context as InstanceType<typeof AudioContext>).close(); } catch {}
|
|
199
|
+
pendingDecodes.clear();
|
|
200
|
+
bufferCache.clear();
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let currentMusicId: (keyof T & string) | null = null;
|
|
205
|
+
let currentMusicNode: { stop(when?: number): void } | null = null;
|
|
206
|
+
let musicGeneration = 0;
|
|
207
|
+
let activeMusicGeneration = 0;
|
|
208
|
+
let pendingMusic: { id: keyof T & string; generation: number } | null = null;
|
|
209
|
+
const activeVoices = new Set<unknown>();
|
|
210
|
+
const concurrencyMap = new Map<string, Set<unknown>>();
|
|
211
|
+
// GS-AUDIO-02: pending reservations are tracked distinctly from live
|
|
212
|
+
// voices. Only pending work takes cancellation tokens (removed when its
|
|
213
|
+
// continuation runs); live voices use normal stop/cleanup and lose all
|
|
214
|
+
// owner references immediately — never a token.
|
|
215
|
+
const pendingReservations = new Set<unknown>();
|
|
216
|
+
const cancelledReservations = new Set<unknown>();
|
|
217
|
+
|
|
218
|
+
function isEffectivelyPaused(): boolean {
|
|
219
|
+
return userPaused || sessionPaused || appPaused || interruptionPaused || muted;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// GS-AUDIO-04: serialized suspend/resume intents. Every pause-affecting
|
|
223
|
+
// call enqueues one link; each link re-reads the CURRENT effective pause
|
|
224
|
+
// state and converges the context to it, so rapid intent changes settle
|
|
225
|
+
// to the latest request instead of racing delayed native promises.
|
|
226
|
+
// Native calls never overlap: at most one suspend/resume is in flight.
|
|
227
|
+
let suspendChain: Promise<void> = Promise.resolve();
|
|
228
|
+
async function runSuspendLink(): Promise<void> {
|
|
229
|
+
if (disposed || !context) return;
|
|
230
|
+
if (isEffectivelyPaused()) {
|
|
231
|
+
if (idleSuspendTimer) {
|
|
232
|
+
clearTimeout(idleSuspendTimer);
|
|
233
|
+
idleSuspendTimer = null;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await (context as InstanceType<typeof AudioContext>).suspend();
|
|
237
|
+
} catch {}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const state = (context as unknown as { state: string }).state;
|
|
242
|
+
if (state === 'suspended') {
|
|
243
|
+
await (context as InstanceType<typeof AudioContext>).resume();
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
246
|
+
applyAllGains();
|
|
247
|
+
// If we have deferred music intent and now unpaused, start it (F4)
|
|
248
|
+
if (pendingMusic && !isEffectivelyPaused()) {
|
|
249
|
+
const { id, generation } = pendingMusic;
|
|
250
|
+
pendingMusic = null;
|
|
251
|
+
void startMusicInternal(id, generation);
|
|
252
|
+
}
|
|
253
|
+
scheduleIdleSuspend();
|
|
254
|
+
}
|
|
255
|
+
function requestSuspendUpdate(): void {
|
|
256
|
+
suspendChain = suspendChain.then(runSuspendLink, runSuspendLink);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function updateSuspendState(): void {
|
|
260
|
+
requestSuspendUpdate();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function scheduleIdleSuspend(): void {
|
|
264
|
+
if (idleSuspendTimer) clearTimeout(idleSuspendTimer);
|
|
265
|
+
if (disposed || !context || isEffectivelyPaused()) return;
|
|
266
|
+
if (activeVoices.size === 0 && currentMusicNode === null && !pendingMusic) {
|
|
267
|
+
idleSuspendTimer = setTimeout(() => {
|
|
268
|
+
if (!disposed && context && activeVoices.size===0 && currentMusicNode===null && !pendingMusic && !isEffectivelyPaused()) {
|
|
269
|
+
void (context as InstanceType<typeof AudioContext>).suspend().catch(()=>{});
|
|
270
|
+
}
|
|
271
|
+
}, 1500);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Drop one live voice: normal stop/cleanup with no token (GS-AUDIO-02). */
|
|
276
|
+
function dropVoice(voice: unknown, concurrencyKey: string | undefined): void {
|
|
277
|
+
activeVoices.delete(voice);
|
|
278
|
+
if (concurrencyKey !== undefined) {
|
|
279
|
+
const set = concurrencyMap.get(concurrencyKey);
|
|
280
|
+
if (set) {
|
|
281
|
+
set.delete(voice);
|
|
282
|
+
if (set.size === 0) concurrencyMap.delete(concurrencyKey);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
cancelledReservations.delete(voice);
|
|
286
|
+
scheduleIdleSuspend();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Drop one pending reservation from every collection that holds it. */
|
|
290
|
+
function dropReservation(reservation: unknown, concurrencyKey: string): void {
|
|
291
|
+
pendingReservations.delete(reservation);
|
|
292
|
+
const set = concurrencyMap.get(concurrencyKey);
|
|
293
|
+
if (set) {
|
|
294
|
+
set.delete(reservation);
|
|
295
|
+
if (set.size === 0) concurrencyMap.delete(concurrencyKey);
|
|
296
|
+
}
|
|
297
|
+
cancelledReservations.delete(reservation);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ensureNotDisposed = (): void => {
|
|
301
|
+
if (disposed) throw new GameAudioError('GameAudio is disposed');
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
function handleInterruption(event: unknown): void {
|
|
305
|
+
const e = event as { type?: string; shouldResume?: boolean };
|
|
306
|
+
if (e.type === 'began') {
|
|
307
|
+
interruptionPaused = true;
|
|
308
|
+
interruptionRequiresExplicitResume = false;
|
|
309
|
+
requestSuspendUpdate();
|
|
310
|
+
} else if (e.type === 'ended') {
|
|
311
|
+
if (e.shouldResume) {
|
|
312
|
+
if (interruptionRequiresExplicitResume) {
|
|
313
|
+
// Denied auto-resume persists until explicit user action (F2)
|
|
314
|
+
requestSuspendUpdate();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
interruptionPaused = false;
|
|
318
|
+
interruptionRequiresExplicitResume = false;
|
|
319
|
+
updateSuspendState();
|
|
320
|
+
} else {
|
|
321
|
+
// Platform says don't auto-resume: keep paused until explicit user action (F2)
|
|
322
|
+
interruptionPaused = true;
|
|
323
|
+
interruptionRequiresExplicitResume = true;
|
|
324
|
+
requestSuspendUpdate();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// GS-AUDIO-03: interruption observation is explicitly optional. Without
|
|
330
|
+
// an AudioManager the service still plays, but warns once so the gap
|
|
331
|
+
// is visible instead of silently disappearing behind a no-op fallback.
|
|
332
|
+
if (AudioManager) {
|
|
333
|
+
if (interruptionObserverCount === 0) {
|
|
334
|
+
AudioManager.observeAudioInterruptions(true);
|
|
335
|
+
}
|
|
336
|
+
interruptionObserverCount++;
|
|
337
|
+
interruptionEnabled = true;
|
|
338
|
+
interruptionSub = AudioManager.addSystemEventListener('interruption', handleInterruption as never);
|
|
339
|
+
} else {
|
|
340
|
+
console.warn('[GameAudio] AudioManager unavailable — interruption observation disabled');
|
|
341
|
+
}
|
|
342
|
+
} catch { interruptionSub = null; }
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
346
|
+
const RN = require('react-native') as { AppState?: { currentState?: string; addEventListener: (t:string, cb:(s:string)=>void)=>{ remove:()=>void } } };
|
|
347
|
+
const AppState = RN?.AppState;
|
|
348
|
+
if (AppState?.addEventListener) {
|
|
349
|
+
const isInactive = (s: string | null | undefined) => s === 'inactive' || s === 'background';
|
|
350
|
+
if (isInactive(AppState.currentState)) {
|
|
351
|
+
appPaused = true;
|
|
352
|
+
requestSuspendUpdate();
|
|
353
|
+
}
|
|
354
|
+
appStateSub = AppState.addEventListener('change', (next: string) => {
|
|
355
|
+
if (isInactive(next)) {
|
|
356
|
+
appPaused = true;
|
|
357
|
+
requestSuspendUpdate();
|
|
358
|
+
} else if (next === 'active') {
|
|
359
|
+
appPaused = false;
|
|
360
|
+
updateSuspendState();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
} catch {}
|
|
365
|
+
|
|
366
|
+
applyAllGains();
|
|
367
|
+
// Initial idle suspend (F2): resource with no playback should suspend
|
|
368
|
+
scheduleIdleSuspend();
|
|
369
|
+
|
|
370
|
+
async function startMusicInternal(id: keyof T & string, generation: number): Promise<void> {
|
|
371
|
+
if (disposed || !context) return;
|
|
372
|
+
if (isEffectivelyPaused()) {
|
|
373
|
+
// Defer until resume
|
|
374
|
+
pendingMusic = { id, generation };
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (generation !== musicGeneration) return;
|
|
378
|
+
try {
|
|
379
|
+
const buffer = await getBuffer(id);
|
|
380
|
+
if (disposed || !context) return;
|
|
381
|
+
if (generation !== musicGeneration) return;
|
|
382
|
+
if (isEffectivelyPaused()) {
|
|
383
|
+
pendingMusic = { id, generation };
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (currentMusicId !== id || activeMusicGeneration !== generation) {
|
|
387
|
+
// If another generation has taken over, don't start
|
|
388
|
+
if (generation !== musicGeneration) return;
|
|
389
|
+
}
|
|
390
|
+
const source = (context as unknown as { createBufferSource(): { buffer: unknown | null; loop: boolean; connect(dest: unknown): void; disconnect?(): void; start(when?: number): void; stop(when?: number): void } }).createBufferSource();
|
|
391
|
+
source.buffer = buffer;
|
|
392
|
+
source.loop = true;
|
|
393
|
+
const dest = categoryGains['music'] ?? masterGain ?? (context as unknown as { destination: unknown }).destination;
|
|
394
|
+
source.connect(dest as unknown);
|
|
395
|
+
// Stop previous
|
|
396
|
+
if (currentMusicNode) { try { currentMusicNode.stop(); } catch {} }
|
|
397
|
+
currentMusicNode = source;
|
|
398
|
+
currentMusicId = id;
|
|
399
|
+
activeMusicGeneration = generation;
|
|
400
|
+
try {
|
|
401
|
+
source.start();
|
|
402
|
+
} catch (e) {
|
|
403
|
+
// GS-AUDIO-01: a failed music start must not wedge the music node —
|
|
404
|
+
// clear it so idle suspension and retries proceed, then report.
|
|
405
|
+
try { source.disconnect?.(); } catch {}
|
|
406
|
+
if (currentMusicNode === source) {
|
|
407
|
+
currentMusicNode = null;
|
|
408
|
+
}
|
|
409
|
+
scheduleIdleSuspend();
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
if (idleSuspendTimer) { clearTimeout(idleSuspendTimer); idleSuspendTimer=null; }
|
|
413
|
+
const state = (context as unknown as { state: string }).state;
|
|
414
|
+
if (state === 'suspended' && !isEffectivelyPaused()) await (context as InstanceType<typeof AudioContext>).resume().catch(()=>{});
|
|
415
|
+
} catch (e) {
|
|
416
|
+
if (generation === musicGeneration) {
|
|
417
|
+
console.warn(`[GameAudio] playMusic("${String(id)}") failed:`, e);
|
|
418
|
+
if (currentMusicId === id && activeMusicGeneration === generation) {
|
|
419
|
+
currentMusicId = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let lifecycleDetach: (() => void) | null = null;
|
|
426
|
+
const setSessionPaused = (paused: boolean): void => {
|
|
427
|
+
sessionPaused = paused;
|
|
428
|
+
requestSuspendUpdate();
|
|
429
|
+
};
|
|
430
|
+
const audio: GameAudio<T> & {
|
|
431
|
+
_setSessionPaused?: (p:boolean)=>void;
|
|
432
|
+
_isIdleSuspended?: ()=>boolean;
|
|
433
|
+
_getConcurrencyCount?: (key:string)=>number;
|
|
434
|
+
_handleInterruption?: (e:unknown)=>void;
|
|
435
|
+
} = {
|
|
436
|
+
play<K extends keyof T & string>(id: K, opts?: GameAudioPlayOptions): void {
|
|
437
|
+
ensureNotDisposed();
|
|
438
|
+
if (isEffectivelyPaused()) return;
|
|
439
|
+
if (opts?.category !== undefined) assertCategory(opts.category);
|
|
440
|
+
if (opts?.volume !== undefined) assertVolume(opts.volume);
|
|
441
|
+
const category: AudioCategory = (opts?.category as AudioCategory) ?? 'sfx';
|
|
442
|
+
const concurrencyKey = opts?.concurrency?.key ?? String(id);
|
|
443
|
+
const limit = opts?.concurrency?.limit;
|
|
444
|
+
const overflow = opts?.concurrency?.overflow ?? 'drop-new';
|
|
445
|
+
let reservation: unknown = null;
|
|
446
|
+
if (limit !== undefined) {
|
|
447
|
+
if (!Number.isFinite(limit) || limit < 1 || Math.floor(limit) !== limit) {
|
|
448
|
+
throw new GameAudioError(`Concurrency limit must be a positive integer, got ${String(limit)}`);
|
|
449
|
+
}
|
|
450
|
+
let set = concurrencyMap.get(concurrencyKey);
|
|
451
|
+
if (!set) { set = new Set(); concurrencyMap.set(concurrencyKey, set); }
|
|
452
|
+
if (set.size >= limit) {
|
|
453
|
+
if (overflow === 'drop-new') return;
|
|
454
|
+
if (overflow === 'stop-oldest') {
|
|
455
|
+
const oldest = set.values().next().value as unknown;
|
|
456
|
+
if (oldest) {
|
|
457
|
+
if (pendingReservations.has(oldest)) {
|
|
458
|
+
cancelledReservations.add(oldest);
|
|
459
|
+
} else {
|
|
460
|
+
try { (oldest as { stop: (w?:number)=>void }).stop(); } catch {}
|
|
461
|
+
dropVoice(oldest, concurrencyKey);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
reservation = {};
|
|
467
|
+
set.add(reservation);
|
|
468
|
+
pendingReservations.add(reservation);
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const state = (context as unknown as { state: string }).state;
|
|
472
|
+
if (state === 'suspended' && !isEffectivelyPaused()) void (context as InstanceType<typeof AudioContext>).resume().catch(()=>{});
|
|
473
|
+
} catch {}
|
|
474
|
+
if (idleSuspendTimer) { clearTimeout(idleSuspendTimer); idleSuspendTimer = null; }
|
|
475
|
+
void (async () => {
|
|
476
|
+
if (disposed || !context || isEffectivelyPaused()) {
|
|
477
|
+
if (reservation) {
|
|
478
|
+
dropReservation(reservation, concurrencyKey);
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const buffer = await getBuffer(id as keyof T & string);
|
|
484
|
+
if (disposed || !context || isEffectivelyPaused()) {
|
|
485
|
+
if (reservation) {
|
|
486
|
+
dropReservation(reservation, concurrencyKey);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (reservation && cancelledReservations.has(reservation)) {
|
|
491
|
+
dropReservation(reservation, concurrencyKey);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
type VoiceSource = {
|
|
495
|
+
buffer: unknown | null;
|
|
496
|
+
loop?: boolean;
|
|
497
|
+
connect(dest: unknown): void;
|
|
498
|
+
disconnect?(): void;
|
|
499
|
+
start(when?: number, offset?: number, duration?: number): void;
|
|
500
|
+
stop(when?: number): void;
|
|
501
|
+
addEventListener?: (type: string, cb: () => void) => void;
|
|
502
|
+
onended?: (()=>void)|null;
|
|
503
|
+
};
|
|
504
|
+
type ContextSource = {
|
|
505
|
+
createBufferSource(): VoiceSource;
|
|
506
|
+
};
|
|
507
|
+
// GS-AUDIO-01: one idempotent cleanup owns every stage from here.
|
|
508
|
+
// A failure in create/connect/start removes BOTH the reservation
|
|
509
|
+
// and the actual source, detaches handlers, disconnects owned
|
|
510
|
+
// nodes, and reconsiders idle suspension — never half-registered.
|
|
511
|
+
let voiceGain: GainLike | null = null;
|
|
512
|
+
let cleaned = false;
|
|
513
|
+
let source: VoiceSource | null = null;
|
|
514
|
+
const releaseVoice = (): void => {
|
|
515
|
+
if (cleaned) return;
|
|
516
|
+
cleaned = true;
|
|
517
|
+
if (source !== null) {
|
|
518
|
+
dropVoice(source, limit !== undefined ? concurrencyKey : undefined);
|
|
519
|
+
}
|
|
520
|
+
if (reservation && limit !== undefined) {
|
|
521
|
+
dropReservation(reservation, concurrencyKey);
|
|
522
|
+
}
|
|
523
|
+
const anySource = source as unknown as { onended?: (()=>void)|null } | null;
|
|
524
|
+
if (anySource !== null && anySource.onended === cleanup) anySource.onended = null;
|
|
525
|
+
try { source?.disconnect?.(); } catch {}
|
|
526
|
+
try {
|
|
527
|
+
if (voiceGain !== null) {
|
|
528
|
+
(voiceGain as unknown as { disconnect?(): void }).disconnect?.();
|
|
529
|
+
}
|
|
530
|
+
} catch {}
|
|
531
|
+
scheduleIdleSuspend();
|
|
532
|
+
};
|
|
533
|
+
const cleanup = (): void => {
|
|
534
|
+
releaseVoice();
|
|
535
|
+
};
|
|
536
|
+
try {
|
|
537
|
+
source = (context as unknown as ContextSource).createBufferSource();
|
|
538
|
+
source.buffer = buffer;
|
|
539
|
+
if (opts?.loop) source.loop = true;
|
|
540
|
+
let dest: unknown = (context as unknown as { destination: unknown }).destination;
|
|
541
|
+
const catGain = categoryGains[category];
|
|
542
|
+
if (catGain) dest = catGain;
|
|
543
|
+
else if (masterGain) dest = masterGain;
|
|
544
|
+
if (opts?.volume !== undefined) {
|
|
545
|
+
try {
|
|
546
|
+
voiceGain = (context as unknown as { createGain: ()=> GainLike }).createGain();
|
|
547
|
+
if (voiceGain) {
|
|
548
|
+
const v = opts.volume as number;
|
|
549
|
+
try { (voiceGain.gain as unknown as { value:number }).value = v; } catch {}
|
|
550
|
+
voiceGain.connect(dest);
|
|
551
|
+
dest = voiceGain;
|
|
552
|
+
}
|
|
553
|
+
} catch {}
|
|
554
|
+
}
|
|
555
|
+
source.connect(dest);
|
|
556
|
+
activeVoices.add(source);
|
|
557
|
+
if (reservation) {
|
|
558
|
+
pendingReservations.delete(reservation);
|
|
559
|
+
const set = concurrencyMap.get(concurrencyKey);
|
|
560
|
+
if (set) {
|
|
561
|
+
set.delete(reservation);
|
|
562
|
+
set.add(source);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const anySource = source as unknown as { onended?: (()=>void)|null; addEventListener?: (type:string, cb:()=>void)=>void };
|
|
566
|
+
if (typeof anySource.addEventListener === 'function') anySource.addEventListener('ended', cleanup);
|
|
567
|
+
else anySource.onended = cleanup;
|
|
568
|
+
source.start();
|
|
569
|
+
if (idleSuspendTimer) { clearTimeout(idleSuspendTimer); idleSuspendTimer=null; }
|
|
570
|
+
} catch (e) {
|
|
571
|
+
releaseVoice();
|
|
572
|
+
console.warn(`[GameAudio] play("${String(id)}") failed:`, e);
|
|
573
|
+
}
|
|
574
|
+
} catch (e) {
|
|
575
|
+
// Pre-voice stages (decode/buffer) failed: no voice exists, but
|
|
576
|
+
// the reservation must still be released exactly once.
|
|
577
|
+
if (reservation) {
|
|
578
|
+
const set = concurrencyMap.get(concurrencyKey);
|
|
579
|
+
if (set) { set.delete(reservation); if (set.size===0) concurrencyMap.delete(concurrencyKey); }
|
|
580
|
+
cancelledReservations.delete(reservation);
|
|
581
|
+
}
|
|
582
|
+
console.warn(`[GameAudio] play("${String(id)}") failed:`, e);
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
async playMusic<K extends keyof T & string>(id: K): Promise<void> {
|
|
588
|
+
ensureNotDisposed();
|
|
589
|
+
musicGeneration++;
|
|
590
|
+
const gen = musicGeneration;
|
|
591
|
+
// Stop previous immediately and mark generation
|
|
592
|
+
if (currentMusicNode) {
|
|
593
|
+
try { currentMusicNode.stop(); } catch {}
|
|
594
|
+
currentMusicNode = null;
|
|
595
|
+
}
|
|
596
|
+
currentMusicId = id;
|
|
597
|
+
// If effectively paused, defer (F4)
|
|
598
|
+
if (isEffectivelyPaused()) {
|
|
599
|
+
pendingMusic = { id: id as keyof T & string, generation: gen };
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await startMusicInternal(id as keyof T & string, gen);
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
stopMusic(): void {
|
|
606
|
+
ensureNotDisposed();
|
|
607
|
+
musicGeneration++;
|
|
608
|
+
if (currentMusicNode) { try { currentMusicNode.stop(); } catch {} currentMusicNode=null; }
|
|
609
|
+
currentMusicId = null;
|
|
610
|
+
pendingMusic = null;
|
|
611
|
+
scheduleIdleSuspend();
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
pause(): void {
|
|
615
|
+
ensureNotDisposed();
|
|
616
|
+
if (userPaused) return;
|
|
617
|
+
userPaused = true;
|
|
618
|
+
requestSuspendUpdate();
|
|
619
|
+
// FF1: keep denied-interruption flag intact; only explicit resume() clears it
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
resume(): void {
|
|
623
|
+
ensureNotDisposed();
|
|
624
|
+
if (!userPaused && !interruptionRequiresExplicitResume) return;
|
|
625
|
+
const wasExplicit = interruptionRequiresExplicitResume;
|
|
626
|
+
userPaused = false;
|
|
627
|
+
if (wasExplicit) {
|
|
628
|
+
// Explicit user action clears denied auto-resume (F2)
|
|
629
|
+
interruptionPaused = false;
|
|
630
|
+
interruptionRequiresExplicitResume = false;
|
|
631
|
+
}
|
|
632
|
+
updateSuspendState();
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
setVolume(category: AudioCategory, volume: number): void {
|
|
636
|
+
ensureNotDisposed();
|
|
637
|
+
assertCategory(category);
|
|
638
|
+
assertVolume(volume);
|
|
639
|
+
volumes[category] = volume;
|
|
640
|
+
applyGain(category);
|
|
641
|
+
// F1: don't re-apply master when changing category, and vice versa — only own gain
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
getVolume(category: AudioCategory): number {
|
|
645
|
+
ensureNotDisposed();
|
|
646
|
+
assertCategory(category);
|
|
647
|
+
return volumes[category];
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
setMuted(next: boolean): void {
|
|
651
|
+
ensureNotDisposed();
|
|
652
|
+
muted = Boolean(next);
|
|
653
|
+
updateSuspendState();
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
isMuted(): boolean {
|
|
657
|
+
ensureNotDisposed();
|
|
658
|
+
return muted;
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
setPaused(next: boolean): void {
|
|
662
|
+
ensureNotDisposed();
|
|
663
|
+
setSessionPaused(Boolean(next));
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
bindLifecycle(source: GameLifecycleSource): () => void {
|
|
667
|
+
return bindAudioLifecycle(source);
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
/** Compat alias for the pre-T20.7 session channel; use setPaused. */
|
|
671
|
+
_setSessionPaused(p: boolean): void {
|
|
672
|
+
ensureNotDisposed();
|
|
673
|
+
setSessionPaused(Boolean(p));
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
dispose(): void {
|
|
677
|
+
if (disposed) return;
|
|
678
|
+
disposed = true;
|
|
679
|
+
currentMusicId = null;
|
|
680
|
+
pendingMusic = null;
|
|
681
|
+
if (currentMusicNode) { try { currentMusicNode.stop(); } catch {} currentMusicNode=null; }
|
|
682
|
+
for (const v of activeVoices) { try { (v as { stop:(w?:number)=>void }).stop(); } catch {} }
|
|
683
|
+
activeVoices.clear();
|
|
684
|
+
concurrencyMap.clear();
|
|
685
|
+
cancelledReservations.clear();
|
|
686
|
+
pendingDecodes.clear();
|
|
687
|
+
bufferCache.clear();
|
|
688
|
+
if (idleSuspendTimer) { clearTimeout(idleSuspendTimer); idleSuspendTimer=null; }
|
|
689
|
+
lifecycleDetach?.();
|
|
690
|
+
lifecycleDetach = null;
|
|
691
|
+
if (interruptionSub) { try { interruptionSub.remove(); } catch {} interruptionSub=null; }
|
|
692
|
+
if (interruptionEnabled) {
|
|
693
|
+
interruptionObserverCount = Math.max(0, interruptionObserverCount - 1);
|
|
694
|
+
if (interruptionObserverCount === 0) {
|
|
695
|
+
try { AudioManager?.observeAudioInterruptions(false); } catch {}
|
|
696
|
+
}
|
|
697
|
+
interruptionEnabled=false;
|
|
698
|
+
}
|
|
699
|
+
if (appStateSub) { try { appStateSub.remove(); } catch {} appStateSub=null; }
|
|
700
|
+
if (context) void (context as InstanceType<typeof AudioContext>).close().catch(()=>{});
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const bindAudioLifecycle = (source: GameLifecycleSource): (() => void) => {
|
|
705
|
+
ensureNotDisposed();
|
|
706
|
+
// One active source: a replacement detaches the previous one first.
|
|
707
|
+
lifecycleDetach?.();
|
|
708
|
+
lifecycleDetach = null;
|
|
709
|
+
// Apply the current status immediately, then follow transitions.
|
|
710
|
+
// AppState backgrounding stays independent (separate flag).
|
|
711
|
+
setSessionPaused(source.getStatus() !== 'running');
|
|
712
|
+
const sourceDetach = source.subscribe((status) => {
|
|
713
|
+
setSessionPaused(status !== 'running');
|
|
714
|
+
});
|
|
715
|
+
let detached = false;
|
|
716
|
+
const detach = (): void => {
|
|
717
|
+
if (detached) return;
|
|
718
|
+
detached = true;
|
|
719
|
+
sourceDetach();
|
|
720
|
+
if (lifecycleDetach === detach) lifecycleDetach = null;
|
|
721
|
+
};
|
|
722
|
+
lifecycleDetach = detach;
|
|
723
|
+
return detach;
|
|
724
|
+
};
|
|
725
|
+
(audio as unknown as { _isIdleSuspended: ()=>boolean })._isIdleSuspended = () => {
|
|
726
|
+
try { return (context as unknown as { state: string }).state === 'suspended'; } catch { return false; }
|
|
727
|
+
};
|
|
728
|
+
(audio as unknown as { _getConcurrencyCount: (k:string)=>number })._getConcurrencyCount = (k: string) => concurrencyMap.get(k)?.size ?? 0;
|
|
729
|
+
// Test-only collection sizes for the ownership suites (GS-AUDIO-01/02).
|
|
730
|
+
(audio as unknown as { _getVoiceCounts: ()=>{ activeVoices: number; concurrencyKeys: number; cancelled: number } })._getVoiceCounts = () => ({
|
|
731
|
+
activeVoices: activeVoices.size,
|
|
732
|
+
concurrencyKeys: concurrencyMap.size,
|
|
733
|
+
cancelled: cancelledReservations.size,
|
|
734
|
+
});
|
|
735
|
+
(audio as unknown as { _handleInterruption: (e:unknown)=>void })._handleInterruption = handleInterruption;
|
|
736
|
+
(audio as unknown as { _getVolumes: ()=>typeof volumes })._getVolumes = () => ({ ...volumes });
|
|
737
|
+
(audio as unknown as { _getGainTargets: ()=>Record<string, number> })._getGainTargets = () => {
|
|
738
|
+
const out: Record<string, number> = {};
|
|
739
|
+
for (const c of CATEGORIES) {
|
|
740
|
+
const g = categoryGains[c];
|
|
741
|
+
if (g) {
|
|
742
|
+
try { out[c] = (g.gain as unknown as { value:number }).value; } catch { out[c] = volumes[c]; }
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
return audio as GameAudio<T>;
|
|
749
|
+
}
|