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>
149 lines
6.5 KiB
Python
149 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Hypothesis-discrimination renderer for the cockpit PUNCH panes.
|
|
For every mech, render the *_COP shell from the authored eye three ways:
|
|
H1 punch panes DROPPED entirely (current port behavior)
|
|
H2 punch panes 50% dark translucent (triangle_fifty / stipple average)
|
|
H3 punch panes OPAQUE (state-gated opaque-until-damaged, undamaged state)
|
|
Non-punch frame = dark cockpit purple; punch pane (when visible) = warm tint so
|
|
we can tell WHICH pixels the pane owns. World = bright sky/ground gradient so
|
|
translucency dimming is measurable. Output: scratchpad/HYP_MATRIX.png plus
|
|
per-mech rows, and per-mode luminance stats printed for the pane-covered pixels.
|
|
"""
|
|
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 = {"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
|
|
FRAME_COL = (52, 42, 78) # dark cockpit purple (reference frame color family)
|
|
PANE_COL = (40, 32, 60) # the pane's own material color (black mech skin family)
|
|
PANE_TINT = (255, 120, 60) # H3 overlay tint so opaque panes are identifiable
|
|
|
|
def jointeye(skl_path):
|
|
t = open(skl_path, encoding="latin1").read()
|
|
m = re.search(r"\[jointeye\]", t)
|
|
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 world_bg():
|
|
img = np.zeros((H, W, 3), np.float32)
|
|
for y in range(H):
|
|
t = y / H
|
|
if t < 0.52: # sky: bright grey-lavender like the reference footage
|
|
k = t / 0.52
|
|
img[y,:] = (1-k)*np.array((196,192,214)) + k*np.array((214,208,224))
|
|
else: # ground: light purple terrain
|
|
k = (t-0.52)/0.48
|
|
img[y,:] = (1-k)*np.array((178,168,198)) + k*np.array((150,140,178))
|
|
return img
|
|
|
|
def project(tri, e, r, u2, f, tanf, aspect):
|
|
s = []
|
|
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: return None
|
|
s.append((cx/(cz*tanf*aspect)*0.5*W + 0.5*W, -cy/(cz*tanf)*0.5*H + 0.5*H, cz))
|
|
return s
|
|
|
|
def raster(buf, zb, s, col, mask=None):
|
|
(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; buf[py,px]=col
|
|
if mask is not None: mask[py,px]=True
|
|
|
|
def render(model, mode):
|
|
pfx, xpfx = MECHS[model]
|
|
eye = np.array(jointeye(os.path.join(ROOT,"content","VIDEO",pfx+".SKL")), float)
|
|
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)
|
|
f = np.array([0.,0.,-1.]); u = np.array([0.,1.,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 = world_bg()
|
|
zb = np.full((H,W), 1e9)
|
|
pane_mask = np.zeros((H,W), bool)
|
|
|
|
def faces(punch_sel):
|
|
for pt in P:
|
|
if pt["punch"] != punch_sel: continue
|
|
V = pt["verts"]
|
|
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; fc = tri.mean(0)
|
|
inward = n if np.dot(n, cen-fc) >= 0 else -n
|
|
if np.dot(inward, eye-fc) <= 0: continue # single-sided
|
|
s = project(tri, eye, r, u2, f, tanf, aspect)
|
|
if s is None: continue
|
|
shade = 0.55 + 0.45*abs(np.dot(inward, f))
|
|
yield s, shade
|
|
|
|
# pass 1: opaque non-punch frame
|
|
for s, shade in faces(False):
|
|
raster(img, zb, s, tuple(ch*shade for ch in FRAME_COL))
|
|
|
|
# pass 2: punch panes per hypothesis
|
|
if mode == "H3": # opaque
|
|
for s, shade in faces(True):
|
|
col = tuple(0.6*ch*shade + 0.4*tc*shade for ch,tc in zip(PANE_COL, PANE_TINT))
|
|
raster(img, zb, s, col, pane_mask)
|
|
elif mode == "H2": # 50% dark translucent: blend pane color with what's behind
|
|
pane_z = np.full((H,W), 1e9)
|
|
pane_sh = np.zeros((H,W), np.float32)
|
|
dummy = np.zeros((H,W,3), np.float32)
|
|
for s, shade in faces(True):
|
|
m = np.zeros((H,W), bool)
|
|
raster(dummy, pane_z, s, (0,0,0), m)
|
|
pane_sh[m] = shade
|
|
vis = pane_z < zb # pane in front of frame at this pixel
|
|
pane_mask |= vis
|
|
pc = np.array(PANE_COL, np.float32)
|
|
img[vis] = 0.5*img[vis] + 0.5*(pc[None,:]*pane_sh[vis,None])
|
|
# H1: nothing (dropped)
|
|
|
|
lum = img.mean(axis=2)
|
|
stats = (lum[pane_mask].mean() if pane_mask.any() else float('nan'),
|
|
pane_mask.mean())
|
|
return np.clip(img,0,255).astype(np.uint8), stats
|
|
|
|
if __name__ == "__main__":
|
|
rows = []
|
|
print(f"{'mech':8s} {'mode':3s} pane_px% pane_lum")
|
|
for m in MECHS:
|
|
tiles = []
|
|
for mode in ("H1","H2","H3"):
|
|
im, (plum, pfrac) = render(m, mode)
|
|
print(f"{m:8s} {mode} {100*pfrac:6.1f}% {plum:7.1f}")
|
|
fr = Image.fromarray(im); d = ImageDraw.Draw(fr)
|
|
d.text((4,3), f"{m} {mode}", fill=(20,20,25))
|
|
d.text((3,2), f"{m} {mode}", fill=(255,255,140))
|
|
tiles.append(np.array(fr))
|
|
g = np.full((H,4,3),255,np.uint8)
|
|
row = np.concatenate([tiles[0],g,tiles[1],g,tiles[2]],axis=1)
|
|
rows.append(row); rows.append(np.full((4,row.shape[1],3),255,np.uint8))
|
|
out = np.concatenate(rows[:-1],axis=0)
|
|
p = os.path.join(ROOT,"scratchpad","HYP_MATRIX.png")
|
|
Image.fromarray(out).save(p)
|
|
print("saved", p)
|