ultimatedarktower 3.0.0 → 4.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +28 -15
  3. package/dist/esm/index.mjs +1039 -45
  4. package/dist/src/UltimateDarkTower.d.ts +42 -0
  5. package/dist/src/UltimateDarkTower.js +102 -2
  6. package/dist/src/UltimateDarkTower.js.map +1 -1
  7. package/dist/src/adapters/NodeBluetoothAdapter.js +9 -5
  8. package/dist/src/adapters/NodeBluetoothAdapter.js.map +1 -1
  9. package/dist/src/adapters/WebBluetoothAdapter.js +11 -8
  10. package/dist/src/adapters/WebBluetoothAdapter.js.map +1 -1
  11. package/dist/src/index.d.ts +8 -0
  12. package/dist/src/index.js +34 -1
  13. package/dist/src/index.js.map +1 -1
  14. package/dist/src/sinks/IndexedDBSink.d.ts +26 -0
  15. package/dist/src/sinks/IndexedDBSink.js +165 -0
  16. package/dist/src/sinks/IndexedDBSink.js.map +1 -0
  17. package/dist/src/udtBleConnection.d.ts +24 -1
  18. package/dist/src/udtBleConnection.js +68 -2
  19. package/dist/src/udtBleConnection.js.map +1 -1
  20. package/dist/src/udtBluetoothAdapter.d.ts +6 -6
  21. package/dist/src/udtBluetoothAdapter.js.map +1 -1
  22. package/dist/src/udtCommandQueue.d.ts +4 -1
  23. package/dist/src/udtCommandQueue.js +26 -2
  24. package/dist/src/udtCommandQueue.js.map +1 -1
  25. package/dist/src/udtConstants.d.ts +2 -0
  26. package/dist/src/udtConstants.js +2 -0
  27. package/dist/src/udtConstants.js.map +1 -1
  28. package/dist/src/udtDiagnostics.d.ts +122 -0
  29. package/dist/src/udtDiagnostics.js +228 -0
  30. package/dist/src/udtDiagnostics.js.map +1 -0
  31. package/dist/src/udtGameBoard.d.ts +38 -0
  32. package/dist/src/udtGameBoard.js +86 -0
  33. package/dist/src/udtGameBoard.js.map +1 -0
  34. package/dist/src/udtLogger.d.ts +15 -0
  35. package/dist/src/udtLogger.js +17 -0
  36. package/dist/src/udtLogger.js.map +1 -1
  37. package/dist/src/udtSeedParser.d.ts +124 -0
  38. package/dist/src/udtSeedParser.js +369 -0
  39. package/dist/src/udtSeedParser.js.map +1 -0
  40. package/dist/src/udtSystemRandom.d.ts +58 -0
  41. package/dist/src/udtSystemRandom.js +154 -0
  42. package/dist/src/udtSystemRandom.js.map +1 -0
  43. package/dist/src/udtTowerCommands.d.ts +2 -0
  44. package/dist/src/udtTowerCommands.js +7 -29
  45. package/dist/src/udtTowerCommands.js.map +1 -1
  46. package/package.json +5 -1
@@ -125,7 +125,9 @@ var init_udtConstants = __esm({
125
125
  rotationDrumTop: 16,
126
126
  rotationDrumMiddle: 17,
127
127
  rotationDrumBottom: 18,
128
- monthStarted: 19
128
+ monthStarted: 19,
129
+ wholeTowerBreathing: 20,
130
+ slowFlareThenFade: 21
129
131
  };
130
132
  TOWER_MESSAGES = {
131
133
  TOWER_STATE: { name: "Tower State", value: 0, critical: false },
@@ -452,9 +454,10 @@ var init_WebBluetoothAdapter = __esm({
452
454
  await this.rxCharacteristic.startNotifications();
453
455
  this.boundOnCharacteristicValueChanged = (event) => {
454
456
  const target = event.target;
455
- const receivedData = new Uint8Array(target.value.byteLength);
456
- for (let i = 0; i < target.value.byteLength; i++) {
457
- receivedData[i] = target.value.getUint8(i);
457
+ const dataView = target.value;
458
+ const receivedData = new Uint8Array(dataView.byteLength);
459
+ for (let i = 0; i < dataView.byteLength; i++) {
460
+ receivedData[i] = dataView.getUint8(i);
458
461
  }
459
462
  if (this.characteristicCallback) {
460
463
  this.characteristicCallback(receivedData);
@@ -468,11 +471,12 @@ var init_WebBluetoothAdapter = __esm({
468
471
  if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothUserCancelledError || error instanceof BluetoothConnectionError) {
469
472
  throw error;
470
473
  }
471
- const errorMsg = error?.message ?? String(error);
474
+ const errorMsg = error instanceof Error ? error.message : String(error);
475
+ const errorName = error instanceof Error ? error.name : "";
472
476
  if (errorMsg.includes("User cancelled")) {
473
477
  throw new BluetoothUserCancelledError("User cancelled device selection", error);
474
478
  }
475
- if (errorMsg.includes("not found") || error?.name === "NotFoundError") {
479
+ if (errorMsg.includes("not found") || errorName === "NotFoundError") {
476
480
  throw new BluetoothDeviceNotFoundError("Device not found", error);
477
481
  }
478
482
  throw new BluetoothConnectionError(`Failed to connect: ${errorMsg}`, error);
@@ -482,11 +486,11 @@ var init_WebBluetoothAdapter = __esm({
482
486
  if (!this.device) {
483
487
  return;
484
488
  }
485
- if (this.device.gatt.connected) {
489
+ if (this.device.gatt?.connected) {
486
490
  if (this.boundOnDeviceDisconnected) {
487
491
  this.device.removeEventListener("gattserverdisconnected", this.boundOnDeviceDisconnected);
488
492
  }
489
- await this.device.gatt.disconnect();
493
+ await this.device.gatt?.disconnect();
490
494
  }
491
495
  this.device = null;
492
496
  this.txCharacteristic = null;
@@ -602,8 +606,9 @@ var init_NodeBluetoothAdapter = __esm({
602
606
  try {
603
607
  await noble.waitForPoweredOnAsync();
604
608
  } catch (error) {
609
+ const msg = error instanceof Error ? error.message : String(error);
605
610
  throw new BluetoothConnectionError(
606
- `Bluetooth adapter not ready: ${error.message}`,
611
+ `Bluetooth adapter not ready: ${msg}`,
607
612
  error
608
613
  );
609
614
  }
@@ -638,10 +643,10 @@ var init_NodeBluetoothAdapter = __esm({
638
643
  this.allCharacteristics = characteristics;
639
644
  this.txCharacteristic = characteristics.find(
640
645
  (c) => this.normalizeUuid(c.uuid) === txUuid
641
- );
646
+ ) ?? null;
642
647
  this.rxCharacteristic = characteristics.find(
643
648
  (c) => this.normalizeUuid(c.uuid) === rxUuid
644
- );
649
+ ) ?? null;
645
650
  if (!this.txCharacteristic || !this.rxCharacteristic) {
646
651
  throw new BluetoothConnectionError(
647
652
  "TX or RX characteristic not found on device"
@@ -659,8 +664,9 @@ var init_NodeBluetoothAdapter = __esm({
659
664
  if (error instanceof BluetoothDeviceNotFoundError || error instanceof BluetoothConnectionError || error instanceof BluetoothTimeoutError) {
660
665
  throw error;
661
666
  }
667
+ const msg = error instanceof Error ? error.message : String(error);
662
668
  throw new BluetoothConnectionError(
663
- `Connection failed: ${error.message}`,
669
+ `Connection failed: ${msg}`,
664
670
  error
665
671
  );
666
672
  }
@@ -698,8 +704,9 @@ var init_NodeBluetoothAdapter = __esm({
698
704
  const buffer = Buffer.from(data);
699
705
  await this.txCharacteristic.writeAsync(buffer, false);
700
706
  } catch (error) {
707
+ const msg = error instanceof Error ? error.message : String(error);
701
708
  throw new BluetoothConnectionError(
702
- `Write failed: ${error.message}`,
709
+ `Write failed: ${msg}`,
703
710
  error
704
711
  );
705
712
  }
@@ -1227,11 +1234,19 @@ var Logger = class _Logger {
1227
1234
  constructor() {
1228
1235
  this.outputs = [];
1229
1236
  this.enabledLevels = /* @__PURE__ */ new Set(["all"]);
1237
+ this.diagnosticsTarget = null;
1230
1238
  this.outputs.push(new ConsoleOutput());
1231
1239
  }
1232
1240
  static {
1233
1241
  this.instance = null;
1234
1242
  }
1243
+ /**
1244
+ * Bridge warn/error log lines into a diagnostics recorder so they appear
1245
+ * in the disconnect incident ring buffer in correct chronological order.
1246
+ */
1247
+ setDiagnosticsTarget(target) {
1248
+ this.diagnosticsTarget = target;
1249
+ }
1235
1250
  static getInstance() {
1236
1251
  if (!_Logger.instance) {
1237
1252
  _Logger.instance = new _Logger();
@@ -1286,6 +1301,13 @@ var Logger = class _Logger {
1286
1301
  console.error("Logger output error:", error);
1287
1302
  }
1288
1303
  });
1304
+ if ((level === "warn" || level === "error") && this.diagnosticsTarget?.enabled) {
1305
+ try {
1306
+ this.diagnosticsTarget.recordLog(level, message, context);
1307
+ } catch (error) {
1308
+ console.error("Diagnostics log bridge error:", error);
1309
+ }
1310
+ }
1289
1311
  }
1290
1312
  debug(message, context) {
1291
1313
  this.log("debug", message, context);
@@ -1531,7 +1553,12 @@ var BluetoothAdapterFactory = class {
1531
1553
 
1532
1554
  // src/udtBleConnection.ts
1533
1555
  var UdtBleConnection = class {
1534
- constructor(logger2, callbacks, adapter) {
1556
+ constructor(logger2, callbacks, adapter, recorder) {
1557
+ this.recorder = null;
1558
+ // Snapshot providers wired by UltimateDarkTower so the recorder can capture
1559
+ // higher-level state (command queue, tower state, broken seals) at the
1560
+ // moment a disconnect cause fires.
1561
+ this.snapshotProviders = null;
1535
1562
  // Connection state
1536
1563
  this.isConnected = false;
1537
1564
  this.isDisposed = false;
@@ -1576,6 +1603,7 @@ var UdtBleConnection = class {
1576
1603
  this.logger = logger2;
1577
1604
  this.callbacks = callbacks;
1578
1605
  this.responseProcessor = new TowerResponseProcessor();
1606
+ this.recorder = recorder ?? null;
1579
1607
  this.bluetoothAdapter = adapter || BluetoothAdapterFactory.create("auto" /* AUTO */);
1580
1608
  this.bluetoothAdapter.onCharacteristicValueChanged((data) => {
1581
1609
  this.onRxData(data);
@@ -1587,6 +1615,35 @@ var UdtBleConnection = class {
1587
1615
  this.bleAvailabilityChange(available);
1588
1616
  });
1589
1617
  }
1618
+ setDiagnosticsSnapshotProviders(providers) {
1619
+ this.snapshotProviders = providers;
1620
+ }
1621
+ /**
1622
+ * Record a disconnect incident with the recorder. Public so higher layers
1623
+ * (e.g. UltimateDarkTower's beforeunload handler) can synthesize causes
1624
+ * like 'page_unload' that aren't tied to a specific BLE detection path.
1625
+ */
1626
+ recordIncidentPublic(cause) {
1627
+ return this.recordIncident(cause);
1628
+ }
1629
+ recordIncident(cause) {
1630
+ if (!this.recorder || !this.recorder.enabled) return null;
1631
+ const queueSnapshot = this.snapshotProviders?.commandQueue() ?? {
1632
+ queueLength: 0,
1633
+ isProcessing: false,
1634
+ currentCommand: null
1635
+ };
1636
+ const towerState = this.snapshotProviders?.towerState() ?? null;
1637
+ const brokenSeals = this.snapshotProviders?.brokenSeals() ?? [];
1638
+ return this.recorder.recordIncident({
1639
+ cause,
1640
+ connectionStatus: this.getConnectionStatus(),
1641
+ deviceInformation: this.getDeviceInformation(),
1642
+ commandQueue: queueSnapshot,
1643
+ towerState,
1644
+ brokenSeals
1645
+ });
1646
+ }
1590
1647
  async connect() {
1591
1648
  if (this.isDisposed) {
1592
1649
  throw new Error("UdtBleConnection instance has been disposed and cannot reconnect");
@@ -1601,6 +1658,7 @@ var UdtBleConnection = class {
1601
1658
  this.isConnected = true;
1602
1659
  this.lastSuccessfulCommand = Date.now();
1603
1660
  this.lastBatteryHeartbeat = Date.now();
1661
+ this.recorder?.beginSession();
1604
1662
  await this.readDeviceInformation();
1605
1663
  if (this.enableConnectionMonitoring) {
1606
1664
  this.startConnectionMonitoring();
@@ -1609,11 +1667,14 @@ var UdtBleConnection = class {
1609
1667
  } catch (error) {
1610
1668
  this.logger.error(`Tower Connection Error: ${error}`, "[UDT][BLE]");
1611
1669
  this.isConnected = false;
1612
- this.callbacks.onTowerDisconnect();
1670
+ throw error;
1613
1671
  }
1614
1672
  }
1615
1673
  async disconnect() {
1616
1674
  this.stopConnectionMonitoring();
1675
+ if (this.isConnected) {
1676
+ this.recordIncident("user_initiated");
1677
+ }
1617
1678
  if (this.bluetoothAdapter.isConnected()) {
1618
1679
  await this.bluetoothAdapter.disconnect();
1619
1680
  this.logger.info("Tower disconnected", "[UDT]");
@@ -1625,6 +1686,7 @@ var UdtBleConnection = class {
1625
1686
  * Used by UdtTowerCommands instead of direct characteristic access.
1626
1687
  */
1627
1688
  async writeCommand(command) {
1689
+ this.recorder?.recordCommandPayload("cmd_sent", command, { len: command.length });
1628
1690
  return await this.bluetoothAdapter.writeCharacteristic(command);
1629
1691
  }
1630
1692
  /**
@@ -1635,6 +1697,9 @@ var UdtBleConnection = class {
1635
1697
  this.lastSuccessfulCommand = Date.now();
1636
1698
  const { cmdKey } = this.responseProcessor.getTowerCommand(receivedData[0]);
1637
1699
  const isBattery = this.responseProcessor.isBatteryResponse(cmdKey);
1700
+ if (this.recorder?.enabled && !isBattery) {
1701
+ this.recorder.recordCommandPayload("cmd_response", receivedData, { cmdKey, len: receivedData.length });
1702
+ }
1638
1703
  const shouldLogCommand = this.logTowerResponses && this.responseProcessor.shouldLogResponse(cmdKey, this.logTowerResponseConfig) && !isBattery;
1639
1704
  if (shouldLogCommand) {
1640
1705
  this.logger.info(`${cmdKey}`, "[UDT][BLE][RCVD]");
@@ -1649,6 +1714,7 @@ var UdtBleConnection = class {
1649
1714
  this.lastBatteryHeartbeat = Date.now();
1650
1715
  const millivolts = getMilliVoltsFromTowerResponse(receivedData);
1651
1716
  const batteryPercentage = milliVoltsToPercentage(millivolts);
1717
+ this.recorder?.recordBattery(millivolts, milliVoltsToPercentageNumber(millivolts));
1652
1718
  const didBatteryLevelChange = this.lastBatteryPercentage !== "" && this.lastBatteryPercentage !== batteryPercentage;
1653
1719
  const batteryLogFrequencyPassed = Date.now() - this.lastBatteryLog >= this.batteryLogFrequency;
1654
1720
  const shouldLog = this.batteryLogEnabled && (this.batteryLogOnChangeOnly ? didBatteryLevelChange || this.lastBatteryPercentage === "" : batteryLogFrequencyPassed);
@@ -1668,17 +1734,20 @@ var UdtBleConnection = class {
1668
1734
  const dataSkullDropCount = receivedData[SKULL_DROP_COUNT_POS];
1669
1735
  const state = rtdt_unpack_state(receivedData);
1670
1736
  this.logger.debug(`Tower State: ${JSON.stringify(state)} `, "[UDT][BLE]");
1737
+ this.recorder?.recordEvent("tower_state_response");
1671
1738
  if (this.performingCalibration) {
1672
1739
  this.performingCalibration = false;
1673
1740
  this.performingLongCommand = false;
1674
1741
  this.lastBatteryHeartbeat = Date.now();
1675
1742
  this.callbacks.onCalibrationComplete();
1676
1743
  this.logger.info("Tower calibration complete", "[UDT]");
1744
+ this.recorder?.recordEvent("calibration_complete");
1677
1745
  }
1678
1746
  if (dataSkullDropCount !== this.towerSkullDropCount) {
1679
1747
  if (dataSkullDropCount) {
1680
1748
  this.callbacks.onSkullDrop(dataSkullDropCount);
1681
1749
  this.logger.info(`Skull drop detected: app:${this.towerSkullDropCount < 0 ? "empty" : this.towerSkullDropCount} tower:${dataSkullDropCount}`, "[UDT]");
1750
+ this.recorder?.recordEvent("skull_drop", { count: dataSkullDropCount });
1682
1751
  } else {
1683
1752
  this.logger.info(`Skull count reset to ${dataSkullDropCount}`, "[UDT]");
1684
1753
  }
@@ -1704,11 +1773,15 @@ var UdtBleConnection = class {
1704
1773
  this.logger.info("Bluetooth availability changed", "[UDT][BLE]");
1705
1774
  if (!available && this.isConnected) {
1706
1775
  this.logger.warn("Bluetooth became unavailable - handling disconnection", "[UDT][BLE]");
1776
+ this.recordIncident("bt_unavailable");
1707
1777
  this.handleDisconnection();
1708
1778
  }
1709
1779
  }
1710
1780
  onTowerDeviceDisconnected() {
1711
1781
  this.logger.warn("Tower device disconnected unexpectedly", "[UDT][BLE]");
1782
+ if (this.isConnected) {
1783
+ this.recordIncident("adapter_event");
1784
+ }
1712
1785
  this.handleDisconnection();
1713
1786
  }
1714
1787
  handleDisconnection() {
@@ -1741,6 +1814,7 @@ var UdtBleConnection = class {
1741
1814
  }
1742
1815
  if (!this.bluetoothAdapter.isGattConnected()) {
1743
1816
  this.logger.warn("GATT connection lost detected during health check", "[UDT][BLE]");
1817
+ this.recordIncident("gatt_health_check");
1744
1818
  this.handleDisconnection();
1745
1819
  return;
1746
1820
  }
@@ -1758,12 +1832,17 @@ var UdtBleConnection = class {
1758
1832
  this.logger.info("Verifying tower connection status before triggering disconnection...", "[UDT][BLE]");
1759
1833
  if (this.bluetoothAdapter.isGattConnected()) {
1760
1834
  this.logger.info("GATT connection still available - heartbeat timeout may be temporary", "[UDT][BLE]");
1835
+ this.recorder?.recordEvent("heartbeat_late", {
1836
+ sinceMs: timeSinceLastBatteryHeartbeat,
1837
+ threshold: timeoutThreshold
1838
+ });
1761
1839
  this.lastBatteryHeartbeat = Date.now();
1762
1840
  this.logger.info("Reset battery heartbeat timer - will monitor for another timeout period", "[UDT][BLE]");
1763
1841
  return;
1764
1842
  }
1765
1843
  }
1766
1844
  this.logger.warn("Tower possibly disconnected due to battery depletion or power loss", "[UDT][BLE]");
1845
+ this.recordIncident("heartbeat_timeout");
1767
1846
  this.handleDisconnection();
1768
1847
  return;
1769
1848
  }
@@ -1771,6 +1850,7 @@ var UdtBleConnection = class {
1771
1850
  const timeSinceLastResponse = Date.now() - this.lastSuccessfulCommand;
1772
1851
  if (timeSinceLastResponse > this.connectionTimeoutThreshold) {
1773
1852
  this.logger.warn("General connection timeout detected - no responses received", "[UDT][BLE]");
1853
+ this.recordIncident("response_timeout");
1774
1854
  this.handleDisconnection();
1775
1855
  }
1776
1856
  }
@@ -2103,8 +2183,7 @@ init_udtConstants();
2103
2183
 
2104
2184
  // src/udtCommandQueue.ts
2105
2185
  var CommandQueue = class {
2106
- // 30 seconds
2107
- constructor(logger2, sendCommandFn) {
2186
+ constructor(logger2, sendCommandFn, recorder) {
2108
2187
  this.logger = logger2;
2109
2188
  this.sendCommandFn = sendCommandFn;
2110
2189
  this.queue = [];
@@ -2112,6 +2191,12 @@ var CommandQueue = class {
2112
2191
  this.timeoutHandle = null;
2113
2192
  this.isProcessing = false;
2114
2193
  this.timeoutMs = 3e4;
2194
+ // 30 seconds
2195
+ this.recorder = null;
2196
+ this.recorder = recorder ?? null;
2197
+ }
2198
+ setRecorder(recorder) {
2199
+ this.recorder = recorder;
2115
2200
  }
2116
2201
  /**
2117
2202
  * Enqueue a command for processing
@@ -2128,6 +2213,11 @@ var CommandQueue = class {
2128
2213
  };
2129
2214
  this.queue.push(queuedCommand);
2130
2215
  this.logger.debug(`Command queued: ${description || "unnamed"} (queue size: ${this.queue.length})`, "[UDT]");
2216
+ this.recorder?.recordEvent("cmd_enqueued", {
2217
+ id: queuedCommand.id,
2218
+ description,
2219
+ queueDepth: this.queue.length
2220
+ });
2131
2221
  if (!this.isProcessing) {
2132
2222
  this.processNext();
2133
2223
  }
@@ -2151,6 +2241,11 @@ var CommandQueue = class {
2151
2241
  await this.sendCommandFn(command);
2152
2242
  } catch (error) {
2153
2243
  this.clearTimeout();
2244
+ this.recorder?.recordEvent("cmd_failed", {
2245
+ id,
2246
+ description,
2247
+ error: error?.message ?? String(error)
2248
+ });
2154
2249
  this.currentCommand = null;
2155
2250
  this.isProcessing = false;
2156
2251
  reject(error);
@@ -2176,8 +2271,14 @@ var CommandQueue = class {
2176
2271
  */
2177
2272
  onTimeout() {
2178
2273
  if (this.currentCommand) {
2179
- const { description, id } = this.currentCommand;
2274
+ const { description, id, timestamp } = this.currentCommand;
2180
2275
  this.logger.warn(`Command timeout after ${this.timeoutMs}ms: ${description || id}`, "[UDT]");
2276
+ this.recorder?.recordEvent("cmd_timeout", {
2277
+ id,
2278
+ description,
2279
+ ageMs: Date.now() - timestamp,
2280
+ queueDepth: this.queue.length
2281
+ });
2181
2282
  const reject = this.currentCommand.reject;
2182
2283
  this.currentCommand = null;
2183
2284
  this.isProcessing = false;
@@ -2232,7 +2333,8 @@ var UdtTowerCommands = class {
2232
2333
  this.deps = dependencies;
2233
2334
  this.commandQueue = new CommandQueue(
2234
2335
  this.deps.logger,
2235
- (command) => this.sendTowerCommandDirect(command)
2336
+ (command) => this.sendTowerCommandDirect(command),
2337
+ this.deps.recorder
2236
2338
  );
2237
2339
  }
2238
2340
  /**
@@ -2291,6 +2393,7 @@ var UdtTowerCommands = class {
2291
2393
  async calibrate() {
2292
2394
  if (!this.deps.bleConnection.performingCalibration) {
2293
2395
  this.deps.logger.info("Performing Tower Calibration", "[UDT][CMD]");
2396
+ this.deps.recorder?.recordEvent("calibration_started");
2294
2397
  await this.sendTowerCommand(new Uint8Array([TOWER_COMMANDS.calibration]), "calibrate");
2295
2398
  this.deps.bleConnection.performingCalibration = true;
2296
2399
  this.deps.bleConnection.performingLongCommand = true;
@@ -2584,29 +2687,8 @@ var UdtTowerCommands = class {
2584
2687
  stateWithVolume.audio = { sample: 0, loop: false, volume: actualVolume };
2585
2688
  await this.sendTowerStateStateful(stateWithVolume);
2586
2689
  }
2587
- this.deps.logger.info("Playing tower seal sound", "[UDT]");
2588
- await this.playSoundStateful(TOWER_AUDIO_LIBRARY.TowerSeal.value, false, actualVolume);
2589
- const sideCorners = {
2590
- north: ["northeast", "northwest"],
2591
- east: ["northeast", "southeast"],
2592
- south: ["southeast", "southwest"],
2593
- west: ["southwest", "northwest"]
2594
- };
2595
- const ledgeLights = sideCorners[seal.side].map((corner) => ({
2596
- position: corner,
2597
- style: "on"
2598
- }));
2599
- const doorwayLights = [{
2600
- level: seal.level,
2601
- position: seal.side,
2602
- style: "breatheFast"
2603
- }];
2604
- const lights = {
2605
- ledge: ledgeLights,
2606
- doorway: doorwayLights
2607
- };
2608
- this.deps.logger.info(`Breaking seal ${seal.level}-${seal.side} - lighting ledges and doorways with breath effect`, "[UDT]");
2609
- await this.lights(lights);
2690
+ this.deps.logger.info(`Breaking seal ${seal.level}-${seal.side} - triggering firmware sealReveal animation`, "[UDT]");
2691
+ await this.lightOverrides(TOWER_LIGHT_SEQUENCES.sealReveal, TOWER_AUDIO_LIBRARY.TowerSeal.value);
2610
2692
  }
2611
2693
  /**
2612
2694
  * Randomly rotates specified tower levels to random positions.
@@ -2807,9 +2889,201 @@ var UdtTowerCommands = class {
2807
2889
  }
2808
2890
  };
2809
2891
 
2892
+ // src/udtDiagnostics.ts
2893
+ var RING_BUFFER_SIZE = 500;
2894
+ var RING_BUFFER_DRAIN = 50;
2895
+ var BATTERY_HISTORY_SIZE = 60;
2896
+ var PAYLOAD_MAX_BYTES = 32;
2897
+ var LIBRARY_VERSION = "3.0.0";
2898
+ function detectPlatform() {
2899
+ if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
2900
+ return "web";
2901
+ }
2902
+ if (typeof process !== "undefined" && process.versions?.node) {
2903
+ return "node";
2904
+ }
2905
+ return "custom";
2906
+ }
2907
+ function makeId() {
2908
+ const g = globalThis;
2909
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
2910
+ try {
2911
+ return g.crypto.randomUUID();
2912
+ } catch {
2913
+ }
2914
+ }
2915
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
2916
+ }
2917
+ function bytesToHex(data, maxBytes = PAYLOAD_MAX_BYTES) {
2918
+ const slice = data.length > maxBytes ? data.subarray(0, maxBytes) : data;
2919
+ let out = "";
2920
+ for (let i = 0; i < slice.length; i++) {
2921
+ out += slice[i].toString(16).padStart(2, "0");
2922
+ }
2923
+ if (data.length > maxBytes) {
2924
+ out += `..(+${data.length - maxBytes})`;
2925
+ }
2926
+ return out;
2927
+ }
2928
+ var InMemorySink = class {
2929
+ constructor(maxIncidents = 50) {
2930
+ this.incidents = [];
2931
+ this.maxIncidents = maxIncidents;
2932
+ }
2933
+ onIncident(report) {
2934
+ this.incidents.push(report);
2935
+ if (this.incidents.length > this.maxIncidents) {
2936
+ this.incidents.splice(0, this.incidents.length - this.maxIncidents);
2937
+ }
2938
+ }
2939
+ list() {
2940
+ return [...this.incidents];
2941
+ }
2942
+ get(incidentId) {
2943
+ return this.incidents.find((r) => r.incidentId === incidentId);
2944
+ }
2945
+ clear() {
2946
+ this.incidents = [];
2947
+ }
2948
+ };
2949
+ var UdtDiagnosticsRecorder = class {
2950
+ constructor(config) {
2951
+ this.events = [];
2952
+ this.batteryHistory = [];
2953
+ this.sessionId = "";
2954
+ this.connectedAt = null;
2955
+ this.lastIncident = null;
2956
+ this.enabled = config.enabled;
2957
+ this.capturePayloads = config.capturePayloads ?? false;
2958
+ this.sinks = config.sinks ?? [];
2959
+ }
2960
+ setSinks(sinks) {
2961
+ this.sinks = sinks;
2962
+ }
2963
+ getSinks() {
2964
+ return [...this.sinks];
2965
+ }
2966
+ addSink(sink) {
2967
+ this.sinks.push(sink);
2968
+ }
2969
+ /** Mark the start of a connected session. Called from BLE connect path. */
2970
+ beginSession() {
2971
+ if (!this.enabled) return;
2972
+ this.sessionId = makeId();
2973
+ this.connectedAt = Date.now();
2974
+ this.events = [];
2975
+ this.batteryHistory = [];
2976
+ this.recordEvent("connect");
2977
+ }
2978
+ recordEvent(kind, data) {
2979
+ if (!this.enabled) return;
2980
+ const event = { t: Date.now(), kind };
2981
+ if (data) event.data = data;
2982
+ this.events.push(event);
2983
+ if (this.events.length > RING_BUFFER_SIZE) {
2984
+ this.events.splice(0, RING_BUFFER_DRAIN);
2985
+ }
2986
+ for (const sink of this.sinks) {
2987
+ if (sink.onEvent) {
2988
+ try {
2989
+ sink.onEvent(event);
2990
+ } catch (e) {
2991
+ console.error("Diagnostics sink onEvent error:", e);
2992
+ }
2993
+ }
2994
+ }
2995
+ }
2996
+ recordCommandPayload(kind, data, extra) {
2997
+ if (!this.enabled) return;
2998
+ const payload = { ...extra };
2999
+ if (this.capturePayloads) {
3000
+ payload.payloadHex = bytesToHex(data);
3001
+ payload.payloadLen = data.length;
3002
+ }
3003
+ this.recordEvent(kind, payload);
3004
+ }
3005
+ recordBattery(mv, pct) {
3006
+ if (!this.enabled) return;
3007
+ this.batteryHistory.push({ t: Date.now(), mv, pct });
3008
+ if (this.batteryHistory.length > BATTERY_HISTORY_SIZE) {
3009
+ this.batteryHistory.splice(0, this.batteryHistory.length - BATTERY_HISTORY_SIZE);
3010
+ }
3011
+ }
3012
+ /** Forwards a log line into the events ring (called by Logger when bridged). */
3013
+ recordLog(level, message, context) {
3014
+ if (!this.enabled) return;
3015
+ this.recordEvent("log", { level, message, context });
3016
+ }
3017
+ /**
3018
+ * Capture an incident snapshot and dispatch to sinks.
3019
+ * Must be called BEFORE the BLE layer clears state.
3020
+ */
3021
+ recordIncident(inputs) {
3022
+ if (!this.enabled) return null;
3023
+ const triggeredAt = Date.now();
3024
+ const inFlightCommandAgeMs = inputs.commandQueue.currentCommand ? triggeredAt - inputs.commandQueue.currentCommand.timestamp : null;
3025
+ const report = {
3026
+ schemaVersion: 1,
3027
+ incidentId: makeId(),
3028
+ sessionId: this.sessionId || makeId(),
3029
+ cause: inputs.cause,
3030
+ triggeredAt,
3031
+ connectedAt: this.connectedAt,
3032
+ sessionDurationMs: this.connectedAt ? triggeredAt - this.connectedAt : 0,
3033
+ connectionStatus: { ...inputs.connectionStatus },
3034
+ deviceInformation: { ...inputs.deviceInformation },
3035
+ commandQueue: {
3036
+ queueLength: inputs.commandQueue.queueLength,
3037
+ isProcessing: inputs.commandQueue.isProcessing,
3038
+ currentCommand: inputs.commandQueue.currentCommand ? { ...inputs.commandQueue.currentCommand } : null
3039
+ },
3040
+ inFlightCommandAgeMs,
3041
+ towerState: inputs.towerState ? JSON.parse(JSON.stringify(inputs.towerState)) : null,
3042
+ brokenSeals: [...inputs.brokenSeals],
3043
+ recentEvents: [...this.events],
3044
+ batteryHistory: [...this.batteryHistory],
3045
+ library: { version: LIBRARY_VERSION, platform: detectPlatform() },
3046
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
3047
+ };
3048
+ this.lastIncident = report;
3049
+ this.recordEvent("disconnect", { cause: inputs.cause });
3050
+ for (const sink of this.sinks) {
3051
+ try {
3052
+ const result = sink.onIncident(report);
3053
+ if (result && typeof result.then === "function") {
3054
+ result.catch((e) => console.error("Diagnostics sink onIncident error:", e));
3055
+ }
3056
+ } catch (e) {
3057
+ console.error("Diagnostics sink onIncident error:", e);
3058
+ }
3059
+ }
3060
+ return report;
3061
+ }
3062
+ getRingBuffer() {
3063
+ return [...this.events];
3064
+ }
3065
+ getBatteryHistory() {
3066
+ return [...this.batteryHistory];
3067
+ }
3068
+ getSessionId() {
3069
+ return this.sessionId;
3070
+ }
3071
+ getConnectedAt() {
3072
+ return this.connectedAt;
3073
+ }
3074
+ getLastIncident() {
3075
+ return this.lastIncident;
3076
+ }
3077
+ clearRingBuffer() {
3078
+ this.events = [];
3079
+ this.batteryHistory = [];
3080
+ }
3081
+ };
3082
+
2810
3083
  // src/UltimateDarkTower.ts
2811
3084
  var UltimateDarkTower = class {
2812
3085
  constructor(config) {
3086
+ this.beforeUnloadHandler = null;
2813
3087
  // tower configuration
2814
3088
  this.retrySendCommandCountRef = { value: 0 };
2815
3089
  this.retrySendCommandMax = DEFAULT_RETRY_SEND_COMMAND_MAX;
@@ -2851,8 +3125,10 @@ var UltimateDarkTower = class {
2851
3125
  // utility
2852
3126
  this._logDetail = false;
2853
3127
  this.initializeLogger();
3128
+ this.initializeDiagnostics(config?.diagnostics);
2854
3129
  this.initializeComponents(config);
2855
3130
  this.setupTowerResponseCallback();
3131
+ this.installBeforeUnloadHandler();
2856
3132
  }
2857
3133
  /**
2858
3134
  * Initialize the logger with default console output
@@ -2861,6 +3137,20 @@ var UltimateDarkTower = class {
2861
3137
  this.logger = new Logger();
2862
3138
  this.logger.addOutput(new ConsoleOutput());
2863
3139
  }
3140
+ /**
3141
+ * Initialize the diagnostics recorder. Always constructed; `enabled` defaults
3142
+ * to false, so when no config is supplied the recorder is a no-op aside from
3143
+ * a single boolean check at each hook site.
3144
+ */
3145
+ initializeDiagnostics(config) {
3146
+ const sinks = config?.sinks ?? (config?.enabled ? [new InMemorySink()] : []);
3147
+ this.diagnosticsRecorder = new UdtDiagnosticsRecorder({
3148
+ enabled: config?.enabled ?? false,
3149
+ capturePayloads: config?.capturePayloads,
3150
+ sinks
3151
+ });
3152
+ this.logger.setDiagnosticsTarget(this.diagnosticsRecorder);
3153
+ }
2864
3154
  /**
2865
3155
  * Initialize all tower components and their dependencies
2866
3156
  */
@@ -2872,11 +3162,16 @@ var UltimateDarkTower = class {
2872
3162
  adapter = BluetoothAdapterFactory.create(config.platform);
2873
3163
  }
2874
3164
  this.towerEventCallbacks = this.createTowerEventCallbacks();
2875
- this.bleConnection = new UdtBleConnection(this.logger, this.towerEventCallbacks, adapter);
3165
+ this.bleConnection = new UdtBleConnection(this.logger, this.towerEventCallbacks, adapter, this.diagnosticsRecorder);
2876
3166
  this.responseProcessor = new TowerResponseProcessor(this.logDetail);
2877
3167
  this.commandFactory = new UdtCommandFactory();
2878
3168
  const commandDependencies = this.createCommandDependencies();
2879
3169
  this.towerCommands = new UdtTowerCommands(commandDependencies);
3170
+ this.bleConnection.setDiagnosticsSnapshotProviders({
3171
+ commandQueue: () => this.towerCommands.getQueueStatus(),
3172
+ towerState: () => this.currentTowerState,
3173
+ brokenSeals: () => Array.from(this.brokenSeals)
3174
+ });
2880
3175
  if (config?.brokenSeals) {
2881
3176
  for (const seal of config.brokenSeals) {
2882
3177
  const sealKey = `${seal.level}-${seal.side}`;
@@ -2884,6 +3179,23 @@ var UltimateDarkTower = class {
2884
3179
  }
2885
3180
  }
2886
3181
  }
3182
+ /**
3183
+ * Browser-only: synthesize a `page_unload` incident if the page closes while
3184
+ * connected. Without this, refreshing the page during a hang loses the
3185
+ * lead-up context. IndexedDB writes during unload are best-effort.
3186
+ */
3187
+ installBeforeUnloadHandler() {
3188
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
3189
+ this.beforeUnloadHandler = () => {
3190
+ if (this.diagnosticsRecorder.enabled && this.bleConnection?.isConnected) {
3191
+ try {
3192
+ this.bleConnection.recordIncidentPublic("page_unload");
3193
+ } catch {
3194
+ }
3195
+ }
3196
+ };
3197
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
3198
+ }
2887
3199
  /**
2888
3200
  * Set up the tower response callback after all components are initialized
2889
3201
  */
@@ -2938,7 +3250,8 @@ var UltimateDarkTower = class {
2938
3250
  retrySendCommandCount: this.retrySendCommandCountRef,
2939
3251
  retrySendCommandMax: this.retrySendCommandMax,
2940
3252
  getCurrentTowerState: () => this.currentTowerState,
2941
- setTowerState: (newState, source) => this.setTowerState(newState, source)
3253
+ setTowerState: (newState, source) => this.setTowerState(newState, source),
3254
+ recorder: this.diagnosticsRecorder
2942
3255
  };
2943
3256
  }
2944
3257
  /**
@@ -3230,6 +3543,7 @@ var UltimateDarkTower = class {
3230
3543
  updateTowerStateFromResponse(stateData) {
3231
3544
  const newState = rtdt_unpack_state(stateData);
3232
3545
  newState.audio = { sample: 0, loop: false, volume: this.currentTowerState.audio.volume };
3546
+ newState.led_sequence = 0;
3233
3547
  this.setTowerState(newState, "tower response");
3234
3548
  }
3235
3549
  //#endregion
@@ -3547,19 +3861,680 @@ var UltimateDarkTower = class {
3547
3861
  async cleanup() {
3548
3862
  this.logger.info("Cleaning up UltimateDarkTower instance", "[UDT]");
3549
3863
  this.towerCommands.clearQueue();
3864
+ if (this.beforeUnloadHandler && typeof window !== "undefined") {
3865
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
3866
+ this.beforeUnloadHandler = null;
3867
+ }
3868
+ this.logger.setDiagnosticsTarget(null);
3550
3869
  await this.bleConnection.cleanup();
3551
3870
  }
3552
3871
  //#endregion
3872
+ //#region Diagnostics (BLE flight recorder)
3873
+ /**
3874
+ * Get the diagnostics recorder for direct access (live ring buffer, sinks,
3875
+ * runtime enable/disable). Always returns a recorder; check `.enabled` to
3876
+ * see whether capture is active.
3877
+ */
3878
+ getDiagnosticsRecorder() {
3879
+ return this.diagnosticsRecorder;
3880
+ }
3881
+ /**
3882
+ * Toggle diagnostics capture at runtime without reconstructing the tower.
3883
+ * When enabled mid-session, the next BLE event begins populating the buffer.
3884
+ */
3885
+ setDiagnosticsEnabled(enabled) {
3886
+ this.diagnosticsRecorder.enabled = enabled;
3887
+ }
3888
+ /**
3889
+ * Whether diagnostics capture is currently active.
3890
+ */
3891
+ isDiagnosticsEnabled() {
3892
+ return this.diagnosticsRecorder.enabled;
3893
+ }
3894
+ /**
3895
+ * Get the most recent disconnect incident report, or null if none captured
3896
+ * since this instance was created.
3897
+ */
3898
+ getLastIncident() {
3899
+ return this.diagnosticsRecorder.getLastIncident();
3900
+ }
3901
+ /**
3902
+ * Export current ring buffer + last incident as JSON for sharing/analysis.
3903
+ * Useful as a one-liner in a "copy diagnostic info" button.
3904
+ */
3905
+ exportDiagnosticsJSON() {
3906
+ return JSON.stringify({
3907
+ schemaVersion: 1,
3908
+ capturedAt: Date.now(),
3909
+ sessionId: this.diagnosticsRecorder.getSessionId(),
3910
+ ringBuffer: this.diagnosticsRecorder.getRingBuffer(),
3911
+ batteryHistory: this.diagnosticsRecorder.getBatteryHistory(),
3912
+ lastIncident: this.diagnosticsRecorder.getLastIncident()
3913
+ }, null, 2);
3914
+ }
3915
+ //#endregion
3553
3916
  };
3554
3917
  var UltimateDarkTower_default = UltimateDarkTower;
3555
3918
 
3556
3919
  // src/index.ts
3557
3920
  init_udtConstants();
3558
3921
  init_udtBluetoothAdapter();
3922
+
3923
+ // src/sinks/IndexedDBSink.ts
3924
+ var DB_NAME = "udt-diagnostics";
3925
+ var DB_VERSION = 1;
3926
+ var STORE_NAME = "incidents";
3927
+ function indexedDBAvailable() {
3928
+ try {
3929
+ return typeof indexedDB !== "undefined";
3930
+ } catch {
3931
+ return false;
3932
+ }
3933
+ }
3934
+ function openDb() {
3935
+ return new Promise((resolve, reject) => {
3936
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
3937
+ req.onupgradeneeded = () => {
3938
+ const db = req.result;
3939
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3940
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "incidentId" });
3941
+ store.createIndex("triggeredAt", "triggeredAt", { unique: false });
3942
+ }
3943
+ };
3944
+ req.onsuccess = () => resolve(req.result);
3945
+ req.onerror = () => reject(req.error);
3946
+ });
3947
+ }
3948
+ var IndexedDBSink = class {
3949
+ constructor(maxIncidents = 50) {
3950
+ this.dbPromise = null;
3951
+ this.maxIncidents = maxIncidents;
3952
+ this.available = indexedDBAvailable();
3953
+ }
3954
+ async getDb() {
3955
+ if (!this.available) return null;
3956
+ if (!this.dbPromise) {
3957
+ this.dbPromise = openDb().catch((err) => {
3958
+ this.available = false;
3959
+ this.dbPromise = null;
3960
+ throw err;
3961
+ });
3962
+ }
3963
+ try {
3964
+ return await this.dbPromise;
3965
+ } catch {
3966
+ return null;
3967
+ }
3968
+ }
3969
+ async onIncident(report) {
3970
+ const db = await this.getDb();
3971
+ if (!db) return;
3972
+ await new Promise((resolve, reject) => {
3973
+ const tx = db.transaction(STORE_NAME, "readwrite");
3974
+ const store = tx.objectStore(STORE_NAME);
3975
+ store.put(report);
3976
+ tx.oncomplete = () => resolve();
3977
+ tx.onerror = () => reject(tx.error);
3978
+ tx.onabort = () => reject(tx.error);
3979
+ }).catch((e) => console.error("IndexedDBSink put failed:", e));
3980
+ await this.evictOld();
3981
+ }
3982
+ async list() {
3983
+ const db = await this.getDb();
3984
+ if (!db) return [];
3985
+ return new Promise((resolve, reject) => {
3986
+ const tx = db.transaction(STORE_NAME, "readonly");
3987
+ const store = tx.objectStore(STORE_NAME);
3988
+ const req = store.getAll();
3989
+ req.onsuccess = () => {
3990
+ const all = req.result.slice();
3991
+ all.sort((a, b) => b.triggeredAt - a.triggeredAt);
3992
+ resolve(all);
3993
+ };
3994
+ req.onerror = () => reject(req.error);
3995
+ });
3996
+ }
3997
+ async get(incidentId) {
3998
+ const db = await this.getDb();
3999
+ if (!db) return void 0;
4000
+ return new Promise((resolve, reject) => {
4001
+ const tx = db.transaction(STORE_NAME, "readonly");
4002
+ const store = tx.objectStore(STORE_NAME);
4003
+ const req = store.get(incidentId);
4004
+ req.onsuccess = () => resolve(req.result);
4005
+ req.onerror = () => reject(req.error);
4006
+ });
4007
+ }
4008
+ async delete(incidentId) {
4009
+ const db = await this.getDb();
4010
+ if (!db) return;
4011
+ await new Promise((resolve, reject) => {
4012
+ const tx = db.transaction(STORE_NAME, "readwrite");
4013
+ tx.objectStore(STORE_NAME).delete(incidentId);
4014
+ tx.oncomplete = () => resolve();
4015
+ tx.onerror = () => reject(tx.error);
4016
+ }).catch((e) => console.error("IndexedDBSink delete failed:", e));
4017
+ }
4018
+ async clear() {
4019
+ const db = await this.getDb();
4020
+ if (!db) return;
4021
+ await new Promise((resolve, reject) => {
4022
+ const tx = db.transaction(STORE_NAME, "readwrite");
4023
+ tx.objectStore(STORE_NAME).clear();
4024
+ tx.oncomplete = () => resolve();
4025
+ tx.onerror = () => reject(tx.error);
4026
+ }).catch((e) => console.error("IndexedDBSink clear failed:", e));
4027
+ }
4028
+ /** Insert an externally-supplied report (e.g. from a JSON import). */
4029
+ async put(report) {
4030
+ return this.onIncident(report);
4031
+ }
4032
+ async evictOld() {
4033
+ const db = await this.getDb();
4034
+ if (!db) return;
4035
+ await new Promise((resolve) => {
4036
+ const tx = db.transaction(STORE_NAME, "readwrite");
4037
+ const store = tx.objectStore(STORE_NAME);
4038
+ const countReq = store.count();
4039
+ countReq.onsuccess = () => {
4040
+ const total = countReq.result;
4041
+ if (total <= this.maxIncidents) {
4042
+ resolve();
4043
+ return;
4044
+ }
4045
+ const toRemove = total - this.maxIncidents;
4046
+ const idx = store.index("triggeredAt");
4047
+ const cursorReq = idx.openCursor();
4048
+ let removed = 0;
4049
+ cursorReq.onsuccess = () => {
4050
+ const cursor = cursorReq.result;
4051
+ if (!cursor || removed >= toRemove) {
4052
+ resolve();
4053
+ return;
4054
+ }
4055
+ cursor.delete();
4056
+ removed++;
4057
+ cursor.continue();
4058
+ };
4059
+ cursorReq.onerror = () => resolve();
4060
+ };
4061
+ countReq.onerror = () => resolve();
4062
+ });
4063
+ }
4064
+ };
4065
+
4066
+ // src/udtSeedParser.ts
4067
+ var ALPHABET = "a123456789bcdefghijklmnpqrstuvwxyz";
4068
+ var BASE = 34;
4069
+ var SETUP_LENGTH = 6;
4070
+ var RNG_SEED_LENGTH = 6;
4071
+ var SEED_LENGTH = SETUP_LENGTH + RNG_SEED_LENGTH;
4072
+ var CHAR_TO_VALUE = /* @__PURE__ */ new Map();
4073
+ var VALUE_TO_CHAR = /* @__PURE__ */ new Map();
4074
+ for (let i = 0; i < ALPHABET.length; i++) {
4075
+ CHAR_TO_VALUE.set(ALPHABET[i], i);
4076
+ VALUE_TO_CHAR.set(i, ALPHABET[i]);
4077
+ }
4078
+ var TIER1_FOES = ["Brigands", "Oreks", "Shadow Wolves", "Spine Fiends"];
4079
+ var TIER2_FOES = ["Frost Trolls", "Clan of Neuri", "Lemures", "Widowmade Spiders"];
4080
+ var TIER3_FOES = ["Dragons", "Mormos", "Striga", "Titans"];
4081
+ var ADVERSARIES = [
4082
+ "Ashstrider",
4083
+ "Bane of Omens",
4084
+ "Empress of Shades",
4085
+ "Gaze Eternal",
4086
+ "Gravemaw",
4087
+ "Isa the Exile",
4088
+ "Lingering Rot",
4089
+ "Utuk'Ku"
4090
+ ];
4091
+ var ALLIES = [
4092
+ "Gleb",
4093
+ "Grigor",
4094
+ "Hakan",
4095
+ "Letha",
4096
+ "Miras",
4097
+ "Nimet",
4098
+ "Tomas",
4099
+ "Vasa",
4100
+ "Yana",
4101
+ "Zaida"
4102
+ ];
4103
+ var DIFFICULTIES = ["Heroic", "Gritty"];
4104
+ var GAME_SOURCES = ["Core", "Competitive"];
4105
+ function charToValue(c) {
4106
+ const v = CHAR_TO_VALUE.get(c.toLowerCase());
4107
+ if (v === void 0) {
4108
+ throw new Error(`Invalid seed character: '${c}'`);
4109
+ }
4110
+ return v;
4111
+ }
4112
+ function valueToChar(v) {
4113
+ const c = VALUE_TO_CHAR.get(v);
4114
+ if (c === void 0) {
4115
+ throw new Error(`Invalid seed value: ${v} (must be 0\u2013${BASE - 1})`);
4116
+ }
4117
+ return c;
4118
+ }
4119
+ function validateSeed(seed) {
4120
+ const stripped = seed.replace(/[-\s]/g, "").toLowerCase();
4121
+ if (stripped.length !== SEED_LENGTH) {
4122
+ throw new Error(`Invalid seed length: expected ${SEED_LENGTH} characters, got ${stripped.length}`);
4123
+ }
4124
+ for (const c of stripped) {
4125
+ if (!CHAR_TO_VALUE.has(c)) {
4126
+ throw new Error(`Invalid seed character: '${c}'`);
4127
+ }
4128
+ }
4129
+ const upper = stripped.toUpperCase();
4130
+ return `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4131
+ }
4132
+ function decodeSeed(seed) {
4133
+ const normalized = validateSeed(seed);
4134
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4135
+ const setup = [];
4136
+ for (let i = 0; i < SETUP_LENGTH; i++) {
4137
+ setup.push(charToValue(stripped[i]));
4138
+ }
4139
+ let rngSeed = 0;
4140
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4141
+ const value = charToValue(stripped[SETUP_LENGTH + i]);
4142
+ rngSeed += value * Math.round(Math.pow(BASE, i));
4143
+ }
4144
+ const foeByteA = setup[0];
4145
+ const tier1 = foeByteA & 3;
4146
+ const tier2 = (foeByteA & 12) >> 2;
4147
+ const foeByteB = setup[1];
4148
+ const tier3 = (foeByteA & 16) >> 4 | (foeByteB & 16) >> 3;
4149
+ const adversaryIndex = foeByteB & 15;
4150
+ const allyIndex = setup[2];
4151
+ const extra = setup[3];
4152
+ const difficultyIndex = extra & 1;
4153
+ const expansionBits = (extra & 6) >> 1;
4154
+ const sourceBits = (extra & 8) >> 2;
4155
+ const playerCount = (setup[5] & 3) + 1;
4156
+ const expansions = [];
4157
+ if (expansionBits & 1) expansions.push("Monuments");
4158
+ if (expansionBits & 2) expansions.push("Alliances");
4159
+ let source;
4160
+ switch (sourceBits) {
4161
+ case 2:
4162
+ source = "Competitive";
4163
+ break;
4164
+ default:
4165
+ source = "Core";
4166
+ break;
4167
+ }
4168
+ const seedBank = {
4169
+ initializationSeed: rngSeed,
4170
+ questSeed: rngSeed - 1,
4171
+ seedString: normalized
4172
+ };
4173
+ return {
4174
+ seed: normalized,
4175
+ tier1Foe: TIER1_FOES[tier1],
4176
+ tier2Foe: TIER2_FOES[tier2],
4177
+ tier3Foe: TIER3_FOES[tier3],
4178
+ adversary: ADVERSARIES[adversaryIndex],
4179
+ ally: ALLIES[allyIndex],
4180
+ difficulty: DIFFICULTIES[difficultyIndex],
4181
+ source,
4182
+ expansions,
4183
+ playerCount,
4184
+ rngSeed,
4185
+ seedBank,
4186
+ setup
4187
+ };
4188
+ }
4189
+ function decodeRngSeed(seed) {
4190
+ const normalized = validateSeed(seed);
4191
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4192
+ let rngSeed = 0;
4193
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4194
+ const value = charToValue(stripped[SETUP_LENGTH + i]);
4195
+ rngSeed += value * Math.round(Math.pow(BASE, i));
4196
+ }
4197
+ return rngSeed;
4198
+ }
4199
+ function createSeed(config) {
4200
+ let foeByteA = 0;
4201
+ let foeByteB = 0;
4202
+ const tier1Index = TIER1_FOES.indexOf(config.foes[0]);
4203
+ const tier2Index = TIER2_FOES.indexOf(config.foes[1]);
4204
+ const tier3Index = TIER3_FOES.indexOf(config.foes[2]);
4205
+ if (tier1Index < 0) throw new Error(`Invalid Tier 1 foe: ${config.foes[0]}`);
4206
+ if (tier2Index < 0) throw new Error(`Invalid Tier 2 foe: ${config.foes[1]}`);
4207
+ if (tier3Index < 0) throw new Error(`Invalid Tier 3 foe: ${config.foes[2]}`);
4208
+ foeByteA = tier1Index & 3;
4209
+ foeByteA |= (tier2Index & 3) << 2;
4210
+ foeByteA |= (tier3Index & 1) << 4;
4211
+ foeByteB |= (tier3Index >> 1 & 1) << 4;
4212
+ const adversaryIndex = ADVERSARIES.indexOf(config.adversary);
4213
+ if (adversaryIndex < 0) throw new Error(`Invalid adversary: ${config.adversary}`);
4214
+ foeByteB |= adversaryIndex & 15;
4215
+ const allyIndex = ALLIES.indexOf(config.ally);
4216
+ if (allyIndex < 0) throw new Error(`Invalid ally: ${config.ally}`);
4217
+ let extraByte = 0;
4218
+ if (config.difficulty === "Gritty") extraByte |= 1;
4219
+ for (const expansion of config.expansions) {
4220
+ switch (expansion) {
4221
+ case "Monuments":
4222
+ extraByte |= 2;
4223
+ break;
4224
+ case "Alliances":
4225
+ extraByte |= 4;
4226
+ break;
4227
+ }
4228
+ }
4229
+ if (config.source === "Competitive") extraByte |= 8;
4230
+ const versionByte = 0;
4231
+ const playerCountByte = Math.max(0, Math.min(3, config.playerCount - 1));
4232
+ let seedStr = valueToChar(foeByteA) + valueToChar(foeByteB) + valueToChar(allyIndex) + valueToChar(extraByte) + valueToChar(versionByte) + valueToChar(playerCountByte);
4233
+ let rngValue = 0;
4234
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4235
+ const value = Math.floor(Math.random() * BASE);
4236
+ seedStr += valueToChar(value);
4237
+ rngValue += value * Math.round(Math.pow(BASE, i));
4238
+ }
4239
+ const upper = seedStr.toUpperCase();
4240
+ const formatted = `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4241
+ return { seed: formatted, rngValue };
4242
+ }
4243
+ function encodeSeed(config, rngValue) {
4244
+ let foeByteA = 0;
4245
+ let foeByteB = 0;
4246
+ const tier1Index = TIER1_FOES.indexOf(config.foes[0]);
4247
+ const tier2Index = TIER2_FOES.indexOf(config.foes[1]);
4248
+ const tier3Index = TIER3_FOES.indexOf(config.foes[2]);
4249
+ if (tier1Index < 0) throw new Error(`Invalid Tier 1 foe: ${config.foes[0]}`);
4250
+ if (tier2Index < 0) throw new Error(`Invalid Tier 2 foe: ${config.foes[1]}`);
4251
+ if (tier3Index < 0) throw new Error(`Invalid Tier 3 foe: ${config.foes[2]}`);
4252
+ foeByteA = tier1Index & 3;
4253
+ foeByteA |= (tier2Index & 3) << 2;
4254
+ foeByteA |= (tier3Index & 1) << 4;
4255
+ foeByteB |= (tier3Index >> 1 & 1) << 4;
4256
+ const adversaryIndex = ADVERSARIES.indexOf(config.adversary);
4257
+ if (adversaryIndex < 0) throw new Error(`Invalid adversary: ${config.adversary}`);
4258
+ foeByteB |= adversaryIndex & 15;
4259
+ const allyIndex = ALLIES.indexOf(config.ally);
4260
+ if (allyIndex < 0) throw new Error(`Invalid ally: ${config.ally}`);
4261
+ let extraByte = 0;
4262
+ if (config.difficulty === "Gritty") extraByte |= 1;
4263
+ for (const expansion of config.expansions) {
4264
+ switch (expansion) {
4265
+ case "Monuments":
4266
+ extraByte |= 2;
4267
+ break;
4268
+ case "Alliances":
4269
+ extraByte |= 4;
4270
+ break;
4271
+ }
4272
+ }
4273
+ if (config.source === "Competitive") extraByte |= 8;
4274
+ const versionByte = 0;
4275
+ const playerCountByte = Math.max(0, Math.min(3, config.playerCount - 1));
4276
+ let seedStr = valueToChar(foeByteA) + valueToChar(foeByteB) + valueToChar(allyIndex) + valueToChar(extraByte) + valueToChar(versionByte) + valueToChar(playerCountByte);
4277
+ let remaining = rngValue;
4278
+ for (let i = 0; i < RNG_SEED_LENGTH; i++) {
4279
+ const digit = remaining % BASE;
4280
+ seedStr += valueToChar(digit);
4281
+ remaining = Math.floor(remaining / BASE);
4282
+ }
4283
+ const upper = seedStr.toUpperCase();
4284
+ return `${upper.slice(0, 4)}-${upper.slice(4, 8)}-${upper.slice(8, 12)}`;
4285
+ }
4286
+ function compareSeedsRaw(seed1, seed2) {
4287
+ const n1 = validateSeed(seed1);
4288
+ const n2 = validateSeed(seed2);
4289
+ const s1 = n1.replace(/-/g, "").toLowerCase();
4290
+ const s2 = n2.replace(/-/g, "").toLowerCase();
4291
+ const diffs = [];
4292
+ for (let i = 0; i < SEED_LENGTH; i++) {
4293
+ const v1 = charToValue(s1[i]);
4294
+ const v2 = charToValue(s2[i]);
4295
+ if (v1 !== v2) {
4296
+ diffs.push({
4297
+ charIndex: i,
4298
+ value1: v1,
4299
+ value2: v2,
4300
+ char1: s1[i],
4301
+ char2: s2[i]
4302
+ });
4303
+ }
4304
+ }
4305
+ return {
4306
+ seed1: n1,
4307
+ seed2: n2,
4308
+ diffs,
4309
+ setupDiffs: diffs.filter((d) => d.charIndex < SETUP_LENGTH),
4310
+ rngDiffs: diffs.filter((d) => d.charIndex >= SETUP_LENGTH)
4311
+ };
4312
+ }
4313
+ var SETUP_FIELD_LABELS = {
4314
+ 0: "Tier1/Tier2/Tier3lo",
4315
+ 1: "Adversary/Tier3hi",
4316
+ 2: "Ally",
4317
+ 3: "Difficulty/Expansions/Source",
4318
+ 4: "Version",
4319
+ 5: "PlayerCount"
4320
+ };
4321
+ function dumpSeedChars(seed) {
4322
+ const normalized = validateSeed(seed);
4323
+ const stripped = normalized.replace(/-/g, "").toLowerCase();
4324
+ const chars = [];
4325
+ for (let i = 0; i < SEED_LENGTH; i++) {
4326
+ const isSetup = i < SETUP_LENGTH;
4327
+ chars.push({
4328
+ index: i,
4329
+ char: stripped[i],
4330
+ value: charToValue(stripped[i]),
4331
+ section: isSetup ? "setup" : "rng",
4332
+ field: isSetup ? SETUP_FIELD_LABELS[i] : void 0
4333
+ });
4334
+ }
4335
+ return { seed: normalized, chars };
4336
+ }
4337
+
4338
+ // src/udtSystemRandom.ts
4339
+ var INT32_MAX = 2147483647;
4340
+ var MSEED = 161803398;
4341
+ function toInt32(n) {
4342
+ return n | 0;
4343
+ }
4344
+ var SystemRandom = class {
4345
+ /**
4346
+ * Create a new PRNG instance with the given seed.
4347
+ * Matches C# `new System.Random(seed)` exactly.
4348
+ */
4349
+ constructor(seed) {
4350
+ this.seedArray = new Array(56).fill(0);
4351
+ this.inext = 0;
4352
+ this.inextp = 0;
4353
+ this.initialize(seed);
4354
+ }
4355
+ /**
4356
+ * Replicate .NET's System.Random constructor seeding algorithm.
4357
+ */
4358
+ initialize(seed) {
4359
+ let subtraction;
4360
+ if (seed === -2147483648) {
4361
+ subtraction = INT32_MAX;
4362
+ } else {
4363
+ subtraction = Math.abs(seed);
4364
+ }
4365
+ let mj = toInt32(MSEED - subtraction);
4366
+ this.seedArray[55] = mj;
4367
+ let mk = 1;
4368
+ for (let i = 1; i < 55; i++) {
4369
+ const ii = 21 * i % 55;
4370
+ this.seedArray[ii] = mk;
4371
+ mk = toInt32(mj - mk);
4372
+ if (mk < 0) mk = toInt32(mk + INT32_MAX);
4373
+ mj = this.seedArray[ii];
4374
+ }
4375
+ for (let k = 1; k < 5; k++) {
4376
+ for (let i = 1; i < 56; i++) {
4377
+ this.seedArray[i] = toInt32(this.seedArray[i] - this.seedArray[1 + (i + 30) % 55]);
4378
+ if (this.seedArray[i] < 0) {
4379
+ this.seedArray[i] = toInt32(this.seedArray[i] + INT32_MAX);
4380
+ }
4381
+ }
4382
+ }
4383
+ this.inext = 0;
4384
+ this.inextp = 21;
4385
+ }
4386
+ /**
4387
+ * Internal sample — returns value in range [0, Int32.MaxValue).
4388
+ * Matches C#'s InternalSample().
4389
+ */
4390
+ internalSample() {
4391
+ let retVal;
4392
+ let locINext = this.inext;
4393
+ let locINextp = this.inextp;
4394
+ if (++locINext >= 56) locINext = 1;
4395
+ if (++locINextp >= 56) locINextp = 1;
4396
+ retVal = toInt32(this.seedArray[locINext] - this.seedArray[locINextp]);
4397
+ if (retVal === INT32_MAX) retVal--;
4398
+ if (retVal < 0) retVal = toInt32(retVal + INT32_MAX);
4399
+ this.seedArray[locINext] = retVal;
4400
+ this.inext = locINext;
4401
+ this.inextp = locINextp;
4402
+ return retVal;
4403
+ }
4404
+ /**
4405
+ * Sample — returns a double in range [0.0, 1.0).
4406
+ * Matches C#'s Sample().
4407
+ */
4408
+ sample() {
4409
+ return this.internalSample() * (1 / INT32_MAX);
4410
+ }
4411
+ /**
4412
+ * Returns a non-negative random integer less than Int32.MaxValue.
4413
+ * Matches C# `Random.Next()`.
4414
+ */
4415
+ next() {
4416
+ return this.internalSample();
4417
+ }
4418
+ /**
4419
+ * Returns a non-negative random integer less than maxValue.
4420
+ * Matches C# `Random.Next(maxValue)`.
4421
+ */
4422
+ nextMax(maxValue) {
4423
+ if (maxValue < 0) {
4424
+ throw new Error("maxValue must be non-negative");
4425
+ }
4426
+ return toInt32(this.sample() * maxValue);
4427
+ }
4428
+ /**
4429
+ * Returns a random integer in range [minValue, maxValue).
4430
+ * Matches C# `Random.Next(minValue, maxValue)`.
4431
+ */
4432
+ nextRange(minValue, maxValue) {
4433
+ if (minValue > maxValue) {
4434
+ throw new Error("minValue must be less than or equal to maxValue");
4435
+ }
4436
+ const range = maxValue - minValue;
4437
+ if (range <= INT32_MAX) {
4438
+ return toInt32(this.sample() * range) + minValue;
4439
+ }
4440
+ return toInt32(this.internalSample() * (1 / INT32_MAX) * range) + minValue;
4441
+ }
4442
+ /**
4443
+ * Returns a random double in range [0.0, 1.0).
4444
+ * Matches C# `Random.NextDouble()`.
4445
+ */
4446
+ nextDouble() {
4447
+ return this.sample();
4448
+ }
4449
+ };
4450
+
4451
+ // src/udtGameBoard.ts
4452
+ var BOARD_GROUPINGS = {
4453
+ /** Dayside and Fivepint (North kingdom lakes). */
4454
+ LONG_WATER: "Long Water",
4455
+ /** Delmsmire, Arkartus, and Yellowpike (West kingdom forests). */
4456
+ THE_GREAT_WOODS: "The Great Woods",
4457
+ /** The Throne, The Cloister, and Archmont (South kingdom grasslands). */
4458
+ REGAL_RUN: "Regal Run"
4459
+ };
4460
+ var BOARD_LOCATIONS = [
4461
+ // ── North ───────────────────────────────────────────────────────────────
4462
+ { name: "Broken Lands", terrain: "Hills", kingdom: "north" },
4463
+ { name: "Dayside", terrain: "Lake", building: "Bazaar", kingdom: "north", grouping: BOARD_GROUPINGS.LONG_WATER },
4464
+ { name: "Egan's End", terrain: "Grasslands", building: "Village", kingdom: "north" },
4465
+ { name: "Fivepint", terrain: "Lake", kingdom: "north", grouping: BOARD_GROUPINGS.LONG_WATER },
4466
+ { name: "Green Bridge", terrain: "Grasslands", kingdom: "north" },
4467
+ { name: "Lodestone Mountains", terrain: "Mountains", kingdom: "north" },
4468
+ { name: "Lower Ice Fangs", terrain: "Mountains", kingdom: "north" },
4469
+ { name: "Muted Forest", terrain: "Forest", kingdom: "north" },
4470
+ { name: "Peaks of the Djinn", terrain: "Mountains", kingdom: "north" },
4471
+ { name: "Pearl of the North", terrain: "Grasslands", kingdom: "north" },
4472
+ { name: "Radiant Mountains", terrain: "Mountains", building: "Citadel", kingdom: "north" },
4473
+ { name: "Rimeweald", terrain: "Forest", kingdom: "north" },
4474
+ { name: "The Tundra", terrain: "Desert", kingdom: "north" },
4475
+ { name: "Tower Scar Desert", terrain: "Desert", kingdom: "north" },
4476
+ { name: "Upper Ice Fangs", terrain: "Mountains", building: "Sanctuary", kingdom: "north" },
4477
+ // ── East ────────────────────────────────────────────────────────────────
4478
+ { name: "Big Sister", terrain: "Mountains", kingdom: "east" },
4479
+ { name: "Bleak Wastes", terrain: "Desert", kingdom: "east" },
4480
+ { name: "Copper Grove", terrain: "Forest", kingdom: "east" },
4481
+ { name: "Dragontooth Lake", terrain: "Lake", kingdom: "east" },
4482
+ { name: "Duwani", terrain: "Grasslands", building: "Village", kingdom: "east" },
4483
+ { name: "Forest of Shades", terrain: "Forest", kingdom: "east" },
4484
+ { name: "Greater Tombstones", terrain: "Hills", building: "Sanctuary", kingdom: "east" },
4485
+ { name: "Inner Kinghills", terrain: "Hills", building: "Citadel", kingdom: "east" },
4486
+ { name: "Jewel Hills", terrain: "Hills", kingdom: "east" },
4487
+ { name: "Lake of Songs", terrain: "Lake", kingdom: "east" },
4488
+ { name: "Lesser Tombstones", terrain: "Hills", kingdom: "east" },
4489
+ { name: "Outer Kinghills", terrain: "Hills", kingdom: "east" },
4490
+ { name: "The Decaying Wilds", terrain: "Grasslands", kingdom: "east" },
4491
+ { name: "Three Rivers", terrain: "Grasslands", building: "Bazaar", kingdom: "east" },
4492
+ { name: "Utar's Barrows", terrain: "Desert", kingdom: "east" },
4493
+ // ── West ────────────────────────────────────────────────────────────────
4494
+ { name: "Anza", terrain: "Grasslands", building: "Village", kingdom: "west" },
4495
+ { name: "Arkartus", terrain: "Forest", building: "Sanctuary", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4496
+ { name: "Ash Hills", terrain: "Hills", kingdom: "west" },
4497
+ { name: "Cloudhold", terrain: "Mountains", kingdom: "west" },
4498
+ { name: "Delmsmire", terrain: "Forest", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4499
+ { name: "Hissing Groves", terrain: "Forest", building: "Citadel", kingdom: "west" },
4500
+ { name: "Idran Forest", terrain: "Forest", kingdom: "west" },
4501
+ { name: "Lonelight Hills", terrain: "Hills", kingdom: "west" },
4502
+ { name: "Lost Lands", terrain: "Desert", kingdom: "west" },
4503
+ { name: "Plains of Plovo", terrain: "Grasslands", building: "Bazaar", kingdom: "west" },
4504
+ { name: "Plains of Woldra", terrain: "Grasslands", kingdom: "west" },
4505
+ { name: "The Empty Glade", terrain: "Grasslands", kingdom: "west" },
4506
+ { name: "The Grass Sea", terrain: "Grasslands", kingdom: "west" },
4507
+ { name: "Weeping Waters", terrain: "Lake", kingdom: "west" },
4508
+ { name: "Yellowpike", terrain: "Forest", kingdom: "west", grouping: BOARD_GROUPINGS.THE_GREAT_WOODS },
4509
+ // ── South ───────────────────────────────────────────────────────────────
4510
+ { name: "Archmont", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4511
+ { name: "Azkol's Bane", terrain: "Desert", kingdom: "south" },
4512
+ { name: "Bone Hills", terrain: "Hills", kingdom: "south" },
4513
+ { name: "Howling Desert", terrain: "Desert", building: "Citadel", kingdom: "south" },
4514
+ { name: "Irontops", terrain: "Mountains", kingdom: "south" },
4515
+ { name: "Little Sister", terrain: "Mountains", kingdom: "south" },
4516
+ { name: "Middle Sister", terrain: "Mountains", kingdom: "south" },
4517
+ { name: "Mountains of the Watchers", terrain: "Mountains", kingdom: "south" },
4518
+ { name: "Pine Barrens", terrain: "Forest", kingdom: "south" },
4519
+ { name: "Sands of Madness", terrain: "Desert", building: "Sanctuary", kingdom: "south" },
4520
+ { name: "Southern Wastes", terrain: "Desert", building: "Village", kingdom: "south" },
4521
+ { name: "The Cloister", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4522
+ { name: "The Emerald Expanse", terrain: "Grasslands", building: "Bazaar", kingdom: "south" },
4523
+ { name: "The Throne", terrain: "Grasslands", kingdom: "south", grouping: BOARD_GROUPINGS.REGAL_RUN },
4524
+ { name: "Ulamel's Hollow", terrain: "Grasslands", kingdom: "south" }
4525
+ ];
4526
+ var BOARD_LOCATION_BY_NAME = Object.fromEntries(BOARD_LOCATIONS.map((loc) => [loc.name, loc]));
4527
+
4528
+ // src/index.ts
3559
4529
  var index_default = UltimateDarkTower_default;
3560
4530
  export {
4531
+ ADVERSARIES,
4532
+ ALLIES,
3561
4533
  AUDIO_COMMAND_POS,
3562
4534
  BATTERY_STATUS_FREQUENCY,
4535
+ BOARD_GROUPINGS,
4536
+ BOARD_LOCATIONS,
4537
+ BOARD_LOCATION_BY_NAME,
3563
4538
  BluetoothAdapterFactory,
3564
4539
  BluetoothConnectionError,
3565
4540
  BluetoothDeviceNotFoundError,
@@ -3573,6 +4548,7 @@ export {
3573
4548
  DEFAULT_CONNECTION_MONITORING_FREQUENCY,
3574
4549
  DEFAULT_CONNECTION_MONITORING_TIMEOUT,
3575
4550
  DEFAULT_RETRY_SEND_COMMAND_MAX,
4551
+ DIFFICULTIES,
3576
4552
  DIS_FIRMWARE_REVISION_UUID,
3577
4553
  DIS_HARDWARE_REVISION_UUID,
3578
4554
  DIS_IEEE_REGULATORY_UUID,
@@ -3585,7 +4561,10 @@ export {
3585
4561
  DIS_SYSTEM_ID_UUID,
3586
4562
  DOMOutput,
3587
4563
  DRUM_PACKETS,
4564
+ GAME_SOURCES,
3588
4565
  GLYPHS,
4566
+ InMemorySink,
4567
+ IndexedDBSink,
3589
4568
  LAYER_TO_POSITION,
3590
4569
  LEDGE_BASE_LIGHT_POSITIONS,
3591
4570
  LED_CHANNEL_LOOKUP,
@@ -3595,7 +4574,11 @@ export {
3595
4574
  RING_LIGHT_POSITIONS,
3596
4575
  SKULL_DROP_COUNT_POS,
3597
4576
  STATE_DATA_LENGTH,
4577
+ SystemRandom,
3598
4578
  TC,
4579
+ TIER1_FOES,
4580
+ TIER2_FOES,
4581
+ TIER3_FOES,
3599
4582
  TOWER_AUDIO_LIBRARY,
3600
4583
  TOWER_COMMANDS,
3601
4584
  TOWER_COMMAND_HEADER_SIZE,
@@ -3612,18 +4595,29 @@ export {
3612
4595
  UART_RX_CHARACTERISTIC_UUID,
3613
4596
  UART_SERVICE_UUID,
3614
4597
  UART_TX_CHARACTERISTIC_UUID,
4598
+ UdtDiagnosticsRecorder,
3615
4599
  UltimateDarkTower_default as UltimateDarkTower,
3616
4600
  VOLTAGE_LEVELS,
3617
4601
  VOLUME_DESCRIPTIONS,
3618
4602
  VOLUME_ICONS,
4603
+ bytesToHex,
4604
+ charToValue,
4605
+ compareSeedsRaw,
3619
4606
  createDefaultTowerState,
4607
+ createSeed,
4608
+ decodeRngSeed,
4609
+ decodeSeed,
3620
4610
  index_default as default,
3621
4611
  drumPositionCmds,
4612
+ dumpSeedChars,
4613
+ encodeSeed,
3622
4614
  isCalibrated,
3623
4615
  logger,
3624
4616
  milliVoltsToPercentage,
3625
4617
  milliVoltsToPercentageNumber,
3626
4618
  parseDifferentialReadings,
3627
4619
  rtdt_pack_state,
3628
- rtdt_unpack_state
4620
+ rtdt_unpack_state,
4621
+ validateSeed,
4622
+ valueToChar
3629
4623
  };