WELTKARTE mit Levelauswahl -- Pfad/Punkte/Namen live aus dem Binary
game/worldmap.py: parst Kartenpfad, Levelpunkt-Positionen und die 22 Levelnamen zur Laufzeit aus START.EXE (NE-Datensegment, Anker-Suche, Fallback W1L1). Links/Rechts laesst die Kartenfigur (Mini-Kopf KARTE4-7 je Charakter) die Original-Wegpunkte ablaufen, der aktive Levelpunkt blinkt orange/blau (KARTE0/2 -- Blinksequenz stand exakt so in der Binary-Animationstabelle), Space/Return startet das Level, ESC -> Menue. Kartenposition bleibt ueber Levelbesuche erhalten. Level ohne handgepflegte LEVEL_INFO erhalten einen generischen Spawn (erste Bodenstelle) -- damit sind ALLE 22 Level anwaehlbar und spielbar (Basis-Physik; Wasser/Tueren dort folgen mit den echten Leveldaten). E2E: Karte -> laufen -> W1L3 spielen -> zurueck -> W1L2 -> QUIT rc=0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""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 # orange/blau-Wechsel des aktiven Punkts
|
||||
|
||||
|
||||
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('<H', d[p:p + 2])[0]
|
||||
p += 2
|
||||
if v == 0xffff:
|
||||
return vals, p
|
||||
vals.append(v)
|
||||
if len(vals) > 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']
|
||||
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))
|
||||
for i, seg in enumerate(self.segments):
|
||||
if seg['label'] is None:
|
||||
continue
|
||||
lx, ly = seg['label']
|
||||
if i == self.current:
|
||||
dot = self.dot_big_on if int(t / BLINK_SECONDS) % 2 == 0 else self.dot_big_off
|
||||
else:
|
||||
dot = self.dot_small
|
||||
canvas.blit(dot, (lx - dot.get_width() // 2, ly - dot.get_height() // 2))
|
||||
# Spielfigur-Kopf
|
||||
px, py = walk_pos if walk_pos else self.segments[self.current]['stand']
|
||||
head = self.heads.get(char, self.heads['TONY'])
|
||||
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()
|
||||
Reference in New Issue
Block a user