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.
This commit is contained in:
Игорь Федосеев 2026-11-01 22:41:36 +03:00
parent ce5121debd
commit ef5ebff353
15 changed files with 190 additions and 4 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

View file

@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher
{
static class Program
{
const string ExpectedPokerSha256 = "EC24778AE95D57B80247180AB0121279C8224A2B7E36801A168AEEF391E6B34B";
const string ExpectedPokerSha256 = "D25606FE934E0D447DC4791218F8213BB1A21B0F2F7C8AFC0B61D419C03E92C0";
[STAThread]
static void Main()

Binary file not shown.

Binary file not shown.

View file

@ -13,6 +13,16 @@ frame count, payload bytes), then row-major BGRA frames. Nearest-neighbour only.
| `faces.bin` | 24×32 | Five health bands × four expressions, then dead; bottom-centred |
| `impacts.bin` | 32×32 | Four `PUFF` frames, shared sprite origin; transparent |
| `painting.bin` | 512×256 | One opaque, power-of-two wall texture; full-frame UVs |
| `pistol.bin`, `shotgun.bin` | 320×160 | Resting barrel centred at x=160; every frame bottom-anchored |
Weapon art retains the shared animation scale from a 192×160 fit. The wider
transparent canvas preserves the shotgun's leftward pump movement; horizontal
translation is shared across each weapon's frames. Each frame's opaque bottom
is registered at y=160, keeping wrists attached to the status bar during recoil.
Rendered at (80,176), size 480×240: resting barrel x=320, hands end at HUD y=416.
`python tools/tests/check_hell_weapons.py` checks all asset anchors;
`python tools/tests/check_hell_fps.py` also compares all nine weapon poses against
the real ASM framebuffer and writes `build/scratch/hell-weapon-native.png`.
Painting composes Freedoom sky, lava, masonry and monsters with an authored gilt
border. Its preview uses intended landscape aspect. Engine maps the texture onto

BIN
poker.exe

Binary file not shown.

BIN
poker.obj

Binary file not shown.

View file

@ -299,7 +299,9 @@ hf_shotgun_pose:
jbe hf_weapon_draw
mov edi,1
hf_weapon_draw:
invoke DrawAnimSprite3D,esi,edi,176,176,288,240
; 320x160 packs: resting barrel at x=160, every wrist reaches row 160.
; 1.5x scaling puts barrel at screen x=320 and wrists under HUD y=416.
invoke DrawAnimSprite3D,esi,edi,80,176,480,240
cmp hfFlash,0
je @F
invoke DrawAnimSprite3D,OFFSET hfFireArt,0,296,202,48,48

View file

@ -101,6 +101,24 @@ def aligned(wad, names, size=(128, 128)):
return result
def weapon_frames(wad, names):
"""Register by resting barrel, not animation bounds; keep wrists at HUD edge."""
frames = aligned(wad, names, (192,160))
alpha = frames[0].getchannel('A')
top = alpha.getbbox()[1]
tip = alpha.crop((0,top,192,top+4)).getbbox()
shift_x = round(160-(tip[0]+tip[2])/2)
result = []
for image in frames:
bottom = image.getchannel('A').getbbox()[3]
canvas = Image.new('RGBA',(320,160))
# Extra horizontal canvas retains the shotgun's leftward pump motion.
# Same scale and horizontal translation for every frame of each weapon.
canvas.paste(image,(shift_x,160-bottom))
result.append(canvas)
return result
def interface_assets(wad, out, manifest):
"""Doom-layout status bar, native-size glyphs and common-origin face frames."""
pack(out/'hud.bin', [wad.patch('STBAR')[0]])
@ -187,7 +205,7 @@ def main():
if not choices:
raise KeyError(lump)
names[i] = sorted(choices)[0]
images = aligned(wad, names, (192, 160) if name in ('pistol','shotgun') else (128,128))
images = weapon_frames(wad,names) if name in ('pistol','shotgun') else aligned(wad,names)
if name == 'portal':
# Recolour original Freedoom teleport animation to infernal orange/red.
for image in images:

View file

@ -43,10 +43,13 @@ def main():
if options.window:
print('PASS: real Win32 window, 120 presented frames, mouse-down/up events, firing, Mick Gordon playback, six SFX channels, clean return')
return
from check_hell_weapons import check_assets, check_native
check_assets()
check_native()
print('PASS: painting entrance, collision/LOS, multi-target seven-pellet shotgun, '
'single-ray pistol, armour/death/retry, gates/secrets, '
'1050 AI ticks, finite-ammo boss fight with dodging, first-clear reward, '
'native/preview save roundtrips, return, 4 native render captures')
'native/preview save roundtrips, return, 4 native scene captures, 9 weapon poses')
if __name__ == '__main__':

View file

@ -0,0 +1,94 @@
"""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')

View file

@ -10,7 +10,65 @@ hfTestProfileDir db 'build\scratch',0
hfTestProfileName db 'hell-profile.dat',0
hfTestBoss dd 0
hfTestStrafe dd 5000
hfTestWeaponBase0 db 'build\scratch\hell-weapon-pistol-base.bgra',0
hfTestWeaponBase1 db 'build\scratch\hell-weapon-shotgun-base.bgra',0
hfTestWeapon0 db 'build\scratch\hell-weapon-0.bgra',0
hfTestWeapon1 db 'build\scratch\hell-weapon-1.bgra',0
hfTestWeapon2 db 'build\scratch\hell-weapon-2.bgra',0
hfTestWeapon3 db 'build\scratch\hell-weapon-3.bgra',0
hfTestWeapon4 db 'build\scratch\hell-weapon-4.bgra',0
hfTestWeapon5 db 'build\scratch\hell-weapon-5.bgra',0
hfTestWeapon6 db 'build\scratch\hell-weapon-6.bgra',0
hfTestWeapon7 db 'build\scratch\hell-weapon-7.bgra',0
hfTestWeapon8 db 'build\scratch\hell-weapon-8.bgra',0
; Weapon, cooldown (-1 hides both packs for a clean reference), capture path.
hfTestWeaponCases dd 0,-1,OFFSET hfTestWeaponBase0
dd 0,0,OFFSET hfTestWeapon0
dd 0,9,OFFSET hfTestWeapon1
dd 0,7,OFFSET hfTestWeapon2
dd 0,5,OFFSET hfTestWeapon3
dd 0,3,OFFSET hfTestWeapon4
dd 1,-1,OFFSET hfTestWeaponBase1
dd 1,0,OFFSET hfTestWeapon5
dd 1,29,OFFSET hfTestWeapon6
dd 1,18,OFFSET hfTestWeapon7
dd 1,10,OFFSET hfTestWeapon8
HFTestCapture PROTO :DWORD
.code
HFTestWeaponFrames PROC uses esi ebx
local pistolPixels:DWORD,shotgunPixels:DWORD
mov eax,hfPistol.pixels
mov pistolPixels,eax
mov eax,hfShotgun.pixels
mov shotgunPixels,eax
call HFReset
mov hfHasShotgun,1
mov esi,OFFSET hfTestWeaponCases
mov ebx,11
hf_test_weapon_frame:
mov eax,[esi]
mov hfWeapon,eax
mov eax,[esi+4]
mov hfCooldown,eax
cmp eax,-1
jne @F
mov hfCooldown,0
mov hfPistol.pixels,0
mov hfShotgun.pixels,0
@@:
call RenderHellFPS3D
invoke HFTestCapture,DWORD PTR [esi+8]
mov eax,pistolPixels
mov hfPistol.pixels,eax
mov eax,shotgunPixels
mov hfShotgun.pixels,eax
add esi,12
dec ebx
jnz hf_test_weapon_frame
call HFReset
ret
HFTestWeaponFrames ENDP
HFTestAimBoss PROC uses esi edi ebx
local vx:DWORD,vy:DWORD,best:DWORD
mov esi,hfTestBoss
@ -106,6 +164,7 @@ RunHellFPSTest PROC uses esi edi ebx
mov hfOldCeiling,FP_1*5/2
call HFReset
mov hfTestStage,3
call HFTestWeaponFrames
call RenderHellFPS3D
invoke HFTestCapture,OFFSET hfTestFrame
invoke HFLineClear,3*FP_1+FP_HALF,26*FP_1+FP_HALF,7*FP_1+FP_HALF,26*FP_1+FP_HALF