KAZIK/tools/tests/check_hell_weapons.py
Игорь Федосеев ef5ebff353 fix: align hell weapons with crosshair and HUD
Center resting barrels and anchor every firing pose to the HUD edge.

Add native framebuffer regression checks for all nine poses and update game assets, binaries and launcher integrity hash.
2026-11-01 22:41:36 +03:00

94 lines
4.6 KiB
Python

"""Check first-person weapon registration, including every firing pose."""
from pathlib import Path
import struct
from PIL import Image, ImageChops, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'build/scratch'
def check_assets():
OUT.mkdir(parents=True, exist_ok=True)
sheet = Image.new('RGB', (5*288, 2*260), (40,44,50))
draw = ImageDraw.Draw(sheet)
errors = []
for row, name in enumerate(('pistol','shotgun')):
raw = (ROOT/f'assets/hell/{name}.bin').read_bytes()
magic,w,h,count,size = struct.unpack_from('<5I',raw)
assert magic == 0x314d4e41 and (w,h) == (320,160)
assert size == w*h*count*4 == len(raw)-20
for frame in range(count):
start = 20+frame*w*h*4
image = Image.frombytes('RGBA',(w,h),raw[start:start+w*h*4],'raw','BGRA')
alpha = image.getchannel('A')
bounds = alpha.getbbox()
if bounds[0] == 0 or bounds[2] == w:
errors.append(f'{name} frame {frame}: weapon touches horizontal canvas edge')
if bounds[3] != h:
errors.append(f'{name} frame {frame}: hands end {h-bounds[3]} px above panel')
if frame == 0:
# Upper tip of resting barrel must register at screen centre.
tip = alpha.crop((0,bounds[1],w,bounds[1]+4)).getbbox()
centre = (tip[0]+tip[2])/2
if abs(centre-w/2) > 1:
errors.append(f'{name}: resting barrel x={centre:g}, expected {w/2:g}')
# Crop equal empty margins for the compact contact sheet.
preview = image.resize((480,240),Image.Resampling.NEAREST).crop((96,0,384,240))
sheet.paste(preview,(frame*288,row*260),preview)
draw.line((frame*288+144,row*260,frame*288+144,row*260+239), fill=(170,60,60))
draw.line((frame*288,row*260+240,frame*288+287,row*260+240), fill=(200,180,110))
draw.text((frame*288+4,row*260+245),f'{name} {frame}',fill='white')
sheet.save(OUT/'hell-weapon-alignment.png')
assert not errors, '\n'.join(errors)
def check_native():
"""Compare full ASM scene captures with fixed-point, nearest-neighbour overlays."""
sheet = Image.new('RGB', (5*320, 2*240))
draw = ImageDraw.Draw(sheet)
index = 0
for row, name in enumerate(('pistol','shotgun')):
raw = (ROOT/f'assets/hell/{name}.bin').read_bytes()
_,w,h,count,_ = struct.unpack_from('<5I',raw)
base = Image.frombytes('RGBA',(640,480),
(OUT/f'hell-weapon-{name}-base.bgra').read_bytes(),
'raw','BGRA').convert('RGB')
for frame in range(count):
start = 20+frame*w*h*4
source = Image.frombytes('RGBA',(w,h),raw[start:start+w*h*4],'raw','BGRA')
expected = base.copy()
pixels = expected.load()
sprite = source.load()
# Match DrawAnimSprite3D's truncated 16.16 steps, not Pillow's sampling.
xstep,ystep = (w<<16)//480,(h<<16)//240
for y in range(240):
for x in range(480):
color = sprite[(x*xstep)>>16,(y*ystep)>>16]
if color[3]:
assert color[3] == 255
pixels[80+x,176+y] = color[:3]
# Crosshair is drawn over the weapon, as in the native renderer.
for bounds in ((318,234,322,248),(312,240,328,242)):
expected.paste(base.crop(bounds),bounds[:2])
actual = Image.frombytes('RGBA',(640,480),
(OUT/f'hell-weapon-{index}.bgra').read_bytes(),
'raw','BGRA').convert('RGB')
assert ImageChops.difference(actual,expected).getbbox() is None, \
f'{name} frame {frame}: ASM placement/pose differs from asset contract'
assert ImageChops.difference(actual,base).crop((80,415,560,416)).getbbox(), \
f'{name} frame {frame}: native hands do not meet HUD at y=416'
sheet.paste(actual.resize((320,240),Image.Resampling.NEAREST),(frame*320,row*240))
draw.text((frame*320+4,row*240+4),f'{name} {frame}',fill='white')
index += 1
sheet.save(OUT/'hell-weapon-native.png')
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--native',action='store_true',help='also validate captures from check_hell_fps.py')
options = parser.parse_args()
check_assets()
if options.native:
check_native()
print('PASS: centred resting barrels; all weapon poses meet the status bar')