re: in-flight HUD — independent confirmation of the ammo mapping
The tutorials need no save and are ~1 min from a cold boot, so they are the cheapest route into a live flight scene. The HUD prints the equipped weapons as NOSE BM 06000 / MAIN MPM 00300 — matching wep_01's LoadingCount 6000 and wep_02's 300, with the weapons identified from the HANGAR loadout rather than from the ammo number. That makes it a genuinely independent confirmation of Ammo Capacity == LoadingCount, from a different renderer in a different mode. Also inventories the HUD elements (heat gauges, shield/armor pools, target armor gauge, per-weapon RANGE markers) and notes that the RANGE gauge draws each weapon's MaximumRange on a scale — a route to a real number where the Arsenal panel only gives a letter class. Adds autopilot.py (waypoint-arrow chaser). It detects reliably but oscillates; recorded as such rather than as a working tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
docs/re/captures/hud-runtime/hud-afterburner-1193.png
Normal file
BIN
docs/re/captures/hud-runtime/hud-afterburner-1193.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/re/captures/hud-runtime/hud-cruise-loadout.png
Normal file
BIN
docs/re/captures/hud-runtime/hud-cruise-loadout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
docs/re/captures/hud-runtime/hud-target-armor-gauge.png
Normal file
BIN
docs/re/captures/hud-runtime/hud-target-armor-gauge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
@@ -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/)
|
Evidence PNGs are committed under [`captures/weapon-datasheet/`](captures/weapon-datasheet/)
|
||||||
(64-colour quantized for size; the numbers stay legible).
|
(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 <TYPE> <AMMO>` and
|
||||||
|
`MAIN <TYPE> <AMMO>`. 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.
|
||||||
|
|||||||
95
tools/re-capture/autopilot.py
Normal file
95
tools/re-capture/autopilot.py
Normal file
@@ -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())
|
||||||
@@ -5,6 +5,10 @@
|
|||||||
# Static logo screens are left alone, so a stray tap can never land on NEW GAME.
|
# Static logo screens are left alone, so a stray tap can never land on NEW GAME.
|
||||||
set -u
|
set -u
|
||||||
export HOME=/sylph-home/re
|
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} ))
|
deadline=$(( SECONDS + ${1:-900} ))
|
||||||
while [ $SECONDS -lt $deadline ]; do
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
screenshot /tmp/f1.png >/dev/null 2>&1; sleep 0.6
|
screenshot /tmp/f1.png >/dev/null 2>&1; sleep 0.6
|
||||||
|
|||||||
Reference in New Issue
Block a user