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:
@@ -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,
|
vermutlich Levelobjekt-/Gegner-Slots (Ladeliste 'ani',A-O,gamebar,keys,
|
||||||
vitamin,platfw1,doors,blume,wolke,KROKO,BOSSNAKE,w2snake,DRAGON @4984).
|
vitamin,platfw1,doors,blume,wolke,KROKO,BOSSNAKE,w2snake,DRAGON @4984).
|
||||||
Noch offen.
|
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
@@ -201,10 +201,18 @@ def _parse_bob_executable(data):
|
|||||||
BOB_STRIDE = 84
|
BOB_STRIDE = 84
|
||||||
|
|
||||||
class BobFrame:
|
class BobFrame:
|
||||||
__slots__ = ('width', 'height', 'pixels') # pixels: list of palette indices, width*height, -1 = transparent
|
# pixels: list of palette indices, width*height, -1 = transparent.
|
||||||
def __init__(self, width, height):
|
# 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.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
|
self.slot = slot
|
||||||
|
self.anchor_x = anchor_x
|
||||||
|
self.anchor_y = anchor_y
|
||||||
self.pixels = [-1] * (width * height)
|
self.pixels = [-1] * (width * height)
|
||||||
|
|
||||||
def set(self, x, y, idx):
|
def set(self, x, y, idx):
|
||||||
@@ -221,6 +229,9 @@ def parse_bob(data):
|
|||||||
header = data[pos:pos+14]
|
header = data[pos:pos+14]
|
||||||
if len(header) < 14:
|
if len(header) < 14:
|
||||||
raise ValueError(f'truncated header at {pos}')
|
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]
|
width = struct.unpack_from('<h', header, 6)[0]
|
||||||
height = struct.unpack_from('<h', header, 8)[0]
|
height = struct.unpack_from('<h', header, 8)[0]
|
||||||
ptr_seg_len = struct.unpack_from('<h', header, 10)[0]
|
ptr_seg_len = struct.unpack_from('<h', header, 10)[0]
|
||||||
@@ -238,7 +249,7 @@ def parse_bob(data):
|
|||||||
if instrs is None:
|
if instrs is None:
|
||||||
raise ValueError(f'failed to parse exec segment for frame {len(frames)} (len={len(exec_segment)})')
|
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 instr in instrs:
|
||||||
for i, byte in enumerate(instr.data):
|
for i, byte in enumerate(instr.data):
|
||||||
p = instr.offset + i
|
p = instr.offset + i
|
||||||
|
|||||||
+172
-114
@@ -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
|
DEATH_ANIM_SECONDS = 2.4 # Tuch faellt, Figur fliegt aus dem Bild
|
||||||
JUMP_SQUAT_SECONDS = 0.09 # Absprung-Hocke (DOSBox-Referenz: kurzer
|
JUMP_SQUAT_SECONDS = 0.09 # Absprung-Hocke (DOSBox-Referenz: kurzer
|
||||||
# Kauer-Frame im Absprungmoment, dann Streckung)
|
# 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
|
SOLID_BIT = 32
|
||||||
SLOPE_CT = 16
|
SLOPE_CT = 16
|
||||||
|
|
||||||
# Charakter-Definitionen. Richtungs-Paritaet: gerade Frames = rechts
|
# --- SLOT-System (komplett aus dem Binary, NOTES Eintrag (18)) -------------
|
||||||
# (an Tony per Live-Test bestaetigt; bei Smacks analog angenommen).
|
# 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 = {
|
CHAR_DEFS = {
|
||||||
'TONY': dict(
|
'TONY': dict(bob='TONY', item_icon='ITE0', head_index=0),
|
||||||
bob='TONY', can_run=True, can_swim=False,
|
'SMACKS': dict(bob='SMACKS', item_icon='ITE1', head_index=1),
|
||||||
walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15],
|
'COCO': dict(bob='COCO', item_icon='ITE3', head_index=2),
|
||||||
run_r=[26, 28, 30, 32], run_l=[27, 29, 31, 33],
|
'TOUCAN': dict(bob='TOUCAN', item_icon='ITE2', head_index=3),
|
||||||
# 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,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
CHAR_ORDER = ['TONY', 'SMACKS', 'COCO', 'TOUCAN']
|
CHAR_ORDER = ['TONY', 'SMACKS', 'COCO', 'TOUCAN']
|
||||||
|
|
||||||
@@ -275,9 +260,14 @@ class Player:
|
|||||||
self.anim_t = 0.0
|
self.anim_t = 0.0
|
||||||
self.idle_t = 0.0
|
self.idle_t = 0.0
|
||||||
self.spawn_t = 0.0 # Start-Sprite-Sequenz (Arm hoch/Daumen hoch)
|
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.hurt_t = 0.0 # Rest-Blinkzeit nach Schaden
|
||||||
self.jump_squat_t = 0.0 # Absprung-Hocke (kurz nach dem Absprung)
|
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._grounded_before = False
|
||||||
self.set_character(char, pos)
|
self.set_character(char, pos)
|
||||||
|
|
||||||
@@ -286,13 +276,28 @@ class Player:
|
|||||||
self.d = CHAR_DEFS[char]
|
self.d = CHAR_DEFS[char]
|
||||||
frames = parse_bob(self.assets.container.entries[self.d['bob'] + '.BOB'])
|
frames = parse_bob(self.assets.container.entries[self.d['bob'] + '.BOB'])
|
||||||
palette = get_bob_palette(self.assets.container.entries, self.d['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:
|
if pos is not None:
|
||||||
self.x, self.y = float(pos[0]), float(pos[1])
|
self.x, self.y = float(pos[0]), float(pos[1])
|
||||||
# Start-Sprite-Sequenz beim Levelstart/Charakterwechsel (Stefans
|
# Start-Sprite-Sequenz beim Levelstart/Charakterwechsel (Stefans
|
||||||
# Beobachtung: Tony streckt die Hand nach oben, Smacks Daumen hoch)
|
# Beobachtung: Tony streckt die Hand nach oben, Smacks Daumen hoch)
|
||||||
self.spawn_t = SPAWN_ANIM_SECONDS
|
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):
|
def rect(self):
|
||||||
return pygame.Rect(int(self.x - self.HITBOX_W / 2),
|
return pygame.Rect(int(self.x - self.HITBOX_W / 2),
|
||||||
int(self.y - self.HITBOX_H),
|
int(self.y - self.HITBOX_H),
|
||||||
@@ -310,7 +315,9 @@ class Player:
|
|||||||
pygame.K_UP, pygame.K_DOWN, pygame.K_SPACE)):
|
pygame.K_UP, pygame.K_DOWN, pygame.K_SPACE)):
|
||||||
self.spawn_t = 0.0
|
self.spawn_t = 0.0
|
||||||
in_water = level.in_water(self.y)
|
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
|
# Ducken geht NICHT im Wasser (Stefans Regel -- auch nicht fuer
|
||||||
# Nicht-Schwimmer, die auf der Oberflaeche huepfen)
|
# Nicht-Schwimmer, die auf der Oberflaeche huepfen)
|
||||||
self.ducking = (self.on_ground and not self.swimming and not in_water
|
self.ducking = (self.on_ground and not self.swimming and not in_water
|
||||||
@@ -323,7 +330,7 @@ class Player:
|
|||||||
move += 1
|
move += 1
|
||||||
mods = pygame.key.get_mods()
|
mods = pygame.key.get_mods()
|
||||||
wants_run = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT))
|
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
|
speed = RUN_SPEED if self.running else WALK_SPEED
|
||||||
if self.swimming:
|
if self.swimming:
|
||||||
speed *= 0.7
|
speed *= 0.7
|
||||||
@@ -336,27 +343,74 @@ class Player:
|
|||||||
self.anim_t = 0.0
|
self.anim_t = 0.0
|
||||||
self.idle_t += dt
|
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_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:
|
if self.swimming:
|
||||||
wy = level.info.get('water_y')
|
wy = level.info.get('water_y')
|
||||||
near_surface = wy is not None and (self.y - wy) < 24
|
near_surface = wy is not None and (self.y - wy) < 24
|
||||||
|
self.gliding = False
|
||||||
if jump_pressed and near_surface:
|
if jump_pressed and near_surface:
|
||||||
# Stefans Bug "kommt nicht mehr aus dem Wasser raus": nahe der
|
# Stefans Bug "kommt nicht mehr aus dem Wasser raus": nahe der
|
||||||
# Oberflaeche wird ein ECHTER Sprung ausgeloest (traegt auf die
|
# Oberflaeche wird ein ECHTER Sprung ausgeloest (traegt auf die
|
||||||
# Plattform), nicht nur ein Schwimmstoss.
|
# Plattform), nicht nur ein Schwimmstoss.
|
||||||
self.vy = JUMP_VELOCITY
|
self.vy = JUMP_VELOCITY
|
||||||
self.swimming = False
|
self.swimming = False
|
||||||
|
self.flap_strength = JUMP_VELOCITY * (5.0 / 6.0)
|
||||||
elif jump_pressed:
|
elif jump_pressed:
|
||||||
self.vy = min(self.vy, SWIM_KICK)
|
self.vy = min(self.vy, SWIM_KICK)
|
||||||
|
if jump_edge:
|
||||||
|
self.swim_stroke_t = len(ANIM_SWIM) / SWIM_ANIM_FPS
|
||||||
if self.swimming:
|
if self.swimming:
|
||||||
self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0)
|
self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0)
|
||||||
else:
|
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:
|
if self.on_ground and not self.ducking and jump_pressed:
|
||||||
self.vy = JUMP_VELOCITY
|
self.vy = JUMP_VELOCITY
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
self.idle_t = 0.0
|
self.idle_t = 0.0
|
||||||
self.jump_squat_t = JUMP_SQUAT_SECONDS
|
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
|
# X + Wandkollision. STEP_UP: die untersten Pixel der Hitbox blocken
|
||||||
# nicht -- kleine Stufen (z.B. der 4px-Rest an der Plateau-Kante ueber
|
# 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
|
ny = self.y + self.vy * dt
|
||||||
left_tx = int((self.x - self.HITBOX_W / 2) // TILE)
|
left_tx = int((self.x - self.HITBOX_W / 2) // TILE)
|
||||||
right_tx = int((self.x + self.HITBOX_W / 2 - 1) // 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)
|
ty = int(ny // TILE)
|
||||||
if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)):
|
if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)):
|
||||||
ny = ty * TILE - 0.01
|
ny = ty * TILE - 0.01
|
||||||
@@ -397,7 +453,7 @@ class Player:
|
|||||||
# Schraegen in der Luft"): in der LUFT nur snappen, wenn der Fuss die
|
# Schraegen in der Luft"): in der LUFT nur snappen, wenn der Fuss die
|
||||||
# Diagonale wirklich durchdrungen hat (ny >= surf). Nur wer schon am
|
# Diagonale wirklich durchdrungen hat (ny >= surf). Nur wer schon am
|
||||||
# Boden ist (bergauf laufen), darf mit Toleranz nach OBEN aufsteigen.
|
# 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
|
was_grounded = self.on_ground or self._grounded_before
|
||||||
old_ty = int(self.y // TILE)
|
old_ty = int(self.y // TILE)
|
||||||
foot_ty = int(ny // TILE)
|
foot_ty = int(ny // TILE)
|
||||||
@@ -420,7 +476,7 @@ class Player:
|
|||||||
# traegt wie weicher Boden, die Beine sinken nur WADE_DEPTH tief ein.
|
# traegt wie weicher Boden, die Beine sinken nur WADE_DEPTH tief ein.
|
||||||
# Springen von dort geht (on_ground), Herzverlust laeuft weiter.
|
# Springen von dort geht (on_ground), Herzverlust laeuft weiter.
|
||||||
wy = level.info.get('water_y')
|
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
|
wade_max = wy + WATER_WADE_DEPTH
|
||||||
if ny >= wade_max:
|
if ny >= wade_max:
|
||||||
ny = wade_max
|
ny = wade_max
|
||||||
@@ -435,38 +491,43 @@ class Player:
|
|||||||
self.y = ny
|
self.y = ny
|
||||||
self._grounded_before = self.on_ground
|
self._grounded_before = self.on_ground
|
||||||
|
|
||||||
def _pick(self, key_r, key_l):
|
def current_slot(self):
|
||||||
i = self.d[key_r] if self.facing > 0 else self.d[key_l]
|
if self.force_slot is not None:
|
||||||
return i
|
return self.force_slot
|
||||||
|
if self.spawn_t > 0 and self.on_ground:
|
||||||
def current_frame_index(self):
|
return SLOT_ARMUP
|
||||||
d = self.d
|
if self.swimming:
|
||||||
right = self.facing > 0
|
if self.swim_stroke_t > 0:
|
||||||
if self.force_frame is not None:
|
i = int((len(ANIM_SWIM) / SWIM_ANIM_FPS - self.swim_stroke_t)
|
||||||
return self.force_frame
|
* SWIM_ANIM_FPS)
|
||||||
if self.spawn_t > 0 and d.get('spawn_frame') is not None and self.on_ground:
|
return ANIM_SWIM[min(i, len(ANIM_SWIM) - 1)]
|
||||||
return d['spawn_frame']
|
return SLOT_SWIM_IDLE
|
||||||
if self.swimming and d['swim_r']:
|
if self.hanging:
|
||||||
seq = d['swim_r'] if right else d['swim_l']
|
# Coco haengt am Ast (Slots 35-44, Anker-Y 64 haengt ihn unter
|
||||||
return seq[int(self.anim_t * 6) % len(seq)] if self.vx else seq[0]
|
# 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:
|
if self.ducking:
|
||||||
return self._pick('duck_r', 'duck_l')
|
return SLOT_DUCK
|
||||||
if not self.on_ground:
|
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:
|
if self.jump_squat_t > 0:
|
||||||
return self._pick('duck_r', 'duck_l') # Absprung-Hocke
|
return SLOT_SQUAT # Absprung-Hocke (Original-Tabelle [4][10,10,10])
|
||||||
return self._pick('jump_r', 'jump_l')
|
return SLOT_JUMP
|
||||||
if self.vx:
|
if self.vx:
|
||||||
if self.running and d['run_r']:
|
seq, fps = (ANIM_RUN, RUN_ANIM_FPS) if (self.running and self.can_run) \
|
||||||
seq = d['run_r'] if right else d['run_l']
|
else (ANIM_WALK, WALK_ANIM_FPS)
|
||||||
fps = RUN_ANIM_FPS
|
|
||||||
else:
|
|
||||||
seq = d['walk_r'] if right else d['walk_l']
|
|
||||||
fps = WALK_ANIM_FPS
|
|
||||||
return seq[int(self.anim_t * fps) % len(seq)]
|
return seq[int(self.anim_t * fps) % len(seq)]
|
||||||
if self.idle_t >= IDLE_ANIM_AFTER and d['idle_anim_r']:
|
if self.idle_t >= IDLE_ANIM_AFTER:
|
||||||
seq = d['idle_anim_r'] if right else d['idle_anim_l']
|
i = int((self.idle_t - IDLE_ANIM_AFTER) * IDLE_WAIT_FPS) % len(ANIM_IDLE_WAIT)
|
||||||
return seq[int((self.idle_t - IDLE_ANIM_AFTER) * IDLE_ANIM_FPS) % len(seq)]
|
return ANIM_IDLE_WAIT[i]
|
||||||
return self._pick('idle_r', 'idle_l')
|
return SLOT_IDLE
|
||||||
|
|
||||||
def draw(self, screen, cam_x, cam_y):
|
def draw(self, screen, cam_x, cam_y):
|
||||||
# Schadens-Blinken: nach Herzverlust flackert die Figur kurz
|
# Schadens-Blinken: nach Herzverlust flackert die Figur kurz
|
||||||
@@ -474,10 +535,11 @@ class Player:
|
|||||||
# es in den BOBs nicht)
|
# es in den BOBs nicht)
|
||||||
if self.hurt_t > 0 and int(self.hurt_t * 12) % 2 == 0:
|
if self.hurt_t > 0 and int(self.hurt_t * 12) % 2 == 0:
|
||||||
return
|
return
|
||||||
i = self.current_frame_index()
|
surf, ax, ay = self.sprite(self.current_slot())
|
||||||
surf = self.surfs[i] if i < len(self.surfs) else self.surfs[0]
|
# Anker aus dem BOB-Header: (x,y) der Figur = Fusspunkt; SPRITE_SINK
|
||||||
screen.blit(surf, (int(self.x - surf.get_width() / 2 - cam_x),
|
# gleicht die Gras-Ueberlappung der Tiles aus (fuer alle Frames gleich)
|
||||||
int(self.y - surf.get_height() - cam_y) + SPRITE_SINK))
|
screen.blit(surf, (int(self.x - ax - cam_x),
|
||||||
|
int(self.y - ay - cam_y) + SPRITE_SINK))
|
||||||
|
|
||||||
|
|
||||||
class Hud:
|
class Hud:
|
||||||
@@ -636,7 +698,7 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
map_w_px = level.width * TILE
|
map_w_px = level.width * TILE
|
||||||
map_h_px = level.height * TILE
|
map_h_px = level.height * TILE
|
||||||
hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot
|
hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot
|
||||||
water_t = 0.0
|
water_t = None # None = aktuell kein Wasserkontakt
|
||||||
duck_hold = 0.0
|
duck_hold = 0.0
|
||||||
down_gap = 0.0 # Grace gegen Taste/on_ground-Flackern
|
down_gap = 0.0 # Grace gegen Taste/on_ground-Flackern
|
||||||
look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken
|
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_y'] += dying['drop_vy'] * dt
|
||||||
dying['drop_vy'] = min(dying['drop_vy'] + 60 * dt, 40.0)
|
dying['drop_vy'] = min(dying['drop_vy'] + 60 * dt, 40.0)
|
||||||
if dying['t'] >= DEATH_ANIM_SECONDS:
|
if dying['t'] >= DEATH_ANIM_SECONDS:
|
||||||
player.force_frame = None
|
player.force_slot = None
|
||||||
dying = None
|
dying = None
|
||||||
lives -= 1
|
lives -= 1
|
||||||
if lives <= 0:
|
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
|
# Tuer-Sequenz: offen zeigen (Figur schaut rein), nach 1s die Aktion
|
||||||
if door_open_t is not None:
|
if door_open_t is not None:
|
||||||
door_open_t += dt
|
door_open_t += dt
|
||||||
if player.d.get('peek_frame') is not None:
|
player.force_slot = SLOT_PEEK
|
||||||
player.force_frame = player.d['peek_frame']
|
|
||||||
if door_open_t >= 1.0:
|
if door_open_t >= 1.0:
|
||||||
door_open_t = None
|
door_open_t = None
|
||||||
player.force_frame = None
|
player.force_slot = None
|
||||||
choice = run_character_select(clock, assets,
|
choice = run_character_select(clock, assets,
|
||||||
info['chars'], player.char)
|
info['chars'], player.char)
|
||||||
if choice == 'quit':
|
if choice == 'quit':
|
||||||
@@ -699,29 +760,24 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
if choice and choice != player.char:
|
if choice and choice != player.char:
|
||||||
player.set_character(choice)
|
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:
|
if dying is None and level.in_water(player.y) and not player.swimming:
|
||||||
water_t += dt
|
take_heart = water_t is None or water_t >= WATER_DAMAGE_EVERY
|
||||||
if water_t >= WATER_DAMAGE_EVERY:
|
water_t = 0.0 if take_heart else water_t + dt
|
||||||
water_t = 0.0
|
if take_heart:
|
||||||
hearts -= 1
|
hearts -= 1
|
||||||
player.hurt_t = 0.9 # Schadens-Blinken
|
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
|
# 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.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,
|
dying = dict(t=0.0, vy=-260.0,
|
||||||
drop_y=player.y - 40.0, drop_vy=-30.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:
|
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
|
# Kamera: Standard so, dass die Figur bei ~75%% der Hoehe steht
|
||||||
# (Stefans Referenz-Screenshot); haelt man DUCKEN gedrueckt, faehrt
|
# (Stefans Referenz-Screenshot); haelt man DUCKEN gedrueckt, faehrt
|
||||||
@@ -752,8 +808,10 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
else:
|
else:
|
||||||
canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y))
|
canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y))
|
||||||
player.draw(canvas, cam_x, cam_y)
|
player.draw(canvas, cam_x, cam_y)
|
||||||
if dying is not None and player.d.get('drop_frame') is not None:
|
if dying is not None and SLOT_DROP1 in player.slots:
|
||||||
drop = player.surfs[player.d['drop_frame']]
|
# 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),
|
canvas.blit(drop, (int(player.x - drop.get_width() / 2 - cam_x),
|
||||||
int(dying['drop_y'] - cam_y)))
|
int(dying['drop_y'] - cam_y)))
|
||||||
# Tuer offen: Level abdunkeln (Original dimmt die Szene waehrend der
|
# Tuer offen: Level abdunkeln (Original dimmt die Szene waehrend der
|
||||||
|
|||||||
Reference in New Issue
Block a user