diff --git a/docs/re/captures/hud-runtime/hud-afterburner-1193.png b/docs/re/captures/hud-runtime/hud-afterburner-1193.png new file mode 100644 index 0000000..40bc08e Binary files /dev/null and b/docs/re/captures/hud-runtime/hud-afterburner-1193.png differ diff --git a/docs/re/captures/hud-runtime/hud-cruise-loadout.png b/docs/re/captures/hud-runtime/hud-cruise-loadout.png new file mode 100644 index 0000000..268d6af Binary files /dev/null and b/docs/re/captures/hud-runtime/hud-cruise-loadout.png differ diff --git a/docs/re/captures/hud-runtime/hud-target-armor-gauge.png b/docs/re/captures/hud-runtime/hud-target-armor-gauge.png new file mode 100644 index 0000000..32797c3 Binary files /dev/null and b/docs/re/captures/hud-runtime/hud-target-armor-gauge.png differ diff --git a/docs/re/weapon-datasheet-runtime.md b/docs/re/weapon-datasheet-runtime.md index 5e34756..c68ff91 100644 --- a/docs/re/weapon-datasheet-runtime.md +++ b/docs/re/weapon-datasheet-runtime.md @@ -170,3 +170,63 @@ Screenshots for every row above: `/sylph-home/re/caps/` in the container. Evidence PNGs are committed under [`captures/weapon-datasheet/`](captures/weapon-datasheet/) (64-colour quantized for size; the numbers stay legible). + +--- + +# Addendum — the in-flight HUD (2026-07-28) + +Reached via **TUTORIAL → BASIC CONTROLS / HEADS-UP DISPLAY** from the title menu (no +save needed). Evidence: [`captures/hud-runtime/`](captures/hud-runtime/). + +## The HUD corroborates the ammo mapping, independently + +The Delta Saber's HUD prints its two equipped weapons as `NOSE ` and +`MAIN `. With the loadout known from the HANGAR (nose = STILETTO BG I, main = +FALCON 9AM) the HUD reads **`NOSE BM 06000`** and **`MAIN MPM 00300`** — exactly +`wep_01_Beam`'s `LoadingCount = 6000` and `wep_02_Missile`'s `300`. + +This matters because it is **not** the Arsenal panel: the weapons were identified from the +HANGAR loadout, and the numbers come from a different renderer in a different game mode. It +is a genuinely independent confirmation of `Ammo Capacity == LoadingCount`. + +The type tags (`BM`, `MPM`) are the same short codes as the Arsenal type tabs. + +## HUD element inventory + +| HUD element | Backing data | Conf. | +|---|---|---| +| `NOSE` / `MAIN` + type tag + 5-digit ammo | equipped weapon, `LoadingCount` | ✅ | +| `HEAT` / `L.HEAT` bar under each weapon | the `Heating` / `Cooling` pair | 🟡 | +| Speed readout + throttle scale (`0`, `100`, `A/B`) | craft velocity | ✅ | +| `SHIELD` and `ARMOR` bars (two separate pools) | craft `HP` + a shield pool | 🟡 | +| Target reticle: name / type / distance + ring **Armor Gauge** | target unit record | 🟡 | +| `RANGE` gauge with `MAIN` and `NOSE` tick markers | each equipped weapon's `MaximumRange`, plotted against target distance | 🟡 | +| `YOU KILLED: WARSHIPS nnnn` + a second counter | score / `ScorePoint` | ❔ | + +The `RANGE` gauge is the interesting one for future work: it draws a **per-weapon marker on +a distance scale**, so a weapon whose `MaximumRange` is defaulted on disc (`wep_25`, +`wep_58`, `wep_82`) would have its value *drawn* rather than bucketed into a letter — if the +scale can be calibrated against two weapons with known ranges, that recovers a real number +where the Arsenal panel only gives a class. + +## Craft velocity + +Full afterburner (RT) peaked at **1193** against `UN_f001_TCAF_DeltaSaber_T`'s on-disc +`MaximumVelocity = 1200`. 🟡 PROBABLE — one observation, and the readout may have been +still climbing. Cruise sat at 350, which is *not* the record's `CruisingVelocity` (700), so +the number is the current throttle setting, not a named constant; don't read more into it. + +## Notes for the next session + +- The tutorials need **no save game** and are reachable in ~1 min from a cold boot, which + makes them the cheapest way back into a live flight scene. +- `tools/re-capture/autopilot.py` chases the yellow off-screen waypoint arrow (colour + + shape, `-sample` not `-resize`, ~0.65 s per detection). It **finds the arrow reliably but + oscillates** — the proportional gain is too high for the craft's turn rate. It needs a + damping/derivative term before it can actually fly a waypoint. +- The HEADS-UP DISPLAY tutorial reaches a scripted targeting segment where the ship stops + moving (target distance pinned) and the on-screen controller highlights **A**; tapping and + holding A did not advance it. **NEEDS-HUMAN**: what input that segment wants. +- Canary is unstable here: it died twice mid-session (once loading BEAM `Resource3D`, once + hanging on tutorial teardown) with no crash dump. Re-launch is cheap; just don't assume a + long session survives. diff --git a/tools/re-capture/autopilot.py b/tools/re-capture/autopilot.py new file mode 100644 index 0000000..736f622 --- /dev/null +++ b/tools/re-capture/autopilot.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Fly the Delta Saber toward the tutorial waypoint by chasing the yellow +off-screen direction arrow. + +Detection: no PIL/numpy in the box, so the frame comes from `convert ... txt:-`. +Two things make it fast AND correct: + * `-sample` (point sampling, not `-resize`/`-scale` averaging) — a 1-px-thin + glyph keeps its true colour, so the exact colour predicate still fires while + the dump shrinks ~9x (3.5s -> 0.65s). + * shape, not just colour — the instruction-frame bars and the throttle marker + are the same dim yellow, so the arrow is the largest blob that is roughly as + tall as it is wide, with the fixed throttle marker blacklisted by position. +A ~1.1 s control loop is fast enough to converge; the earlier ~6 s loop was not. +""" +import re, subprocess, sys, time + +X0, Y0, W, H = 80, 60, 820, 640 # flight view (the arrow also rides the bottom edge) +K = 3.03 # 1/0.33 sample factor +CX, CY = 640, 405 # crosshair +THROTTLE = (382, 456) # fixed yellow decoy on the speed gauge +PX = re.compile(r"^(\d+),(\d+):.*?srgb\((\d+),(\d+),(\d+)\)") + + +def pad(*a): + subprocess.run(["/opt/sylph/vgamepad.py", *a], check=False) + + +def arrow(png="/tmp/ap.png"): + subprocess.run(["/opt/sylph/screenshot.sh", png], check=False, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + out = subprocess.run(["convert", png, "-crop", f"{W}x{H}+{X0}+{Y0}", "+repage", + "-sample", "33%", "txt:-"], capture_output=True, text=True).stdout + pts = set() + for l in out.splitlines()[1:]: + m = PX.match(l) + if not m: + continue + x, y, r, g, b = (int(v) for v in m.groups()) + if r >= 60 and g >= 48 and b <= 0.35 * g and (r - g) <= 0.45 * r: + pts.add((x, y)) + seen, best = set(), None + for p in pts: + if p in seen: + continue + st, c = [p], [] + seen.add(p) + while st: + x, y = st.pop(); c.append((x, y)) + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + q = (x + dx, y + dy) + if q in pts and q not in seen: + seen.add(q); st.append(q) + xs = [q[0] for q in c]; ys = [q[1] for q in c] + cx, cy = X0 + sum(xs) / len(c) * K, Y0 + sum(ys) / len(c) * K + if abs(cx - THROTTLE[0]) < 30 and abs(cy - THROTTLE[1]) < 30: + continue + if len(c) < 8: + continue + if best is None or len(c) > best[2]: + best = (cx, cy, len(c)) + return best + + +def main(): + steps = int(sys.argv[1]) if len(sys.argv) > 1 else 25 + centred = 0 + for i in range(steps): + a = arrow() + if a is None: + print(f"{i:2d}: no arrow -> boost (target ahead)") + pad("trig", "RT", "0.55"); time.sleep(1.2); pad("trig", "RT", "0") + centred += 1 + if centred >= 6: + print(" arrival likely — stopping"); return 0 + continue + x, y, n = a + dx, dy = x - CX, y - CY + if abs(dx) < 70 and abs(dy) < 70: + centred += 1 + print(f"{i:2d}: arrow ({x:.0f},{y:.0f}) centred -> boost") + pad("trig", "RT", "0.55"); time.sleep(1.2); pad("trig", "RT", "0") + continue + centred = 0 + lx = max(-1.0, min(1.0, dx / 200)) + ly = max(-1.0, min(1.0, dy / 160)) + print(f"{i:2d}: arrow ({x:.0f},{y:.0f}) n={n} -> LX {lx:+.2f} LY {ly:+.2f}") + pad("axis", "LX", f"{lx:.2f}"); pad("axis", "LY", f"{ly:.2f}") + time.sleep(0.45) + pad("axis", "LX", "0"); pad("axis", "LY", "0") + print("steps exhausted") + return 1 + + +sys.exit(main()) diff --git a/tools/re-capture/skip_intro.sh b/tools/re-capture/skip_intro.sh index d2b804c..8eaa3f1 100755 --- a/tools/re-capture/skip_intro.sh +++ b/tools/re-capture/skip_intro.sh @@ -5,6 +5,10 @@ # Static logo screens are left alone, so a stray tap can never land on NEW GAME. set -u export HOME=/sylph-home/re +# The X root keeps the DEAD session's last frame, so a fresh launch would be +# detected as "already at the title". Blank it, and wait for the new window. +xsetroot -solid black 2>/dev/null || true +until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do sleep 1; done deadline=$(( SECONDS + ${1:-900} )) while [ $SECONDS -lt $deadline ]; do screenshot /tmp/f1.png >/dev/null 2>&1; sleep 0.6