plinkit 1.0.0-dev.4 → 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 CHANGED
@@ -1,20 +1,20 @@
1
1
  # plinkit
2
2
 
3
- Минимальная WebGL-библиотека для игры в стиле Plinko.
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 "plinkit"
14
+ import { Plinkit } from 'plinkit'
15
15
 
16
- const canvas = document.querySelector("canvas")
17
- if (!canvas) throw new Error("Canvas not found")
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()` — добавить новый шарик, возвращает `SpawnResult`.
58
- - `resize()` — перечитать ширину родителя и пересчитать viewport.
59
- - `destroy()` — остановить цикл и освободить ресурсы.
60
- - `getState()` — получить `{ balance, ballCost }`.
61
- - `setBallCost(value)` — изменить цену шарика на лету. Обновляет `getState().ballCost`
62
- и синхронно вызывает `onBalanceChange`, чтобы UI мог пересчитать disabled-состояние
63
- кнопки. Уже летящие шарики сохраняют свой исходный `wager` payout считается по
64
- той цене, по которой шарик был запущен. Бросает `TypeError`, если передано не
65
- конечное число или отрицательное значение.
66
- - `setMuted(value)` — включить/выключить звук. No-op, если `options.audio` не задан.
67
- - `isMuted()` текущее состояние mute. Возвращает `true`, если аудио не сконфигурировано.
68
- - `setVolume(value)` — общая громкость 0..1. No-op, если `options.audio` не задан.
69
- - `playUiTap()` проиграть UI-сэмпл из `options.audio.uiTapUrl`. Удобно вызывать
70
- из click/pointerup-обработчиков кнопок продукта, чтобы UI-звуки разделяли
71
- mute/volume/auto-unlock с физикой.
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
- audio: {
83
- pegHitUrl: "/sounds/peg.mp3", // удар шарика о peg/guide
84
- bucketHitUrl: "/sounds/bucket.mp3", // попадание в корзину (момент settle)
85
- uiTapUrl: "/sounds/tap.mp3", // UI-клик, проигрывается через playUiTap()
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
- game.playUiTap()
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
- `pegHitUrl` срабатывает на каждом столкновении с пегом — громкость линейно
101
- зависит от скорости столкновения, плюс throttling per-ball ≤1 звук в 35 мс.
102
- `bucketHitUrl` играется один раз при попадании шарика в корзину (без throttling
103
- и с фиксированной громкостью) это сигнал «раунд завершён». `uiTapUrl`
104
- короткий сэмпл UI-клика; его проигрывает `playUiTap()`, который продукт
105
- вызывает из своих click/pointerup-обработчиков. Все три звука разделяют общий
106
- `masterVolume` и состояние mute.
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
- Параметр `houseEdge` (0..1) управляет вероятностью попадания в крайние корзины:
115
+ The `houseEdge` parameter (0..1) controls the probability of landing in edge buckets:
138
116
 
139
- | houseEdge | Эффект | RTP* |
140
- |-----------|----------------------------|--------|
141
- | 0 | Натуральная физика | ~120% |
142
- | 0.5 | Баланс стабилен | ~97% |
143
- | 1.0 | Баланс тает | ~73% |
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) зависит от множителей. Значения указаны для `[7, 1.5, 0.5, 0.2, 0.1, 0, 0.1, 0.2, 0.5, 1.5, 7]`.
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
- Внутри `houseEdge` управляет тремя механизмами: центростремительная микро-сила,
148
- коррекция скорости при отскоке от пега и Gaussian-распределение точки спавна.
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
- > **Важно:** множители откалиброваны под конкретную геометрию поля (`topPegCount`,
151
- > `bottomPegCount`, `radius`, `verticalStepRatio`, `sidePaddingPx`). Изменение любого
152
- > из этих параметров сдвигает распределение шаров и требует повторной калибровки
153
- > множителей. Для калибровки используйте `scripts/bench-distribution.mjs`.
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
- Скрипт прогоняет 3000 шаров на каждом значении `houseEdge` и выводит RTP.
169
- Подстройте множители в скрипте так, чтобы RTP 100% при `houseEdge: 0.5`.
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`.