Files
BT411/scratchpad/cop_eyeview.py
T
arcattackandClaude Fable 5 8ed6184d65 Combat: AUTHENTIC weapon groups -- streamed per-mech button bindings + the real fire chain (task #5)
The recovered system: fire channels = LBE4ControlsManager buttonGroups
(0x40/0x45/0x46/0x47); default groups = the per-mech type-6 controls-map
resource in BTL4.RES, installed by the T0 CreateStreamedMappings the port
already called -- it needed only the TriggerState attribute (id 0x13 PINNED
to the binary value; fireImpulse@0x31C is the binary's TriggerState) and an
input feed.  Keyboard/harness now push press/release edges into the button
groups; the gBT*Trigger bypasses, per-type keyboard split and 1,0 pulse
hack are retired -- weapons sharing a button fire TOGETHER (madcat Trigger
= 4 weapons).  Myomers @4b9550/@4b95b8 misattribution corrected (they are
MechWeapon ConfigureMappables/ChooseButton).  Verified 2-node: kill through
the authentic chain (12 hits vs ~36 pre-groups).  Config-mode session
(regrouping UI) = the remaining stage, KB-scoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 13:06:29 -05:00

127 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Render every mech's *_COP canopy from its AUTHORED eye position (SKL [jointeye]
translation, same torso-local frame as the _cop mesh), single-sided with per-face
INWARD winding -- the port's exact treatment -- with faces coloured by patch +
punch flag so we can see WHICH geometry blocks each mech's view.
frame colours: punch patches = warm (red/orange/yellow), non-punch = cool (blue/teal)
world (holes) = lavender background
Env: FWD=+z|-z (look direction, default -z), CULL=0 disables single-siding,
DROP_PUNCH=1 drops punch patches entirely.
"""
import re, os, math, sys
import numpy as np
from PIL import Image, ImageDraw
import importlib.util
spec = importlib.util.spec_from_file_location("bgf_parse", os.path.join(os.path.dirname(__file__), "bgf_parse.py"))
bgf = importlib.util.module_from_spec(spec); spec.loader.exec_module(bgf)
ROOT = r"C:\git\bt411"
MECHS = { # model: (outside skl prefix, X-variant cop prefix)
"madcat": ("MAD", "MAX"), "bhk1": ("BLH", "BLX"), "thor": ("THR", "THX"),
"owens": ("OWN", "OWX"), "sunder": ("SND", "SNX"), "loki": ("LOK", "LOX"),
"vulture": ("VUL", "VUX"), "avatar": ("AVA", "AVX"),
}
W, H = 300, 225
PUNCH_COLS = [(200,60,50),(230,140,40),(230,210,60)] # warm = punch
FRAME_COLS = [(70,90,200),(60,170,180),(120,80,200)] # cool = non-punch
def jointeye(skl_path):
t = open(skl_path, encoding="latin1").read()
m = re.search(r"\[jointeye\]", t)
if not m: return None
blk = t[m.start():m.start()+400]
g = lambda k: float((re.search(rf"{k}=([-\d.e]+)", blk) or [None,"0"])[1])
return (g("tranx"), g("trany"), g("tranz"))
def render(model, out_path, fwd_sign=-1.0, cull=True, drop_punch=False):
pfx, xpfx = MECHS[model]
eye = jointeye(os.path.join(ROOT, "content", "VIDEO", pfx + ".SKL"))
P = bgf.parse(os.path.join(ROOT, "content", "VIDEO", "GEO", xpfx + "_COP.BGF"))
allv = np.array([v[:3] for pt in P for v in pt["verts"]])
cen = allv.mean(0)
e = np.array(eye, float)
f = np.array([0.0, 0.0, fwd_sign]); u = np.array([0.0, 1.0, 0.0])
r = np.cross(u, f); r /= np.linalg.norm(r); u2 = np.cross(f, r)
tanf = math.tan(math.radians(75)/2); aspect = W/H
img = np.zeros((H, W, 3), np.uint8)
for y in range(H): # lavender world bg
t = y/H
img[y,:] = (150,148,205) if t < 0.5 else (185,178,200)
zb = np.full((H, W), 1e9)
npi = nfi = 0
for pi, pt in enumerate(P):
V = pt["verts"]
if pt["punch"]:
col = PUNCH_COLS[npi % 3]; npi += 1
if drop_punch: continue
else:
col = FRAME_COLS[nfi % 3]; nfi += 1
for (a, b, c) in pt["faces"]:
if max(a, b, c) >= len(V): continue
tri = np.array([V[a][:3], V[b][:3], V[c][:3]])
n = np.cross(tri[1]-tri[0], tri[2]-tri[0]); nl = np.linalg.norm(n)
if nl < 1e-9: continue
n /= nl
# per-face INWARD orientation (the loader's rule): normal toward mesh centroid
fc = tri.mean(0)
inward = n if np.dot(n, cen - fc) >= 0 else -n
if cull and np.dot(inward, e - fc) <= 0: # single-sided: cull faces facing away from the eye
continue
s = []; ok = True
for p in tri:
d = p - e; cx = np.dot(d, r); cy = np.dot(d, u2); cz = np.dot(d, f)
if cz <= 0.02: ok = False; break
s.append((cx/(cz*tanf*aspect)*0.5*W + 0.5*W, -cy/(cz*tanf)*0.5*H + 0.5*H, cz))
if not ok: continue
shade = 0.55 + 0.45*abs(np.dot(inward, f))
cc = tuple(min(255, int(ch*shade)) for ch in col)
raster(img, zb, s, cc)
im = Image.new("RGB", (W, H+22), (18,18,22)); im.paste(Image.fromarray(img), (0,22))
d = ImageDraw.Draw(im)
d.text((4,5), f"{model} eye=({eye[0]:+.2f},{eye[1]:.2f},{eye[2]:+.2f}) fwd={'-Z' if fwd_sign<0 else '+Z'}", fill=(235,235,240))
im.save(out_path)
return eye
def raster(img, zb, s, col):
(ax,ay,az),(bx,by,bz),(cx,cy,cz) = s
x0=max(0,int(min(ax,bx,cx))); x1=min(W-1,int(max(ax,bx,cx)))
y0=max(0,int(min(ay,by,cy))); y1=min(H-1,int(max(ay,by,cy)))
den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
if abs(den)<1e-6 or x1<x0 or y1<y0: return
for py in range(y0,y1+1):
for px in range(x0,x1+1):
w0=((by-cy)*(px+.5-cx)+(cx-bx)*(py+.5-cy))/den
w1=((cy-ay)*(px+.5-cx)+(ax-cx)*(py+.5-cy))/den
w2=1-w0-w1
if w0<0 or w1<0 or w2<0: continue
z=w0*az+w1*bz+w2*cz
if z<zb[py,px]: zb[py,px]=z; img[py,px]=col
if __name__ == "__main__":
fwd = -1.0 if os.getenv("FWD","-z") == "-z" else 1.0
cull = os.getenv("CULL","1") != "0"
dropp = os.getenv("DROP_PUNCH","0") == "1"
tag = sys.argv[1] if len(sys.argv) > 1 else "eyeview"
tiles = []
for m in MECHS:
p = os.path.join(ROOT, "scratchpad", f"ev_{m}.png")
render(m, p, fwd, cull, dropp)
tiles.append(np.array(Image.open(p)))
g = np.full((tiles[0].shape[0], 4, 3), 255, np.uint8)
rows = []
for i in range(0, len(tiles), 4):
row = []
for t in tiles[i:i+4]: row.append(t); row.append(g)
rows.append(np.concatenate(row[:-1], axis=1))
wmax = max(r.shape[1] for r in rows)
rows = [np.pad(r, ((0,0),(0,wmax-r.shape[1]),(0,0)), constant_values=18) for r in rows]
vg = np.full((4, wmax, 3), 255, np.uint8)
out = []
for r in rows: out.append(r); out.append(vg)
outp = os.path.join(ROOT, "scratchpad", f"{tag}.png")
Image.fromarray(np.concatenate(out[:-1], axis=0)).save(outp)
print("saved", outp)