DOM-based visual readout for decoded Ultimate Dark Tower tower state.
This package renders a live tower dashboard into a DOM element. It is intended for tools that already know how to obtain or decode a TowerState and want a compact on-screen display of:
- LED layers and per-light effects
- Drum positions and calibration status
- Visible glyph on each calibrated drum
- Active audio sample, loop flag, and volume
- Beam/skull count and skull-drop transitions
- Active LED sequence override
ultimatedarktowerdisplay is a renderer. It does not talk to the physical tower, decode packets, or create TowerState objects on its own.
Use it alongside ultimatedarktower, which provides the TowerState type and helpers such as createDefaultTowerState().
This package intentionally keeps a narrow API surface. It exports display components and display-specific types only. Import protocol constants such as LIGHT_EFFECTS, TOWER_AUDIO_LIBRARY, and TOWER_LIGHT_SEQUENCES from ultimatedarktower.
npm install ultimatedarktowerdisplay ultimatedarktowerultimatedarktower is a peer dependency and must be installed by the consuming app.
import { TowerDisplay } from 'ultimatedarktowerdisplay';
import { createDefaultTowerState } from 'ultimatedarktower';
const container = document.getElementById('tower');
if (!container) {
throw new Error('Missing #tower container');
}
const display = new TowerDisplay({ container });
const state = createDefaultTowerState();
display.applyState(state);
// Later, when the tower sends an updated decoded state:
// display.applyState(nextState);
// Reset to the idle placeholder.
display.showIdle();
// Remove rendered DOM when tearing down the view.
display.dispose();Minimal HTML:
<div id="tower"></div>In most applications the flow looks like this:
- Use
ultimatedarktowerto create or decode aTowerState. - Create a
TowerDisplayfor a container element. - Call
applyState(state)whenever a new decoded state arrives. - Call
dispose()when removing the view.
Example with a manually adjusted state:
import { TowerDisplay } from 'ultimatedarktowerdisplay';
import { createDefaultTowerState, LIGHT_EFFECTS, TOWER_AUDIO_LIBRARY } from 'ultimatedarktower';
const container = document.getElementById('tower');
if (!container) {
throw new Error('Missing #tower container');
}
const display = new TowerDisplay({ container });
const state = createDefaultTowerState();
state.layer[0].light[0].effect = LIGHT_EFFECTS.on;
state.layer[0].light[1].effect = LIGHT_EFFECTS.breathe;
state.drum[0].position = 1;
state.drum[0].calibrated = true;
state.audio.sample = TOWER_AUDIO_LIBRARY.Ashstrider.value;
state.audio.loop = true;
state.beam.count = 2;
display.applyState(state);You usually do not construct TowerState by hand. Start from createDefaultTowerState() and mutate fields you care about.
Conceptually, the renderer expects a state shaped like this:
type TowerState = {
drum: Array<{
calibrated: boolean;
position: number;
}>;
layer: Array<{
light: Array<{
effect: number;
loop: boolean;
}>;
}>;
audio: {
sample: number;
loop: boolean;
volume: number;
};
beam: {
count: number;
fault: boolean;
};
led_sequence: number;
};For a valid starting point, prefer:
import { createDefaultTowerState } from 'ultimatedarktower';
const state = createDefaultTowerState();The example page in example/index.html follows this pattern.
- The constructor immediately renders an idle message:
Waiting for tower state… - Styles are injected into
document.headautomatically on first use - A skull-drop highlight appears only when
beam.countincreases between successiveapplyState()calls dispose()clears the container and resets internal state, including skull-drop tracking
Primary entry point. Composes one or both renderers into a container.
new TowerDisplay(options: TowerDisplayOptions)| Option | Type | Default | Description |
|---|---|---|---|
container |
HTMLElement |
— | DOM element that will receive the rendered output |
renderers |
RendererType | RendererType[] |
['readout', 'side-view'] |
Which renderer(s) to show: 'readout', 'side-view', or both |
onSealClick |
(seal: SealIdentifier) => void |
— | Called whenever the user clicks a seal overlay in the side view |
clickToToggleSeals |
boolean |
true |
When true, clicking a seal toggles its visibility independent of game state. Set to false to disable. |
Methods:
applyState(state: TowerState): void— update all renderers with a new decoded tower stateapplySeals(brokenSeals: SealIdentifier[]): void— hide seal overlays for broken seals; pass the current list of broken seals each time it changesshowIdle(): void— reset all renderers to their idle placeholderdispose(): void— remove all rendered DOM and reset internal state
SVG side-view renderer. Shows a rotatable view of one tower face with seal overlays and LED markers. Can be used standalone or composed via TowerDisplay.
const view = new TowerSideView(container);
view.onSealClick = (seal) => console.log(seal);
view.clickToToggleSeals = true; // default| Property | Type | Default | Description |
|---|---|---|---|
onSealClick |
(seal: SealIdentifier) => void |
— | Callback fired on every seal click |
clickToToggleSeals |
boolean |
true |
Enables built-in click-to-toggle visibility on seal overlays |
Text-based readout renderer. Lower-level; takes a container directly.
new TowerStateReadout(container: HTMLElement)Exposes the same methods as TowerDisplay (applyState, applySeals, showIdle, dispose).
ITowerDisplay— common interface implemented by all renderersTowerDisplayOptions— configuration object forTowerDisplayRendererType—'readout' | 'side-view'TowerSide—'north' | 'east' | 'south' | 'west'SealIdentifier—{ side: TowerSide, level: TowerLevels }
ultimatedarktowerdisplay does not re-export tower protocol constants or helpers from ultimatedarktower.
Install dependencies:
npm installAvailable commands:
npm run typecheck
npm run lint
npm test
npm run test:coverage
npm run build
npm run dev
npm run dev:example
npm run cinpm run dev:example starts the Vite example page and opens example/index.html.
MIT