plinkit 1.0.0-dev.3 → 1.0.0
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 +69 -91
- package/dist/index.cjs +4 -4
- package/dist/index.d.ts +10 -32
- package/dist/index.es.js +1444 -1609
- package/dist/plinkit.umd.js +4 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# plinkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal WebGL library for Plinko-style games.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install plinkit
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { Plinkit } from
|
|
14
|
+
import { Plinkit } from 'plinkit'
|
|
15
15
|
|
|
16
|
-
const canvas = document.querySelector(
|
|
17
|
-
if (!canvas) throw new Error(
|
|
16
|
+
const canvas = document.querySelector('canvas')
|
|
17
|
+
if (!canvas) throw new Error('Canvas not found')
|
|
18
18
|
|
|
19
19
|
const game = new Plinkit({
|
|
20
20
|
canvas,
|
|
@@ -53,117 +53,95 @@ game.spawnBall()
|
|
|
53
53
|
|
|
54
54
|
## API
|
|
55
55
|
|
|
56
|
-
- `new Plinkit(options)` —
|
|
57
|
-
- `spawnBall()` —
|
|
58
|
-
- `resize()` —
|
|
59
|
-
- `destroy()` —
|
|
60
|
-
- `getState()` —
|
|
61
|
-
- `setBallCost(value)` —
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
### Звуки
|
|
74
|
-
|
|
75
|
-
Опциональный встроенный аудио-движок на Web Audio API. По умолчанию звук
|
|
76
|
-
**включён** (`muted: false`); UX-toggle делается через `setMuted(value)` и
|
|
77
|
-
`isMuted()`.
|
|
56
|
+
- `new Plinkit(options)` — creates a game instance.
|
|
57
|
+
- `spawnBall()` — adds a new ball and returns a `SpawnResult`.
|
|
58
|
+
- `resize()` — reads the parent width again and recalculates the viewport.
|
|
59
|
+
- `destroy()` — stops the loop and releases resources.
|
|
60
|
+
- `getState()` — returns `{ balance, ballCost }`.
|
|
61
|
+
- `setBallCost(value)` — changes the ball cost at runtime. Updates `getState().ballCost`
|
|
62
|
+
and synchronously calls `onBalanceChange`, so the UI can recalculate the button's
|
|
63
|
+
disabled state. Balls that are already in flight keep their original `wager`: payout
|
|
64
|
+
is calculated from the cost the ball was launched with. Throws `TypeError` if the
|
|
65
|
+
value is not a finite number or is negative.
|
|
66
|
+
- `onCollision(collision, state)` in `options` — a ball collision event for `peg`/`guide`.
|
|
67
|
+
Use it to integrate app-side external audio.
|
|
68
|
+
|
|
69
|
+
### External Audio Integration
|
|
70
|
+
|
|
71
|
+
Use any audio library, or wire up your own Web Audio/HTMLAudio layer in the host application.
|
|
78
72
|
|
|
79
73
|
```ts
|
|
74
|
+
interface SoundPlayer {
|
|
75
|
+
play(): void | Promise<void>
|
|
76
|
+
setVolume?(value: number): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pegHit: SoundPlayer = createSoundPlayer("/sounds/peg.mp3")
|
|
80
|
+
const bucketHit: SoundPlayer = createSoundPlayer("/sounds/bucket.mp3")
|
|
81
|
+
const uiTap: SoundPlayer = createSoundPlayer("/sounds/ui-tap.mp3")
|
|
82
|
+
|
|
83
|
+
// Optional: one-time audio initialization/unlock on the first user gesture
|
|
84
|
+
prepareAudioOnFirstGesture()
|
|
85
|
+
|
|
80
86
|
const game = new Plinkit({
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
masterVolume: 0.6,
|
|
87
|
+
// ...other options
|
|
88
|
+
onCollision: ({ speed }) => {
|
|
89
|
+
const hitVolume = Math.max(0.08, Math.min(0.45, speed / 12))
|
|
90
|
+
pegHit.setVolume?.(hitVolume)
|
|
91
|
+
pegHit.play()
|
|
87
92
|
},
|
|
93
|
+
onBallSettled: () => bucketHit.play(),
|
|
88
94
|
})
|
|
89
95
|
|
|
90
96
|
spawnButton.addEventListener("click", () => {
|
|
91
|
-
|
|
97
|
+
uiTap.play()
|
|
92
98
|
game.spawnBall()
|
|
93
99
|
})
|
|
94
|
-
|
|
95
|
-
soundToggle.addEventListener("click", () => {
|
|
96
|
-
game.setMuted(!game.isMuted())
|
|
97
|
-
})
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **Авто-разблокировка на мобильных.** `PlinkitAudio` ставит глобальные слушатели
|
|
111
|
-
`pointerdown` / `pointerup` / `touchstart` / `touchend` / `keydown` на `window`
|
|
112
|
-
с `capture: true`. На первом же жесте `AudioContext.resume()` снимает
|
|
113
|
-
suspended-state; слушатели сразу же снимаются. Дополнительно играется
|
|
114
|
-
silent-buffer ping (`(1, 1, sampleRate)` с gain `0.00001`), чтобы окончательно
|
|
115
|
-
«разбудить» iOS Web Audio — без этого первый реальный звук на iOS Safari
|
|
116
|
-
иногда не воспроизводится.
|
|
117
|
-
- **`visibilitychange`.** При возврате вкладки в видимое состояние контекст
|
|
118
|
-
повторно резюмится — iOS/Safari часто оставляет его `interrupted` после фона.
|
|
119
|
-
- **Анти-«пулемёт».** Лимит одновременных голосов (16) и лёгкая вариация
|
|
120
|
-
`playbackRate` (`±8%`) — чтобы частые peg-удары не сливались в монотонный шум.
|
|
121
|
-
- **До первого жеста** `playHit` тихо игнорируется. Это нормальное поведение
|
|
122
|
-
любой Web Audio игры на iOS.
|
|
123
|
-
|
|
124
|
-
### Экономика
|
|
125
|
-
|
|
126
|
-
- Значения передаются через `initialBalance`, `ballCost`, `multipliers`.
|
|
127
|
-
- При попадании в корзину начисляется `ballCost * multiplier`.
|
|
128
|
-
- Количество корзин = `multipliers.length` = `bottomPegCount - 1`.
|
|
129
|
-
- `multipliers` — это RTP-таблица и не должны меняться вместе с `ballCost`:
|
|
130
|
-
ставку масштабируйте через `setBallCost`, а множители оставляйте фиксированными.
|
|
131
|
-
- Для предсказуемого UX на лендинге держите одну ставку в пределах ~5% от банка
|
|
132
|
-
(`initialBalance`). Иначе один шарик с пиковым множителем (например `×7`) может
|
|
133
|
-
дать выплату, сравнимую со всем стартовым балансом.
|
|
102
|
+
### Economy
|
|
103
|
+
|
|
104
|
+
- Values are passed through `initialBalance`, `ballCost`, and `multipliers`.
|
|
105
|
+
- When a ball lands in a bucket, it awards `ballCost * multiplier`.
|
|
106
|
+
- The number of buckets = `multipliers.length` = `bottomPegCount - 1`.
|
|
107
|
+
- `multipliers` is the RTP table and should not be changed together with `ballCost`:
|
|
108
|
+
scale the wager through `setBallCost`, and keep the multipliers fixed.
|
|
109
|
+
- For predictable landing-page UX, keep a single wager within roughly 5% of the balance
|
|
110
|
+
(`initialBalance`). Otherwise, one ball with a peak multiplier (for example, `×7`) can
|
|
111
|
+
pay out an amount comparable to the entire starting balance.
|
|
134
112
|
|
|
135
113
|
### houseEdge
|
|
136
114
|
|
|
137
|
-
|
|
115
|
+
The `houseEdge` parameter (0..1) controls the probability of landing in edge buckets:
|
|
138
116
|
|
|
139
|
-
| houseEdge |
|
|
140
|
-
|
|
141
|
-
| 0 |
|
|
142
|
-
| 0.5 |
|
|
143
|
-
| 1.0 |
|
|
117
|
+
| houseEdge | Effect | RTP* |
|
|
118
|
+
|-----------|------------------|-------|
|
|
119
|
+
| 0 | Natural physics | ~120% |
|
|
120
|
+
| 0.5 | Stable balance | ~97% |
|
|
121
|
+
| 1.0 | Balance declines | ~73% |
|
|
144
122
|
|
|
145
|
-
*RTP (Return to Player)
|
|
123
|
+
*RTP (Return to Player) depends on the multipliers. Values are shown for `[7, 1.5, 0.5, 0.2, 0.1, 0, 0.1, 0.2, 0.5, 1.5, 7]`.
|
|
146
124
|
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
Internally, `houseEdge` controls three mechanisms: a centripetal micro-force,
|
|
126
|
+
velocity correction on peg bounce, and a Gaussian distribution for the spawn point.
|
|
149
127
|
|
|
150
|
-
>
|
|
151
|
-
> `bottomPegCount`, `radius`, `verticalStepRatio`, `sidePaddingPx`).
|
|
152
|
-
>
|
|
153
|
-
>
|
|
128
|
+
> **Important:** multipliers are calibrated for a specific board geometry (`topPegCount`,
|
|
129
|
+
> `bottomPegCount`, `radius`, `verticalStepRatio`, `sidePaddingPx`). Changing any of
|
|
130
|
+
> these parameters shifts the ball distribution and requires recalibrating the
|
|
131
|
+
> multipliers. Use `scripts/bench-distribution.mjs` for calibration.
|
|
154
132
|
|
|
155
|
-
##
|
|
133
|
+
## Development
|
|
156
134
|
|
|
157
135
|
```bash
|
|
158
|
-
npm run dev # dev
|
|
136
|
+
npm run dev # dev server with a demo page
|
|
159
137
|
npm run verify # lint + typecheck + smoke test
|
|
160
138
|
```
|
|
161
139
|
|
|
162
|
-
###
|
|
140
|
+
### Multiplier Calibration
|
|
163
141
|
|
|
164
142
|
```bash
|
|
165
143
|
npx vite-node scripts/bench-distribution.mjs
|
|
166
144
|
```
|
|
167
145
|
|
|
168
|
-
|
|
169
|
-
|
|
146
|
+
The script runs 3000 balls for each `houseEdge` value and prints RTP.
|
|
147
|
+
Adjust the multipliers in the script so RTP is approximately 100% at `houseEdge: 0.5`.
|