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,
|
||||
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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user