@stage5/lumine 0.3.90 → 0.3.91

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/lib/constants.js CHANGED
@@ -64,6 +64,8 @@ export const BUILD_VENDOR_THREE_TSL_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX
64
64
  export const BUILD_VENDOR_THREE_ADDONS_IMPORT_PREFIX = `${BUILD_VENDOR_THREE_PREFIX}addons/`;
65
65
  export const BUILD_VENDOR_KATEX_VERSION = "0.18.9";
66
66
  export const BUILD_VENDOR_KATEX_PREFIX = `/build/vendor/katex/${BUILD_VENDOR_KATEX_VERSION}/`;
67
+ export const BUILD_VENDOR_GROOVE_LAB_VERSION = "1.0.0";
68
+ export const BUILD_VENDOR_GROOVE_LAB_PLAYER_IMPORT = `/build/vendor/groove-lab/${BUILD_VENDOR_GROOVE_LAB_VERSION}/player.js`;
67
69
  export const PROJECT_METADATA_DIR = ".twinkle";
68
70
  export const PROJECT_METADATA_FILE = "lumine-project.json";
69
71
  export const ASSETS_METADATA_FILE = "assets.json";
@@ -183,6 +185,9 @@ export const LUMINE_THREE_VENDOR_GUIDANCE = `- For Three.js, use the first-party
183
185
  - Size both WebGLRenderer and EffectComposer from Twinkle.preview layout dimensions, coalesce resize work, and render with composer.render() when a composer owns the pass chain.`;
184
186
  export const LUMINE_MATH_VENDOR_GUIDANCE = `- For math formulas, Twinkle serves KaTeX ${BUILD_VENDOR_KATEX_VERSION}: link <link rel="stylesheet" href="${BUILD_VENDOR_KATEX_PREFIX}katex.min.css"> (its fonts load from the same place), then import katex from '${BUILD_VENDOR_KATEX_PREFIX}katex.js' and call katex.render(tex, element, { displayMode, throwOnError: false }), or import renderMathInElement from '${BUILD_VENDOR_KATEX_PREFIX}contrib/auto-render.js' to typeset $...$ and $$...$$ inside existing text.
185
187
  - Use it when an app shows fractions, roots, exponents, integrals, matrices or other formulas that plain text renders poorly; keep plain text for simple arithmetic. Do not paste KaTeX source into project files or use npm/CDN copies.`;
188
+ // Mirrors twinkle-api helpers/build/lumine/soundVendorGuidance.ts (Mikey 11-08).
189
+ export const LUMINE_SOUND_VENDOR_GUIDANCE = `- For sound effects and music, Twinkle serves the Groove Lab player: import * as groove from '${BUILD_VENDOR_GROOVE_LAB_PLAYER_IMPORT}', await groove.unlock() inside the first tap (browsers stay silent until then), then groove.sfx('click' | 'coin' | 'jump' | 'hit' | 'correct' | 'wrong' | 'win' | 'lose' | 'levelup' | 'explosion' ...) at game moments; music is groove.createSoundtrack(spec).play({ fadeIn: 1 }) with a Groove Lab song spec (bpm, bars, key, scale, tracks with notes, phrases like 'D4:1 F4:.5 A4:.5' or drum grids) kept in a project module (soundtrack/<name>.js), or await groove.loadPublishedSong(entryId) for the creator's published Groove Lab song (show its credit). ${SDK_REFERENCE_FILE}'s Groove Lab player note lists every function, effect, instrument and the song format.
190
+ - Games and apps with taps, answers, wins or other moments that deserve feedback should have sound: build it into new games and big changes (with a mute toggle kept in privateDb), and when an app you are changing has none, suggest it in one line naming the moments that would get sounds. Compose real music for the mood (melody, bass, drums, a loop after any intro), never a two-note loop. Do not load audio libraries from a CDN or paste Groove Lab source into project files.`;
186
191
  export const LUMINE_AGENT_INSTRUCTIONS = `${LUMINE_AGENT_INSTRUCTIONS_MARKER}
187
192
  # Lumine Project Agent Guide
188
193
 
@@ -366,8 +371,10 @@ lumine save --summary "Describe the change"
366
371
  Compare against the source, separate stems when useful, and clean timing and
367
372
  octave errors. Record the URL, excerpt timestamps, tools, and limitations;
368
373
  do not claim to have listened when only inspecting note or signal data.
369
- Keep reference media and probes outside project source. Upload final game
370
- media through the authorized Lumine assets command and reference its URL.
374
+ Keep reference media and probes outside project source. Music the app plays
375
+ is best written as a Groove Lab song spec for the Groove Lab player (small,
376
+ loopable, editable in Groove Lab); upload rendered audio through the
377
+ authorized Lumine assets command only when a spec cannot carry it.
371
378
 
372
379
  ## Thumbnail
373
380
 
@@ -402,6 +409,7 @@ The Platform Rules above come first; these add detail.
402
409
  ${LUMINE_MOBILE_SELECTION_GUIDANCE}
403
410
  ${LUMINE_THREE_VENDOR_GUIDANCE}
404
411
  ${LUMINE_MATH_VENDOR_GUIDANCE}
412
+ ${LUMINE_SOUND_VENDOR_GUIDANCE}
405
413
  - Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
406
414
  ${LUMINE_WORLD_UPDATE_GUIDANCE}
407
415
 
@@ -1,4 +1,4 @@
1
1
  // Generated by twinkle-api/scripts/generate-build-sdk-index.mjs from
2
2
  // constants/buildSdkManifest.json (platformRules, essentialSdk). Do not edit by hand.
3
3
  export const LUMINE_PLATFORM_RULES = "Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed frame, even when the code itself is correct. They outrank every other guideline: follow them in every change, including small ones, and fix any break of them you see in code you touch.\n\n1. Every page that draws on a <canvas> (2D, WebGL or Three.js) says what kind of page it is on <html>: data-twinkle-game for a game that fills the screen, data-twinkle-document for a normal scrolling page with a drawing pad, chart, visualizer or background effect. Without it, unless the page lays itself out with Twinkle.preview, Twinkle guesses from the canvas, and any canvas larger than 160 px, even one hidden in a tab, locks the page to one screen so it no longer scrolls. Add the attribute together with the first canvas.\n2. Games (data-twinkle-game) size themselves from Twinkle.preview: subscribe with Twinkle.preview.subscribe(listener, { immediate: true }), size canvases and renderers from layout.stage or layout.viewport, and report the play area with Twinkle.preview.setPlayfield(). Never size a game from 100vh/100vw, window.innerWidth/innerHeight or visualViewport, and never draw a fake phone or browser frame unless the creator asks for a device mockup: those fight the host and leave the game blank, cropped or scrolling on phones. Pages marked data-twinkle-document use ordinary CSS layout instead.\n3. alert(), confirm() and prompt() are silently blocked: alert shows nothing, confirm() always returns false (so the action it guards never happens) and prompt() returns null. Use in-page UI instead: a message element, an in-page confirm step, an <input>.\n4. Apps cannot open new windows or tabs: window.open() and target=\"_blank\" links do nothing, and neither does a download link to an uploaded file. Open Twinkle pages with Twinkle.app.openContent(url) from a tap, save files with Twinkle.files.saveAs({ url, fileName }) (or { blob, fileName } for a file the app made), and show other web addresses as plain text.\n5. Do not use <form> elements: native form submission is blocked, so a submit button inside a form does nothing. Handle inputs and buttons in JavaScript.\n6. Code loads only from project files and Twinkle's vendor paths (/build/vendor/...). Package imports, CDN scripts, external network calls and app-local /api/* routes fail at runtime; use the Twinkle SDK for data, AI and storage.\n7. Call only Twinkle.* methods documented in the SDK reference. An invented or guessed method name throws and breaks the feature.\n8. Twinkle.privateDb and Twinkle.sharedDb save on a user action or as an occasional snapshot, never per frame, tick or keystroke: fast repeated writes are refused with 429 and the data is lost. A privateDb value is at most 16 KB. Keep live state in memory, and use Twinkle.world for realtime multiplayer.\n9. Before changing an existing view, read its relevant event handlers, animations and state flow, and identify the behavior the edit must keep. A new appearance, reference image, renderer or performance improvement does not authorize removing an interaction, animation or capability; preserve it unless the creator explicitly asks for that change. After editing, compare the affected behavior before and after, including its active and inactive states (for example, playing and paused). Exercise those states when tools allow; a still screenshot or a clean console cannot prove motion or interaction. If a state cannot be exercised, report that limit rather than claiming it is preserved.\n10. Change how something looks by editing the code that actually renders it. First find it (search for its visible text, id or class) and read that markup, CSS rule or canvas drawing code; text and shapes drawn on a canvas change in the drawing code, not in CSS. Then style that element through its own selector or code. Do not cover a guess with broad selectors that match class-name fragments or tags across the app (for example [class*=\"panel\"] or every h1) or with !important overrides of rules you have not read: on someone else's app or a contribution branch they restyle screens nobody asked to change. If you cannot find the element, say so and name what you searched for instead of guessing broadly.\n11. When changing stored data or a renderer, inspect both the old save/resume shape and every reader, including fallback views. Keep existing saves readable with an explicit, non-destructive conversion and retain fields needed by another supported renderer. Verify an old save in the new version, new state after reload, and switching to the fallback (for example, 2D x/y toppings becoming 3D positions and returning to a flat view). Never silently drop saved objects or reset progress to make the new view work; an incomplete migration is part of this change to finish, not unrelated future polish.\n12. Keep local gameplay and unsaved edits separate from confirmed server state. Await storage writes and replace saved counters, scores and other durable state from the documented canonical response; Twinkle.privateDb.get/set return { item }, and the stored data is item.value (get may return item: null). Never read a guessed response shape, present a local increment as saved, or swallow a save/load error. Keep unfinished work recoverable, show a clear pending or failed-save state, and let the viewer retry deliberately without counting the same action twice. Verify canonical response restoration, rejected saves and delayed replies, not only a successful local animation.\n13. Start sound and image generation inside the viewer's tap or click handler. A Web Audio AudioContext stays silent until it is created or resumed in a tap (iPhones always enforce this), and Twinkle.ai.generateImage called from load or a timer fails with USER_ACTIVATION_REQUIRED.\n14. Call setPointerCapture only inside a pointerdown handler with that event's pointerId, wrapped in try/catch, and release only when hasPointerCapture says the element holds it. From click, mouse, timer or synthetic handlers it throws and the control stops working.\n15. Keyboard games call preventDefault for their keys (Space, arrows, WASD) from a window or document keydown listener added with { passive: false }, skipping inputs, textareas, selects and contenteditable, and make the playfield focusable; otherwise those keys scroll or jump the page during play.\n16. On phones a long press on game controls selects text or opens the Copy/Look Up menu unless the whole game area (controls, HUD, menus, empty play space) is marked data-twinkle-no-select and styled user-select: none; -webkit-user-select: none; -webkit-touch-callout: none. A game drawn without a canvas sets data-twinkle-game on <html> too, so Twinkle's phone touch protections apply. Keep inputs and copyable text selectable with data-twinkle-selectable.";
4
- export const LUMINE_ESSENTIAL_SDK = "Features every app needs whenever they apply. When you build a new app or make a substantial change, add every one that applies as part of that change, without being asked. On a small, targeted request, add a missing one only when the request touches what it covers (a change to levels, scoring or saving: keep-place; a new screen: back-button); otherwise name it in one line so the creator can ask. Say in one line what you added. Never add one that does not apply.\n\n1. When: The app has a place worth keeping: a game, a level or score in play, an open document, drawing or editor, a quiz in progress, a chosen view or tab, a long page the viewer scrolls. Use: Register Twinkle.app.lifecycle.onSuspend(() => smallState, { work: [...] }) and, at startup, restore from await Twinkle.app.lifecycle.getResumeState(). The snapshot holds where the viewer is looking (scroll, open tab or view, selected item) and unfinished work (a game or level in play, a half-written answer, an unsaved drawing, a quiz in progress); list the work keys in { work }. A return (the phone unloaded the app, a reload, a quick switch away) gets everything back; a new visit (the next day, a new tab) starts at the app's start and gets only the work keys. Phones unload apps that are not on screen (3D apps right away) and browsers kill tabs; without this the player starts over every time.\n2. When: The app has more than one view: a list and its details, menus, chapters, pages, settings. Use: Twinkle.app.history.push on each confirmed view change and Twinkle.app.history.subscribe to restore views, so Back and Forward move inside the app instead of leaving it.\n3. When: Progress, collections, unlocks, high scores or settings the viewer expects to still have tomorrow. Use: Twinkle.privateDb (Twinkle.sharedDb for what everyone sees), written on a user action or as an occasional snapshot, never per frame.\n4. When: The app shares or links to something inside itself (a level, a post, a result). Use: Twinkle.app.getShareUrl(pathSegment) for the link, and read the segment back at startup, instead of building twin-kle.com URLs by hand.";
4
+ export const LUMINE_ESSENTIAL_SDK = "Features every app needs whenever they apply. When you build a new app or make a substantial change, add every one that applies as part of that change, without being asked. On a small, targeted request, add a missing one only when the request touches what it covers (a change to levels, scoring or saving: keep-place; a new screen: back-button); otherwise name it in one line so the creator can ask. Say in one line what you added. Never add one that does not apply.\n\n1. When: The app has a place worth keeping: a game, a level or score in play, an open document, drawing or editor, a quiz in progress, a chosen view or tab, a long page the viewer scrolls. Use: Register Twinkle.app.lifecycle.onSuspend(() => smallState, { work: [...] }) and, at startup, restore from await Twinkle.app.lifecycle.getResumeState(). The snapshot holds where the viewer is looking (scroll, open tab or view, selected item) and unfinished work (a game or level in play, a half-written answer, an unsaved drawing, a quiz in progress); list the work keys in { work }. A return (the phone unloaded the app, a reload, a quick switch away) gets everything back; a new visit (the next day, a new tab) starts at the app's start and gets only the work keys. Phones unload apps that are not on screen (3D apps right away) and browsers kill tabs; without this the player starts over every time.\n2. When: The app has more than one view: a list and its details, menus, chapters, pages, settings. Use: Twinkle.app.history.push on each confirmed view change and Twinkle.app.history.subscribe to restore views, so Back and Forward move inside the app instead of leaving it.\n3. When: Progress, collections, unlocks, high scores or settings the viewer expects to still have tomorrow. Use: Twinkle.privateDb (Twinkle.sharedDb for what everyone sees), written on a user action or as an occasional snapshot, never per frame.\n4. When: The app shares or links to something inside itself (a level, a post, a result). Use: Twinkle.app.getShareUrl(pathSegment) for the link, and read the segment back at startup, instead of building twin-kle.com URLs by hand.\n5. When: A game, or an app whose taps, answers, wins or other moments would feel better with sound (a quiz's right and wrong answers, a button that does something, a level clear), and the app has no sound yet. Use: The Groove Lab player (import * as groove from '/build/vendor/groove-lab/1.0.0/player.js'): await groove.unlock() inside the first tap, then groove.sfx('click' | 'coin' | 'jump' | 'hit' | 'correct' | 'wrong' | 'win' | 'lose' | 'levelup' ...) at those moments, with a mute toggle kept in Twinkle.privateDb. Offer music too: compose a soundtrack as a Groove Lab song spec in a project module (soundtrack/<name>.js with export default { ...spec }, played with groove.createSoundtrack(spec).play({ fadeIn: 1 }) after unlock), or play the creator's published Groove Lab song with groove.loadPublishedSong(entryId) and show its credit.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.90",
3
+ "version": "0.3.91",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  # Build SDK Index
2
2
 
3
- Version: 1.82.1
3
+ Version: 1.83.0
4
4
  Updated: 2026-11-08
5
- Generated: 2026-11-08T00:36:53.981Z
5
+ Generated: 2026-11-08T05:30:44.050Z
6
6
 
7
7
  ## Platform rules
8
8
  Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed frame, even when the code itself is correct. They outrank every other guideline: follow them in every change, including small ones, and fix any break of them you see in code you touch.
@@ -31,6 +31,7 @@ Features every app needs whenever they apply. When you build a new app or make a
31
31
  2. When: The app has more than one view: a list and its details, menus, chapters, pages, settings. Use: Twinkle.app.history.push on each confirmed view change and Twinkle.app.history.subscribe to restore views, so Back and Forward move inside the app instead of leaving it.
32
32
  3. When: Progress, collections, unlocks, high scores or settings the viewer expects to still have tomorrow. Use: Twinkle.privateDb (Twinkle.sharedDb for what everyone sees), written on a user action or as an occasional snapshot, never per frame.
33
33
  4. When: The app shares or links to something inside itself (a level, a post, a result). Use: Twinkle.app.getShareUrl(pathSegment) for the link, and read the segment back at startup, instead of building twin-kle.com URLs by hand.
34
+ 5. When: A game, or an app whose taps, answers, wins or other moments would feel better with sound (a quiz's right and wrong answers, a button that does something, a level clear), and the app has no sound yet. Use: The Groove Lab player (import * as groove from '/build/vendor/groove-lab/1.0.0/player.js'): await groove.unlock() inside the first tap, then groove.sfx('click' | 'coin' | 'jump' | 'hit' | 'correct' | 'wrong' | 'win' | 'lose' | 'levelup' ...) at those moments, with a mute toggle kept in Twinkle.privateDb. Offer music too: compose a soundtrack as a Groove Lab song spec in a project module (soundtrack/<name>.js with export default { ...spec }, played with groove.createSoundtrack(spec).play({ fadeIn: 1 }) after unlock), or play the creator's published Groove Lab song with groove.loadPublishedSong(entryId) and show its credit.
34
35
 
35
36
  ## Notes
36
37
  - Reward is part of completion: a reward-paying run must stay pending until awarded:true and its canonical receipt (or getReceipt status awarded). Do not show final solved/completed status, advance rewarded progress, or discard finished proof before that. Preserve the exact challenge/proof and acknowledged progress in a durable per-viewer outbox; reconcile getReceipt after a lost reply and retry the same challenge, never inventing a payment. Explain explicit practice, already collected, exhausted approved limits, preview and disclosed zero-value retry outcomes. Use the start response as a conservative elapsed-time reference, and test delayed start, network failure, lost claim reply, reload, day/viewer change and duplicate recovery.
@@ -73,6 +74,7 @@ Features every app needs whenever they apply. When you build a new app or make a
73
74
  - word-game-puzzle-v1 preserves Word Search streak bonuses using canonical puzzle reward history: daily-easy/medium/hard with optional -s10 and -s20 tiers. A learner earns each difficulty once per UTC day across every tier and release. rewards.getStatus returns wordGamePuzzle { streakDays, ruleIds, earnedLevels }; choose its ruleIds and display canonical rule amounts. Streaks count consecutive days with a puzzle reward, capped at 30. This profile checks streak eligibility and duplicate difficulty claims in addition to the approved timer; it does not simulate or prove the puzzle solve.
74
75
  - word-game-puzzle-v2 is the same daily rule and also requires a server-verified finished solve of that difficulty today (Twinkle.wordMaster.startPuzzle and sendPuzzleEvents). word-search-champion-v1 pays only the rank-1 viewer of a completed UTC month on the server-written verified:month:YYYY-MM board (score, then earliest), once per month and only after the month is final (07:00 UTC on the 2nd, when its last boards can no longer take scores); once anyone collected a month, nobody else can; a removed or fully banned #1 passes the prize to the next eligible player; The daily Twinkle review confirms each month’s winner from the server’s record of their solves before the prize opens (a disqualified winner passes it to the next eligible player). rewards.getStatus returns wordSearchChampion: { month, months, claimable, checking } — months are ready to collect; checking lists wins still being confirmed (show a checking message, no collect button). Show the champion banner from that status, never from app storage.
75
76
  - Twinkle.coins prices are part of the app's rewards review (rewards.json prices + spendBudget). Using Twinkle.coins requires the same approval as Twinkle.rewards; the website, never the app, confirms every purchase. Use getReceipts as the only proof of who owns a bought item.
77
+ - Groove Lab player (/build/vendor/groove-lab/1.0.0/player.js): sound effects and music for any app, built from Groove Lab's own instruments, so a song sounds the same as in Groove Lab. Browsers stay silent until a tap: await unlock() inside a tap or click handler before the first sound. Effects: sfx(name, { pitch, volume }) with name one of SFX (click, select, coin, jump, land, step, hit, hurt, explosion, laser, powerup, win, lose, correct, wrong, levelup, open, close, countdown, go) or your own steps [{ instrument, note, seconds, delay, velocity } | { drum, kit, delay, velocity }]; playNote({ instrument, note, seconds, velocity, delay }); playDrum({ kit, sound, velocity, delay }) with sound a DRUMS name (kick, snare, hat, openHat, crash, clap, rim, lowTom, ...) or GM key; setEffectsVolume(0..1). Music: loadSong(spec) checks a Groove Lab song spec and returns { song, warnings }; createSoundtrack(specOrSong, { loop = true, volume }) returns { play({ fadeIn, from }), stop({ fadeOut }), setVolume(v), preload(), playing, time, duration, onEnd, credit }; setMusicVolume(0..1, seconds) ducks music without touching effects; stopMusic({ fadeOut }); loadPublishedSong(entryId) returns { song, credit } via Twinkle.soundtracks.getSong. One soundtrack plays at a time. Song spec (the same format as Groove Lab's load_song): { title, bpm, bars, key: 'D', scale: 'minor', sound: 'studio' | 'retro', loop: { startBar, endBar }, tracks: [{ kind: 'synth' | 'drums', name, instrument, gain, notes: [[tick, midi, lengthTicks, velocity127]] | phrases: [{ bar, beat, text: 'D4:1 F4:.5 A4:.5 | C5:2' }] | grids: [{ bar, bars, pattern: { kick: 'x...x...x...x...', hat: '..x...x...x...x.' } }] }] }; ticks are 96 per quarter note; drum notes are [tick, gmKey, velocity127]. INSTRUMENTS and KITS list the instrument ids (piano, epiano, organ, strings, guitar, brass, flute, bass303, subbass, supersaw, squarelead, pluck, bell, the Command Nexus nx* and kh* voices; kits kit, kit808, gmkit, nexuskit). Write a real piece for the app's mood (melody, bass, drums, a loop section after any intro), not a two-note loop; keep game music loopable and leave room for effects.
76
78
 
77
79
  ## AI decision design
78
80
  - Twinkle.ai.decide runs JEV, a model for narrow decisions over supplied text or structured state, and it is free for viewers: decisions are covered by Twinkle and never use their AI Energy. Whenever an app makes a judgment that ordinary code can only approximate, consider JEV first. Proven patterns: an opponent or boss coach that reads the player's habits (favourite lane, repeated trick, cheese move) and picks a counter-plan from a fixed list, as in Red Square's boss and Open Football's opponent coach; NPCs that understand what a player types and choose an action; classifying or scoring user submissions; ranking supplied candidates; and picking the next exercise or difficulty from evidence about the learner. Any app with an AI opponent, NPC, or adaptive difficulty should decide explicitly whether JEV improves it. These are examples to reason from, not a keyword checklist or a requirement to add AI to every app.
@@ -2067,6 +2069,13 @@ showHint(`${latest.item.emoji} ${latest.item.name} = ${latest.a.name} + ${latest
2067
2069
  - Lumine Network Build 2742 only. Replace UI state from confirmed server responses.
2068
2070
 
2069
2071
  ### Twinkle.soundtracks
2072
+ - async getSong({ entryId }) | scopes: sharedDb:read
2073
+ - Returns: { song: { entryId, title, creator: { id, username }, url, updatedAt, coverUrl, data, parts } } | { song: { entryId, title, creator, url, songLocked: true, coverUnlockHint } }
2074
+ - Read one published Groove Lab song so this app can play it with the Groove Lab player.
2075
+ - entryId is the published song's id (in Groove Lab: the song's Use in your app button). Only published songs are returned, never private projects; a song in the middle of being re-published returns 409 (try again shortly).
2076
+ - Play it with the Groove Lab player, which assembles data and parts for you: const { song, credit } = await groove.loadPublishedSong(entryId) after import * as groove from '/build/vendor/groove-lab/1.0.0/player.js'. Show the credit in the app ("Music: Night Drive by @someone").
2077
+ - Load the song when the app needs it instead of copying it into project files: the creator's later re-publishes from Groove Lab then reach the app automatically.
2078
+ - Story-locked songs (Red Square's Full Circle and Starlight) return songLocked with coverUnlockHint and no music until this viewer unlocks them: play something else and show the hint.
2070
2079
  - async searchSources({ query } = {}) | scopes: sharedDb:read
2071
2080
  - Returns: { sources: [{ kind, appId?, title, href, thumbnailUrl? }] }
2072
2081
  - Find a public app or Grammarbles Quest for a song tag.
@@ -2437,3 +2446,17 @@ if (status.mode === 'live') {
2437
2446
  // Display only result.balances and result.receipt after awarded === true.
2438
2447
  }
2439
2448
  ```
2449
+
2450
+ ### Sound effects and a soundtrack
2451
+
2452
+ ```js
2453
+ import * as groove from '/build/vendor/groove-lab/1.0.0/player.js';
2454
+ import theme from './soundtrack/theme.js'; // export default { title, bpm, bars, key, scale, tracks: [...] }
2455
+ const music = groove.createSoundtrack(theme);
2456
+ startButton.addEventListener('click', async () => {
2457
+ await groove.unlock(); // inside the tap: browsers stay silent until then
2458
+ music.play({ fadeIn: 1 });
2459
+ });
2460
+ function onCoin() { groove.sfx('coin'); }
2461
+ function onGameOver() { music.stop({ fadeOut: 1.5 }); groove.sfx('lose'); }
2462
+ ```