Detonate rifle grenades on impact with original Half-Life fireball

This commit is contained in:
enne2
2026-10-15 14:20:07 +02:00
parent ae32ad9615
commit 5761e160c1
12 changed files with 581 additions and 81 deletions
+66
View File
@@ -0,0 +1,66 @@
"""Convert the original GoldSrc additive explosion into a portable PNG atlas.
Read-only on the Half-Life installation. No native SPR importer needed at runtime.
"""
import argparse
import hashlib
import json
import struct
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
def extract(source: Path, destination: Path) -> dict:
raw = source.read_bytes()
ident, version, orientation, fmt, radius, width, height, count, beam, sync = (
struct.unpack_from('<4siiifiiifi', raw)
)
assert (ident, version, fmt) == (b'IDSP', 2, 1), 'Expected GoldSrc additive SPR v2'
palette_count = struct.unpack_from('<H', raw, 40)[0]
assert 1 <= palette_count <= 256 and count == 15 and (width, height) == (64, 64)
palette = raw[42:42 + palette_count * 3]
offset = 42 + len(palette)
columns, rows, gutter = 5, 3, 2
tile_w, tile_h = width + 2 * gutter, height + 2 * gutter
atlas = Image.new('RGB', (columns * tile_w, rows * tile_h))
origins = []
for index in range(count):
kind, x, y, w, h = struct.unpack_from('<5i', raw, offset)
assert kind == 0 and (w, h) == (width, height), 'Unsupported frame/group'
offset += 20
pixels = raw[offset:offset + w * h]
assert len(pixels) == w * h and max(pixels) < palette_count
frame = Image.frombytes('P', (w, h), pixels)
frame.putpalette(palette)
# Preserve RGB: black contributes zero under additive blending, NOT alphatest.
atlas.paste(frame.convert('RGB'),
((index % columns) * tile_w + gutter, (index // columns) * tile_h + gutter))
origins.append([x, y])
offset += w * h
assert offset == len(raw), 'Trailing or truncated sprite data'
destination.mkdir(parents=True, exist_ok=True)
output = destination / 'zerogxplode_atlas.png'
atlas.save(output, optimize=True)
manifest = {
'source': 'Half-Life/valve/sprites/zerogxplode.spr',
'source_sha256': hashlib.sha256(raw).hexdigest(),
'destination': str(output.relative_to(ROOT)),
'destination_sha256': hashlib.sha256(output.read_bytes()).hexdigest(),
'format': 'IDSP v2 / SPR_ADDITIVE', 'frame_size': [width, height],
'frame_count': count, 'columns': columns, 'rows': rows, 'gutter': gutter,
'origins': origins, 'fps': 15,
'fps_source': 'https://github.com/ValveSoftware/halflife/blob/master/dlls/ggrenade.cpp',
}
(destination / 'source_manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
return manifest
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=Path.home() /
'.steam/steam/steamapps/common/Half-Life/valve/sprites/zerogxplode.spr')
args = parser.parse_args()
print(json.dumps(extract(args.source, ROOT / 'assets/effects/explosion'), indent=2))