# Reusable Odometer

`reusable_odometer` is the extracted Pygame odometer package used by both
`ohd_test.py` and the full `DyzineFrame.py` player. It separates the odometer
engine, theme/layout loading, and widget rendering so the same code can be used
in standalone tools or embedded in other Pygame applications.

## Main pieces

- `core/engine.py`: chase, lag, carry, and display state
- `theme/layout.py`: JSON layout dataclasses and parsing
- `theme/aligned.py`: aligned cell library and glyph lookup
- `render/widget.py`: `OdometerWidget` draw/update API
- `adapters/pygame_assets.py`: bridge from the current OHD asset tree into the package

## Primary API

Typical host-side usage:

```python
widget = create_current_widget(game_id=1, resolution="1920x1080")
widget.display_immediate_pennies(250_000)

while running:
    target = get_latest_progressive_value()
    if target is not None:
        widget.set_target_pennies(target)
    widget.update(dt_seconds)
    widget.draw(screen)
```

Important methods:

- `display_immediate_pennies(value)`: snap target and display immediately
- `set_target_pennies(value)`: animate toward a new live value
- `set_target_pennies(value, immediate=True)`: force a snap instead of animation
- `update(dt_seconds)`: advance the odometer engine
- `draw(surface)` / `draw_at(surface, rect_or_anchor)`: render to a Pygame surface
- `get_draw_ops(...)`: return `BlitOp` records for hosts that want to compose manually

## Display semantics

The package supports both static and live usage patterns:

- Static/inactive game: call `display_immediate_pennies(seed_value)` and do not push live updates
- First live value immediate: call `set_target_pennies(value, first_value_immediate=True)`
- Per-slide immediate entry: call `set_target_pennies(value, immediate=True)` when the slide becomes visible, then use plain `set_target_pennies(value)` for live changes while it remains on screen

`DyzineFrame/player_odometer.py` uses that third pattern. Each time the
progressive slide begins, it snaps to the current EtherGold value, then animates
only changes that arrive during that on-screen showing.

## Curve tuning

The reusable engine still supports the original scalar tuning fields, but it can
now also read optional interpolation curves from the `odometer` block in
`layout.json`.

Curve fields:

- `rate_curve`: lag pennies -> chase rate in cents/sec
- `roll_curve_sec`: lag pennies -> seconds per penny roll step

Each curve can be written as either `[lag, value]` pairs or as objects. Example:

```json
"odometer": {
  "rate_curve": [
    [0, 0.15],
    [100, 1.3],
    [300, 3.1],
    [500, 5.0],
    [1000, 8.0]
  ],
  "roll_curve_sec": [
    [0, 4.0],
    [100, 1.4],
    [300, 0.5],
    [500, 0.2]
  ]
}
```

When present, the curves override the older linear ramp formula and are linearly
interpolated between points. This makes it much easier to match a physical sign
that speeds up earlier, later, or with a smoother response than the original
two-band tuning model.

## Asset expectations

The current adapter expects the OHD asset tree:

- shared layout/config under `Assets/Shared/<resolution>/layout.json`
- per-game art assets under the indexed game folders
- optional background/frame layers included by the player slide renderer

The reusable package itself is layout-driven; the adapter is the only layer that
knows about the current DyzineFrame asset naming and folder structure.
