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

This commit is contained in:
2026-07-25 16:10:21 +02:00
parent 6ac9d7fc6b
commit 09264109ca
3 changed files with 224 additions and 117 deletions
+38
View File
@@ -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.
+14 -3
View File
@@ -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('<h', header, 0)[0]
anchor_x = struct.unpack_from('<h', header, 2)[0]
anchor_y = struct.unpack_from('<h', header, 4)[0]
width = struct.unpack_from('<h', header, 6)[0]
height = struct.unpack_from('<h', header, 8)[0]
ptr_seg_len = struct.unpack_from('<h', header, 10)[0]
@@ -238,7 +249,7 @@ def parse_bob(data):
if instrs is None:
raise ValueError(f'failed to parse exec segment for frame {len(frames)} (len={len(exec_segment)})')
frame = BobFrame(width, height)
frame = BobFrame(width, height, slot, anchor_x, anchor_y)
for instr in instrs:
for i, byte in enumerate(instr.data):
p = instr.offset + i
+172 -114
View File
@@ -58,64 +58,49 @@ WATER_BOUNCE_V = -110.0 # Auto-Huepfer auf der Wasseroberflaeche
DEATH_ANIM_SECONDS = 2.4 # Tuch faellt, Figur fliegt aus dem Bild
JUMP_SQUAT_SECONDS = 0.09 # Absprung-Hocke (DOSBox-Referenz: kurzer
# Kauer-Frame im Absprungmoment, dann Streckung)
SWIM_ANIM_FPS = 7.0 # Schwimmstoss [10 Ticks] = 7 fps
FLAP_ANIM_FPS = 5.8 # Fluegelschlag [12 Ticks]
FLAP_DECAY = 48.0 # jeder Schlag wird schwaecher (Original +0x2000)
FLAP_MIN = -70.0 # schwaecher als das -> 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