From 696e3c1d8ec997e11ae83978ec4d2d4faadc9385 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 26 Jul 2026 03:10:21 +0200 Subject: [PATCH] ARE-Format geknackt (Sektor-Tabelle + Objektrecords, tools/are_dump.py): echte Item-/Tuer-/Gegner-Positionen -- plus Smacks' Wasserblasen v1 (ALT beim Schwimmen) --- NOTES.md | 29 +++++++++++++++++++ game/level.py | 25 ++++++++++++++++ tools/are_dump.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 tools/are_dump.py diff --git a/NOTES.md b/NOTES.md index 8619509..083df51 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1936,3 +1936,32 @@ Tony-Kopf an den Ast, jeweils aus dem Stand. JUMP_VELOCITY -380 -> -400 Smacks y=514 (Blatt) -- per Level-Render am Baum verifiziert (apex_TONY/SMACKS.png). Sprungkraft bleibt fuer beide gleich; die Zielhoehen ergeben sich rein aus den Koerpergroessen (56 vs 44 px). + +## 26.07.2026 (24) -- ARE-FORMAT GEKNACKT + Smacks' Wasserblasen (v1) + +Anlass: Stefan will Blasen (Gegner fangen) + Gegner mit Sterbeanimation +-> dafuer braucht es die echten Gegner-/Item-Positionen = ARE-Format. + +### ARE-Struktur (Decoder: tools/are_dump.py) + @8..48 Header: 10 BE-u16-Paare (Bedeutung teils offen) + @352 SEKTOR-TABELLE: ceil(map_px/64) x ceil(map_px_h/64) BE-u16- + Offsets (W1L1: 55x12=660), ffff = leer, sonst Offset+352 -> + Objektliste des 64x64-Sektors + Liste: ffff-Marker, dann 3xBE-u16-Records [Typ][dx][dy] (Pixeloffset + im Sektor) bis zum naechsten ffff +W1L1: 193 Objekte. Verifizierte/plausible Typen: + 151 = Cerealie (x100 -- der HUD-Zaehler!), 154 = Cerealie im Wasser + (x37, liegt in den Unterwasser-Clustern) + 199 = TUER (exakt auf unserer Haus-Tuer Tile 12,37!) + 71/72/73/76 = ?-Box-Inhalte (sitzen auf ctype-4-Tiles => ctype 4 = + ?-BOX, nicht "Item" wie bisher angenommen!) + 194..208 = Level-Moebel/Deko (Lampen etc.), 101/102 = Levelende? + 1/2/3/11/12/17/18/20/31 = vermutlich GEGNER-Spawns (Typ->BOB-Zuordnung + per DOSBox-Abgleich offen; Kandidaten: A-O.BOB, 'ani'-Ladeliste) + +### Wasserblasen v1 (game/level.py) +ALT beim Schwimmen/Tauchen (nur mit Slots 29-31 = Smacks): Blase waechst +29->30->31, treibt in Blickrichtung (vx 55, Daempfung), steigt nach 0.5s, +platzt an der Oberflaeche oder nach 4s. Gegner-Fang (Blase + rauswerfen) +kommt zusammen mit den Gegnern, sobald die Typ-Zuordnung steht. Gegner- +Sterbeanimationen (wie bei den Spielfiguren) ebenfalls dann. diff --git a/game/level.py b/game/level.py index a63312f..83f0ba9 100644 --- a/game/level.py +++ b/game/level.py @@ -751,6 +751,7 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): down_gap = 0.0 # Grace gegen Taste/on_ground-Flackern look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken dying = None # dict(t, vy, drop_y, drop_vy) waehrend Tod-Anim + bubbles = [] # Smacks' Wasserblasen (dict x,y,vx,t) while True: dt = min(clock.tick(60) / 1000.0, 1 / 20) @@ -771,6 +772,12 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): and player.on_ground and abs(player.vx) < 1 and door_open_t is None): door_open_t = 0.0 + elif player.swimming and 29 in player.slots: + # Smacks: Wasserblase (Handbuch: ALT beim Schwimmen/ + # Tauchen). Waechst 29->30->31, treibt vor und steigt. + bubbles.append(dict(x=player.x + 14 * player.facing, + y=player.y - 26, + vx=55.0 * player.facing, t=0.0)) keys = pygame.key.get_pressed() if dying is None: @@ -861,6 +868,24 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): canvas.blit(door_open_sprite, (door.x - cam_x, door.y - cam_y)) else: canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y)) + # Wasserblasen: wachsen (29->30->31), treiben vorwaerts, steigen, + # platzen an der Oberflaeche. (Gegner-Fang kommt mit den Gegnern.) + wy_b = info.get('water_y') + for b in bubbles[:]: + b['t'] += dt + b['x'] += b['vx'] * dt + b['vx'] *= 0.96 + if b['t'] > 0.5: + b['y'] -= 28 * dt + if (wy_b is not None and b['y'] < wy_b + 8) or b['t'] > 4.0: + bubbles.remove(b) + continue + slot = 29 if b['t'] < 0.25 else (30 if b['t'] < 0.5 else 31) + entry = player.slots.get(slot) + if entry: + bs = entry[0] + canvas.blit(bs, (int(b['x'] - bs.get_width() / 2 - cam_x), + int(b['y'] - bs.get_height() / 2 - cam_y))) player.draw(canvas, cam_x, cam_y) if dying is not None and SLOT_DROP1 in player.slots: # fallendes Accessoire (16x16, Slots 48/49 im Wechsel = Flattern) diff --git a/tools/are_dump.py b/tools/are_dump.py new file mode 100644 index 0000000..cf56d82 --- /dev/null +++ b/tools/are_dump.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""ARE-Format-Decoder (geknackt 26.07.2026, NOTES Eintrag (24)). + +Aufbau einer .ARE-Datei (alles Big-Endian): + @8..48 Header: 10 u16-Paare (u.a. Zeit?, Masse, markante Punkte -- + Bedeutung teils offen; Paar 0 = (Levelzeit?, Pixelbreite?)) + @60 weiteres Koordinatenpaar + @352 SEKTOR-TABELLE: (map_px_w/64 aufgerundet) x (map_px_h/64 + aufgerundet) BE-u16-Offsets, Zeile fuer Zeile; 0xffff = leerer + Sektor, sonst zeigt (Offset + 352) auf die Objektliste. + Objektliste: 0xffff-Marker, dann Records aus 3 BE-u16: + [Typ] [dx] [dy] (dx/dy = Pixeloffset im 64x64-Sektor) + bis zum naechsten 0xffff (Start des naechsten Blocks). + +Bekannte Typen (W1L1-Abgleich): + 151 = Cerealie (Sammelitem, HUD-Zaehler), 154 = Cerealie im Wasser + 199 = Tuer (liegt exakt auf der Haus-Tuer 12,37) + 71/72/73/76 = ?-Box-Inhalte (liegen auf den ctype-4-Tiles = ?-Boxen) + 194..208 = Level-Moebel (Lampen, Deko), 1/2/3/11/12/17/18/20/31 = + vermutlich GEGNER-Spawns (per DOSBox noch zuzuordnen), 101/102 = Levelende? + +Aufruf: python3 tools/are_dump.py W1L1 +""" +import os +import struct +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from game import formats # noqa: E402 + + +def parse_are(are_bytes, map_w_tiles, map_h_tiles): + '''Liefert Liste (x_px, y_px, typ), Koordinaten in Map-Pixeln.''' + d = are_bytes + sec_w = (map_w_tiles * 16 + 63) // 64 + sec_h = (map_h_tiles * 16 + 63) // 64 + objs = [] + for sy in range(sec_h): + for sx in range(sec_w): + idx = sy * sec_w + sx + off = struct.unpack('>H', d[352 + idx * 2:354 + idx * 2])[0] + if off == 0xffff: + continue + q = 352 + off + while q < len(d) - 1: + v = struct.unpack('>H', d[q:q + 2])[0] + if v == 0xffff: + if q == 352 + off: + q += 2 + continue + break + typ, dx, dy = struct.unpack('>3H', d[q:q + 6]) + objs.append((sx * 64 + dx, sy * 64 + dy, typ)) + q += 6 + return objs + + +def main(): + name = sys.argv[1] if len(sys.argv) > 1 else 'W1L1' + dat = os.environ.get('KELLOGG_DAT', os.path.join( + os.path.dirname(__file__), '..', 'game', 'original', 'PCKELL.DAT')) + c = formats.DATContainer(dat) + w, h, _ = formats.parse_map(c.entries[name + '.MAP']) + objs = parse_are(c.entries[name + '.ARE'], w, h) + from collections import Counter + print(f'{name}: {len(objs)} Objekte, Map {w}x{h} Tiles') + print('Typen:', dict(sorted(Counter(t for _, _, t in objs).items()))) + for x, y, t in objs: + print(f' Typ {t:3d} (0x{t:02x}) px ({x:4d},{y:4d}) Tile ({x // 16},{y // 16})') + + +if __name__ == '__main__': + main()