diff --git a/assets/hell/pistol.bin b/assets/hell/pistol.bin index 8f95040..e82922d 100644 Binary files a/assets/hell/pistol.bin and b/assets/hell/pistol.bin differ diff --git a/assets/hell/preview.png b/assets/hell/preview.png index 79f641b..08339e7 100644 Binary files a/assets/hell/preview.png and b/assets/hell/preview.png differ diff --git a/assets/hell/shotgun.bin b/assets/hell/shotgun.bin index 0e9f95c..714b15d 100644 Binary files a/assets/hell/shotgun.bin and b/assets/hell/shotgun.bin differ diff --git a/bin/poker.exe b/bin/poker.exe index abd39a1..b5929a1 100644 Binary files a/bin/poker.exe and b/bin/poker.exe differ diff --git a/build/AssemblerCasinoLauncher.cs b/build/AssemblerCasinoLauncher.cs index 1462d69..782a01c 100644 --- a/build/AssemblerCasinoLauncher.cs +++ b/build/AssemblerCasinoLauncher.cs @@ -8,7 +8,7 @@ namespace AssemblerCasinoLauncher { static class Program { - const string ExpectedPokerSha256 = "EC24778AE95D57B80247180AB0121279C8224A2B7E36801A168AEEF391E6B34B"; + const string ExpectedPokerSha256 = "D25606FE934E0D447DC4791218F8213BB1A21B0F2F7C8AFC0B61D419C03E92C0"; [STAThread] static void Main() diff --git a/build/graphics/poker.exe b/build/graphics/poker.exe index abd39a1..b5929a1 100644 Binary files a/build/graphics/poker.exe and b/build/graphics/poker.exe differ diff --git a/dist/Assembler Casino.exe b/dist/Assembler Casino.exe index 746c604..a3f424d 100644 Binary files a/dist/Assembler Casino.exe and b/dist/Assembler Casino.exe differ diff --git a/docs/HELL_ASSET_PIPELINE.md b/docs/HELL_ASSET_PIPELINE.md index c29f9fc..7f1e093 100644 --- a/docs/HELL_ASSET_PIPELINE.md +++ b/docs/HELL_ASSET_PIPELINE.md @@ -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 diff --git a/poker.exe b/poker.exe index abd39a1..b5929a1 100644 Binary files a/poker.exe and b/poker.exe differ diff --git a/poker.obj b/poker.obj index dbcf28a..e91f7a5 100644 Binary files a/poker.obj and b/poker.obj differ diff --git a/src_graphics/hell_fps_render.inc b/src_graphics/hell_fps_render.inc index be0cf64..7138ab2 100644 --- a/src_graphics/hell_fps_render.inc +++ b/src_graphics/hell_fps_render.inc @@ -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 diff --git a/tools/generators/import_freedoom.py b/tools/generators/import_freedoom.py index 9b0d892..a70efbd 100644 --- a/tools/generators/import_freedoom.py +++ b/tools/generators/import_freedoom.py @@ -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: diff --git a/tools/tests/check_hell_fps.py b/tools/tests/check_hell_fps.py index f1069bc..b2c1fb1 100644 --- a/tools/tests/check_hell_fps.py +++ b/tools/tests/check_hell_fps.py @@ -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__': diff --git a/tools/tests/check_hell_weapons.py b/tools/tests/check_hell_weapons.py new file mode 100644 index 0000000..8390e44 --- /dev/null +++ b/tools/tests/check_hell_weapons.py @@ -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') diff --git a/tools/tests/hell_fps_test.inc b/tools/tests/hell_fps_test.inc index c47f758..f89eeda 100644 --- a/tools/tests/hell_fps_test.inc +++ b/tools/tests/hell_fps_test.inc @@ -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