ultimatedarktower 2.2.0 → 2.5.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/CHANGELOG.md CHANGED
@@ -6,100 +6,147 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.5.0] - 2026-03-23
10
+
11
+ ### Added
12
+
13
+ - **`markSealBroken()` method** — Marks a seal as broken in software tracking without sending hardware commands. Enables restoring game state (e.g., resuming a saved game).
14
+ - **`markSealRestored()` method** — Marks a seal as unbroken in software tracking without sending hardware commands. Enables undoing a seal break or restoring individual seals.
15
+ - **`brokenSeals` config option** — Accepts an array of `SealIdentifier` in `UltimateDarkTowerConfig` to initialize seal state at construction time.
16
+
17
+ ### Changed
18
+
19
+ - **Improved seal management documentation** — Reference.md now explains that seals are physical plastic covers on the tower (12 total), that seal state is tracked purely in software (not by firmware), and documents all new seal state management APIs.
20
+
21
+ ## [2.4.0] - 2026-03-19
22
+
23
+ ### Changed
24
+
25
+ - **Migrated project baseline to Node.js 18+** — Updated `engines.node` to `>=18.0.0`, aligned CI matrix validation to Node 18 and 20, and refreshed contributing guidance to reflect active runtime support.
26
+
27
+ ### Fixed
28
+
29
+ - **Cleared development-tooling security advisories without permanent overrides** — Upgraded direct dev dependencies (`ts-jest`, `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, and `esbuild`) so the lockfile now resolves patched transitive versions for `minimatch`, `ajv`, `js-yaml`, and `flatted` without retaining temporary npm `overrides`. Full `npm audit` now reports zero vulnerabilities while preserving the existing Jest, ESLint, and build configuration.
30
+ - **Stabilised `ts-jest` coverage resolution after dependency cleanup** — Added an explicit `jest-util` devDependency so `ts-jest` can resolve its runtime helper consistently during Jest coverage runs.
31
+ - **Adjusted BLE device-info fallback for newer lint rules** — Removed an unused catch binding in `readDeviceInformation()` so the code remains compatible with the stricter `@typescript-eslint` rules introduced by the dependency upgrades.
32
+ - **Started staged modernization dependency refresh** — Updated core dev tooling to current non-breaking lines (`typescript` to `^5.9.3`, `prettier` to `3.8.1`, `@types/node` to `^24.12.0`, `@types/jest` to `^30.0.0`, and `@stoprocent/noble` to `^2.3.17`) and revalidated with full CI and `npm audit`.
33
+ - **Consolidated duplicate ESLint configuration files** — Unified lint configuration into `.eslintrc.js` and removed `.eslintrc.json` to reduce rule drift and prepare cleanly for future ESLint major migration work.
34
+ - **Updated Node matrix CI for active support policy** — CI matrix now validates Node 18 and 20 only, matching the new runtime baseline.
35
+ - **Added ESLint flat-config preview path for staged migration** — Added `eslint.config.mjs` and preview scripts (`lint:flat:preview`) so ESLint 9 compatibility can be tested incrementally while the current CI lint path remains unchanged.
36
+ - **Achieved ESLint rule parity between legacy and flat-config paths** — Updated `eslint.config.mjs` to include `js.configs.recommended` (base ESLint rules) and explicitly disable `no-unused-vars` in favour of `@typescript-eslint/no-unused-vars` with argument patterns, ensuring both `npm run lint` and `npm run lint:flat:preview` produce identical output (86 warnings). Both lint paths now feature full parity for future seamless migration to ESLint 9.
37
+ - **Upgraded Jest toolchain toward current major** — Upgraded `jest` and `jest-util` to the 30.x line and aligned Jest type definitions to `@types/jest` 30.x while keeping `ts-jest` on latest available stable 29.x until a compatible 30.x release is published.
38
+
39
+ ## [2.3.1] - 2026-03-09
40
+
41
+ ### Added
42
+
43
+ - **Troubleshooting modal in TowerController example** — A "Troubleshooting" button now appears in the TowerController web app button bar. Clicking it opens a modal overlay displaying the full Restoration Games troubleshooting guide (tower jams, disconnects, firmware errors 133/257, and battery specifications). The modal can be dismissed via the close button, clicking the backdrop, or pressing Escape. The button is visually de-emphasised (reduced opacity, smaller text, extra left margin) to indicate it is secondary to Connect/Disconnect/Calibrate.
44
+
45
+ ## [2.3.0] - 2026-02-23
46
+
47
+ ### Added
48
+
49
+ - **`allLightsOn(effect?)` and `allLightsOff()` convenience methods** — Turns all 24 tower LEDs on or off with a single command packet. `allLightsOn` accepts an optional `effect` parameter (default: `LIGHT_EFFECTS.on`); `allLightsOff` is a convenience wrapper around `allLightsOn(LIGHT_EFFECTS.off)`. Both preserve existing drum, beam, and audio state.
50
+
51
+ ### Changed
52
+
53
+ - **Public audio volume API now clamps inputs to 0–3** — The `volume` parameter accepted by `playSoundStateful`, `breakSeal`, and related methods is now clamped to the range 0–3 (0=loudest, 1=medium, 2=quiet, 3=softest/mute) before being sent to the tower. The tower's 4-bit device field accepts 0–15, but the firmware only defines behaviour for 0–3; out-of-range inputs now silently clamp rather than producing undefined tower behaviour. If you were passing values outside 0–3, update them to the equivalent in-range value.
54
+
9
55
  ## [2.2.0] - 2026-02-20
10
56
 
11
57
  ### Fixed
12
58
 
13
- - **`setLEDStateful` stale-state accumulation** — `setLEDStateful` never called `setTowerState`, so `onTowerStateUpdate` callbacks were never fired for LED changes and any code calling it in a loop (including `lights()`) risked reading stale state if `this.currentTowerState` was replaced between iterations. State is now updated explicitly before the command is sent.
14
- - **`cleanup()` reconnect hazard** — `cleanup()` called `disconnect()` which fired `onTowerDisconnect`, meaning a reconnect-on-disconnect handler could call `connect()` on an instance mid-teardown. `isDisposed` is now set before any disconnect logic runs so the callback cannot re-enter `connect()`.
15
- - **`cleanup()` not idempotent** — Calling `cleanup()` more than once would re-run the full teardown sequence. It now returns early if the instance is already disposed.
16
- - **`MockBluetoothAdapter.cleanup()` leaving callbacks registered** — The mock adapter's `cleanup()` now clears all three event callbacks, matching the behaviour of `NodeBluetoothAdapter`.
59
+ - **`setLEDStateful` stale-state accumulation** — `setLEDStateful` never called `setTowerState`, so `onTowerStateUpdate` callbacks were never fired for LED changes and any code calling it in a loop (including `lights()`) risked reading stale state if `this.currentTowerState` was replaced between iterations. State is now updated explicitly before the command is sent.
60
+ - **`cleanup()` reconnect hazard** — `cleanup()` called `disconnect()` which fired `onTowerDisconnect`, meaning a reconnect-on-disconnect handler could call `connect()` on an instance mid-teardown. `isDisposed` is now set before any disconnect logic runs so the callback cannot re-enter `connect()`.
61
+ - **`cleanup()` not idempotent** — Calling `cleanup()` more than once would re-run the full teardown sequence. It now returns early if the instance is already disposed.
62
+ - **`MockBluetoothAdapter.cleanup()` leaving callbacks registered** — The mock adapter's `cleanup()` now clears all three event callbacks, matching the behaviour of `NodeBluetoothAdapter`.
17
63
 
18
64
  ### Changed
19
65
 
20
- - **`connect()` throws after disposal** — Calling `connect()` on a `UdtBleConnection` instance after `cleanup()` now throws `Error: UdtBleConnection instance has been disposed and cannot reconnect`. Use `disconnect()` for reversible disconnection.
66
+ - **`connect()` throws after disposal** — Calling `connect()` on a `UdtBleConnection` instance after `cleanup()` now throws `Error: UdtBleConnection instance has been disposed and cannot reconnect`. Use `disconnect()` for reversible disconnection.
21
67
 
22
68
  ## [2.1.3] - 2026-02-19
23
69
 
24
70
  ### Fixed
25
71
 
26
- - **`@stoprocent/noble` not loading in ESM build** — In Node.js ESM contexts, `require` is not defined, causing esbuild's `__require` shim to silently fail and leave `noble` as `undefined`. The ESM bundle now injects `import{createRequire}from'module';const require=createRequire(import.meta.url);` as a banner so `@stoprocent/noble` loads correctly via CJS `require` within the ESM module.
72
+ - **`@stoprocent/noble` not loading in ESM build** — In Node.js ESM contexts, `require` is not defined, causing esbuild's `__require` shim to silently fail and leave `noble` as `undefined`. The ESM bundle now injects `import{createRequire}from'module';const require=createRequire(import.meta.url);` as a banner so `@stoprocent/noble` loads correctly via CJS `require` within the ESM module.
27
73
 
28
74
  ## [2.1.2] - 2026-02-19
29
75
 
30
76
  ### Fixed
31
77
 
32
- - **ESM named imports broken** — `import { UltimateDarkTower } from 'ultimatedarktower'` previously threw `SyntaxError: The requested module does not provide an export named 'UltimateDarkTower'` in Node.js ESM projects because the `"import"` export condition pointed to the CommonJS build. The package now ships a true ES Module bundle so named imports work correctly.
78
+ - **ESM named imports broken** — `import { UltimateDarkTower } from 'ultimatedarktower'` previously threw `SyntaxError: The requested module does not provide an export named 'UltimateDarkTower'` in Node.js ESM projects because the `"import"` export condition pointed to the CommonJS build. The package now ships a true ES Module bundle so named imports work correctly.
33
79
 
34
80
  ### Added
35
81
 
36
- - **ESM build** (`dist/esm/index.mjs`) — a native ES Module bundle produced by esbuild, included in the published package alongside the existing CommonJS build
82
+ - **ESM build** (`dist/esm/index.mjs`) — a native ES Module bundle produced by esbuild, included in the published package alongside the existing CommonJS build
37
83
 
38
84
  ### Changed
39
85
 
40
- - `package.json` `exports["import"]` condition now points to `dist/esm/index.mjs` instead of the CommonJS output; `exports["require"]` is unchanged
41
- - `package.json` `files` now includes `dist/esm/**/*`
86
+ - `package.json` `exports["import"]` condition now points to `dist/esm/index.mjs` instead of the CommonJS output; `exports["require"]` is unchanged
87
+ - `package.json` `files` now includes `dist/esm/**/*`
42
88
 
43
89
  ## [2.1.1] - 2026-02-19
44
90
 
45
91
  ### Added
46
92
 
47
- - Integration test for tower calibration using Node.js Bluetooth adapter, located in `tests/integration/calibration.integration.ts`
48
- - `npm run test:integration` script to run integration tests requiring real hardware
49
- - Integration tests are now organized under `tests/integration/` and are not run by default with unit tests or during publish
93
+ - Integration test for tower calibration using Node.js Bluetooth adapter, located in `tests/integration/calibration.integration.ts`
94
+ - `npm run test:integration` script to run integration tests requiring real hardware
95
+ - Integration tests are now organized under `tests/integration/` and are not run by default with unit tests or during publish
50
96
 
51
97
  ## [2.1.0] - 2026-02-19
52
98
 
53
99
  ### Added
54
100
 
55
- - **Public Tower State Types** — Exported `TowerState`, `Light`, `Layer`, `Drum`, `Audio`, and `Beam` type interfaces for direct tower state manipulation
56
- - **Tower State Utilities** — Exported `rtdt_unpack_state`, `rtdt_pack_state`, `isCalibrated`, and `createDefaultTowerState` for converting between `TowerState` objects and binary tower data
57
- - **Differential Readings** — Exported `parseDifferentialReadings` function and `ParsedDifferentialReadings` type for parsing tower sensor data
58
- - **`TowerResponseConfig` Type** — Exported interface for controlling which tower responses are logged via `logTowerResponseConfig`
101
+ - **Public Tower State Types** — Exported `TowerState`, `Light`, `Layer`, `Drum`, `Audio`, and `Beam` type interfaces for direct tower state manipulation
102
+ - **Tower State Utilities** — Exported `rtdt_unpack_state`, `rtdt_pack_state`, `isCalibrated`, and `createDefaultTowerState` for converting between `TowerState` objects and binary tower data
103
+ - **Differential Readings** — Exported `parseDifferentialReadings` function and `ParsedDifferentialReadings` type for parsing tower sensor data
104
+ - **`TowerResponseConfig` Type** — Exported interface for controlling which tower responses are logged via `logTowerResponseConfig`
59
105
 
60
106
  ### Changed
61
107
 
62
- - **`TowerResponseConfig`** — Moved from private interface in `UltimateDarkTower.ts` to exported interface in `udtTowerResponse.ts`
63
- - **`shouldLogResponse`** — Updated parameter type from `any` to `TowerResponseConfig` for type safety
64
- - **Controller Example** — Updated imports to use the package index instead of internal module paths
108
+ - **`TowerResponseConfig`** — Moved from private interface in `UltimateDarkTower.ts` to exported interface in `udtTowerResponse.ts`
109
+ - **`shouldLogResponse`** — Updated parameter type from `any` to `TowerResponseConfig` for type safety
110
+ - **Controller Example** — Updated imports to use the package index instead of internal module paths
65
111
 
66
112
  ## [2.0.0] - 2025-02-18
67
113
 
68
114
  ### Added
69
115
 
70
- - **Node.js Support** — `NodeBluetoothAdapter` using `@stoprocent/noble` for BLE communication in Node.js environments (macOS, Linux, Windows)
71
- - **Platform Auto-Detection** — `BluetoothAdapterFactory` automatically selects the correct adapter based on the runtime environment (browser vs Node.js vs Electron)
72
- - **`BluetoothPlatform` Enum** — Explicit platform selection via `BluetoothPlatform.WEB`, `BluetoothPlatform.NODE`, or `BluetoothPlatform.AUTO`
73
- - **`IBluetoothAdapter` Interface** — Public adapter interface for implementing custom Bluetooth adapters (React Native, Cordova, etc.)
74
- - **Platform-Agnostic Error Types** — `BluetoothConnectionError`, `BluetoothDeviceNotFoundError`, `BluetoothNotAvailableError`, `BluetoothCharacteristicError`, `BluetoothAdapterError` for consistent error handling across platforms
75
- - **Node.js CLI Example** — Interactive command-line example application (`examples/node/`)
76
- - **Adapter Layer Tests** — Unit tests for `NodeBluetoothAdapter`, `WebBluetoothAdapter`, `BluetoothAdapterFactory`, `UdtBleConnection`, and error types
116
+ - **Node.js Support** — `NodeBluetoothAdapter` using `@stoprocent/noble` for BLE communication in Node.js environments (macOS, Linux, Windows)
117
+ - **Platform Auto-Detection** — `BluetoothAdapterFactory` automatically selects the correct adapter based on the runtime environment (browser vs Node.js vs Electron)
118
+ - **`BluetoothPlatform` Enum** — Explicit platform selection via `BluetoothPlatform.WEB`, `BluetoothPlatform.NODE`, or `BluetoothPlatform.AUTO`
119
+ - **`IBluetoothAdapter` Interface** — Public adapter interface for implementing custom Bluetooth adapters (React Native, Cordova, etc.)
120
+ - **Platform-Agnostic Error Types** — `BluetoothConnectionError`, `BluetoothDeviceNotFoundError`, `BluetoothNotAvailableError`, `BluetoothCharacteristicError`, `BluetoothAdapterError` for consistent error handling across platforms
121
+ - **Node.js CLI Example** — Interactive command-line example application (`examples/node/`)
122
+ - **Adapter Layer Tests** — Unit tests for `NodeBluetoothAdapter`, `WebBluetoothAdapter`, `BluetoothAdapterFactory`, `UdtBleConnection`, and error types
77
123
 
78
124
  ### Changed
79
125
 
80
- - **`udtBleConnection`** — Refactored to use `IBluetoothAdapter` interface instead of direct Web Bluetooth API calls, enabling multi-platform support
81
- - **`UltimateDarkTower` Constructor** — Now accepts `UltimateDarkTowerConfig` with optional `platform` or `adapter` properties for platform selection
82
- - **Peer Dependency** — Updated `@stoprocent/noble` peer dependency from `^1.15.0` to `^2.0.0`
126
+ - **`udtBleConnection`** — Refactored to use `IBluetoothAdapter` interface instead of direct Web Bluetooth API calls, enabling multi-platform support
127
+ - **`UltimateDarkTower` Constructor** — Now accepts `UltimateDarkTowerConfig` with optional `platform` or `adapter` properties for platform selection
128
+ - **Peer Dependency** — Updated `@stoprocent/noble` peer dependency from `^1.15.0` to `^2.0.0`
83
129
 
84
130
  ## [1.0.0] - 2025-08-18
85
131
 
86
132
  ### Added
87
133
 
88
- - Initial release
89
- - Web Bluetooth support for Chrome, Edge, and Samsung Internet
90
- - Tower control API (lights, sounds, drum rotation)
91
- - Glyph position tracking with automatic updates on drum rotation
92
- - Seal management for game mechanics
93
- - Tower state management and validation
94
- - Multi-layered disconnect detection (heartbeat, GATT events, command timeout)
95
- - Callback-based event system for tower events
96
- - Comprehensive logging system with multiple outputs
97
- - Battery monitoring with low battery warnings
98
- - TypeScript definitions and type safety
99
- - Tower Controller example web app
100
- - Tower Game ("The Tower's Challenge") example web app
101
- - Complete API reference documentation
102
-
134
+ - Initial release
135
+ - Web Bluetooth support for Chrome, Edge, and Samsung Internet
136
+ - Tower control API (lights, sounds, drum rotation)
137
+ - Glyph position tracking with automatic updates on drum rotation
138
+ - Seal management for game mechanics
139
+ - Tower state management and validation
140
+ - Multi-layered disconnect detection (heartbeat, GATT events, command timeout)
141
+ - Callback-based event system for tower events
142
+ - Comprehensive logging system with multiple outputs
143
+ - Battery monitoring with low battery warnings
144
+ - TypeScript definitions and type safety
145
+ - Tower Controller example web app
146
+ - Tower Game ("The Tower's Challenge") example web app
147
+ - Complete API reference documentation
148
+
149
+ [2.3.0]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.2.0...v2.3.0
103
150
  [2.2.0]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.3...v2.2.0
104
151
  [2.1.3]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.2...v2.1.3
105
152
  [2.1.2]: https://github.com/ChessMess/UltimateDarkTower/compare/v2.1.1...v2.1.2
package/README.md CHANGED
@@ -158,6 +158,35 @@ npm run test:integration
158
158
  - The test will fail if the tower is not available or calibration does not complete within 60 seconds.
159
159
  - Integration tests are not included in automated test runs or npm publish.
160
160
 
161
+ ### Lights Integration Test
162
+
163
+ The lights integration test validates the `allLightsOn` and `allLightsOff` API methods using real tower hardware.
164
+
165
+ **Test steps:**
166
+
167
+ - Turns all 24 LEDs on (solid effect) for 2 seconds
168
+ - Turns all 24 LEDs on (breathe effect) for 3 seconds
169
+ - Turns all 24 LEDs off
170
+
171
+ **How to run:**
172
+
173
+ ```bash
174
+ npm run test:integration:lights
175
+ ```
176
+
177
+ **Prerequisites:**
178
+
179
+ - Tower must be powered on and in Bluetooth range
180
+ - `@stoprocent/noble` must be installed
181
+
182
+ **Visual verification:**
183
+
184
+ - All lights on (solid) for 2 seconds
185
+ - All lights breathe effect for 3 seconds
186
+ - All lights off
187
+
188
+ See [Reference.md](Reference.md) for API details on `allLightsOn` and `allLightsOff`.
189
+
161
190
  **Prerequisites:**
162
191
 
163
192
  - Tower must be powered on and in Bluetooth range
@@ -6,8 +6,7 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
6
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
7
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
8
  }) : x)(function(x) {
9
- if (typeof require !== "undefined")
10
- return require.apply(this, arguments);
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
10
  throw Error('Dynamic require of "' + x + '" is not supported');
12
11
  });
13
12
  var __esm = (fn, res) => function __init() {
@@ -813,8 +812,7 @@ var init_NodeBluetoothAdapter = __esm({
813
812
  }
814
813
  }
815
814
  async disconnect() {
816
- if (!this.peripheral)
817
- return;
815
+ if (!this.peripheral) return;
818
816
  try {
819
817
  if (this.rxCharacteristic) {
820
818
  if (this.boundDataHandler) {
@@ -916,8 +914,7 @@ var init_NodeBluetoothAdapter = __esm({
916
914
  return cUuid === normalizedUuid || cUuid === shortUuid;
917
915
  }
918
916
  );
919
- if (!char)
920
- continue;
917
+ if (!char) continue;
921
918
  try {
922
919
  const buffer = await char.readAsync();
923
920
  if (binary) {
@@ -1135,8 +1132,7 @@ var DOMOutput = class {
1135
1132
  this.maxLines = maxLines;
1136
1133
  }
1137
1134
  write(level, message, timestamp) {
1138
- if (!this.container)
1139
- return;
1135
+ if (!this.container) return;
1140
1136
  this.allEntries.push({ level, message, timestamp });
1141
1137
  while (this.allEntries.length > this.maxLines) {
1142
1138
  this.allEntries.shift();
@@ -1144,8 +1140,7 @@ var DOMOutput = class {
1144
1140
  this.refreshDisplay();
1145
1141
  }
1146
1142
  refreshDisplay() {
1147
- if (!this.container)
1148
- return;
1143
+ if (!this.container) return;
1149
1144
  this.container.innerHTML = "";
1150
1145
  const enabledLevels = this.getEnabledLevelsFromCheckboxes();
1151
1146
  const textFilter = this.getTextFilter();
@@ -1258,12 +1253,9 @@ var Logger = class _Logger {
1258
1253
  return Array.from(this.enabledLevels);
1259
1254
  }
1260
1255
  shouldLog(level) {
1261
- if (this.enabledLevels.has("all"))
1262
- return true;
1263
- if (level === "all")
1264
- return true;
1265
- if (this.enabledLevels.has(level))
1266
- return true;
1256
+ if (this.enabledLevels.has("all")) return true;
1257
+ if (level === "all") return true;
1258
+ if (this.enabledLevels.has(level)) return true;
1267
1259
  if (this.enabledLevels.size === 1) {
1268
1260
  const singleLevel = Array.from(this.enabledLevels)[0];
1269
1261
  if (singleLevel !== "all") {
@@ -1276,8 +1268,7 @@ var Logger = class _Logger {
1276
1268
  return false;
1277
1269
  }
1278
1270
  log(level, message, context) {
1279
- if (!this.shouldLog(level))
1280
- return;
1271
+ if (!this.shouldLog(level)) return;
1281
1272
  const contextPrefix = context ? `${context} ` : "";
1282
1273
  const finalMessage = `${contextPrefix}${message}`;
1283
1274
  const timestamp = /* @__PURE__ */ new Date();
@@ -1835,13 +1826,12 @@ var UdtBleConnection = class {
1835
1826
  this.logger.info(`Device ${key}: ${value}`, "[UDT][BLE]");
1836
1827
  }
1837
1828
  }
1838
- } catch (error) {
1829
+ } catch {
1839
1830
  this.logger.debug("Device Information Service not available", "[UDT][BLE]");
1840
1831
  }
1841
1832
  }
1842
1833
  async cleanup() {
1843
- if (this.isDisposed)
1844
- return;
1834
+ if (this.isDisposed) return;
1845
1835
  this.isDisposed = true;
1846
1836
  this.logger.info("Cleaning up UdtBleConnection instance", "[UDT][BLE]");
1847
1837
  this.stopConnectionMonitoring();
@@ -1958,7 +1948,7 @@ var UdtCommandFactory = class {
1958
1948
  * @param currentState - The current complete tower state
1959
1949
  * @param sample - Audio sample index to play (0-127)
1960
1950
  * @param loop - Whether to loop the audio
1961
- * @param volume - Audio volume (0-15), optional
1951
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
1962
1952
  * @returns 20-byte command packet
1963
1953
  */
1964
1954
  createStatefulAudioCommand(currentState, sample, loop = false, volume) {
@@ -1971,10 +1961,10 @@ var UdtCommandFactory = class {
1971
1961
  /**
1972
1962
  * Creates a transient audio command that includes current tower state but doesn't persist audio state.
1973
1963
  * This prevents audio from being included in subsequent commands.
1974
- * @param currentState - The current complete tower state
1964
+ * @param currentState - The current complete tower state
1975
1965
  * @param sample - Audio sample index to play
1976
1966
  * @param loop - Whether to loop the audio
1977
- * @param volume - Audio volume (0-15), optional
1967
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
1978
1968
  * @returns Object containing the command packet and the state without audio for local tracking
1979
1969
  */
1980
1970
  createTransientAudioCommand(currentState, sample, loop = false, volume) {
@@ -1988,12 +1978,12 @@ var UdtCommandFactory = class {
1988
1978
  return { command, stateWithoutAudio };
1989
1979
  }
1990
1980
  /**
1991
- * Creates a transient audio command with additional modifications that includes current tower state
1981
+ * Creates a transient audio command with additional modifications that includes current tower state
1992
1982
  * but doesn't persist audio state. This prevents audio from being included in subsequent commands.
1993
- * @param currentState - The current complete tower state
1983
+ * @param currentState - The current complete tower state
1994
1984
  * @param sample - Audio sample index to play
1995
1985
  * @param loop - Whether to loop the audio
1996
- * @param volume - Audio volume (0-15), optional
1986
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest). Public API clamps inputs to this range before reaching here.
1997
1987
  * @param otherModifications - Other tower state modifications to include
1998
1988
  * @returns Object containing the command packet and the state with modifications but without audio
1999
1989
  */
@@ -2725,7 +2715,7 @@ var UdtTowerCommands = class {
2725
2715
  * Audio state is not persisted to prevent sounds from replaying on subsequent commands.
2726
2716
  * @param soundIndex - Index of the sound to play (1-based)
2727
2717
  * @param loop - Whether to loop the audio
2728
- * @param volume - Audio volume (0-15), optional
2718
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
2729
2719
  * @returns Promise that resolves when command is sent
2730
2720
  */
2731
2721
  async playSoundStateful(soundIndex, loop = false, volume) {
@@ -2734,10 +2724,11 @@ var UdtTowerCommands = class {
2734
2724
  this.deps.logger.error(`attempt to play invalid sound index ${soundIndex}`, "[UDT][CMD]");
2735
2725
  return;
2736
2726
  }
2727
+ const clampedVolume = volume === void 0 ? void 0 : Math.min(3, Math.max(0, Math.round(volume)));
2737
2728
  const currentState = this.deps.getCurrentTowerState();
2738
- const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, loop, volume);
2739
- this.deps.logger.info(`Playing sound ${soundIndex}${loop ? " (looped)" : ""}${volume !== void 0 ? ` at volume ${volume}` : ""}`, "[UDT][CMD]");
2740
- await this.sendTowerCommand(command, `playSoundStateful(${soundIndex}, ${loop}${volume !== void 0 ? `, ${volume}` : ""})`);
2729
+ const { command } = this.deps.commandFactory.createTransientAudioCommand(currentState, soundIndex, loop, clampedVolume);
2730
+ this.deps.logger.info(`Playing sound ${soundIndex}${loop ? " (looped)" : ""}${clampedVolume !== void 0 ? ` at volume ${clampedVolume}` : ""}`, "[UDT][CMD]");
2731
+ await this.sendTowerCommand(command, `playSoundStateful(${soundIndex}, ${loop}${clampedVolume !== void 0 ? `, ${clampedVolume}` : ""})`);
2741
2732
  }
2742
2733
  /**
2743
2734
  * Rotates a single drum using stateful commands that preserve existing tower state.
@@ -2835,10 +2826,15 @@ var UltimateDarkTower = class {
2835
2826
  this.onCalibrationComplete = () => {
2836
2827
  };
2837
2828
  this.onSkullDrop = (towerSkullCount) => {
2829
+ void towerSkullCount;
2838
2830
  };
2839
2831
  this.onBatteryLevelNotify = (millivolts) => {
2832
+ void millivolts;
2840
2833
  };
2841
2834
  this.onTowerStateUpdate = (newState, oldState, source) => {
2835
+ void newState;
2836
+ void oldState;
2837
+ void source;
2842
2838
  };
2843
2839
  // utility
2844
2840
  this._logDetail = false;
@@ -2869,6 +2865,12 @@ var UltimateDarkTower = class {
2869
2865
  this.commandFactory = new UdtCommandFactory();
2870
2866
  const commandDependencies = this.createCommandDependencies();
2871
2867
  this.towerCommands = new UdtTowerCommands(commandDependencies);
2868
+ if (config?.brokenSeals) {
2869
+ for (const seal of config.brokenSeals) {
2870
+ const sealKey = `${seal.level}-${seal.side}`;
2871
+ this.brokenSeals.add(sealKey);
2872
+ }
2873
+ }
2872
2874
  }
2873
2875
  /**
2874
2876
  * Set up the tower response callback after all components are initialized
@@ -3107,7 +3109,7 @@ var UltimateDarkTower = class {
3107
3109
  * Plays a sound using stateful commands that preserve existing tower state.
3108
3110
  * @param soundIndex - Index of the sound to play (1-based)
3109
3111
  * @param loop - Whether to loop the audio
3110
- * @param volume - Audio volume (0-15), optional
3112
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
3111
3113
  * @returns Promise that resolves when command is sent
3112
3114
  */
3113
3115
  async playSoundStateful(soundIndex, loop = false, volume) {
@@ -3142,6 +3144,31 @@ var UltimateDarkTower = class {
3142
3144
  this.calculateAndUpdateGlyphPositions("bottom", oldBottomPosition, bottom);
3143
3145
  return result;
3144
3146
  }
3147
+ /**
3148
+ * Turns all tower LEDs on with the specified light effect, sending a single command packet.
3149
+ * Preserves current drum, beam, and audio state while overriding all 6 layers of lights.
3150
+ * @param effect - Light effect to apply (default: LIGHT_EFFECTS.on). Use LIGHT_EFFECTS constants for named values.
3151
+ * @returns Promise that resolves when the command is sent
3152
+ */
3153
+ async allLightsOn(effect = LIGHT_EFFECTS.on) {
3154
+ const currentState = this.getCurrentTowerState();
3155
+ const loop = effect !== LIGHT_EFFECTS.off;
3156
+ const newState = {
3157
+ ...currentState,
3158
+ layer: currentState.layer.map((layer) => ({
3159
+ light: layer.light.map(() => ({ effect, loop }))
3160
+ }))
3161
+ };
3162
+ return this.sendTowerState(newState);
3163
+ }
3164
+ /**
3165
+ * Turns all tower LEDs off, sending a single command packet.
3166
+ * Convenience wrapper around allLightsOn(LIGHT_EFFECTS.off).
3167
+ * @returns Promise that resolves when the command is sent
3168
+ */
3169
+ async allLightsOff() {
3170
+ return this.allLightsOn(LIGHT_EFFECTS.off);
3171
+ }
3145
3172
  //#endregion
3146
3173
  //#region Tower State Management
3147
3174
  /**
@@ -3354,6 +3381,25 @@ var UltimateDarkTower = class {
3354
3381
  return { level, side };
3355
3382
  });
3356
3383
  }
3384
+ /**
3385
+ * Marks a seal as broken in software tracking without sending any commands to the tower.
3386
+ * Use this to restore game state (e.g., resuming a game where seals were already broken).
3387
+ * Unlike breakSeal(), this does NOT trigger sound or light effects on the tower.
3388
+ * @param seal - Seal identifier to mark as broken
3389
+ */
3390
+ markSealBroken(seal) {
3391
+ const sealKey = `${seal.level}-${seal.side}`;
3392
+ this.brokenSeals.add(sealKey);
3393
+ }
3394
+ /**
3395
+ * Marks a seal as unbroken in software tracking without sending any commands to the tower.
3396
+ * Use this to undo a seal break or restore individual seals for game state management.
3397
+ * @param seal - Seal identifier to mark as unbroken
3398
+ */
3399
+ markSealRestored(seal) {
3400
+ const sealKey = `${seal.level}-${seal.side}`;
3401
+ this.brokenSeals.delete(sealKey);
3402
+ }
3357
3403
  /**
3358
3404
  * Resets the broken seals tracking (clears all broken seals).
3359
3405
  */
@@ -3502,7 +3548,7 @@ var UltimateDarkTower_default = UltimateDarkTower;
3502
3548
  init_udtConstants();
3503
3549
  init_udtBluetoothAdapter();
3504
3550
  init_udtTowerState();
3505
- var src_default = UltimateDarkTower_default;
3551
+ var index_default = UltimateDarkTower_default;
3506
3552
  export {
3507
3553
  AUDIO_COMMAND_POS,
3508
3554
  BATTERY_STATUS_FREQUENCY,
@@ -3563,7 +3609,7 @@ export {
3563
3609
  VOLUME_DESCRIPTIONS,
3564
3610
  VOLUME_ICONS,
3565
3611
  createDefaultTowerState,
3566
- src_default as default,
3612
+ index_default as default,
3567
3613
  drumPositionCmds,
3568
3614
  isCalibrated,
3569
3615
  logger,
@@ -14,6 +14,8 @@ export interface UltimateDarkTowerConfig {
14
14
  platform?: BluetoothPlatform;
15
15
  /** Custom Bluetooth adapter (for testing or custom platforms like React Native) */
16
16
  adapter?: IBluetoothAdapter;
17
+ /** Initial broken seals to restore game state (software-only, no hardware effects) */
18
+ brokenSeals?: SealIdentifier[];
17
19
  }
18
20
  /**
19
21
  * @title UltimateDarkTower
@@ -174,7 +176,7 @@ declare class UltimateDarkTower {
174
176
  * Plays a sound using stateful commands that preserve existing tower state.
175
177
  * @param soundIndex - Index of the sound to play (1-based)
176
178
  * @param loop - Whether to loop the audio
177
- * @param volume - Audio volume (0-15), optional
179
+ * @param volume - Audio volume (0-3, 0=loudest, 3=softest), optional. Out-of-range values are clamped.
178
180
  * @returns Promise that resolves when command is sent
179
181
  */
180
182
  playSoundStateful(soundIndex: number, loop?: boolean, volume?: number): Promise<void>;
@@ -196,6 +198,19 @@ declare class UltimateDarkTower {
196
198
  * @returns Promise that resolves when rotate command is sent
197
199
  */
198
200
  rotateWithState(top: TowerSide, middle: TowerSide, bottom: TowerSide, soundIndex?: number): Promise<void>;
201
+ /**
202
+ * Turns all tower LEDs on with the specified light effect, sending a single command packet.
203
+ * Preserves current drum, beam, and audio state while overriding all 6 layers of lights.
204
+ * @param effect - Light effect to apply (default: LIGHT_EFFECTS.on). Use LIGHT_EFFECTS constants for named values.
205
+ * @returns Promise that resolves when the command is sent
206
+ */
207
+ allLightsOn(effect?: number): Promise<void>;
208
+ /**
209
+ * Turns all tower LEDs off, sending a single command packet.
210
+ * Convenience wrapper around allLightsOn(LIGHT_EFFECTS.off).
211
+ * @returns Promise that resolves when the command is sent
212
+ */
213
+ allLightsOff(): Promise<void>;
199
214
  /**
200
215
  * Gets the current complete tower state if available.
201
216
  * @returns The current tower state object
@@ -295,6 +310,19 @@ declare class UltimateDarkTower {
295
310
  * @returns Array of SealIdentifier objects representing all broken seals
296
311
  */
297
312
  getBrokenSeals(): SealIdentifier[];
313
+ /**
314
+ * Marks a seal as broken in software tracking without sending any commands to the tower.
315
+ * Use this to restore game state (e.g., resuming a game where seals were already broken).
316
+ * Unlike breakSeal(), this does NOT trigger sound or light effects on the tower.
317
+ * @param seal - Seal identifier to mark as broken
318
+ */
319
+ markSealBroken(seal: SealIdentifier): void;
320
+ /**
321
+ * Marks a seal as unbroken in software tracking without sending any commands to the tower.
322
+ * Use this to undo a seal break or restore individual seals for game state management.
323
+ * @param seal - Seal identifier to mark as unbroken
324
+ */
325
+ markSealRestored(seal: SealIdentifier): void;
298
326
  /**
299
327
  * Resets the broken seals tracking (clears all broken seals).
300
328
  */