Skip to content

Dash-Encounter Telemetry — Design

A live D1 query found human players die to chargers/spiders 27.2% of the time vs. 60-70% for Ace’s soak sweeps — real evidence humans have a technique the AI lacks. But existing telemetry only records end-of-run outcomes (death_cause, aggregate damage_taken) — there’s no way to see what a human actually does DURING a charger/spider encounter: do they move laterally during the telegraph, dash through the lunge, or something else entirely. Chris: “lets try this first but it may be worth expanding the payload later, but lets see what this tells us” — explicitly a first pass, not a final design.

What already exists (verified against source)

Section titled “What already exists (verified against source)”
  • Telemetry is fire-and-forget, one POST per event, no queue/batching (net/gameplay_telemetry.gd — a single HTTPRequest child node, _http.request() called once per event, errors silently ignored). This session’s earlier is_auto addition is the most recent precedent for extending this pipeline.
  • Two routes exist on the worker (telemetry/src/worker.js): POST /sample (perf, every 5s) and POST /gameplay (run summary, run-end only). No generic per-event route exists.
  • Two D1 tables exist (telemetry/schema.sql): samples and runs. No per-encounter table.
  • No existing code anywhere (render, main.gd, sim) already detects “a charge encounter started/ended” as a discrete event — this needs to be built from scratch, reusing the same sim.enemies.dash_phase[i]/dash_timer[i]/vel[i]/pos[i] fields this session’s AI work already established as the source of truth for dash state.
  • sim.enemies contact-damage attribution (_hurt_player, sim/sim.gd:2753) passes only the enemy’s TYPE NAME as source, not an enemy index — so there is no reliable way to attribute a hit to one SPECIFIC tracked enemy via the existing damage-source string if multiple same-type dash enemies are active at once. This design avoids that ambiguity entirely by using its OWN geometric contact check (pilot/enemy hitbox overlap) instead of the sim’s damage attribution — deterministic and per-enemy-specific by construction.
  • No AI/attract-mode capture — this only matters for human sessions, since Ace’s full movement state is already available for free via local soak tooling. Gated on not _auto.
  • No true damage/HP-based “was this a real hit” check — outcome is a pure GEOMETRIC contact check (pilot/enemy hitbox overlap at any sampled tick during the tracked enemy’s lunge). This means a player who overlaps the enemy’s hitbox but is invulnerable (mid-dash i-frames) is still recorded as outcome: "contact", not distinguished from a real hit — a known first-pass simplification, not a bug. player_dashed (below) lets a later analysis pass partially disambiguate this.
  • No batching — one POST per resolved encounter, matching the existing fire-and-forget pattern. A busy run could fire several small requests; consistent with existing net/ patterns, not a new scaling concern for a 2-player personal project.
  • No dashboard visualization in this pass — raw D1 rows only, queried by hand (as done for the death-cause analysis this session). A dashboard view is a natural later addition, not built now.

A new per-tick tracker (client-side) watches nearby BEHAVIOR_DASH enemies (charger/spider) relative to the human-controlled pilot, using the same detection radius the AI’s flee zone already uses (THREAT_RADIUS = 300.0, ai/auto_player.gd) — a reused, already-tuned distance, not a new arbitrary one.

  • Start: an enemy within range transitions to dash_phase == DASH_CHARGE (or, for an Elite Charger’s chain second dash, the grow_t chain-telegraph condition this session’s AI work already established in ai/skilled_auto_player.gd’s _is_dash_telegraphing).
  • Sample: every ~0.1s (≈6 ticks) during the window, record the player’s position RELATIVE to the tracked enemy (pilot.pos - enemy.pos), plus which phase it’s currently in (charge/lunge). Capped at 40 samples as a hard safety bound (covers even a full double-dash chain with margin).
  • End: the tracked enemy’s lunge completes (phase leaves DASH_LUNGE with no further chain pending) or the enemy dies/leaves detection range.
  • Outcome: "contact" if the pilot’s and the tracked enemy’s hitboxes overlapped at ANY sampled tick during its lunge phase; "clear" otherwise.
  • Also recorded: whether the pilot’s dash was used at any point during the window (player_dashed), the closest approach distance observed, whether this was an Elite chain encounter (chain), and total duration.
{
"session": "...", "build": 175, "is_auto": 0,
"enemy_type": "charger",
"chain": false,
"outcome": "clear",
"player_dashed": true,
"closest_approach_px": 38.2,
"duration_s": 2.14,
"samples": [{"t": 0.00, "dx": 150.0, "dy": 0.0, "phase": "charge"}, ...]
}
  • telemetry/schema.sql: new encounters table (id, ts, session, build, is_auto, enemy_type, chain, outcome, player_dashed, closest_approach_px, duration_s, samples_json).
  • telemetry/src/worker.js: new POST /encounter route, mirroring the existing /gameplay handler’s parse-and-insert shape.
  • net/dash_encounter_telemetry.gd: new client-side tracker (a Node hosting its own HTTPRequest, matching net/gameplay_telemetry.gd’s pattern exactly), ticked once per sim tick from main.gd, gated on not _auto.
  • Unit tests for the detection/capture logic in isolation (start/end/outcome/sample-cap correctness) against a constructed Sim with a known dash-enemy trajectory — same pattern as this session’s ai/skilled_auto_player.gd dodge/orbit tests.
  • The worker route gets a manual smoke test (a real POST against the dev/staging endpoint) since this project’s telemetry worker has no existing automated test suite (verify this assumption at plan-writing time rather than guessing).
  • Real acceptance: after deploying, play a session (Chris and/or Toby) and directly query the new encounters table to confirm rows land with sane values — the same manual-query pattern used for tonight’s death-cause analysis.