"""Weltkarte mit Levelauswahl -- Pfad, Levelpunkte und Namen aus dem START.EXE-Binary (NE-Datensegment), Marker/Koepfe aus KARTE0-9.PCC. Binary-Struktur (Fund 25.07.2026, Offsets ~145114..146760 in START.EXE): Header, dann pro Level ein Segment aus 0xffff-terminierten u16-Records: [x y 0 25] Standpunkt (Wiederholung des vorigen Endpunkts) [x y 1 25] ... Wegpunkte (die Figur laeuft sie ab; 25 = Ticks) [x y 0 25] Endpunkt = Standpunkt an diesem Level [label_x label_y] Position des Levelpunkt-Markers auf der Karte [0] (Flag/Reserve) danach die 22 Levelnamen als Pascal-Strings (\x04W1L0...). Karten-Sprites: KARTE0/2 = grosser oranger/blauer Punkt, KARTE1/3 = kleine Varianten, KARTE4-7 = Mini-Koepfe (Tony, Smacks, Toucan, Coco) als Spielfigur-Marker. Blink-Animation des aktiven Punkts laut Binary-Tabelle: [delay 100] 900,902,900 = KARTE0/KARTE2-Wechsel. """ import re import struct import sys import time import pygame from . import intro # Mini-Kopf-Sprite je Charakter (KARTE4=Tony, 5=Smacks, 6=Toucan, 7=Coco) HEAD_SPRITE = {'TONY': 'KARTE4', 'SMACKS': 'KARTE5', 'TOUCAN': 'KARTE6', 'COCO': 'KARTE7'} STEP_SECONDS = 0.18 # Lauftempo der Kartenfigur pro Wegpunkt BLINK_SECONDS = 0.5 # Blinkrhythmus der aktiven Position # Die Binary-Koordinaten sind y-GEFLIPPT (DOSBox-Nachmessung 25.07.2026: # Level-1-Punkt real bei (59,153), Binary-Label (53,56) -> screen_y = 209-y; # damit liegen die fruehen Level im Wald unten und W2/W3 in Bergen/Schloss # oben -- deckungsgleich mit Stefans Original-Screenshots). MAP_Y_FLIP = 209 def _tf(p): return (p[0], MAP_Y_FLIP - p[1]) def parse_worldmap(exe_path): '''Liest Pfadsegmente + Levelnamen aus START.EXE. Rueckgabe: (segments, names) mit segments = Liste von dicts {waypoints: [(x,y)...], stand: (x,y), label: (x,y)}. Wirft bei Strukturbruch Exception (Aufrufer faellt zurueck).''' d = open(exe_path, 'rb').read() anchor = struct.pack('<4H', 58, 40, 0, 25) start = d.find(anchor) if start < 0: raise ValueError('Kartenpfad-Anker nicht gefunden') names_at = d.find(b'\x04W1L0\x04W1L1') if names_at < 0: raise ValueError('Levelnamen nicht gefunden') # Namen lesen (22 Pascal-Strings) names = [] p = names_at while len(names) < 22 and p < len(d) - 1: ln = d[p] if not (1 <= ln <= 8): break names.append(d[p + 1:p + 1 + ln].decode('ascii')) p += 1 + ln # Records lesen def read_list(p): vals = [] while p <= names_at - 2: v = struct.unpack(' 8: return None, p return vals, p segments = [] p = start cur = {'waypoints': [], 'stand': None, 'label': None} while p < names_at - 2: vals, p = read_list(p) if vals is None: continue if len(vals) == 4 and vals[3] == 25: x, y, flag = vals[0], vals[1], vals[2] if x > 320 or y > 200: continue # Sonder-/Sprungrecords (Weltwechsel) ueberspringen cur['waypoints'].append((x, y)) if flag == 0 and len(cur['waypoints']) > 1: cur['stand'] = (x, y) elif len(vals) == 2: cur['label'] = (vals[0], vals[1]) if cur['stand'] is None and cur['waypoints']: cur['stand'] = cur['waypoints'][-1] segments.append(cur) cur = {'waypoints': [], 'stand': None, 'label': None} # len==1 ([0]-Flag) und leere Listen: ignorieren return segments, names class WorldMap: '''Zustand + Zeichnen der Karte. current = Index des angewaehlten Levels.''' def __init__(self, assets, menu_assets): self.assets = assets self.karte = menu_assets.karte self.font = menu_assets.font_small exe = None import os base = os.path.dirname(os.path.abspath(assets.container.path)) \ if hasattr(assets.container, 'path') else None try: from .intro import PROJECT_ROOT exe = os.path.join(PROJECT_ROOT, 'game', 'original', 'START.EXE') self.segments, self.names = parse_worldmap(exe) except Exception as exc: print(f'[worldmap] Binary-Pfaddaten nicht lesbar ({exc}) -- ' f'Fallback nur Level 1', file=sys.stderr) self.segments = [dict(waypoints=[(113, 35)], stand=(113, 35), label=(108, 51))] self.names = ['W1L1'] # Positionen ins Screen-System transformieren (y-Flip) for seg in self.segments: seg['waypoints'] = [_tf(p) for p in seg['waypoints']] if seg['stand']: seg['stand'] = _tf(seg['stand']) if seg['label']: seg['label'] = _tf(seg['label']) self.dot_big_on = intro.make_border_bg_transparent(assets.pcc_surface('KARTE0')) self.dot_big_off = intro.make_border_bg_transparent(assets.pcc_surface('KARTE2')) self.dot_small = intro.make_border_bg_transparent(assets.pcc_surface('KARTE3')) self.heads = {c: intro.make_border_bg_transparent(assets.pcc_surface(n)) for c, n in HEAD_SPRITE.items()} self.current = 1 if len(self.segments) > 1 else 0 # Start: Level 1 (Index 0 = W1L0) def draw(self, canvas, char, t, walk_pos=None): canvas.blit(self.karte, (0, 0)) head = self.heads.get(char, self.heads['TONY']) head_phase = int(t / BLINK_SECONDS) % 2 == 0 for i, seg in enumerate(self.segments): if seg['label'] is None: continue lx, ly = seg['label'] if i == self.current and walk_pos is None: # Original-Verhalten (Netz-Referenzbilder + DOSBox-Diff): # an der aktuellen Position wechseln PUNKT und MINI-KOPF if head_phase: canvas.blit(head, (lx - head.get_width() // 2, ly - head.get_height() // 2)) else: canvas.blit(self.dot_big_on, (lx - self.dot_big_on.get_width() // 2, ly - self.dot_big_on.get_height() // 2)) else: dot = self.dot_small canvas.blit(dot, (lx - dot.get_width() // 2, ly - dot.get_height() // 2)) # waehrend des Laufens: Kopf entlang des Pfads if walk_pos is not None: px, py = walk_pos canvas.blit(head, (px - head.get_width() // 2, py - head.get_height())) def run_worldmap(clock, assets, menu_assets, char='TONY', state={}): '''Weltkarte: Links/Rechts wandert zum vorigen/naechsten Levelpunkt (Figur laeuft die Original-Wegpunkte ab), Space/Return startet das angewaehlte Level, ESC -> Menue. Rueckgabe: ('level', name) | 'menu' | 'quit'. state haelt die aktuelle Position ueber Aufrufe hinweg.''' wm = state.get('wm') if wm is None or wm.assets is not assets: wm = WorldMap(assets, menu_assets) state['wm'] = wm walking = None # (Wegpunktliste, laufindex, richtung) waehrend der Bewegung start = time.monotonic() canvas = pygame.Surface((320, 200)) while True: t = time.monotonic() - start for event in pygame.event.get(): if event.type == pygame.QUIT: return 'quit' if event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN and (event.mod & pygame.KMOD_ALT): intro.toggle_fullscreen() continue if event.key == pygame.K_ESCAPE: return 'menu' if walking is None: if event.key in (pygame.K_RIGHT, pygame.K_UP) and wm.current < len(wm.segments) - 1: pts = wm.segments[wm.current + 1]['waypoints'] walking = {'pts': pts, 'i': 0.0, 'target': wm.current + 1} elif event.key in (pygame.K_LEFT, pygame.K_DOWN) and wm.current > 0: pts = list(reversed(wm.segments[wm.current]['waypoints'])) walking = {'pts': pts, 'i': 0.0, 'target': wm.current - 1} elif event.key in (pygame.K_SPACE, pygame.K_RETURN): return ('level', wm.names[wm.current]) dt = clock.tick(60) / 1000.0 walk_pos = None if walking is not None: walking['i'] += dt / STEP_SECONDS pts = walking['pts'] if walking['i'] >= len(pts) - 1 or len(pts) < 2: wm.current = walking['target'] walking = None else: i0 = int(walking['i']) frac = walking['i'] - i0 x0, y0 = pts[i0] x1, y1 = pts[min(i0 + 1, len(pts) - 1)] walk_pos = (int(x0 + (x1 - x0) * frac), int(y0 + (y1 - y0) * frac)) wm.draw(canvas, char, t, walk_pos) intro._blit_centered(intro.build_letterboxed(canvas)) pygame.display.flip()