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:
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.
BIN
bin/poker.exe
BIN
bin/poker.exe
Binary file not shown.
|
|
@ -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.
BIN
dist/Assembler Casino.exe
vendored
BIN
dist/Assembler Casino.exe
vendored
Binary file not shown.
|
|
@ -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
BIN
poker.exe
Binary file not shown.
BIN
poker.obj
BIN
poker.obj
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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__':
|
||||
|
|
|
|||
94
tools/tests/check_hell_weapons.py
Normal file
94
tools/tests/check_hell_weapons.py
Normal 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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue