From 09264109ca15b1f0b236f31c617ebd0f5cac24d0 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 25 Jul 2026 16:10:21 +0200 Subject: [PATCH] Charaktersystem komplett auf Original-Slots umgebaut: alle 4 Figuren mit echten Animationen, Toucan-Flug, Coco-Hangeln, Smacks Sprung/Duck-Fix, Anker aus BOB-Headern, Wasser-Sofortschaden --- NOTES.md | 38 +++++++ game/formats.py | 17 ++- game/level.py | 286 +++++++++++++++++++++++++++++------------------- 3 files changed, 224 insertions(+), 117 deletions(-) diff --git a/NOTES.md b/NOTES.md index 34f21ef..833f4b3 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1848,3 +1848,41 @@ Die 14-Byte-Frame-Header (Felder u0/u1/u2 vor width/height) bedeuten: vermutlich Levelobjekt-/Gegner-Slots (Ladeliste 'ani',A-O,gamebar,keys, vitamin,platfw1,doors,blume,wolke,KROKO,BOSSNAKE,w2snake,DRAGON @4984). Noch offen. + +## 26.07.2026 (19) -- Charaktersystem auf Original-Slots umgebaut (alle 4 Figuren) + +Umsetzung von Eintrag (18) in game/level.py + game/formats.py: + +- formats.BobFrame traegt jetzt slot/anchor_x/anchor_y aus dem Frame-Header. +- Player haelt Sprites als {slot: (Surface, Anker)}; Blickrichtung links = + Slot+50 (Original-Regel), Einzel-Frames fallen auf die Rechts-Variante + zurueck. Gezeichnet wird am Original-ANKER (Fusspunkt) + SPRITE_SINK. +- CHAR_DEFS auf bob/item_icon/head_index geschrumpft; FAEHIGKEITEN werden + aus den vorhandenen Slots abgeleitet (can_run=13, can_swim=25, + can_fly=29+79 als Paar, can_hang=35) -- deckt sich exakt mit Handbuch. +- Animationen = Original-Tabellen: Walk [4][0-7], Run [4][13-16], + Schwimmstoss [10][25-28], Fluegelschlag [12][29,30,31,30], Idle-Warten + [25][23,24,23,22,...,10x5] (= Schachtel-essen-Posen 22-24!). +- Dabei 3 Alt-Bugs gefixt: Smacks Sprung/Ducken waren VERTAUSCHT (Sprung + ist Slot 9 = BOB 18, Ducken Slot 12 = BOB 24 -- Stefans Verdacht war + richtig); Smacks' alte Schwimm-Frames waren die LINKS-Varianten; + Smacks-Idle war ein Laufframe (Slot 6) statt Slot 8. +- NEU Toucan: Flugkette wie im Original -- Absprung armiert + flap_strength=-300, jeder Schlag (Space-Edge in der Luft) wird um 48 + schwaecher, unter -70 nur noch Gleiten (Slot 34, Sinken max 55 px/s, + Pfeil-runter bricht ab). Test: exakt 5 Schlaege bis zum Gleiten ✓. +- NEU Coco: Hangeln -- greift mit Hoch-Taste in der Luft einen "Ast", + hangelt seitlich (Slots 35-44, Anker-Y 64), Runter laesst los. + EXPERIMENTELL: Ast-Trigger = ctype-Bit 8; in W1L2 liegen solche Tiles + aber nur in Zeile 1 (unsichtbar oben) -> echter Ast-ctype noch per + DOSBox/W2-Level zu verifizieren. +- Wasser-Schaden: erster Kontakt kostet SOFORT ein Herz (+Blinken), danach + alle 1.5s (Stefans Erinnerung; DOSBox-Nachmessung steht noch aus). +- Tod: Slot 19 + fallendes Accessoire Slots 48/49 im Wechsel (16x16, + Muetze/Tuch); Tuer-Gucken = Slot 45 (alle Charaktere, wie vorher). +- Smacks-Slots 29-31 = WASSERBLASEN-Posen (Handbuch: ALT-Aktion) -- fuer + spaeter (Projektil fehlt noch); Slots 20/21 (32x16) = Spritzer?, offen. + +Verifiziert headless: alle 4 Chars idle/walk/duck/squat/fall = Slots +8/0/12/10/9; Smacks schwimmt (25, Stoss 25-28); Toucan 5 Schlaege -> +gleiten; Coco haengt/hangelt/loest. --once exit 0. diff --git a/game/formats.py b/game/formats.py index 74851fb..20cbcea 100644 --- a/game/formats.py +++ b/game/formats.py @@ -201,10 +201,18 @@ def _parse_bob_executable(data): BOB_STRIDE = 84 class BobFrame: - __slots__ = ('width', 'height', 'pixels') # pixels: list of palette indices, width*height, -1 = transparent - def __init__(self, width, height): + # pixels: list of palette indices, width*height, -1 = transparent. + # slot/anchor_x/anchor_y kommen aus dem 14-Byte-Frame-Header (Felder + # u0/u1/u2, Fund 26.07.2026): slot = Engine-Sprite-Index (0-49 rechts, + # +50 = Blickrichtung links), anchor = Fusspunkt-Anker relativ zur + # linken oberen Ecke (anchor_y > height z.B. bei Cocos Hangel-Frames). + __slots__ = ('width', 'height', 'pixels', 'slot', 'anchor_x', 'anchor_y') + def __init__(self, width, height, slot=0, anchor_x=0, anchor_y=0): self.width = width self.height = height + self.slot = slot + self.anchor_x = anchor_x + self.anchor_y = anchor_y self.pixels = [-1] * (width * height) def set(self, x, y, idx): @@ -221,6 +229,9 @@ def parse_bob(data): header = data[pos:pos+14] if len(header) < 14: raise ValueError(f'truncated header at {pos}') + slot = struct.unpack_from(' kein Schlag mehr (Original -0x4000) +GLIDE_FALL_MAX = 55.0 # Sinkgeschwindigkeit im Gleitflug +IDLE_WAIT_FPS = 2.8 # Idle-Warteanimation [25 Ticks] SOLID_BIT = 32 SLOPE_CT = 16 -# Charakter-Definitionen. Richtungs-Paritaet: gerade Frames = rechts -# (an Tony per Live-Test bestaetigt; bei Smacks analog angenommen). +# --- SLOT-System (komplett aus dem Binary, NOTES Eintrag (18)) ------------- +# Jeder BOB-Frame traegt seine SLOT-Nummer + Anker im Header. Regel aus +# set_anim (Seg3:0x4cad): sprite = slot; Blickrichtung links -> slot + 50. +# Die Slot-Belegung ist fuer ALLE Charaktere gleich; welche Faehigkeiten ein +# Charakter hat, ergibt sich daraus, WELCHE Slots sein BOB enthaelt +# (nur Tony hat 13-16 = rennen, nur Smacks/Toucan 25-28 = schwimmen, +# nur Toucan 29-31 als Richtungs-Paare = fliegen, nur Coco 35-44 = hangeln). +SLOT_IDLE = 8 +SLOT_JUMP = 9 # Springen/Fallen +SLOT_SQUAT = 10 # Absprung-Hocke +SLOT_ARMUP = 11 # Arm hoch / Daumen hoch (Spawn-Pose, Tabelle [70][10,11,10]) +SLOT_DUCK = 12 +SLOT_HURT = 17 # Schadens-/Schreck-Frames 17,18 +SLOT_DEATH = 19 # Tod (19, Sturz 20) +SLOT_SWIM_IDLE = 25 # Treiben im Wasser (erster Schwimm-Frame; 22-24 sind + # die "Schachtel essen"-Posen der Idle-Warteanimation) +SLOT_GLIDE = 34 # Toucan: Gleiten (Tabelle [4][34,34,34], Flag 0x20) +SLOT_PEEK = 45 # In-die-Tuer-gucken (Einzelframe, alle Charaktere) +SLOT_DROP1, SLOT_DROP2 = 48, 49 # fallendes Accessoire beim Tod (16x16) + +# Animations-Tabellen (Original: [delay Ticks@70Hz][slots...]): +ANIM_WALK = [0, 1, 2, 3, 4, 5, 6, 7] # [4] -> 17.5 fps +ANIM_RUN = [13, 14, 15, 16] # [4] +ANIM_SWIM = [25, 26, 27, 28] # [10] Schwimmstoss +ANIM_FLAP = [29, 30, 31, 30] # [12] Toucan-Fluegelschlag +ANIM_IDLE_WAIT = [23, 24, 23, 22, 23, 22, 23, 22, + 23, 24, 23, 22, 23, 10, 10, 10, 10, 10] # [25] + CHAR_DEFS = { - 'TONY': dict( - bob='TONY', can_run=True, can_swim=False, - walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15], - run_r=[26, 28, 30, 32], run_l=[27, 29, 31, 33], - # 16/17 = echte Steh-Frames (0/1 sind Schrittphasen -- "letzter - # Lauf-Frame bleibt stehen"-Bug) - idle_r=16, idle_l=17, jump_r=18, jump_l=19, duck_r=24, duck_l=25, - swim_r=None, swim_l=None, - # ECHTE Sequenz aus der Binary-Animationstabelle (@143966, x2): - idle_anim_r=[46, 48, 46, 44, 46, 44, 46, 44, 46, 48, 46, 44, 46, 20, 20, 20, 20, 20], - idle_anim_l=[47, 49, 47, 45, 47, 45, 47, 45, 47, 49, 47, 45, 47, 21, 21, 21, 21, 21], - spawn_frame=22, # Arm nach oben gestreckt - death_frame=38, drop_frame=40, # erschrocken; fallendes HALSTUCH - peek_frame=50, # in die Tuer reinschauen (Rueckenansicht -- Stefans Fund) - item_icon='ITE0', head_index=0, - ), - 'SMACKS': dict( - bob='SMACKS', can_run=False, can_swim=True, - # Zyklus OHNE 12-15 (das sind Steh-/Uebergangsframes) - walk_r=[0, 2, 4, 6, 8, 10], walk_l=[1, 3, 5, 7, 9, 11], - run_r=None, run_l=None, - # Idle 12/13: Smacks' echte Steh-Frames (0/1 sind Schrittphasen -- - # Stefans Bug "beim Anhalten wird noch das Laufen-Sprite angezeigt") - idle_r=12, idle_l=13, jump_r=24, jump_l=25, duck_r=18, duck_l=19, - swim_r=[37, 39, 41, 43], swim_l=[38, 40, 42, 44], - idle_anim_r=[31, 33, 35], idle_anim_l=[32, 34, 36], - spawn_frame=22, # Daumen-hoch-Geste - death_frame=26, drop_frame=29, # erschrocken; fallende KAPPE - peek_frame=48, # Reinschau-Frame (letzter vor den Platzhaltern) - item_icon='ITE1', head_index=1, - ), - 'COCO': dict( - bob='COCO', can_run=False, can_swim=False, - walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15], - run_r=None, run_l=None, - idle_r=0, idle_l=1, jump_r=18, jump_l=19, duck_r=24, duck_l=25, - swim_r=None, swim_l=None, idle_anim_r=None, idle_anim_l=None, - spawn_frame=None, - death_frame=None, drop_frame=None, peek_frame=None, - item_icon='ITE3', head_index=2, - ), - 'TOUCAN': dict( - bob='TOUCAN', can_run=False, can_swim=False, - walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15], - run_r=None, run_l=None, - idle_r=0, idle_l=1, jump_r=18, jump_l=19, duck_r=24, duck_l=25, - swim_r=None, swim_l=None, idle_anim_r=None, idle_anim_l=None, - spawn_frame=None, - death_frame=None, drop_frame=None, peek_frame=None, - item_icon='ITE2', head_index=3, - ), + 'TONY': dict(bob='TONY', item_icon='ITE0', head_index=0), + 'SMACKS': dict(bob='SMACKS', item_icon='ITE1', head_index=1), + 'COCO': dict(bob='COCO', item_icon='ITE3', head_index=2), + 'TOUCAN': dict(bob='TOUCAN', item_icon='ITE2', head_index=3), } CHAR_ORDER = ['TONY', 'SMACKS', 'COCO', 'TOUCAN'] @@ -275,9 +260,14 @@ class Player: self.anim_t = 0.0 self.idle_t = 0.0 self.spawn_t = 0.0 # Start-Sprite-Sequenz (Arm hoch/Daumen hoch) - self.force_frame = None # erzwungener Frame (Tuer-Peek, Tod) + self.force_slot = None # erzwungener Slot (Tuer-Peek, Tod) self.hurt_t = 0.0 # Rest-Blinkzeit nach Schaden self.jump_squat_t = 0.0 # Absprung-Hocke (kurz nach dem Absprung) + self.swim_stroke_t = 0.0 # Rest-Dauer der Schwimmstoss-Animation + self.flap_strength = 0.0 # Toucan: aktuelle Fluegelschlag-Kraft + self.flap_t = 0.0 # Rest-Dauer der Fluegelschlag-Animation + self.gliding = False + self.hanging = False # Coco: haengt an einem Ast self._grounded_before = False self.set_character(char, pos) @@ -286,13 +276,28 @@ class Player: self.d = CHAR_DEFS[char] frames = parse_bob(self.assets.container.entries[self.d['bob'] + '.BOB']) palette = get_bob_palette(self.assets.container.entries, self.d['bob']) - self.surfs = _frames_to_surfaces(frames, palette) + surfs = _frames_to_surfaces(frames, palette) + # Slot -> (Surface, Anker) direkt aus den BOB-Headern (Original-Daten) + self.slots = {fr.slot: (surf, fr.anchor_x, fr.anchor_y) + for fr, surf in zip(frames, surfs)} + # Faehigkeiten ergeben sich aus den vorhandenen Slots (wie im Original) + self.can_run = 13 in self.slots + self.can_swim = 25 in self.slots + self.can_fly = 29 in self.slots and 79 in self.slots # nur Paare = Toucan + self.can_hang = 35 in self.slots # Coco if pos is not None: self.x, self.y = float(pos[0]), float(pos[1]) # Start-Sprite-Sequenz beim Levelstart/Charakterwechsel (Stefans # Beobachtung: Tony streckt die Hand nach oben, Smacks Daumen hoch) self.spawn_t = SPAWN_ANIM_SECONDS + def sprite(self, slot): + '''(Surface, ax, ay) fuer einen Slot; Blickrichtung links = +50 + (Original-Regel aus set_anim). Einzel-Frames gibt es nur einseitig.''' + if self.facing < 0 and slot < 50 and slot + 50 in self.slots: + return self.slots[slot + 50] + return self.slots.get(slot) or self.slots[SLOT_IDLE] + def rect(self): return pygame.Rect(int(self.x - self.HITBOX_W / 2), int(self.y - self.HITBOX_H), @@ -310,7 +315,9 @@ class Player: pygame.K_UP, pygame.K_DOWN, pygame.K_SPACE)): self.spawn_t = 0.0 in_water = level.in_water(self.y) - self.swimming = in_water and self.d['can_swim'] + self.swimming = in_water and self.can_swim + if self.on_ground: + self.gliding = False # Ducken geht NICHT im Wasser (Stefans Regel -- auch nicht fuer # Nicht-Schwimmer, die auf der Oberflaeche huepfen) self.ducking = (self.on_ground and not self.swimming and not in_water @@ -323,7 +330,7 @@ class Player: move += 1 mods = pygame.key.get_mods() wants_run = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT)) - self.running = wants_run and self.d['can_run'] and move != 0 and not self.swimming + self.running = wants_run and self.can_run and move != 0 and not self.swimming speed = RUN_SPEED if self.running else WALK_SPEED if self.swimming: speed *= 0.7 @@ -336,27 +343,74 @@ class Player: self.anim_t = 0.0 self.idle_t += dt + if self.swim_stroke_t > 0: + self.swim_stroke_t -= dt + if self.flap_t > 0: + self.flap_t -= dt jump_pressed = keys[pygame.K_SPACE] or keys[pygame.K_UP] + jump_edge = jump_pressed and not getattr(self, '_jump_was_down', False) + self._jump_was_down = jump_pressed if self.swimming: wy = level.info.get('water_y') near_surface = wy is not None and (self.y - wy) < 24 + self.gliding = False if jump_pressed and near_surface: # Stefans Bug "kommt nicht mehr aus dem Wasser raus": nahe der # Oberflaeche wird ein ECHTER Sprung ausgeloest (traegt auf die # Plattform), nicht nur ein Schwimmstoss. self.vy = JUMP_VELOCITY self.swimming = False + self.flap_strength = JUMP_VELOCITY * (5.0 / 6.0) elif jump_pressed: self.vy = min(self.vy, SWIM_KICK) + if jump_edge: + self.swim_stroke_t = len(ANIM_SWIM) / SWIM_ANIM_FPS if self.swimming: self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0) else: + # Coco: Hangeln an Aesten. EXPERIMENTELL: Ast-Tiles = ctype-Bit 8 + # (kommt in Baum-Leveln vor; der Original-Code prueft das Tile + # auf Kopfhoehe -- endgueltige Verifikation per DOSBox steht aus). + head_ty = int((self.y - self.HITBOX_H) // TILE) + tx = int(self.x // TILE) + if (self.can_hang and not self.hanging and not self.on_ground + and keys[pygame.K_UP] and level.ctype(tx, head_ty) & 8): + self.hanging = True + self.y = head_ty * TILE + TILE + self.HITBOX_H - 0.01 + if self.hanging: + if (not level.ctype(tx, head_ty) & 8 or keys[pygame.K_DOWN] + or self.on_ground): + self.hanging = False + else: + self.vy = 0.0 + self.vx *= 0.6 # Hangeln ist langsamer als Laufen if self.on_ground and not self.ducking and jump_pressed: self.vy = JUMP_VELOCITY self.on_ground = False self.idle_t = 0.0 self.jump_squat_t = JUMP_SQUAT_SECONDS - self.vy = min(self.vy + GRAVITY * dt, 480.0) + # Toucan: Fluegelschlag-Kette beginnt mit dem Absprung; jeder + # weitere Schlag wird schwaecher (Original: -0x18000, dann je + # +0x2000 bis unter -0x4000 -> "fuenfmal mit den Fluegeln") + self.flap_strength = JUMP_VELOCITY * (5.0 / 6.0) + self.gliding = False + elif (self.can_fly and jump_edge and not self.on_ground + and not self.gliding): + if self.flap_strength >= FLAP_MIN: + # zu schwach -> ab jetzt nur noch gleiten (Slot 34) + self.gliding = True + else: + self.vy = self.flap_strength + self.flap_strength += FLAP_DECAY + self.flap_t = len(ANIM_FLAP) / FLAP_ANIM_FPS + self.idle_t = 0.0 + if self.hanging: + pass # am Ast: keine Schwerkraft + elif self.gliding and self.vy > 0 and not keys[pygame.K_DOWN]: + # Gleitflug: sanftes Sinken, Pfeil runter bricht ab (Handbuch) + self.vy = min(self.vy + GRAVITY * 0.25 * dt, GLIDE_FALL_MAX) + else: + self.vy = min(self.vy + GRAVITY * dt, 480.0) # X + Wandkollision. STEP_UP: die untersten Pixel der Hitbox blocken # nicht -- kleine Stufen (z.B. der 4px-Rest an der Plateau-Kante ueber @@ -379,7 +433,9 @@ class Player: ny = self.y + self.vy * dt left_tx = int((self.x - self.HITBOX_W / 2) // TILE) right_tx = int((self.x + self.HITBOX_W / 2 - 1) // TILE) - if self.vy >= 0: + if self.hanging: + pass # am Ast: weder Boden-Snap noch Decken-Kollision + elif self.vy >= 0: ty = int(ny // TILE) if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)): ny = ty * TILE - 0.01 @@ -397,7 +453,7 @@ class Player: # Schraegen in der Luft"): in der LUFT nur snappen, wenn der Fuss die # Diagonale wirklich durchdrungen hat (ny >= surf). Nur wer schon am # Boden ist (bergauf laufen), darf mit Toleranz nach OBEN aufsteigen. - if self.vy >= 0: + if self.vy >= 0 and not self.hanging: was_grounded = self.on_ground or self._grounded_before old_ty = int(self.y // TILE) foot_ty = int(ny // TILE) @@ -420,7 +476,7 @@ class Player: # traegt wie weicher Boden, die Beine sinken nur WADE_DEPTH tief ein. # Springen von dort geht (on_ground), Herzverlust laeuft weiter. wy = level.info.get('water_y') - if wy is not None and not self.d['can_swim']: + if wy is not None and not self.can_swim: wade_max = wy + WATER_WADE_DEPTH if ny >= wade_max: ny = wade_max @@ -435,38 +491,43 @@ class Player: self.y = ny self._grounded_before = self.on_ground - def _pick(self, key_r, key_l): - i = self.d[key_r] if self.facing > 0 else self.d[key_l] - return i - - def current_frame_index(self): - d = self.d - right = self.facing > 0 - if self.force_frame is not None: - return self.force_frame - if self.spawn_t > 0 and d.get('spawn_frame') is not None and self.on_ground: - return d['spawn_frame'] - if self.swimming and d['swim_r']: - seq = d['swim_r'] if right else d['swim_l'] - return seq[int(self.anim_t * 6) % len(seq)] if self.vx else seq[0] + def current_slot(self): + if self.force_slot is not None: + return self.force_slot + if self.spawn_t > 0 and self.on_ground: + return SLOT_ARMUP + if self.swimming: + if self.swim_stroke_t > 0: + i = int((len(ANIM_SWIM) / SWIM_ANIM_FPS - self.swim_stroke_t) + * SWIM_ANIM_FPS) + return ANIM_SWIM[min(i, len(ANIM_SWIM) - 1)] + return SLOT_SWIM_IDLE + if self.hanging: + # Coco haengt am Ast (Slots 35-44, Anker-Y 64 haengt ihn unter + # die Astkante); beim Hangeln laufen die Frames durch + if self.vx: + seq = [35, 36, 37, 38, 39, 40, 41, 42, 43, 44] + return seq[int(self.anim_t * WALK_ANIM_FPS) % len(seq)] + return 35 if self.ducking: - return self._pick('duck_r', 'duck_l') + return SLOT_DUCK if not self.on_ground: + if self.flap_t > 0: + i = int((len(ANIM_FLAP) / FLAP_ANIM_FPS - self.flap_t) * FLAP_ANIM_FPS) + return ANIM_FLAP[min(i, len(ANIM_FLAP) - 1)] + if self.gliding and SLOT_GLIDE in self.slots: + return SLOT_GLIDE if self.jump_squat_t > 0: - return self._pick('duck_r', 'duck_l') # Absprung-Hocke - return self._pick('jump_r', 'jump_l') + return SLOT_SQUAT # Absprung-Hocke (Original-Tabelle [4][10,10,10]) + return SLOT_JUMP if self.vx: - if self.running and d['run_r']: - seq = d['run_r'] if right else d['run_l'] - fps = RUN_ANIM_FPS - else: - seq = d['walk_r'] if right else d['walk_l'] - fps = WALK_ANIM_FPS + seq, fps = (ANIM_RUN, RUN_ANIM_FPS) if (self.running and self.can_run) \ + else (ANIM_WALK, WALK_ANIM_FPS) return seq[int(self.anim_t * fps) % len(seq)] - if self.idle_t >= IDLE_ANIM_AFTER and d['idle_anim_r']: - seq = d['idle_anim_r'] if right else d['idle_anim_l'] - return seq[int((self.idle_t - IDLE_ANIM_AFTER) * IDLE_ANIM_FPS) % len(seq)] - return self._pick('idle_r', 'idle_l') + if self.idle_t >= IDLE_ANIM_AFTER: + i = int((self.idle_t - IDLE_ANIM_AFTER) * IDLE_WAIT_FPS) % len(ANIM_IDLE_WAIT) + return ANIM_IDLE_WAIT[i] + return SLOT_IDLE def draw(self, screen, cam_x, cam_y): # Schadens-Blinken: nach Herzverlust flackert die Figur kurz @@ -474,10 +535,11 @@ class Player: # es in den BOBs nicht) if self.hurt_t > 0 and int(self.hurt_t * 12) % 2 == 0: return - i = self.current_frame_index() - surf = self.surfs[i] if i < len(self.surfs) else self.surfs[0] - screen.blit(surf, (int(self.x - surf.get_width() / 2 - cam_x), - int(self.y - surf.get_height() - cam_y) + SPRITE_SINK)) + surf, ax, ay = self.sprite(self.current_slot()) + # Anker aus dem BOB-Header: (x,y) der Figur = Fusspunkt; SPRITE_SINK + # gleicht die Gras-Ueberlappung der Tiles aus (fuer alle Frames gleich) + screen.blit(surf, (int(self.x - ax - cam_x), + int(self.y - ay - cam_y) + SPRITE_SINK)) class Hud: @@ -636,7 +698,7 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): map_w_px = level.width * TILE map_h_px = level.height * TILE hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot - water_t = 0.0 + water_t = None # None = aktuell kein Wasserkontakt duck_hold = 0.0 down_gap = 0.0 # Grace gegen Taste/on_ground-Flackern look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken @@ -674,7 +736,7 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): dying['drop_y'] += dying['drop_vy'] * dt dying['drop_vy'] = min(dying['drop_vy'] + 60 * dt, 40.0) if dying['t'] >= DEATH_ANIM_SECONDS: - player.force_frame = None + player.force_slot = None dying = None lives -= 1 if lives <= 0: @@ -687,11 +749,10 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): # Tuer-Sequenz: offen zeigen (Figur schaut rein), nach 1s die Aktion if door_open_t is not None: door_open_t += dt - if player.d.get('peek_frame') is not None: - player.force_frame = player.d['peek_frame'] + player.force_slot = SLOT_PEEK if door_open_t >= 1.0: door_open_t = None - player.force_frame = None + player.force_slot = None choice = run_character_select(clock, assets, info['chars'], player.char) if choice == 'quit': @@ -699,29 +760,24 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): if choice and choice != player.char: player.set_character(choice) - # Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher + # Wasser-Schaden fuer Nicht-Schwimmer. Stefans Erinnerung ans + # Original: schon der ERSTE Wasserkontakt kostet ein Herz (+Blinken), + # danach alle WATER_DAMAGE_EVERY Sekunden ein weiteres. if dying is None and level.in_water(player.y) and not player.swimming: - water_t += dt - if water_t >= WATER_DAMAGE_EVERY: - water_t = 0.0 + take_heart = water_t is None or water_t >= WATER_DAMAGE_EVERY + water_t = 0.0 if take_heart else water_t + dt + if take_heart: hearts -= 1 player.hurt_t = 0.9 # Schadens-Blinken - if hearts <= 0 and player.d.get('death_frame') is not None: + if hearts <= 0: # Tod-Animation starten: erschrockener Frame, Flug nach - # oben aus dem Bild, Tuch/Kappe faellt herunter + # oben aus dem Bild, Accessoire (Tuch/Kappe) faellt herab player.hurt_t = 0.0 # kein Blink-Toggle waehrend der Anim - player.force_frame = player.d['death_frame'] + player.force_slot = SLOT_DEATH dying = dict(t=0.0, vy=-260.0, drop_y=player.y - 40.0, drop_vy=-30.0) - elif hearts <= 0: - lives -= 1 - hearts = 6 - player.x, player.y = spawn_px - player.vy = 0.0 - if lives <= 0: - return 'map' else: - water_t = 0.0 + water_t = None # None = kein Wasserkontakt (naechster kostet sofort) # Kamera: Standard so, dass die Figur bei ~75%% der Hoehe steht # (Stefans Referenz-Screenshot); haelt man DUCKEN gedrueckt, faehrt @@ -752,8 +808,10 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): else: canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y)) player.draw(canvas, cam_x, cam_y) - if dying is not None and player.d.get('drop_frame') is not None: - drop = player.surfs[player.d['drop_frame']] + if dying is not None and SLOT_DROP1 in player.slots: + # fallendes Accessoire (16x16, Slots 48/49 im Wechsel = Flattern) + slot = SLOT_DROP1 if int(dying['t'] * 6) % 2 == 0 else SLOT_DROP2 + drop = player.slots.get(slot, player.slots[SLOT_DROP1])[0] canvas.blit(drop, (int(player.x - drop.get_width() / 2 - cam_x), int(dying['drop_y'] - cam_y))) # Tuer offen: Level abdunkeln (Original dimmt die Szene waehrend der