star-sdk 0.1.0 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Star SDK
2
2
 
3
- Unified SDK for browser game development. Audio, canvas, and leaderboards.
3
+ Browser game SDK with built-in leaderboards (no backend needed), mobile-safe audio, and multiplayer. Works on iOS Safari. Perfect for AI-generated games.
4
4
 
5
5
  ```javascript
6
6
  import Star from 'star-sdk';
@@ -10,6 +10,47 @@ Star.game(ctx => { ... });
10
10
  Star.leaderboard.submit(1500);
11
11
  ```
12
12
 
13
+ ## Why Star SDK?
14
+
15
+ | Need | Without Star | With Star |
16
+ |------|--------------|-----------|
17
+ | **Leaderboards** | Build a backend, database, auth | `Star.leaderboard.submit(score)` |
18
+ | **Mobile audio** | Handle unlock gestures, AudioContext resume | Just call `Star.audio.play()` |
19
+ | **HiDPI canvas** | Manual DPR scaling, coordinate math | Automatic |
20
+ | **iOS Safari** | Debug audio/touch issues for hours | It just works |
21
+
22
+ ### vs Phaser/PixiJS
23
+ Star SDK is simpler. No scene system, no asset loader config. Built-in leaderboards mean you ship a complete game, not just a demo.
24
+
25
+ ### vs Kaboom.js
26
+ Star SDK works on mobile out of the box. Leaderboards and multiplayer are included.
27
+
28
+ ### vs Vanilla Canvas
29
+ Star SDK handles the annoying stuff: audio unlocking, DPR scaling, touch coordinates, game loop timing. You write game logic, not boilerplate.
30
+
31
+ ## One-Liner Game
32
+
33
+ ```bash
34
+ npx star-sdk init && cat > index.html << 'EOF'
35
+ <script type="module">
36
+ import Star from 'https://esm.sh/star-sdk';
37
+ Star.game(ctx => {
38
+ let score = 0;
39
+ Star.audio.preload({ coin: 'coin' });
40
+ ctx.loop(() => {
41
+ ctx.ctx.fillStyle = '#1a1a2e';
42
+ ctx.ctx.fillRect(0, 0, ctx.width, ctx.height);
43
+ ctx.ctx.fillStyle = '#fff';
44
+ ctx.ctx.font = '48px sans-serif';
45
+ ctx.ctx.fillText(score, ctx.width/2 - 20, ctx.height/2);
46
+ });
47
+ ctx.canvas.onclick = () => { score++; Star.audio.play('coin'); };
48
+ });
49
+ </script>
50
+ EOF
51
+ open index.html # or: python -m http.server
52
+ ```
53
+
13
54
  ## Installation
14
55
 
15
56
  ```bash
package/dist/cli.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import "star-sdk-cli";
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var u=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var x=(e,t)=>{for(var r in t)u(e,r,{get:t[r],enumerable:!0})},h=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of O(t))!M.call(e,s)&&s!==r&&u(e,s,{get:()=>t[s],enumerable:!(o=b(t,s))||o.enumerable});return e};var G=e=>h(u({},"__esModule",{value:!0}),e);var I={};x(I,{Star:()=>c,audio:()=>f.createStarAudio,default:()=>A,game:()=>S.game,leaderboard:()=>g.createLeaderboard,multiplayer:()=>y.createMultiplayer});module.exports=G(I);var f=require("star-audio"),S=require("star-canvas"),g=require("star-leaderboard"),y=require("star-multiplayer"),p=null,l=null,i=null,d=()=>typeof window<"u",L=()=>typeof process<"u"&&process.versions?.node;function m(){if(d()||!L())return null;try{let e=require("fs"),r=require("path").join(process.cwd(),".starrc");if(!e.existsSync(r))return null;let o=e.readFileSync(r,"utf-8");return JSON.parse(o)}catch{return null}}function a(){if(!l&&d()){let{createStarAudio:e}=require("star-audio");l=e()}return l}function n(){if(!i){let e=p?.gameId,t=p?.apiBase;if(!e){let o=m();o?.gameId&&(e=o.gameId)}let{createLeaderboard:r}=require("star-leaderboard");i=r({gameId:e,apiBase:t})}return i}var c={init(e){p=e,i&&(i.destroy?.(),i=null)},audio:{play:(e,t)=>a().play(e,t),preload:e=>a().preload(e),music:{crossfadeTo:(e,t)=>a().music.crossfadeTo(e,t),stop:e=>a().music.stop(e)},setMusicVolume:e=>a().setMusicVolume(e),setSfxVolume:e=>a().setSfxVolume(e),setMute:e=>a().setMute(e),toggleMute:()=>a().toggleMute(),isMuted:()=>a().isMuted()},game(e,t){let{game:r}=require("star-canvas");r(e,t)},leaderboard:{submit:e=>n().submit(e),show:()=>n().show(),getScores:e=>n().getScores(e),share:e=>n().share(e)},multiplayer:{async create(e){let{createMultiplayer:t}=require("star-multiplayer"),r=t();return await r.start(e),r}},loadConfig:m,version:"0.1.0"},A=c;0&&(module.exports={Star,audio,game,leaderboard,multiplayer});
1
+ "use strict";var p=Object.defineProperty;var h=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var L=(e,t)=>{for(var r in t)p(e,r,{get:t[r],enumerable:!0})},I=(e,t,r,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of G(t))!A.call(e,i)&&i!==r&&p(e,i,{get:()=>t[i],enumerable:!(s=h(t,i))||s.enumerable});return e};var w=e=>I(p({},"__esModule",{value:!0}),e);var v={};L(v,{Star:()=>g,audio:()=>b.createStarAudio,default:()=>B,game:()=>O.game,leaderboard:()=>M.createLeaderboard,multiplayer:()=>x.createMultiplayer});module.exports=w(v);var d=require("star-audio"),m=require("star-leaderboard"),c=require("star-canvas"),f=require("star-multiplayer"),b=require("star-audio"),O=require("star-canvas"),M=require("star-leaderboard"),x=require("star-multiplayer"),u=null,l=null,o=null,y=()=>typeof window<"u",C=()=>typeof process<"u"&&process.versions?.node;function S(){if(y()||!C())return null;try{let e=require("fs"),r=require("path").join(process.cwd(),".starrc");if(!e.existsSync(r))return null;let s=e.readFileSync(r,"utf-8");return JSON.parse(s)}catch{return null}}function a(){return!l&&y()&&(l=(0,d.createStarAudio)()),l}var P="https://buildwithstar.com";function n(){if(!o){let e=u?.gameId,t=u?.apiBase??P;if(!e){let r=S();r?.gameId&&(e=r.gameId)}o=(0,m.createLeaderboard)({gameId:e,apiBase:t})}return o}var g={init(e){u=e,o&&(o.destroy?.(),o=null)},audio:{play:(e,t)=>a().play(e,t),preload:e=>a().preload(e),music:{crossfadeTo:(e,t)=>a().music.crossfadeTo(e,t),stop:e=>a().music.stop(e)},setMusicVolume:e=>a().setMusicVolume(e),setSfxVolume:e=>a().setSfxVolume(e),setMute:e=>a().setMute(e),toggleMute:()=>a().toggleMute(),isMuted:()=>a().isMuted()},game(e,t){(0,c.game)(e,t)},leaderboard:{submit:e=>n().submit(e),show:()=>n().show(),getScores:e=>n().getScores(e),share:e=>n().share(e)},multiplayer:{async create(e){let t=(0,f.createMultiplayer)();return await t.start(e),t}},loadConfig:S,version:"0.1.0"},B=g;0&&(module.exports={Star,audio,game,leaderboard,multiplayer});
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- var o=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{createStarAudio as b}from"star-audio";import{game as M}from"star-canvas";import{createLeaderboard as h}from"star-leaderboard";import{createMultiplayer as L}from"star-multiplayer";var l=null,u=null,i=null,p=()=>typeof window<"u",m=()=>typeof process<"u"&&process.versions?.node;function d(){if(p()||!m())return null;try{let e=o("fs"),r=o("path").join(process.cwd(),".starrc");if(!e.existsSync(r))return null;let s=e.readFileSync(r,"utf-8");return JSON.parse(s)}catch{return null}}function a(){if(!u&&p()){let{createStarAudio:e}=o("star-audio");u=e()}return u}function n(){if(!i){let e=l?.gameId,t=l?.apiBase;if(!e){let s=d();s?.gameId&&(e=s.gameId)}let{createLeaderboard:r}=o("star-leaderboard");i=r({gameId:e,apiBase:t})}return i}var c={init(e){l=e,i&&(i.destroy?.(),i=null)},audio:{play:(e,t)=>a().play(e,t),preload:e=>a().preload(e),music:{crossfadeTo:(e,t)=>a().music.crossfadeTo(e,t),stop:e=>a().music.stop(e)},setMusicVolume:e=>a().setMusicVolume(e),setSfxVolume:e=>a().setSfxVolume(e),setMute:e=>a().setMute(e),toggleMute:()=>a().toggleMute(),isMuted:()=>a().isMuted()},game(e,t){let{game:r}=o("star-canvas");r(e,t)},leaderboard:{submit:e=>n().submit(e),show:()=>n().show(),getScores:e=>n().getScores(e),share:e=>n().share(e)},multiplayer:{async create(e){let{createMultiplayer:t}=o("star-multiplayer"),r=t();return await r.start(e),r}},loadConfig:d,version:"0.1.0"},S=c;export{c as Star,b as audio,S as default,M as game,h as leaderboard,L as multiplayer};
1
+ var p=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,a)=>(typeof require<"u"?require:t)[a]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{createStarAudio as m}from"star-audio";import{createLeaderboard as c}from"star-leaderboard";import{game as f}from"star-canvas";import{createMultiplayer as y}from"star-multiplayer";import{createStarAudio as w}from"star-audio";import{game as P}from"star-canvas";import{createLeaderboard as v}from"star-leaderboard";import{createMultiplayer as F}from"star-multiplayer";var n=null,s=null,o=null,l=()=>typeof window<"u",S=()=>typeof process<"u"&&process.versions?.node;function u(){if(l()||!S())return null;try{let e=p("fs"),a=p("path").join(process.cwd(),".starrc");if(!e.existsSync(a))return null;let d=e.readFileSync(a,"utf-8");return JSON.parse(d)}catch{return null}}function r(){return!s&&l()&&(s=m()),s}var g="https://buildwithstar.com";function i(){if(!o){let e=n?.gameId,t=n?.apiBase??g;if(!e){let a=u();a?.gameId&&(e=a.gameId)}o=c({gameId:e,apiBase:t})}return o}var b={init(e){n=e,o&&(o.destroy?.(),o=null)},audio:{play:(e,t)=>r().play(e,t),preload:e=>r().preload(e),music:{crossfadeTo:(e,t)=>r().music.crossfadeTo(e,t),stop:e=>r().music.stop(e)},setMusicVolume:e=>r().setMusicVolume(e),setSfxVolume:e=>r().setSfxVolume(e),setMute:e=>r().setMute(e),toggleMute:()=>r().toggleMute(),isMuted:()=>r().isMuted()},game(e,t){f(e,t)},leaderboard:{submit:e=>i().submit(e),show:()=>i().show(),getScores:e=>i().getScores(e),share:e=>i().share(e)},multiplayer:{async create(e){let t=y();return await t.start(e),t}},loadConfig:u,version:"0.1.0"},A=b;export{b as Star,w as audio,A as default,P as game,v as leaderboard,F as multiplayer};
package/package.json CHANGED
@@ -1,17 +1,25 @@
1
1
  {
2
2
  "name": "star-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
- "description": "Unified Star SDK for game development. Audio, canvas, leaderboards, multiplayer.",
5
+ "description": "Browser game SDK with built-in leaderboards (no backend needed) and mobile-safe audio. Works on iOS Safari. Perfect for AI-generated games.",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "bin": {
9
+ "star-sdk": "./dist/cli.mjs"
10
+ },
8
11
  "starSdk": {
9
12
  "internalImport": "import Star from '/star-sdk/v1/star.js';",
10
13
  "publicImport": "import Star from 'star-sdk';",
11
- "outputs": [{ "src": "dist/index.mjs", "dest": "v1/star.js" }],
14
+ "outputs": [
15
+ {
16
+ "src": "dist/index.mjs",
17
+ "dest": "v1/star.js"
18
+ }
19
+ ],
12
20
  "skill": {
13
21
  "name": "star-sdk",
14
- "description": "Unified Star SDK for game development. Audio, canvas, leaderboards, multiplayer."
22
+ "description": "Browser game SDK with built-in leaderboards (no backend needed) and mobile-safe audio. Works on iOS Safari. Perfect for AI-generated games."
15
23
  }
16
24
  },
17
25
  "exports": {
@@ -25,7 +33,8 @@
25
33
  "module": "./dist/index.mjs",
26
34
  "types": "./dist/index.d.ts",
27
35
  "files": [
28
- "dist"
36
+ "dist",
37
+ "skills"
29
38
  ],
30
39
  "scripts": {
31
40
  "build": "tsup",
@@ -38,14 +47,14 @@
38
47
  "web",
39
48
  "audio",
40
49
  "canvas",
41
- "leaderboard",
42
- "multiplayer"
50
+ "leaderboard"
43
51
  ],
44
52
  "dependencies": {
45
53
  "star-audio": "*",
46
54
  "star-canvas": "*",
47
55
  "star-leaderboard": "*",
48
- "star-multiplayer": "*"
56
+ "star-multiplayer": "*",
57
+ "star-sdk-cli": "*"
49
58
  },
50
59
  "devDependencies": {
51
60
  "tsup": "^8.0.0",
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: star-sdk
3
+ description: Use when you need a game that works on mobile without audio issues, has leaderboards without building a backend, or needs canvas rendering without DPR/sizing bugs. Handles iOS Safari audio unlocking, DPR scaling, and coordinate conversion automatically.
4
+ ---
5
+
6
+ # Star SDK
7
+
8
+ Browser game development SDK with audio, canvas, and leaderboards.
9
+
10
+ ## Import
11
+
12
+ ```javascript
13
+ import Star from 'star-sdk';
14
+ ```
15
+
16
+ **CRITICAL:** Always use `import Star from 'star-sdk'` - not destructured imports.
17
+
18
+ ## API Overview
19
+
20
+ | API | Use When | Docs |
21
+ |-----|----------|------|
22
+ | `Star.game()` | Game loop, canvas, UI, input | [canvas.md](./canvas.md) |
23
+ | `Star.audio` | Sound effects, music | [audio.md](./audio.md) |
24
+ | `Star.leaderboard` | Scores, rankings | [leaderboard.md](./leaderboard.md) |
25
+
26
+ ## Quick Start
27
+
28
+ ```javascript
29
+ import Star from 'star-sdk';
30
+
31
+ Star.game(ctx => {
32
+ const { canvas, width, height, ctx: c } = ctx;
33
+ let score = 0;
34
+
35
+ // Preload audio
36
+ Star.audio.preload({ coin: 'coin', jump: 'jump' });
37
+
38
+ // Game loop
39
+ ctx.loop((dt) => {
40
+ c.fillStyle = '#111827';
41
+ c.fillRect(0, 0, width, height);
42
+ c.fillStyle = '#fff';
43
+ c.font = '24px sans-serif';
44
+ c.fillText(`Score: ${score}`, 20, 40);
45
+ });
46
+
47
+ // Input
48
+ canvas.onclick = () => {
49
+ score += 10;
50
+ Star.audio.play('coin');
51
+ };
52
+ });
53
+ ```
54
+
55
+ ## Initialization (Required for Leaderboards)
56
+
57
+ If using leaderboards outside the Star platform (local dev, self-hosted), initialize with your game ID:
58
+
59
+ ```javascript
60
+ import Star from 'star-sdk';
61
+
62
+ // Get your gameId from .starrc (created by: npx star-sdk init "Game Name")
63
+ Star.init({ gameId: 'your-game-id-here' });
64
+ ```
65
+
66
+ ## Common Patterns
67
+
68
+ ### Game Over -> Submit Score -> Show Leaderboard
69
+
70
+ ```javascript
71
+ function gameOver(finalScore) {
72
+ Star.leaderboard.submit(finalScore);
73
+ Star.leaderboard.show();
74
+ }
75
+ ```
76
+
77
+ ### Audio (It Just Works)
78
+
79
+ Star.audio handles mobile audio unlocking automatically. Just call `play()` - no special handling needed.
80
+
81
+ ```javascript
82
+ Star.audio.preload({ coin: 'coin', jump: 'jump' });
83
+ Star.audio.play('coin'); // Works on mobile, desktop, everywhere
84
+ ```
85
+
86
+ ### Coordinate Handling
87
+
88
+ ```javascript
89
+ canvas.onclick = (e) => {
90
+ const point = ctx.toStagePoint(e); // Correct coordinates
91
+ console.log(point.x, point.y);
92
+ };
93
+ ```
94
+
95
+ ## Don't Do This
96
+
97
+ - **Don't** create canvas manually - use `Star.game()`
98
+ - **Don't** use `setInterval` for game loops - use `ctx.loop()`
99
+ - **Don't** destructure Star - use `Star.audio`, `Star.leaderboard`, etc.
100
+ - **Don't** invent audio preset names - only 17 exist (see audio.md)
101
+
102
+ ## Audio Presets (Full List)
103
+
104
+ Only these 17 presets exist:
105
+ - UI: `beep`, `click`, `select`, `error`, `success`
106
+ - Actions: `jump`, `swoosh`, `shoot`, `laser`, `explosion`
107
+ - Combat: `hit`, `hurt`
108
+ - Collection: `coin`, `pickup`, `bonus`, `unlock`, `powerup`
109
+
110
+ ## Full Game Example
111
+
112
+ ```javascript
113
+ import Star from 'star-sdk';
114
+
115
+ // Initialize for leaderboard support (get gameId from .starrc)
116
+ Star.init({ gameId: 'your-game-id' });
117
+
118
+ Star.game(ctx => {
119
+ const { canvas, width, height, ctx: c } = ctx;
120
+ let score = 0;
121
+ let gameOver = false;
122
+ let playerY = height / 2;
123
+ let obstacles = [];
124
+
125
+ Star.audio.preload({
126
+ jump: 'jump',
127
+ coin: 'coin',
128
+ hurt: 'hurt'
129
+ });
130
+
131
+ // Spawn obstacles
132
+ setInterval(() => {
133
+ if (!gameOver) {
134
+ obstacles.push({ x: width, y: Math.random() * height, passed: false });
135
+ }
136
+ }, 2000);
137
+
138
+ ctx.loop((dt) => {
139
+ if (gameOver) return;
140
+
141
+ // Clear
142
+ c.fillStyle = '#111827';
143
+ c.fillRect(0, 0, width, height);
144
+
145
+ // Update obstacles
146
+ obstacles.forEach(obs => {
147
+ obs.x -= 200 * dt;
148
+
149
+ // Score when passed
150
+ if (!obs.passed && obs.x < 50) {
151
+ obs.passed = true;
152
+ score += 10;
153
+ Star.audio.play('coin');
154
+ }
155
+
156
+ // Collision
157
+ if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
158
+ gameOver = true;
159
+ Star.audio.play('hurt');
160
+ Star.leaderboard.submit(score);
161
+ Star.leaderboard.show();
162
+ }
163
+ });
164
+
165
+ // Remove off-screen
166
+ obstacles = obstacles.filter(o => o.x > -20);
167
+
168
+ // Draw player
169
+ c.fillStyle = '#3b82f6';
170
+ c.beginPath();
171
+ c.arc(50, playerY, 15, 0, Math.PI * 2);
172
+ c.fill();
173
+
174
+ // Draw obstacles
175
+ c.fillStyle = '#a855f7';
176
+ obstacles.forEach(obs => {
177
+ c.fillRect(obs.x - 10, obs.y - 25, 20, 50);
178
+ });
179
+
180
+ // Draw score
181
+ c.fillStyle = '#fff';
182
+ c.font = '24px sans-serif';
183
+ c.fillText(`Score: ${score}`, 20, 40);
184
+ });
185
+
186
+ // Jump on click/tap
187
+ canvas.onclick = () => {
188
+ if (!gameOver) {
189
+ playerY -= 50;
190
+ Star.audio.play('jump');
191
+ }
192
+ };
193
+ });
194
+ ```
195
+
196
+ For detailed API documentation, see the linked files above.
@@ -0,0 +1,119 @@
1
+ **Installation**
2
+
3
+ First, add the package to your project:
4
+ ` ` `bash
5
+ yarn add star-audio
6
+ ` ` `
7
+
8
+ ### Star Audio SDK
9
+
10
+ **Mobile-first, bulletproof audio for web games.** Works on iOS/Android out of the box. Missing files won't crash your game.
11
+
12
+ **Import:**
13
+ ` ` `javascript
14
+ import createAudio from 'star-audio';
15
+ const audio = createAudio();
16
+ ` ` `
17
+
18
+ **CRITICAL:** Import in JavaScript - don't add `<script src="/star-sdk/audio.js">` tags.
19
+
20
+ **Why Star Audio?**
21
+ - ✅ **Mobile-first** - Works on iOS/Android, handles audio unlock automatically, plays even in silent mode
22
+ - ✅ **Never throws** - Missing audio files? Game keeps running with clear warnings
23
+ - ✅ **Zero-config** - Works on first play, no setup needed
24
+ - ✅ **No try/catch needed** - Fire-and-forget API, perfect for AI-generated games
25
+
26
+ ---
27
+
28
+ **Quick Start:**
29
+
30
+ ` ` `javascript
31
+ const audio = createAudio();
32
+
33
+ // CRITICAL: Preload presets before playing them
34
+ // You can set per-sound volumes in preload
35
+ audio.preload({
36
+ jump: { synth: 'jump', volume: 0.5 }, // Quieter jump
37
+ shoot: { synth: 'shoot', volume: 0.8 }, // Loud shoot
38
+ coin: { synth: 'coin', volume: 0.6 },
39
+ explosion: { synth: 'explosion', volume: 1.0 }
40
+ });
41
+
42
+ // Now play them - volumes are already set
43
+ audio.play('jump');
44
+ audio.play('shoot');
45
+ ` ` `
46
+
47
+ **ONLY THESE 17 PRESETS EXIST - DO NOT INVENT NAMES:**
48
+ - UI: `beep`, `click`, `select`, `error`, `success`
49
+ - Actions: `jump`, `swoosh`, `shoot`, `laser`, `explosion`
50
+ - Combat: `hit`, `hurt`
51
+ - Collection: `coin`, `pickup`, `bonus`, `unlock`, `powerup`
52
+
53
+ **If you need a sound that's not in this list, use custom synth or generate audio.**
54
+
55
+ **CRITICAL:** Preload all presets before use. Set volumes in preload:
56
+ ` ` `javascript
57
+ audio.preload({
58
+ jump: { synth: 'jump', volume: 0.5 }, // With custom volume
59
+ coin: 'coin', // Default volume
60
+ explosion: 'explosion' // For crashes/impacts
61
+ });
62
+ ` ` `
63
+
64
+ ---
65
+
66
+ **Custom synth (advanced):**
67
+
68
+ ` ` `javascript
69
+ audio.preload({
70
+ 'sfx.charge': {
71
+ waveform: 'triangle',
72
+ frequency: [200, 300, 450, 650, 900], // Rising charge-up
73
+ duration: 0.40,
74
+ volume: 0.38
75
+ }
76
+ });
77
+ ` ` `
78
+
79
+ **Make sounds feel good:**
80
+ - **Use frequency arrays** - Sweeps/arpeggios are more satisfying than single tones
81
+ - **Rising = positive** - Ascending pitches for rewards (coin, jump, powerup)
82
+ - **Descending = impact** - Falling pitches for actions (shoot, hurt, explosion)
83
+ - **More notes = richer** - 3-6 frequencies sound fuller than 1-2
84
+ - **Musical intervals** - Use harmonious ratios (octaves, fifths, major chords)
85
+
86
+ **Waveform choice:**
87
+ - `sine` - Pure, pleasant (UI, bells, rewards)
88
+ - `triangle` - Warm, full (jumps, explosions, success)
89
+ - `square` - Retro, characterful (powerups, beeps, chiptune)
90
+ - `sawtooth` - Harsh, aggressive (lasers, damage, errors)
91
+
92
+ **Frequency guide:**
93
+ - High (800-2000 Hz): Bright, attention-grabbing (UI, coins)
94
+ - Mid (200-800 Hz): Game actions (jumps, shoots)
95
+ - Low (30-200 Hz): Impacts, bass (explosions, rumbles)
96
+ - Arrays: 3-4 notes for melodies, 6+ for noise-like effects
97
+
98
+ ---
99
+
100
+ **Audio files:**
101
+
102
+ ` ` `javascript
103
+ audio.preload({
104
+ 'sfx.boom': 'assets/boom.mp3',
105
+ 'bgm.theme': 'assets/music.mp3'
106
+ });
107
+ audio.play('sfx.boom');
108
+ audio.music.crossfadeTo('bgm.theme', { duration: 1.5 });
109
+ ` ` `
110
+
111
+ ---
112
+
113
+ **Controls:**
114
+
115
+ ` ` `javascript
116
+ audio.setMusicVolume(0.5);
117
+ audio.setSfxVolume(0.8);
118
+ audio.toggleMute();
119
+ ` ` `
@@ -0,0 +1,611 @@
1
+ **Installation**
2
+
3
+ First, add the package to your project:
4
+
5
+ ```bash
6
+ yarn add star-dom
7
+ ```
8
+
9
+ ### Star DOM SDK
10
+
11
+ Use the **Star DOM SDK** to initialize games reliably.
12
+ It prevents the most common bugs:
13
+
14
+ - ✅ No "cannot read addEventListener of null"
15
+ - ✅ No canvas sizing/DPR/blur issues
16
+ - ✅ No accidentally wiping the canvas with `innerHTML`
17
+ - ✅ Games work identically on ALL devices (fixed 16:9 with letterboxing)
18
+
19
+ -----
20
+
21
+ ### Fixed 16:9 Resolution
22
+
23
+ **Default: 640×360 (landscape) or 360×640 (portrait).** Games work identically on every device.
24
+
25
+ The SDK uses letterboxing to maintain the exact game area. This means:
26
+ - Positions like `x: 320, y: 180` always mean the exact center
27
+ - Two objects at `x: 100` and `x: 540` are always the same distance apart
28
+ - No "works on my screen, breaks on mobile" bugs
29
+
30
+ ```ts
31
+ // These values work identically on ALL devices:
32
+ const player = { x: 320, y: 300 }; // Center-bottom area
33
+ const enemy = { x: 600, y: 50 }; // Top-right area
34
+ const playerSize = 32; // Always 32px
35
+ const speed = 200; // Always 200px/sec
36
+ ```
37
+
38
+ -----
39
+
40
+ ### Golden Path (How to Use)
41
+
42
+ Import `game` and wrap your code in it. The `game` function handles DOM readiness, creates a canvas and a UI overlay, and gives you a safe context to build.
43
+
44
+ ```ts
45
+ import { game } from 'star-canvas';
46
+
47
+ game(({ ctx, width, height, on, loop, ui, canvas }) => {
48
+ // ctx: The 2D canvas context
49
+ // width, height: The logical size (CSS pixels) - READ-ONLY
50
+ // on: Safe, delegated event listener
51
+ // loop: Stable game loop (with dt)
52
+ // ui: Safe overlay for HTML
53
+ // canvas: The <canvas> element
54
+
55
+ // 1. Draw on the canvas
56
+ loop((dt) => {
57
+ ctx.clearRect(0, 0, width, height);
58
+ ctx.fillStyle = '#22d3ee'; // cyan-400
59
+ ctx.fillRect(width / 2 - 25, height / 2 - 25, 50, 50);
60
+ });
61
+
62
+ // 2. Render HTML to the safe UI overlay
63
+ // UI is interactive by default (scroll, buttons work)
64
+ // Adding canvas.addEventListener makes UI click-through automatically
65
+ ui.render(`
66
+ <div class="absolute top-4 left-4 text-white">
67
+ <button id="start-btn" class="px-4 py-2 bg-blue-500 rounded pointer-events-auto">
68
+ Click Me
69
+ </button>
70
+ </div>
71
+ `);
72
+
73
+ // 3. Listen for button clicks
74
+ on('click', '#start-btn', () => {
75
+ console.log('Button clicked!');
76
+ });
77
+
78
+ // 4. For canvas games: listen for taps on canvas
79
+ // This automatically makes UI click-through (taps pass through to canvas)
80
+ // Buttons with pointer-events-auto still work
81
+ canvas.addEventListener('pointerdown', (e) => {
82
+ console.log('Canvas/screen tapped!', e);
83
+ });
84
+ });
85
+ ```
86
+
87
+ > **CRITICAL:** Always import the SDK in your JavaScript/TypeScript.
88
+ > **Do not** add a `<script src="/star-sdk/dom.js">` tag in HTML.
89
+ >
90
+ > **Recommended Import:**
91
+ >
92
+ > ```ts
93
+ > import { game } from 'star-canvas';
94
+ > ```
95
+
96
+ -----
97
+
98
+ ## Core API: `game(setup, options?)`
99
+
100
+ The `setup` function receives one argument: a `GameContext` object with the following properties:
101
+
102
+ ### `ctx: CanvasRenderingContext2D`
103
+
104
+ The 2D drawing context. Its transform is already scaled for DPR. You **always draw in logical CSS pixels**.
105
+
106
+ ### `canvas: HTMLCanvasElement`
107
+
108
+ The `<canvas>` element itself.
109
+
110
+ - **Use this for gameplay input listeners** (e.g., `pointerdown`, `pointermove`).
111
+
112
+ ### `width: number` (getter)
113
+
114
+ ### `height: number` (getter)
115
+
116
+ The logical CSS pixel width and height of the stage. **Use these for all game logic and drawing.** They are getters, so they are always up-to-date.
117
+
118
+ ### `on(type, selector, handler, options?)`
119
+
120
+ Attaches a **delegated event listener** to the document.
121
+
122
+ - ✅ **Use this for UI elements** (buttons, menus) inside your `ui.render()` HTML.
123
+ - ✅ Survives `ui.render()` calls.
124
+ - Returns an `off()` function to unsubscribe.
125
+
126
+ ### `loop(tick)`
127
+
128
+ Starts a `requestAnimationFrame` loop.
129
+
130
+ - `tick` function receives `(dt, now)`, where `dt` is **delta time in seconds**.
131
+ - **ALWAYS** multiply movement by `dt` (e.g., `player.x += speed * dt`).
132
+ - Returns `{ start(), stop(), running }`. The loop starts automatically.
133
+
134
+ ### `ui: GameUI`
135
+
136
+ A safe manager for your HTML overlay, stacked on top of the canvas.
137
+
138
+ - `ui.root`: The `<div>` element for your UI. It is **interactive by default** (standard HTML behavior - scroll, buttons work).
139
+ - `ui.render(html: string)`: **Use this** to set your UI. It's safe and won't destroy the canvas.
140
+ - Automatically skips updates if HTML is unchanged (safe to call in loop for static content)
141
+ - For best performance with dynamic content (score), only call when values actually change
142
+ - `ui.el(selector)`: Scoped `querySelector` for the UI root.
143
+ - `ui.all(selector)`: Scoped `querySelectorAll` for the UI root.
144
+
145
+ **Auto-detection:** When you add `canvas.addEventListener('pointerdown', ...)`, the SDK automatically makes UI click-through so taps reach the canvas. Buttons with `pointer-events-auto` still work.
146
+
147
+ ### Cursor Management
148
+
149
+ **CRITICAL:** Choose cursor based on how players interact. Update cursor when state changes (e.g., menu → playing → gameover).
150
+
151
+ ```ts
152
+ // MOUSE-BASED GAMES (click/point-and-click/puzzle/clicker/strategy/constellation)
153
+ if (state === 'playing') canvas.style.cursor = 'pointer'; // Show where to click
154
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'pointer'; // Keep visible
155
+
156
+ // PRECISION AIMING (shooter/drawing/building)
157
+ if (state === 'playing') canvas.style.cursor = 'crosshair';
158
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto';
159
+
160
+ // KEYBOARD/TOUCH ONLY (platformer/WASD/rhythm/endless runner)
161
+ if (state === 'playing') canvas.style.cursor = 'none'; // Hide (doesn't matter)
162
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto'; // Show for menus!
163
+ ```
164
+
165
+ **Decision:** Does player click on game objects? → `'pointer'` | Aim precisely? → `'crosshair'` | WASD/touch only? → `'none'` during play, `'auto'` for menus
166
+
167
+ ### `toStagePoint(event)`
168
+
169
+ Converts `MouseEvent` or `PointerEvent` client coordinates to the stage's logical coordinates.
170
+
171
+ - **USE THIS** for all canvas pointer input.
172
+
173
+ ### `createDrag()`
174
+
175
+ Creates a drag state helper that handles coordinate conversion and offset tracking automatically.
176
+
177
+ ```ts
178
+ const drag = createDrag();
179
+
180
+ canvas.addEventListener('pointerdown', (e) => {
181
+ canvas.setPointerCapture(e.pointerId); // IMPORTANT: Capture for reliable drags
182
+ const { x, y } = drag.point(e); // Convert coordinates
183
+ const hit = pieces.find(p => /* hit test */);
184
+ if (hit) drag.grab(e, hit); // Start drag with offset
185
+ });
186
+
187
+ canvas.addEventListener('pointermove', (e) => drag.move(e)); // Updates position
188
+ canvas.addEventListener('pointerup', () => {
189
+ const dropped = drag.release(); // Returns dropped object (or null)
190
+ });
191
+ ```
192
+
193
+ **API:**
194
+ - `point(e)` - Pure coordinate conversion, no side effects
195
+ - `grab(e, obj)` - Start dragging an object, computing offset from cursor
196
+ - `move(e)` - Update dragged object's position
197
+ - `release()` - End drag, returns dropped object or null
198
+ - `dragging` - The currently dragged object (or null)
199
+
200
+ ### `GameOptions` (optional)
201
+
202
+ Pass an options object as the second argument to `game()`:
203
+
204
+ - `preset?: 'landscape' | 'portrait' | 'responsive'`: Game orientation preset.
205
+ - `'landscape'` (default): 640×360 - for platformers, shooters, racing
206
+ - `'portrait'`: 360×640 - for puzzle, cards, match-3, mobile-style
207
+ - `'responsive'`: Fills container, no fixed dimensions (legacy - gameplay varies by device)
208
+ - `width?: number`: Override width (default: 640 for landscape, 360 for portrait)
209
+ - `height?: number`: Override height (default: 360 for landscape, 640 for portrait)
210
+ - `fit?: 'contain' | 'cover' | 'stretch'`: How game fits container (default: `'contain'` with letterboxing)
211
+ - `pixelRatio?: 'device' | number`: (default: `'device'`)
212
+ - `maxPixelRatio?: number`: (default: `2`)
213
+ - `preventContextMenu?: boolean`: Prevent right-click context menu on canvas (default: `true`)
214
+
215
+ **Default behavior:** Fixed 640×360 (16:9) with letterboxing. Games work identically on all devices.
216
+
217
+ -----
218
+
219
+ ## Recipes
220
+
221
+ ### Recipe 1: UI-Only Game (e.g., Clicker)
222
+
223
+ Use `game`, `on`, and `ui`.
224
+
225
+ ```ts
226
+ import { game } from 'star-canvas';
227
+
228
+ game(({ on, ui }) => {
229
+ let score = 0;
230
+
231
+ function render() {
232
+ // UI is interactive by default - buttons, scroll, forms all work
233
+ ui.render(`
234
+ <div class="min-h-[100dvh] grid place-items-center bg-purple-900 text-white">
235
+ <div class="text-center space-y-4">
236
+ <h1 class="text-4xl font-bold">Score: \${score}</h1>
237
+ <button id="clickBtn" class="px-8 py-4 rounded-xl bg-cyan-400 text-slate-900 font-bold">
238
+ Click Me!
239
+ </button>
240
+ </div>
241
+ </div>
242
+ `);
243
+ }
244
+
245
+ // Button clicks work by default
246
+ on('click', '#clickBtn', () => {
247
+ score++;
248
+ render();
249
+ });
250
+
251
+ render();
252
+ });
253
+ ```
254
+
255
+ ### Recipe 2: Canvas Game (Landscape)
256
+
257
+ Default pattern - fixed 640×360 resolution. Games work identically on all devices.
258
+
259
+ ```ts
260
+ import { game } from 'star-canvas';
261
+
262
+ game(({ ctx, width, height, loop }) => {
263
+ // width = 640, height = 360 (always, with letterboxing)
264
+ const playerSize = 32;
265
+ const speed = 200; // 200px per second
266
+
267
+ const player = { x: 64, y: 180 }; // Fixed positions work everywhere
268
+
269
+ loop((dt) => {
270
+ player.x += speed * dt;
271
+ if (player.x > width) player.x = -playerSize;
272
+
273
+ ctx.fillStyle = '#0f172a';
274
+ ctx.fillRect(0, 0, width, height);
275
+
276
+ ctx.fillStyle = '#22d3ee';
277
+ ctx.fillRect(player.x, player.y - playerSize/2, playerSize, playerSize);
278
+ });
279
+ });
280
+ // Default: 640×360 landscape with letterboxing
281
+ ```
282
+
283
+ ### Recipe 3: Canvas Game (Portrait)
284
+
285
+ For puzzle games, card games, match-3, mobile-style games - use portrait preset.
286
+
287
+ ```ts
288
+ import { game } from 'star-canvas';
289
+
290
+ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
291
+ // width = 360, height = 640 (always, with letterboxing)
292
+ const cellSize = 40;
293
+ const gridCols = 8;
294
+ const gridRows = 12;
295
+
296
+ // Center the grid
297
+ const gridWidth = gridCols * cellSize;
298
+ const gridX = (width - gridWidth) / 2;
299
+ const gridY = 80;
300
+
301
+ canvas.addEventListener('pointerdown', (e) => {
302
+ const { x, y } = toStagePoint(e);
303
+ // Handle tap on grid...
304
+ });
305
+
306
+ loop((dt) => {
307
+ ctx.fillStyle = '#1e1b4b';
308
+ ctx.fillRect(0, 0, width, height);
309
+
310
+ ctx.strokeStyle = '#4338ca';
311
+ for (let row = 0; row < gridRows; row++) {
312
+ for (let col = 0; col < gridCols; col++) {
313
+ ctx.strokeRect(
314
+ gridX + col * cellSize,
315
+ gridY + row * cellSize,
316
+ cellSize, cellSize
317
+ );
318
+ }
319
+ }
320
+ });
321
+ }, { preset: 'portrait' }); // 360×640 portrait with letterboxing
322
+ ```
323
+
324
+ ### Recipe 4: Custom Resolution
325
+
326
+ For games that need different dimensions (e.g., pixel art at 320×180).
327
+
328
+ ```ts
329
+ import { game } from 'star-canvas';
330
+
331
+ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
332
+ // Custom 320×180 resolution (retro pixel art style)
333
+ const player = { x: 160, y: 90 }; // Center
334
+
335
+ canvas.addEventListener('pointerdown', (e) => {
336
+ const { x, y } = toStagePoint(e);
337
+ console.log('Tapped at:', x, y); // Always 0-320, 0-180
338
+ });
339
+
340
+ loop((dt) => {
341
+ ctx.fillStyle = '#0f172a';
342
+ ctx.fillRect(0, 0, width, height);
343
+
344
+ ctx.fillStyle = '#22d3ee';
345
+ ctx.fillRect(player.x - 8, player.y - 8, 16, 16);
346
+ });
347
+ }, { width: 320, height: 180 }); // Custom resolution with letterboxing
348
+ ```
349
+
350
+ ### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
351
+
352
+ ```ts
353
+ import { game } from 'star-canvas';
354
+ import { createLeaderboard } from '/star-sdk/v1/leaderboard.js';
355
+
356
+ const leaderboard = createLeaderboard();
357
+
358
+ game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
359
+ let score = 0;
360
+ let state = 'menu';
361
+
362
+ function handleTap() {
363
+ if (state === 'menu' || state === 'gameover') {
364
+ startGame();
365
+ } else if (state === 'playing') {
366
+ // ... (player float logic) ...
367
+ }
368
+ }
369
+
370
+ // 1. Listen for screen taps - this makes UI click-through automatically
371
+ canvas.addEventListener('pointerdown', handleTap);
372
+
373
+ // 2. Listen for button clicks - buttons need pointer-events-auto
374
+ on('click', '#leaderboard-btn', (e) => {
375
+ e.stopPropagation();
376
+ leaderboard.show();
377
+ });
378
+
379
+ // 3. Render UI - buttons need pointer-events-auto to intercept clicks
380
+ let lastState = null;
381
+ let lastScore = -1;
382
+
383
+ function updateUI() {
384
+ // CRITICAL: Only render when state/score changes, NOT every frame
385
+ // Calling ui.render() in the loop breaks buttons (DOM recreation)
386
+ if (state === lastState && score === lastScore) return;
387
+ lastState = state;
388
+ lastScore = score;
389
+
390
+ if (state === 'menu') {
391
+ ui.render(`
392
+ <div class="h-full flex flex-col items-center justify-center text-white">
393
+ <h1 class="text-6xl font-bold mb-4">FLOW</h1>
394
+ <div class="text-2xl animate-pulse">TAP TO START</div>
395
+ </div>`);
396
+ } else if (state === 'playing') {
397
+ ui.render(`
398
+ <div class="absolute top-8 left-1/2 -translate-x-1/2 text-white">
399
+ <div class="text-5xl font-bold">\${score}</div>
400
+ </div>`);
401
+ } else if (state === 'gameover') {
402
+ ui.render(`
403
+ <div class="h-full flex flex-col items-center justify-center text-white">
404
+ <div class="text-3xl mb-4">GAME OVER</div>
405
+ <div class="text-6xl mb-4">\${score}</div>
406
+ <button id="leaderboard-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
407
+ VIEW LEADERBOARD
408
+ </button>
409
+ <div class="text-xl animate-pulse">TAP TO RESTART</div>
410
+ </div>`);
411
+ }
412
+ }
413
+
414
+ // 4. Call updateUI when state changes (NOT every frame)
415
+ updateUI();
416
+
417
+ // Update when state transitions happen
418
+ function startGame() {
419
+ state = 'playing';
420
+ score = 0;
421
+ updateUI();
422
+ }
423
+
424
+ function endGame() {
425
+ state = 'gameover';
426
+ // Submit score to leaderboard
427
+ leaderboard.submit(score);
428
+ updateUI();
429
+ }
430
+ });
431
+ ```
432
+
433
+ ### Recipe 6: Safe Canvas Transforms (scoped)
434
+
435
+ When applying temporary transforms (translate, rotate, scale), use `scoped()` to automatically restore the context state:
436
+
437
+ ```ts
438
+ import { game } from 'star-canvas';
439
+
440
+ game(({ ctx, scoped, loop }) => {
441
+ const cards = [
442
+ { x: 100, y: 100, angle: 0.1, visible: true },
443
+ { x: 200, y: 150, angle: -0.2, visible: true },
444
+ ];
445
+
446
+ function drawCard(card) {
447
+ scoped(() => {
448
+ ctx.translate(card.x, card.y);
449
+ ctx.rotate(card.angle);
450
+ if (!card.visible) return; // Safe! restore() still happens
451
+ ctx.fillStyle = '#3b82f6';
452
+ ctx.fillRect(-40, -60, 80, 120);
453
+ });
454
+ }
455
+
456
+ loop(() => {
457
+ ctx.clearRect(0, 0, 800, 600);
458
+ cards.forEach(drawCard);
459
+ });
460
+ });
461
+ ```
462
+
463
+ **Why use `scoped()`:** Prevents transform stack corruption from early returns, exceptions, or forgetting `ctx.restore()`. The context is always restored, even if the function exits early.
464
+
465
+ ### Recipe 7: Drag and Drop with createDrag() (RECOMMENDED)
466
+
467
+ Use the `createDrag()` helper - it handles coordinate conversion and offset tracking automatically.
468
+
469
+ ```ts
470
+ import { game } from 'star-canvas';
471
+
472
+ game(({ ctx, width, height, loop, canvas, createDrag }) => {
473
+ // Size relative to height for consistency
474
+ const pieceSize = height * 0.15;
475
+
476
+ const pieces = [
477
+ { x: width * 0.2, y: height * 0.3, color: '#ef4444' },
478
+ { x: width * 0.4, y: height * 0.4, color: '#22c55e' },
479
+ { x: width * 0.6, y: height * 0.3, color: '#3b82f6' },
480
+ ];
481
+
482
+ // Create drag helper - handles coordinate conversion automatically
483
+ const drag = createDrag();
484
+
485
+ function hitTest(x, y) {
486
+ for (let i = pieces.length - 1; i >= 0; i--) {
487
+ const p = pieces[i];
488
+ if (x >= p.x && x < p.x + pieceSize && y >= p.y && y < p.y + pieceSize) {
489
+ return p;
490
+ }
491
+ }
492
+ return null;
493
+ }
494
+
495
+ canvas.addEventListener('pointerdown', (e) => {
496
+ canvas.setPointerCapture(e.pointerId); // IMPORTANT: Ensures drag works outside canvas
497
+ const { x, y } = drag.point(e); // Convert coordinates
498
+ const hit = hitTest(x, y);
499
+ if (hit) {
500
+ drag.grab(e, hit); // Start drag with offset from cursor
501
+ canvas.style.cursor = 'grabbing';
502
+ }
503
+ });
504
+
505
+ canvas.addEventListener('pointermove', (e) => {
506
+ drag.move(e); // Updates grabbed object position
507
+ if (!drag.dragging) {
508
+ const { x, y } = drag.point(e);
509
+ canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
510
+ }
511
+ });
512
+
513
+ canvas.addEventListener('pointerup', () => {
514
+ const dropped = drag.release(); // Returns dropped object (or null)
515
+ if (dropped) {
516
+ console.log('Dropped:', dropped);
517
+ }
518
+ canvas.style.cursor = 'default';
519
+ });
520
+
521
+ loop(() => {
522
+ ctx.fillStyle = '#1e293b';
523
+ ctx.fillRect(0, 0, width, height);
524
+
525
+ for (const p of pieces) {
526
+ ctx.fillStyle = drag.dragging === p ? '#fbbf24' : p.color;
527
+ ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
528
+ }
529
+ });
530
+ });
531
+ ```
532
+
533
+ **CRITICAL: Always use `setPointerCapture()`** - This ensures drags work even when the pointer moves outside the canvas. Without it, fast drags can leave objects stuck mid-drag.
534
+
535
+ ### Recipe 8: Drag and Drop (Manual Pattern)
536
+
537
+ If you need more control, here's the manual approach with `toStagePoint()`.
538
+
539
+ ```ts
540
+ import { game } from 'star-canvas';
541
+
542
+ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
543
+ const pieceSize = height * 0.15;
544
+ const pieces = [
545
+ { x: width * 0.2, y: height * 0.3, color: '#ef4444' },
546
+ { x: width * 0.4, y: height * 0.4, color: '#22c55e' },
547
+ ];
548
+
549
+ // Manual drag state
550
+ let dragging = null;
551
+ let dragOffsetX = 0;
552
+ let dragOffsetY = 0;
553
+
554
+ function hitTest(px, py) {
555
+ for (let i = pieces.length - 1; i >= 0; i--) {
556
+ const p = pieces[i];
557
+ if (px >= p.x && px < p.x + pieceSize && py >= p.y && py < p.y + pieceSize) {
558
+ return p;
559
+ }
560
+ }
561
+ return null;
562
+ }
563
+
564
+ canvas.addEventListener('pointerdown', (e) => {
565
+ canvas.setPointerCapture(e.pointerId); // Ensures drag works outside canvas
566
+ const { x, y } = toStagePoint(e); // CRITICAL: Convert coordinates!
567
+ const hit = hitTest(x, y);
568
+ if (hit) {
569
+ dragging = hit;
570
+ dragOffsetX = x - hit.x; // Store offset
571
+ dragOffsetY = y - hit.y;
572
+ canvas.style.cursor = 'grabbing';
573
+ }
574
+ });
575
+
576
+ canvas.addEventListener('pointermove', (e) => {
577
+ const { x, y } = toStagePoint(e); // CRITICAL: Convert here too!
578
+ if (dragging) {
579
+ dragging.x = x - dragOffsetX;
580
+ dragging.y = y - dragOffsetY;
581
+ } else {
582
+ canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
583
+ }
584
+ });
585
+
586
+ canvas.addEventListener('pointerup', () => {
587
+ dragging = null;
588
+ canvas.style.cursor = 'default';
589
+ });
590
+
591
+ loop(() => {
592
+ ctx.fillStyle = '#1e293b';
593
+ ctx.fillRect(0, 0, width, height);
594
+
595
+ for (const p of pieces) {
596
+ ctx.fillStyle = dragging === p ? '#fbbf24' : p.color;
597
+ ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
598
+ }
599
+ });
600
+ });
601
+ ```
602
+
603
+ **Common Drag-Drop Mistakes:**
604
+
605
+ 1. ❌ Forgetting `toStagePoint()` in pointermove → `createDrag()` fixes this
606
+ 2. ❌ No drag offset (piece "jumps" to cursor) → `createDrag()` fixes this
607
+ 3. ❌ Using `e.clientX/clientY` directly → `createDrag()` fixes this
608
+ 4. ❌ Not clearing state on pointerup → `createDrag()` fixes this
609
+ 5. ❌ Missing `setPointerCapture()` (drags break outside canvas) → **You must add this!**
610
+
611
+ **Recommendation:** Use `createDrag()` + `setPointerCapture()` for bulletproof drag-and-drop.
@@ -0,0 +1,258 @@
1
+ **Installation**
2
+
3
+ ```bash
4
+ yarn add star-leaderboard
5
+ ```
6
+
7
+ ### Star Leaderboard SDK
8
+
9
+ **Simple leaderboards for Star games.** Submit scores, show rankings, share results. Never crashes your game.
10
+
11
+ **Import:**
12
+ ```javascript
13
+ import { createLeaderboard } from 'star-leaderboard';
14
+ const leaderboard = createLeaderboard();
15
+ ```
16
+
17
+ **CRITICAL:** Import in JavaScript - don't add `<script>` tags.
18
+
19
+ ---
20
+
21
+ **Quick Start:**
22
+
23
+ ```javascript
24
+ import { createLeaderboard } from 'star-leaderboard';
25
+ import { game } from '/star-sdk/v1/dom.js';
26
+
27
+ const leaderboard = createLeaderboard();
28
+
29
+ game(({ ctx, width, height, loop, ui, on, canvas }) => {
30
+ let score = 0;
31
+ let state = 'playing';
32
+
33
+ function endGame() {
34
+ state = 'gameover';
35
+ // Submit score and show leaderboard
36
+ leaderboard.submit(score);
37
+ leaderboard.show();
38
+ }
39
+
40
+ // Game logic...
41
+ });
42
+ ```
43
+
44
+ **That's it!** The SDK handles:
45
+ - Score submission (works for guests and logged-in users)
46
+ - Platform leaderboard UI (modal with rankings)
47
+ - Weekly/all-time timeframes
48
+ - AI-detected scoring (score/time/moves - higher or lower is better)
49
+
50
+ ---
51
+
52
+ **API Reference:**
53
+
54
+ **Core Methods:**
55
+ ```javascript
56
+ leaderboard.submit(score) // Submit score, returns Promise<{ success, rank, scoreId }>
57
+ leaderboard.show() // Show platform leaderboard UI
58
+ leaderboard.getScores(options) // Fetch scores for custom UI
59
+ leaderboard.share(options) // Generate shareable link
60
+ ```
61
+
62
+ **Properties:**
63
+ ```javascript
64
+ leaderboard.ready // true when SDK is initialized
65
+ leaderboard.gameId // Current game ID (auto-detected on platform)
66
+ ```
67
+
68
+ **Aliases (for discoverability):**
69
+ ```javascript
70
+ leaderboard.submitScore(score) // Same as submit()
71
+ leaderboard.showLeaderboard() // Same as show()
72
+ ```
73
+
74
+ ---
75
+
76
+ **Patterns:**
77
+
78
+ ### Pattern 1: Submit and Show (Most Common)
79
+
80
+ ```javascript
81
+ import { createLeaderboard } from 'star-leaderboard';
82
+
83
+ const leaderboard = createLeaderboard();
84
+
85
+ function gameOver(finalScore) {
86
+ // Fire and forget - simplest approach
87
+ leaderboard.submit(finalScore);
88
+ leaderboard.show();
89
+ }
90
+ ```
91
+
92
+ ### Pattern 2: With Rank Feedback
93
+
94
+ ```javascript
95
+ import { createLeaderboard } from 'star-leaderboard';
96
+
97
+ const leaderboard = createLeaderboard();
98
+
99
+ async function gameOver(finalScore) {
100
+ const { success, rank } = await leaderboard.submit(finalScore);
101
+
102
+ if (success && rank) {
103
+ console.log(`You ranked #${rank}!`);
104
+ }
105
+
106
+ leaderboard.show();
107
+ }
108
+ ```
109
+
110
+ ### Pattern 3: Leaderboard Button
111
+
112
+ ```javascript
113
+ import { createLeaderboard } from 'star-leaderboard';
114
+ import { game } from '/star-sdk/v1/dom.js';
115
+
116
+ const leaderboard = createLeaderboard();
117
+
118
+ game(({ ui, on }) => {
119
+ ui.render(`
120
+ <button id="lb-btn" class="pointer-events-auto">
121
+ View Leaderboard
122
+ </button>
123
+ `);
124
+
125
+ on('click', '#lb-btn', (e) => {
126
+ e.stopPropagation();
127
+ leaderboard.show();
128
+ });
129
+ });
130
+ ```
131
+
132
+ ### Pattern 4: Custom Leaderboard UI
133
+
134
+ ```javascript
135
+ import { createLeaderboard } from 'star-leaderboard';
136
+
137
+ const leaderboard = createLeaderboard();
138
+
139
+ async function showCustomLeaderboard() {
140
+ const { scores, you, config } = await leaderboard.getScores({
141
+ timeframe: 'weekly', // or 'all_time'
142
+ limit: 10
143
+ });
144
+
145
+ // Render your own UI
146
+ scores.forEach(entry => {
147
+ console.log(`#${entry.rank} ${entry.playerName}: ${entry.score}`);
148
+ });
149
+
150
+ if (you) {
151
+ console.log(`Your rank: #${you.rank}`);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Pattern 5: Full Game Example
157
+
158
+ ```javascript
159
+ import { createLeaderboard } from 'star-leaderboard';
160
+ import { game } from '/star-sdk/v1/dom.js';
161
+
162
+ const leaderboard = createLeaderboard();
163
+
164
+ game(({ ctx, width, height, loop, ui, on, canvas }) => {
165
+ let score = 0;
166
+ let state = 'menu';
167
+
168
+ function startGame() {
169
+ state = 'playing';
170
+ score = 0;
171
+ updateUI();
172
+ }
173
+
174
+ function endGame() {
175
+ state = 'gameover';
176
+ leaderboard.submit(score);
177
+ updateUI();
178
+ }
179
+
180
+ // UI with leaderboard button
181
+ function updateUI() {
182
+ if (state === 'gameover') {
183
+ ui.render(`
184
+ <div class="h-full flex flex-col items-center justify-center text-white">
185
+ <div class="text-3xl mb-4">GAME OVER</div>
186
+ <div class="text-6xl mb-4">\${score}</div>
187
+ <button id="lb-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
188
+ VIEW LEADERBOARD
189
+ </button>
190
+ <div class="text-xl animate-pulse">TAP TO RESTART</div>
191
+ </div>
192
+ `);
193
+ }
194
+ }
195
+
196
+ on('click', '#lb-btn', (e) => {
197
+ e.stopPropagation();
198
+ leaderboard.show();
199
+ });
200
+
201
+ canvas.addEventListener('pointerdown', () => {
202
+ if (state === 'menu' || state === 'gameover') startGame();
203
+ });
204
+
205
+ loop((dt) => {
206
+ // Game logic...
207
+ });
208
+ });
209
+ ```
210
+
211
+ ---
212
+
213
+ **Options:**
214
+
215
+ ```javascript
216
+ // Default - auto-detects gameId on Star platform
217
+ const leaderboard = createLeaderboard();
218
+
219
+ // Standalone use (outside Star platform)
220
+ const leaderboard = createLeaderboard({
221
+ gameId: 'your-game-uuid',
222
+ apiBase: 'https://buildwithstar.com'
223
+ });
224
+ ```
225
+
226
+ ---
227
+
228
+ **getScores Options:**
229
+
230
+ ```javascript
231
+ const data = await leaderboard.getScores({
232
+ timeframe: 'weekly', // 'weekly' (default) or 'all_time'
233
+ limit: 10 // Number of scores (default: 10)
234
+ });
235
+
236
+ // Returns:
237
+ {
238
+ scores: [{ id, playerName, score, rank, submittedAt }],
239
+ config: { sort: 'DESC', valueType: 'score' },
240
+ timeframe: 'weekly',
241
+ you: { ... } | null, // Your score if outside top scores
242
+ weekResetTime: 1234567890 // Unix ms when weekly resets
243
+ }
244
+ ```
245
+
246
+ ---
247
+
248
+ **Tips:**
249
+
250
+ 1. **Call `submit()` before `show()`** - Ensures your score appears immediately in the leaderboard.
251
+
252
+ 2. **Fire and forget is fine** - `submit()` returns a Promise but you don't need to await it.
253
+
254
+ 3. **Use `show()` for platform UI** - It's the easiest way. Use `getScores()` only if you need custom rendering.
255
+
256
+ 4. **Don't store leaderboard state** - Just call the SDK methods when needed. The platform handles caching.
257
+
258
+ 5. **Works for guests** - Guests get a generated name like "Guest1234". They can sign in later to claim scores.