Charakter-System, Wasser, Renn/Idle-Animationen, echtes HUD
Stefans Feedback-Paket (7 annotierte Screenshots): - Player-Klasse mit CHAR_DEFS: Tony (RENNEN mit ALT -- echte Rad-Animation, Frames 26-33) und Smacks (SCHWIMMEN, Frames 37-44; kein Rennen) spielbar; Coco/Toucan vorbereitet. Idle-Packungs-Animation nach 5s Stillstand (Tony 44-49, Smacks 31-36). Sprung-Frames 18/19 (gestreckt). - Sprunghoehe kalibriert (-360, ~72px: "Tony kommt mit dem Kopf an den Ast"). - WASSER (Stefans Fund: nur bei DOSBox machine=vgaonly sichtbar): water_y pro Level + dunkelblaues Overlay; Nicht-Schwimmer verlieren alle 5s ein Herz, 0 Herzen -> Respawn/Leben-1, 0 Leben -> Karte. Smacks schwimmt schadlos (traege Wasserphysik, Space = Schwimmstoss). - TUER mit Kopf-Symbol (Level-1-Haus): ALT im STEHEN oeffnet das Charakter-Auswahlmenue "MIT WEM MOECHTEST DU SPIELEN?" -- WECH_1-4 = farbige Koepfe, WECH_5-8 = grau (nicht verfuegbar; Level 1: nur Tony+Smacks). Auswahl blinkt, Wechsel uebernimmt die Position. - HUD nach Referenz-Screenshot: Keks-Box mit Cerealien-Icon (ITE0-3) des aktiven Charakters, Herz-Box als 2x2-Grid, Charakter-Kopf-Box (zeigt die gewaehlte Figur) + Leben, Uhr. - Level-Metadaten (Charaktere/Wasserlinie/Tuer/Spawn) v1 als LEVEL_INFO- Konstanten -- Original-Quelle ist vermutlich das .ARE-Format (Header enthaelt BE-Pixelkoordinaten, Analyse in NOTES.md, noch offen). Verifiziert: E2E (Xvfb+XTEST) -- Tuer->Menue->Smacks, HUD wechselt auf Smacks-Kopf+Puff-Icon, Wasser-Overlay sichtbar, ESC/QUIT rc=0; --once ok. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0bb66b73a4
commit
07b347b607
@@ -1438,3 +1438,45 @@ Stefans annotierter Screenshot (4 Punkte), alle behoben:
|
|||||||
Physik-Feel laut Stefan gut. Levelmusik-Song prueft er nach Gehoer.
|
Physik-Feel laut Stefan gut. Levelmusik-Song prueft er nach Gehoer.
|
||||||
Offen bleiben: Tueren oeffnen (ALT an Tuer, Schluessel-Logik), Items,
|
Offen bleiben: Tueren oeffnen (ALT an Tuer, Schluessel-Logik), Items,
|
||||||
Gegner, Karten-Animationen.
|
Gegner, Karten-Animationen.
|
||||||
|
|
||||||
|
## 25.07.2026 (3) -- Stefans grosses Feedback-Paket: Charaktere, Wasser, Animationen, HUD
|
||||||
|
|
||||||
|
Referenz: 7 annotierte Screenshots von Stefan + DOSBox (machine=vgaonly --
|
||||||
|
Stefans Fund: das WASSER wird nur bei vgaonly gerendert, VGA-Palette-Effekt!).
|
||||||
|
|
||||||
|
**Asset-Funde:**
|
||||||
|
- WECH_1-4.PCC = die 4 Charakter-Koepfe FARBIG, WECH_5-8 = GRAU (= im Level
|
||||||
|
nicht verfuegbar) -- exakt das Tuer-Auswahlmenue "MIT WEM MOECHTEST DU
|
||||||
|
SPIELEN?". ITE0-3.PCC = die 4 Cerealien-HUD-Icons (Flake/Puff/Loop/Choco).
|
||||||
|
FROS/SMAC/COCO/LOOP_W1-3.ICO = SAMMEL-Cerealien-Animationsphasen pro Welt
|
||||||
|
(fuer Items spaeter). TONY-Frames: 18/19=Sprung, 24/25=Ducken, 26-33=
|
||||||
|
RENN-"Rad" (Stefans Fuesse-wie-ein-Rad), 44-49=Idle-Packung. SMACKS: 51
|
||||||
|
Frames, 18/19=Ducken, 24/25=Sprung, 31-36=Idle-Packung, 37-44=SCHWIMMEN,
|
||||||
|
45-47=Wasserblasen.
|
||||||
|
- ARE-Format angeschaut (W1L1: 6690B, 3 Regionen; Header ab Byte 8 enthaelt
|
||||||
|
Big-Endian-Pixelkoordinaten-Paare, u.a. (256,592)~Spawn-Kopfposition) --
|
||||||
|
noch NICHT geknackt, v1 nutzt LEVEL_INFO-Konstanten. TODO.
|
||||||
|
|
||||||
|
**Implementiert (game/level.py):**
|
||||||
|
- Player-Klasse mit CHAR_DEFS (Tony/Smacks/Coco/Toucan): Tony rennt (ALT/
|
||||||
|
Shift, Rad-Animation), Smacks schwimmt (kein Wasserschaden, traege
|
||||||
|
Physik, Space=Schwimmstoss), Idle-Packungs-Animation nach 5s Stillstand,
|
||||||
|
Sprung-Frames korrigiert (18/19 gestreckt statt 16/17).
|
||||||
|
- Sprunghoehe kalibriert: -360 (~72px ~ Stefans "Kopf an den Ast").
|
||||||
|
- WASSER: water_y pro Level, dunkelblaues Overlay, Nicht-Schwimmer verlieren
|
||||||
|
alle 5s ein Herz; 0 Herzen -> Leben-1 + Respawn; 0 Leben -> Karte.
|
||||||
|
- TUER (Level-1-Haus, Tiles 12-13/37-39): Kopf-Symbol drueber; ALT im STEHEN
|
||||||
|
(nicht beim Rennen) oeffnet das Charakter-Menue (WECH-Koepfe, nicht
|
||||||
|
verfuegbare grau, Auswahl blinkt), Wechsel uebernimmt Position.
|
||||||
|
- HUD nach Referenz: Keks-Box MIT Cerealien-Icon des aktiven Charakters,
|
||||||
|
Herz-Box als 2x2-GRID, Charakter-Kopf-Box (zeigt IMMER die gewaehlte
|
||||||
|
Figur) + Leben, Uhr.
|
||||||
|
|
||||||
|
**Verifiziert:** E2E (Xvfb+XTEST): Tuer -> Menue -> Smacks gewaehlt -> HUD
|
||||||
|
zeigt Smacks-Kopf+Puff-Icon, Wasser-Overlay sichtbar, laufen/springen/
|
||||||
|
ducken als Smacks, ESC/QUIT rc=0. --once Regression exit 0.
|
||||||
|
|
||||||
|
**Offen:** ARE knacken (Charaktere/Wasser/Tueren/Spawns pro Level echt),
|
||||||
|
Items einsammeln (FROS_W1-Rotationssprites + ctype 4), Smacks-Blickrichtungs-
|
||||||
|
Paritaet pruefen, Coco/Toucan-Frames zuordnen (aktuell Tony-Layout
|
||||||
|
angenommen), Kamera-Hoehe feinjustieren, Gegner, Tuer-/Schluessel-Logik.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[dosbox]
|
||||||
|
machine = vgaonly
|
||||||
[sdl]
|
[sdl]
|
||||||
fullscreen=false
|
fullscreen=false
|
||||||
output=surface
|
output=surface
|
||||||
|
|||||||
+325
-180
@@ -1,33 +1,24 @@
|
|||||||
"""Level-Gameplay: Tile-Rendering, Kamera, Tony-Steuerung, Kollision, HUD.
|
"""Level-Gameplay: Tile-Rendering, Kamera, Spielfiguren, Kollision, HUD.
|
||||||
|
|
||||||
Erster spielbarer Gameplay-Slice (Level 1 = W1L1.MAP, per Template-Matching
|
Basis-Referenzen: DOSBox-Aufnahmen (Xvfb+XTEST) + Stefans annotierte
|
||||||
des DOSBox-Gameplay-Screenshots gegen die gerenderten Maps verifiziert --
|
Live-Screenshots (25.07.2026). Wichtigste Fakten:
|
||||||
NICHT W1L0, das ist vermutlich ein Bonus-/Sonderlevel).
|
- Level 1 = W1L1.MAP (Template-Matching). Viewport 320x176, HUD 320x24.
|
||||||
|
- Kollision: MAP-ctype Bit 32 = solide, ctype 16 = 45-Grad-Schraege.
|
||||||
|
- WASSER existiert (nur mit DOSBox machine=vgaonly sichtbar -- klassischer
|
||||||
|
VGA-Palette-Effekt): dunkles Blau unterhalb der Wasserlinie; wer drin
|
||||||
|
steht, verliert alle 5s ein Herz. SMACKS kann schwimmen (kein Schaden).
|
||||||
|
- Pro Level ist hinterlegt, welche Charaktere waehlbar sind (Level 1: nur
|
||||||
|
TONY und SMACKS) -- Quelle im Original vermutlich .ARE (Format noch nicht
|
||||||
|
geknackt, siehe NOTES), hier v1 als LEVEL_INFO-Konstanten.
|
||||||
|
- Tuer mit Kopf-Symbol: ALT im STEHEN davor oeffnet das Charakter-
|
||||||
|
Auswahlmenue ("MIT WEM MOECHTEST DU SPIELEN?", WECH_1-4 = Koepfe farbig,
|
||||||
|
WECH_5-8 = grau/nicht verfuegbar). ALT beim Laufen = RENNEN (nur Tony).
|
||||||
|
|
||||||
Referenz-Fakten aus der DOSBox-Aufnahme (25.07.2026):
|
Frame-Zuordnungen (nummerierte Sheets gesichtet):
|
||||||
- Auf der Weltkarte startet SPACE das Level (Return tut dort nichts,
|
TONY.BOB (53): 0-15 Gehzyklus (gerade=RECHTS), 18/19 Sprung(gestreckt),
|
||||||
Pfeiltasten bewegen keinen sichtbaren Cursor -- am Spielstart ist nur
|
24/25 Ducken, 26-33 RENN-"Rad" (4 Phasen x 2), 44-49 Idle-Packung (3x2).
|
||||||
Level 1 verfuegbar). Kurzer Schwarzblende-Uebergang, dann steht das Level
|
SMACKS.BOB (51): 0-15 Gehzyklus, 18/19 Ducken(kauern), 24/25 Sprung,
|
||||||
sofort (kein "Get Ready"-Screen).
|
31-36 Idle-Packung, 37-44 SCHWIMMEN (4 Phasen x 2), 45-47 Wasserblasen.
|
||||||
- Viewport: 320x176 (oben), HUD-Balken unten 320x24 (y=176..200).
|
|
||||||
- HUD: Kellogg's-Schriftzug, Keks-Zaehler (000), Item-Box, Score (000000),
|
|
||||||
4 Herzen, Tony-Kopf mit Lebenszahl (3), Uhr zaehlt von 10:00 runter
|
|
||||||
(9:59 ~1.5s nach Levelstart beobachtet).
|
|
||||||
- Tony spawnt im Bereich um Tile ~17 (Kamera-Match bei Map-Pixel 128,496),
|
|
||||||
steht auf der "Saeulen"-Plattform (Zeile 40).
|
|
||||||
|
|
||||||
Kollision (aus dem 7-Bit-Typfeld der MAP-Zellen, empirisch an W1L1):
|
|
||||||
Bit 32 = solide (40 = Boden-Oberkante 32|8, 32 = Fuellung darunter,
|
|
||||||
36/44/41 = Varianten). 0 = Luft, 4 = Item-Marker, 16 = Special (Tuer?).
|
|
||||||
v1 nutzt nur `ctype & 32` als Wand/Boden.
|
|
||||||
|
|
||||||
Tony-Sprite (TONY.BOB, 53 Frames a 32x56): 8-Phasen-Gehzyklus, GERADE
|
|
||||||
Frame-Indizes = Blick links, UNGERADE = rechts (0..15). Reihe 3 (16..23)
|
|
||||||
Arme hoch (Sprung), Reihe 4 (24..31) Ducken/Rollen.
|
|
||||||
|
|
||||||
Musik: ONGAME2.TFX buendelt 14 Songs -- Song 0 als v1-Annahme fuer Level 1
|
|
||||||
(nach Gehoer noch nicht zugeordnet; wie beim Factor5-Sound: Stefan hoert,
|
|
||||||
wir korrigieren den Index bei Bedarf).
|
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -37,41 +28,83 @@ import pygame
|
|||||||
from . import intro
|
from . import intro
|
||||||
from .formats import parse_map, parse_ico, parse_bob, get_world_palette, get_bob_palette
|
from .formats import parse_map, parse_ico, parse_bob, get_world_palette, get_bob_palette
|
||||||
|
|
||||||
VIEW_W, VIEW_H = 320, 176 # sichtbarer Levelausschnitt
|
VIEW_W, VIEW_H = 320, 176
|
||||||
HUD_H = 24 # HUD-Balken unten (320x24)
|
HUD_H = 24
|
||||||
TILE = 16
|
TILE = 16
|
||||||
|
|
||||||
# Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt;
|
WALK_SPEED = 110.0
|
||||||
# Stefans Live-Test 25.07.: "fuehlt sich gut an")
|
RUN_SPEED = 185.0
|
||||||
WALK_SPEED = 110.0 # px/s
|
GRAVITY = 900.0
|
||||||
RUN_SPEED = 185.0 # px/s -- Tonys Faehigkeit: RENNEN (ALT halten,
|
# Sprunghoehe ~72px (Stefans Referenz: Tonys Kopf erreicht gerade den Ast
|
||||||
# wie im Original lt. Anleitung; LSHIFT geht auch)
|
# ueber dem Spawn): v = sqrt(2*g*h) ~= 360.
|
||||||
GRAVITY = 900.0 # px/s^2
|
JUMP_VELOCITY = -360.0
|
||||||
JUMP_VELOCITY = -300.0 # px/s (Sprunghoehe ~50px = gut 3 Tiles)
|
SWIM_GRAVITY = 240.0 # Smacks im Wasser: traege sinken
|
||||||
|
SWIM_KICK = -140.0 # Schwimmstoss mit Space
|
||||||
WALK_ANIM_FPS = 12.0
|
WALK_ANIM_FPS = 12.0
|
||||||
|
RUN_ANIM_FPS = 14.0
|
||||||
# 9:59 statt 10:00: die Original-Anzeige stand ~1.5s nach Levelstart schon auf
|
IDLE_ANIM_AFTER = 5.0 # Sekunden Stillstand bis zur Idle-Animation
|
||||||
# 9:59, und 4-stellig ("9:59") passt sicher in die Uhr-Box (320px-Zeile).
|
IDLE_ANIM_FPS = 2.5
|
||||||
LEVEL_TIME_SECONDS = 599
|
WATER_DAMAGE_EVERY = 5.0 # im Wasser: alle 5s ein Herz (Stefans Angabe)
|
||||||
|
|
||||||
SOLID_BIT = 32
|
SOLID_BIT = 32
|
||||||
SLOPE_CT = 16 # ctype 16 = Schraegen-Tile (Grashaenge) --
|
SLOPE_CT = 16
|
||||||
# als blaue Diagonale in der ctype-Visualisierung
|
|
||||||
# exakt auf dem Hang der DOSBox-Referenz gefunden
|
|
||||||
# ("faellt durch"-Bug von Stefans Live-Test)
|
|
||||||
|
|
||||||
# TONY.BOB-Frame-Zuordnung. KORREKTUR (Stefans Live-Test 25.07.2026: "Figur
|
# Charakter-Definitionen. Richtungs-Paritaet: gerade Frames = rechts
|
||||||
# laeuft rueckwaerts, Richtung stimmt"): GERADE Indizes sind RECHTS,
|
# (an Tony per Live-Test bestaetigt; bei Smacks analog angenommen).
|
||||||
# UNGERADE LINKS -- genau andersrum als zuerst angenommen.
|
CHAR_DEFS = {
|
||||||
IDLE_R, IDLE_L = 0, 1
|
'TONY': dict(
|
||||||
WALK_R = [0, 2, 4, 6, 8, 10, 12, 14]
|
bob='TONY', can_run=True, can_swim=False,
|
||||||
WALK_L = [1, 3, 5, 7, 9, 11, 13, 15]
|
walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15],
|
||||||
JUMP_R, JUMP_L = 16, 17
|
run_r=[26, 28, 30, 32], run_l=[27, 29, 31, 33],
|
||||||
DUCK_R, DUCK_L = 24, 25
|
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=[44, 46, 48], idle_anim_l=[45, 47, 49],
|
||||||
|
item_icon='ITE0', head_index=0,
|
||||||
|
),
|
||||||
|
'SMACKS': dict(
|
||||||
|
bob='SMACKS', can_run=False, can_swim=True,
|
||||||
|
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=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],
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
item_icon='ITE2', head_index=3,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
CHAR_ORDER = ['TONY', 'SMACKS', 'COCO', 'TOUCAN']
|
||||||
|
|
||||||
|
# Level-Metadaten (v1 als Konstanten -- die Quelle im Original ist vermutlich
|
||||||
|
# das noch nicht geknackte .ARE-Format; dessen Header enthaelt Pixel-
|
||||||
|
# Koordinatenpaare, siehe NOTES).
|
||||||
|
LEVEL_INFO = {
|
||||||
|
'W1L1': dict(
|
||||||
|
spawn=(17, 40),
|
||||||
|
time=599,
|
||||||
|
chars=['TONY', 'SMACKS'], # Level 1: nur die beiden (Stefan)
|
||||||
|
water_y=42 * TILE, # Wasseroberflaeche unter der Bruecke
|
||||||
|
door_px=(12 * TILE, 37 * TILE, 2 * TILE, 3 * TILE), # Haus-Tuer (x,y,w,h)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
WATER_TINT = (10, 20, 120, 110) # dunkelblaues Overlay
|
||||||
|
|
||||||
|
|
||||||
def _frames_to_surfaces(frames, palette):
|
def _frames_to_surfaces(frames, palette):
|
||||||
'''BOB-Frames (Palette-Indizes, -1 = transparent) -> pygame-Surfaces.'''
|
|
||||||
out = []
|
out = []
|
||||||
for fr in frames:
|
for fr in frames:
|
||||||
surf = pygame.Surface((fr.width, fr.height), pygame.SRCALPHA)
|
surf = pygame.Surface((fr.width, fr.height), pygame.SRCALPHA)
|
||||||
@@ -86,20 +119,19 @@ def _frames_to_surfaces(frames, palette):
|
|||||||
|
|
||||||
|
|
||||||
class Level:
|
class Level:
|
||||||
'''Laedt eine MAP (+ zugehoeriges Welt-ICO/-Palette) und rendert sie
|
'''MAP+ICO einmal komplett vorrendern; Kamera blittet den Ausschnitt.'''
|
||||||
einmalig komplett auf eine grosse Surface -- die Kamera blittet daraus
|
|
||||||
den sichtbaren Ausschnitt (W1L1: 3488x752 px, als Vollbild-Surface
|
|
||||||
problemlos).'''
|
|
||||||
|
|
||||||
def __init__(self, assets, map_name):
|
def __init__(self, assets, map_name):
|
||||||
container = assets.container
|
container = assets.container
|
||||||
self.name = map_name
|
self.name = map_name
|
||||||
world = map_name[:2] # 'W1'
|
self.info = LEVEL_INFO.get(map_name, dict(spawn=(2, 2), time=599,
|
||||||
|
chars=['TONY'], water_y=None,
|
||||||
|
door_px=None))
|
||||||
|
world = map_name[:2]
|
||||||
self.width, self.height, self.cells = parse_map(container.entries[map_name + '.MAP'])
|
self.width, self.height, self.cells = parse_map(container.entries[map_name + '.MAP'])
|
||||||
palette = get_world_palette(container.entries, world)
|
palette = get_world_palette(container.entries, world)
|
||||||
tiles = parse_ico(container.entries[world + '.ICO'])
|
tiles = parse_ico(container.entries[world + '.ICO'])
|
||||||
|
|
||||||
# Tiles einmalig zu Surfaces
|
|
||||||
tile_surfs = []
|
tile_surfs = []
|
||||||
for grid in tiles:
|
for grid in tiles:
|
||||||
s = pygame.Surface((TILE, TILE))
|
s = pygame.Surface((TILE, TILE))
|
||||||
@@ -110,66 +142,70 @@ class Level:
|
|||||||
s.set_at((x, y), (r, g, b))
|
s.set_at((x, y), (r, g, b))
|
||||||
tile_surfs.append(s.convert())
|
tile_surfs.append(s.convert())
|
||||||
|
|
||||||
# ganze Map vorrendern
|
|
||||||
self.surface = pygame.Surface((self.width * TILE, self.height * TILE))
|
self.surface = pygame.Surface((self.width * TILE, self.height * TILE))
|
||||||
self.surface.fill((0, 0, 0))
|
self.surface.fill((0, 0, 0))
|
||||||
for i, (tile, _ct) in enumerate(self.cells):
|
for i, (tile, _ct) in enumerate(self.cells):
|
||||||
if tile < len(tile_surfs):
|
if tile < len(tile_surfs):
|
||||||
x = (i % self.width) * TILE
|
self.surface.blit(tile_surfs[tile], ((i % self.width) * TILE,
|
||||||
y = (i // self.width) * TILE
|
(i // self.width) * TILE))
|
||||||
self.surface.blit(tile_surfs[tile], (x, y))
|
|
||||||
self.surface = self.surface.convert()
|
self.surface = self.surface.convert()
|
||||||
|
|
||||||
self.palette = palette
|
self.palette = palette
|
||||||
|
|
||||||
def ctype(self, tx, ty):
|
def ctype(self, tx, ty):
|
||||||
if tx < 0 or ty < 0 or tx >= self.width or ty >= self.height:
|
if tx < 0 or ty < 0 or tx >= self.width or ty >= self.height:
|
||||||
return SOLID_BIT # ausserhalb = solide (haelt Tony im Level)
|
return SOLID_BIT
|
||||||
return self.cells[ty * self.width + tx][1]
|
return self.cells[ty * self.width + tx][1]
|
||||||
|
|
||||||
def solid(self, tx, ty):
|
def solid(self, tx, ty):
|
||||||
return bool(self.ctype(tx, ty) & SOLID_BIT)
|
return bool(self.ctype(tx, ty) & SOLID_BIT)
|
||||||
|
|
||||||
def slope_surface(self, px, ty):
|
def slope_surface(self, px, ty):
|
||||||
'''Boden-y der 45-Grad-Schraege im Slope-Tile (ctype 16) der Zeile ty
|
|
||||||
an Pixel-x px -- oder None, wenn dort kein Slope-Tile liegt.
|
|
||||||
Steigungsrichtung per Nachbar-Heuristik: schliesst rechts-oberhalb ein
|
|
||||||
weiteres Slope-/Solid-Tile an, steigt die Diagonale nach rechts
|
|
||||||
(Flaeche faellt nach links ab), sonst gespiegelt.'''
|
|
||||||
tx = int(px // TILE)
|
tx = int(px // TILE)
|
||||||
if self.ctype(tx, ty) != SLOPE_CT:
|
if self.ctype(tx, ty) != SLOPE_CT:
|
||||||
return None
|
return None
|
||||||
fx = px - tx * TILE # 0..15 im Tile
|
fx = px - tx * TILE
|
||||||
rising_right = (self.ctype(tx + 1, ty - 1) == SLOPE_CT or
|
rising_right = (self.ctype(tx + 1, ty - 1) == SLOPE_CT or
|
||||||
self.solid(tx + 1, ty - 1) or self.solid(tx + 1, ty))
|
self.solid(tx + 1, ty - 1) or self.solid(tx + 1, ty))
|
||||||
rising_left = (self.ctype(tx - 1, ty - 1) == SLOPE_CT or
|
rising_left = (self.ctype(tx - 1, ty - 1) == SLOPE_CT or
|
||||||
self.solid(tx - 1, ty - 1) or self.solid(tx - 1, ty))
|
self.solid(tx - 1, ty - 1) or self.solid(tx - 1, ty))
|
||||||
if rising_right and not rising_left:
|
if rising_right and not rising_left:
|
||||||
return (ty + 1) * TILE - 1 - fx # links unten -> rechts oben
|
return (ty + 1) * TILE - 1 - fx
|
||||||
if rising_left and not rising_right:
|
if rising_left and not rising_right:
|
||||||
return ty * TILE + fx # links oben -> rechts unten
|
return ty * TILE + fx
|
||||||
return (ty + 1) * TILE - 1 - fx # Default: nach rechts steigend
|
return (ty + 1) * TILE - 1 - fx
|
||||||
|
|
||||||
|
def in_water(self, py):
|
||||||
|
wy = self.info.get('water_y')
|
||||||
|
return wy is not None and py >= wy
|
||||||
|
|
||||||
|
|
||||||
class Tony:
|
class Player:
|
||||||
'''Spielfigur: Position (px, Fusspunkt-basiert), einfache AABB-Physik
|
'''Spielfigur (Tony/Smacks/...): Fusspunkt-basierte AABB-Physik.'''
|
||||||
gegen das Tile-Grid.'''
|
|
||||||
|
|
||||||
HITBOX_W = 20
|
HITBOX_W = 20
|
||||||
HITBOX_H = 52
|
HITBOX_H = 52
|
||||||
|
|
||||||
def __init__(self, assets, spawn_px):
|
def __init__(self, assets, char, pos):
|
||||||
container = assets.container
|
self.assets = assets
|
||||||
frames = parse_bob(container.entries['TONY.BOB'])
|
self.set_character(char, pos)
|
||||||
palette = get_bob_palette(container.entries, 'TONY')
|
|
||||||
self.surfs = _frames_to_surfaces(frames, palette)
|
|
||||||
self.x, self.y = float(spawn_px[0]), float(spawn_px[1]) # Fussmitte
|
|
||||||
self.vx = 0.0
|
self.vx = 0.0
|
||||||
self.vy = 0.0
|
self.vy = 0.0
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
self.facing = 1 # 1 = rechts, -1 = links
|
self.facing = 1
|
||||||
self.ducking = False
|
self.ducking = False
|
||||||
|
self.running = False
|
||||||
|
self.swimming = False
|
||||||
self.anim_t = 0.0
|
self.anim_t = 0.0
|
||||||
|
self.idle_t = 0.0
|
||||||
|
|
||||||
|
def set_character(self, char, pos=None):
|
||||||
|
self.char = char
|
||||||
|
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)
|
||||||
|
if pos is not None:
|
||||||
|
self.x, self.y = float(pos[0]), float(pos[1])
|
||||||
|
|
||||||
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),
|
||||||
@@ -177,48 +213,56 @@ class Tony:
|
|||||||
self.HITBOX_W, self.HITBOX_H)
|
self.HITBOX_W, self.HITBOX_H)
|
||||||
|
|
||||||
def update(self, level, dt, keys):
|
def update(self, level, dt, keys):
|
||||||
self.ducking = self.on_ground and keys[pygame.K_DOWN]
|
in_water = level.in_water(self.y)
|
||||||
|
self.swimming = in_water and self.d['can_swim']
|
||||||
|
self.ducking = self.on_ground and not self.swimming and keys[pygame.K_DOWN]
|
||||||
move = 0
|
move = 0
|
||||||
if not self.ducking:
|
if not self.ducking:
|
||||||
if keys[pygame.K_LEFT]:
|
if keys[pygame.K_LEFT]:
|
||||||
move -= 1
|
move -= 1
|
||||||
if keys[pygame.K_RIGHT]:
|
if keys[pygame.K_RIGHT]:
|
||||||
move += 1
|
move += 1
|
||||||
# Tonys Faehigkeit: RENNEN -- ALT halten (wie im Original laut
|
|
||||||
# Anleitung "MIT ALT AKTIVIERST DU DIE FAEHIGKEITEN"), LSHIFT als
|
|
||||||
# moderne Alternative. (Alt+Enter bleibt Vollbild -- kein Konflikt,
|
|
||||||
# das faengt die Eventschleife ab.)
|
|
||||||
mods = pygame.key.get_mods()
|
mods = pygame.key.get_mods()
|
||||||
running = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT))
|
wants_run = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT))
|
||||||
self.vx = move * (RUN_SPEED if running else WALK_SPEED)
|
self.running = wants_run and self.d['can_run'] and move != 0 and not self.swimming
|
||||||
|
speed = RUN_SPEED if self.running else WALK_SPEED
|
||||||
|
if self.swimming:
|
||||||
|
speed *= 0.7
|
||||||
|
self.vx = move * speed
|
||||||
if move:
|
if move:
|
||||||
self.facing = move
|
self.facing = move
|
||||||
self.anim_t += dt * (1.5 if running else 1.0)
|
self.anim_t += dt * (RUN_ANIM_FPS / WALK_ANIM_FPS if self.running else 1.0)
|
||||||
|
self.idle_t = 0.0
|
||||||
else:
|
else:
|
||||||
self.anim_t = 0.0
|
self.anim_t = 0.0
|
||||||
|
self.idle_t += dt
|
||||||
|
|
||||||
if self.on_ground and not self.ducking and (keys[pygame.K_SPACE] or keys[pygame.K_UP]):
|
jump_pressed = keys[pygame.K_SPACE] or keys[pygame.K_UP]
|
||||||
self.vy = JUMP_VELOCITY
|
if self.swimming:
|
||||||
self.on_ground = False
|
if jump_pressed:
|
||||||
|
self.vy = min(self.vy, SWIM_KICK)
|
||||||
|
self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0)
|
||||||
|
else:
|
||||||
|
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.vy = min(self.vy + GRAVITY * dt, 480.0)
|
||||||
|
|
||||||
self.vy = min(self.vy + GRAVITY * dt, 480.0)
|
# X + Wandkollision
|
||||||
|
|
||||||
# X-Bewegung + Wandkollision
|
|
||||||
nx = self.x + self.vx * dt
|
nx = self.x + self.vx * dt
|
||||||
r = self.rect()
|
r = self.rect()
|
||||||
if self.vx > 0:
|
if self.vx > 0:
|
||||||
edge = nx + self.HITBOX_W / 2
|
tx = int((nx + self.HITBOX_W / 2) // TILE)
|
||||||
tx = int(edge // TILE)
|
|
||||||
if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)):
|
if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)):
|
||||||
nx = tx * TILE - self.HITBOX_W / 2 - 0.01
|
nx = tx * TILE - self.HITBOX_W / 2 - 0.01
|
||||||
elif self.vx < 0:
|
elif self.vx < 0:
|
||||||
edge = nx - self.HITBOX_W / 2
|
tx = int((nx - self.HITBOX_W / 2) // TILE)
|
||||||
tx = int(edge // TILE)
|
|
||||||
if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)):
|
if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)):
|
||||||
nx = (tx + 1) * TILE + self.HITBOX_W / 2 + 0.01
|
nx = (tx + 1) * TILE + self.HITBOX_W / 2 + 0.01
|
||||||
self.x = nx
|
self.x = nx
|
||||||
|
|
||||||
# Y-Bewegung + Boden-/Deckenkollision
|
# Y + Boden/Decke
|
||||||
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)
|
||||||
@@ -231,18 +275,12 @@ class Tony:
|
|||||||
else:
|
else:
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
else:
|
else:
|
||||||
top = ny - self.HITBOX_H
|
ty = int((ny - self.HITBOX_H) // TILE)
|
||||||
ty = int(top // 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 + 1) * TILE + self.HITBOX_H + 0.01
|
ny = (ty + 1) * TILE + self.HITBOX_H + 0.01
|
||||||
self.vy = 0.0
|
self.vy = 0.0
|
||||||
|
|
||||||
# Schraegen (ctype 16): Boden folgt der 45-Grad-Diagonale. Am
|
# Schraegen (ctype 16)
|
||||||
# FUSSMITTELPUNKT geprueft (nicht an den Hitbox-Kanten -- so "schwebt"
|
|
||||||
# Tony nicht an der Hangkante). Zeile darueber mitpruefen, damit er
|
|
||||||
# beim Bergauflaufen ins naechsthoehere Slope-Tile aufsteigt; Snap nur
|
|
||||||
# bei fallender/stehender Bewegung und kleiner Distanz (kein
|
|
||||||
# Herunterziehen mitten im Sprung).
|
|
||||||
if self.vy >= 0:
|
if self.vy >= 0:
|
||||||
foot_ty = int(ny // TILE)
|
foot_ty = int(ny // TILE)
|
||||||
for ty2 in (foot_ty - 1, foot_ty, foot_ty + 1):
|
for ty2 in (foot_ty - 1, foot_ty, foot_ty + 1):
|
||||||
@@ -254,42 +292,49 @@ class Tony:
|
|||||||
break
|
break
|
||||||
self.y = ny
|
self.y = ny
|
||||||
|
|
||||||
def current_frame(self):
|
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
|
right = self.facing > 0
|
||||||
|
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]
|
||||||
if self.ducking:
|
if self.ducking:
|
||||||
i = DUCK_R if right else DUCK_L
|
return self._pick('duck_r', 'duck_l')
|
||||||
elif not self.on_ground:
|
if not self.on_ground:
|
||||||
i = JUMP_R if right else JUMP_L
|
return self._pick('jump_r', 'jump_l')
|
||||||
elif self.vx:
|
if self.vx:
|
||||||
seq = WALK_R if right else WALK_L
|
if self.running and d['run_r']:
|
||||||
i = seq[int(self.anim_t * WALK_ANIM_FPS) % len(seq)]
|
seq = d['run_r'] if right else d['run_l']
|
||||||
else:
|
fps = RUN_ANIM_FPS
|
||||||
i = IDLE_R if right else IDLE_L
|
else:
|
||||||
if i < len(self.surfs):
|
seq = d['walk_r'] if right else d['walk_l']
|
||||||
return self.surfs[i]
|
fps = WALK_ANIM_FPS
|
||||||
return self.surfs[0]
|
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')
|
||||||
|
|
||||||
def draw(self, screen, cam_x, cam_y):
|
def draw(self, screen, cam_x, cam_y):
|
||||||
surf = self.current_frame()
|
i = self.current_frame_index()
|
||||||
# Fusspunkt-zentriert
|
surf = self.surfs[i] if i < len(self.surfs) else self.surfs[0]
|
||||||
x = int(self.x - surf.get_width() / 2 - cam_x)
|
screen.blit(surf, (int(self.x - surf.get_width() / 2 - cam_x),
|
||||||
y = int(self.y - surf.get_height() - cam_y)
|
int(self.y - surf.get_height() - cam_y)))
|
||||||
screen.blit(surf, (x, y))
|
|
||||||
|
|
||||||
|
|
||||||
class Hud:
|
class Hud:
|
||||||
'''HUD-Balken (320x24) -- v1-Nachbildung des Original-Layouts. Panel
|
'''HUD-Balken nach Stefans Referenz-Screenshot: Kellogg's-Leiste,
|
||||||
(dunkelrote Boxen mit Goldrand) selbst gezeichnet; Zahlen in der kleinen
|
Keks-Box MIT Item-Icon (ITE0-3 je Charakter), Item-Box, Score-Box,
|
||||||
FON-Schrift mit Outline (die Original-HUD-Ziffern sind noch keinem Asset
|
Herz-Box (2x2-GRID), Charakter-Kopf (WECH, zeigt IMMER die aktuell
|
||||||
sauber zugeordnet -- die GAMEBAR.BOB-Ziffern rendern mit der bob-Regel-
|
gewaehlte Figur) + Leben, Uhr.'''
|
||||||
Palette dunkelrot statt weiss, vermutlich andere Laufzeit-Palette).
|
|
||||||
Die GAMEBAR-Schluessel-/Herz-Icons (Frames 10-16) liegen fuer den
|
|
||||||
spaeteren Feinschliff schon in self.icons bereit.'''
|
|
||||||
|
|
||||||
GOLD = (222, 186, 24)
|
GOLD = (222, 186, 24)
|
||||||
RED = (150, 14, 14)
|
RED = (150, 14, 14)
|
||||||
DARK = (84, 6, 6)
|
DARK = (84, 6, 6)
|
||||||
|
|
||||||
WHITE = (248, 248, 248)
|
WHITE = (248, 248, 248)
|
||||||
|
|
||||||
def __init__(self, assets):
|
def __init__(self, assets):
|
||||||
@@ -298,62 +343,122 @@ class Hud:
|
|||||||
frames = parse_bob(container.entries['GAMEBAR.BOB'])
|
frames = parse_bob(container.entries['GAMEBAR.BOB'])
|
||||||
palette = get_bob_palette(container.entries, 'GAMEBAR')
|
palette = get_bob_palette(container.entries, 'GAMEBAR')
|
||||||
self.icons = _frames_to_surfaces(frames, palette)
|
self.icons = _frames_to_surfaces(frames, palette)
|
||||||
self.heart_full = self.icons[10] # 9x8, pink/rot
|
self.heart_full = self.icons[10]
|
||||||
self.heart_empty = self.icons[11]
|
self.heart_empty = self.icons[11]
|
||||||
self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7,
|
self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7,
|
||||||
_mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']),
|
_mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']),
|
||||||
spacing=8)
|
spacing=8)
|
||||||
# Kellogg's-Schriftzug fuer die weisse HUD-Leiste links (Stefans
|
|
||||||
# Feedback: "die weisse Kellogg's-Leiste fehlt"): aus KELLOGGS.PCC
|
|
||||||
# gecroppt (Schriftzug ohne "praesentiert") und klein skaliert.
|
|
||||||
logo = assets.pcc_surface('KELLOGGS').subsurface((36, 26, 252, 66))
|
logo = assets.pcc_surface('KELLOGGS').subsurface((36, 26, 252, 66))
|
||||||
self.kellogg = pygame.transform.smoothscale(logo, (50, 13))
|
self.kellogg = pygame.transform.smoothscale(logo, (50, 13))
|
||||||
|
# Item-Icons (Cerealien) + Charakter-Koepfe klein
|
||||||
|
self.item_icons = {n: intro.make_border_bg_transparent(assets.pcc_surface(n))
|
||||||
|
for n in ('ITE0', 'ITE1', 'ITE2', 'ITE3')}
|
||||||
|
self.heads_small = []
|
||||||
|
for i in range(1, 5):
|
||||||
|
head = intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}'))
|
||||||
|
self.heads_small.append(pygame.transform.smoothscale(head, (17, 16)))
|
||||||
|
|
||||||
def _box(self, canvas, x, y, w, h):
|
def _box(self, canvas, x, y, w, h):
|
||||||
pygame.draw.rect(canvas, self.GOLD, (x - 1, y - 1, w + 2, h + 2))
|
pygame.draw.rect(canvas, self.GOLD, (x - 1, y - 1, w + 2, h + 2))
|
||||||
pygame.draw.rect(canvas, self.RED, (x, y, w, h))
|
pygame.draw.rect(canvas, self.RED, (x, y, w, h))
|
||||||
|
|
||||||
def _text(self, canvas, x, y, text):
|
def _text(self, canvas, x, y, text):
|
||||||
surf = self.font.render(text, outline=(0, 0, 0))
|
canvas.blit(self.font.render(text, outline=(0, 0, 0)), (x, y))
|
||||||
canvas.blit(surf, (x, y))
|
|
||||||
|
|
||||||
def draw(self, canvas, y0, cookies, score, hearts, lives, time_left):
|
def draw(self, canvas, y0, char_def, cookies, score, hearts, lives, time_left):
|
||||||
pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H))
|
pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H))
|
||||||
pad_y = y0 + 8
|
# Kellogg's-Leiste
|
||||||
# Kellogg's-Leiste (weiss, abgerundet) ganz links
|
pygame.draw.rect(canvas, self.WHITE, (2, y0 + 3, 56, HUD_H - 6), border_radius=6)
|
||||||
pygame.draw.rect(canvas, self.WHITE, (2, y0 + 3, 58, HUD_H - 6),
|
canvas.blit(self.kellogg, (5, y0 + 5))
|
||||||
border_radius=6)
|
# Keks-Box mit Item-Icon des aktiven Charakters
|
||||||
canvas.blit(self.kellogg, (6, y0 + 5))
|
self._box(canvas, 62, y0 + 4, 46, 16)
|
||||||
# Keks-Zaehler
|
icon = self.item_icons.get(char_def['item_icon'])
|
||||||
self._box(canvas, 66, pad_y - 3, 32, 14)
|
if icon:
|
||||||
self._text(canvas, 70, pad_y, f'{cookies:03d}')
|
canvas.blit(icon, (64, y0 + 4))
|
||||||
# Item-Box (leer)
|
self._text(canvas, 82, y0 + 9, f'{cookies:03d}')
|
||||||
self._box(canvas, 104, pad_y - 3, 38, 14)
|
# Item-Box
|
||||||
|
self._box(canvas, 114, y0 + 4, 36, 16)
|
||||||
# Score
|
# Score
|
||||||
self._box(canvas, 148, pad_y - 3, 56, 14)
|
self._box(canvas, 156, y0 + 4, 54, 16)
|
||||||
self._text(canvas, 152, pad_y, f'{score:06d}')
|
self._text(canvas, 159, y0 + 9, f'{score:06d}')
|
||||||
# Herzen (echte GAMEBAR-Sprites)
|
# Herz-Box: 2x2-Grid (Referenz-Screenshot)
|
||||||
|
self._box(canvas, 216, y0 + 2, 26, 20)
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
icon = self.heart_full if i < hearts else self.heart_empty
|
icon = self.heart_full if i < hearts else self.heart_empty
|
||||||
canvas.blit(icon, (210 + i * 11, pad_y - 1))
|
hx = 219 + (i % 2) * 10
|
||||||
# Leben
|
hy = y0 + 3 + (i // 2) * 9
|
||||||
self._box(canvas, 259, pad_y - 3, 18, 14)
|
canvas.blit(icon, (hx, hy))
|
||||||
self._text(canvas, 264, pad_y, str(lives))
|
# Charakter-Kopf (immer die gewaehlte Figur) + Leben
|
||||||
# Uhr m:ss (Format 4-stellig, Startzeit 9:59 -> passt immer)
|
self._box(canvas, 248, y0 + 2, 36, 20)
|
||||||
|
canvas.blit(self.heads_small[char_def['head_index']], (250, y0 + 4))
|
||||||
|
self._text(canvas, 272, y0 + 9, str(lives))
|
||||||
|
# Uhr
|
||||||
m, s = divmod(max(0, int(time_left)), 60)
|
m, s = divmod(max(0, int(time_left)), 60)
|
||||||
self._box(canvas, 283, pad_y - 3, 35, 14)
|
self._box(canvas, 288, y0 + 4, 30, 16)
|
||||||
self._text(canvas, 286, pad_y, f'{m}:{s:02d}')
|
self._text(canvas, 290, y0 + 9, f'{m}:{s:02d}')
|
||||||
|
|
||||||
|
|
||||||
def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)):
|
def run_character_select(clock, assets, available, current):
|
||||||
'''Spielt ein Level. ESC -> zurueck zur Karte ('map'), Fenster-X -> 'quit'.
|
'''Tuer-Menue "MIT WEM MOECHTEST DU SPIELEN?" -- Koepfe WECH_1-4 (farbig)
|
||||||
v1: Laufen/Springen/Ducken + Kollision + Kamera + HUD + Levelmusik.
|
bzw. WECH_5-8 (grau = in diesem Level nicht verfuegbar). Links/Rechts +
|
||||||
Noch ohne Gegner/Items/Fahigkeiten -- die folgen schrittweise.'''
|
Return; ESC bricht ab. Rueckgabe: Charaktername, None (abgebrochen) oder
|
||||||
|
'quit' (Fenster-X).'''
|
||||||
|
from .menu import MenuAssets, BLINK_SECONDS
|
||||||
|
m = MenuAssets(assets)
|
||||||
|
heads_color = [intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}')) for i in range(1, 5)]
|
||||||
|
heads_grey = [intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}')) for i in range(5, 9)]
|
||||||
|
names = CHAR_ORDER
|
||||||
|
sel = [names.index(current) if current in names else 0]
|
||||||
|
start = time.monotonic()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
return 'quit'
|
||||||
|
if event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_RETURN and (event.mod & pygame.KMOD_ALT):
|
||||||
|
intro.toggle_fullscreen()
|
||||||
|
continue
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
return None
|
||||||
|
if event.key in (pygame.K_LEFT, pygame.K_UP):
|
||||||
|
sel[0] = (sel[0] - 1) % 4
|
||||||
|
elif event.key in (pygame.K_RIGHT, pygame.K_DOWN):
|
||||||
|
sel[0] = (sel[0] + 1) % 4
|
||||||
|
elif event.key in (pygame.K_RETURN, pygame.K_SPACE):
|
||||||
|
if names[sel[0]] in available:
|
||||||
|
return names[sel[0]]
|
||||||
|
|
||||||
|
t = time.monotonic() - start
|
||||||
|
blink_on = int(t / BLINK_SECONDS) % 2 == 0
|
||||||
|
canvas = m.menu_bg.copy()
|
||||||
|
title = m.font_small.render('MIT WEM M\xd6CHTEST DU SPIELEN?', outline=(0, 0, 0))
|
||||||
|
canvas.blit(title, ((320 - title.get_width()) // 2, 48))
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
x = 52 + i * 60
|
||||||
|
head = heads_color[i] if name in available else heads_grey[i]
|
||||||
|
if i == sel[0] and not blink_on:
|
||||||
|
pass # Auswahl blinkt
|
||||||
|
else:
|
||||||
|
canvas.blit(head, (x, 74))
|
||||||
|
label = m.font_small.render(name, outline=(0, 0, 0))
|
||||||
|
canvas.blit(label, (x + 24 - label.get_width() // 2, 124))
|
||||||
|
intro._blit_centered(intro.build_letterboxed(canvas))
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(30)
|
||||||
|
|
||||||
|
|
||||||
|
def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
||||||
|
'''Level spielen. ESC -> 'map', Fenster-X -> 'quit'.'''
|
||||||
level = Level(assets, map_name)
|
level = Level(assets, map_name)
|
||||||
tony = Tony(assets, (spawn_tile[0] * TILE + 8, spawn_tile[1] * TILE - 0.01))
|
info = level.info
|
||||||
|
spawn_px = (info['spawn'][0] * TILE + 8, info['spawn'][1] * TILE - 0.01)
|
||||||
|
player = Player(assets, info['chars'][0], spawn_px)
|
||||||
hud = Hud(assets)
|
hud = Hud(assets)
|
||||||
|
door = pygame.Rect(info['door_px']) if info.get('door_px') else None
|
||||||
|
door_head = pygame.transform.smoothscale(
|
||||||
|
intro.make_border_bg_transparent(assets.pcc_surface('WECH_1')), (16, 15))
|
||||||
|
|
||||||
# Levelmusik: ONGAME2 Song 0 (v1-Annahme, siehe Docstring). Loopt.
|
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init():
|
||||||
path = audio_paths.get(('ONGAME2', None))
|
path = audio_paths.get(('ONGAME2', None))
|
||||||
if path:
|
if path:
|
||||||
@@ -367,10 +472,11 @@ def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)):
|
|||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
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 = 4, 3
|
||||||
|
water_t = 0.0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
dt = clock.tick(60) / 1000.0
|
dt = min(clock.tick(60) / 1000.0, 1 / 20)
|
||||||
dt = min(dt, 1 / 20) # Physik-Schutz bei Rucklern
|
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
return 'quit'
|
return 'quit'
|
||||||
@@ -379,19 +485,58 @@ def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)):
|
|||||||
intro.toggle_fullscreen()
|
intro.toggle_fullscreen()
|
||||||
continue
|
continue
|
||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
return 'map' # wie im Original: ESC = zurueck zur Karte
|
return 'map'
|
||||||
|
# Tuer benutzen: ALT im STEHEN vor der Tuer (nicht beim
|
||||||
|
# Rennen -- Stefans Regel) -> Charakterwechsel-Menue
|
||||||
|
if event.key in (pygame.K_LALT, pygame.K_RALT):
|
||||||
|
if (door and door.collidepoint(player.x, player.y - 1)
|
||||||
|
and player.on_ground and abs(player.vx) < 1):
|
||||||
|
choice = run_character_select(clock, assets,
|
||||||
|
info['chars'], player.char)
|
||||||
|
if choice == 'quit':
|
||||||
|
return 'quit'
|
||||||
|
if choice and choice != player.char:
|
||||||
|
player.set_character(choice)
|
||||||
|
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
tony.update(level, dt, keys)
|
player.update(level, dt, keys)
|
||||||
|
|
||||||
# Kamera: Tony zentriert, an Levelraender geklemmt
|
# Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher
|
||||||
cam_x = max(0, min(int(tony.x) - VIEW_W // 2, map_w_px - VIEW_W))
|
if level.in_water(player.y) and not player.swimming:
|
||||||
cam_y = max(0, min(int(tony.y) - VIEW_H * 2 // 3, map_h_px - VIEW_H))
|
water_t += dt
|
||||||
|
if water_t >= WATER_DAMAGE_EVERY:
|
||||||
|
water_t = 0.0
|
||||||
|
hearts -= 1
|
||||||
|
if hearts <= 0:
|
||||||
|
lives -= 1
|
||||||
|
hearts = 4
|
||||||
|
player.x, player.y = spawn_px
|
||||||
|
player.vy = 0.0
|
||||||
|
if lives <= 0:
|
||||||
|
return 'map'
|
||||||
|
else:
|
||||||
|
water_t = 0.0
|
||||||
|
|
||||||
|
cam_x = max(0, min(int(player.x) - VIEW_W // 2, map_w_px - VIEW_W))
|
||||||
|
cam_y = max(0, min(int(player.y) - VIEW_H * 2 // 3, map_h_px - VIEW_H))
|
||||||
|
|
||||||
canvas.blit(level.surface, (0, 0), (cam_x, cam_y, VIEW_W, VIEW_H))
|
canvas.blit(level.surface, (0, 0), (cam_x, cam_y, VIEW_W, VIEW_H))
|
||||||
tony.draw(canvas, cam_x, cam_y)
|
# Tuer-Symbol (Kopf ueber dem Tuerfenster -- Charakterwechsel-Hinweis)
|
||||||
time_left = LEVEL_TIME_SECONDS - (time.monotonic() - start)
|
if door:
|
||||||
hud.draw(canvas, VIEW_H, cookies=0, score=0, hearts=4, lives=3, time_left=time_left)
|
canvas.blit(door_head, (door.x + door.w // 2 - 8 - cam_x, door.y - cam_y))
|
||||||
|
player.draw(canvas, cam_x, cam_y)
|
||||||
|
# Wasser: dunkles Overlay unterhalb der Wasserlinie (im Original ein
|
||||||
|
# VGA-Palette-Effekt, nur bei machine=vgaonly sichtbar -- Stefans Fund)
|
||||||
|
wy = info.get('water_y')
|
||||||
|
if wy is not None and wy - cam_y < VIEW_H:
|
||||||
|
top = max(0, wy - cam_y)
|
||||||
|
overlay = pygame.Surface((VIEW_W, VIEW_H - top), pygame.SRCALPHA)
|
||||||
|
overlay.fill(WATER_TINT)
|
||||||
|
canvas.blit(overlay, (0, top))
|
||||||
|
|
||||||
|
time_left = info['time'] - (time.monotonic() - start)
|
||||||
|
hud.draw(canvas, VIEW_H, player.d, cookies=0, score=0,
|
||||||
|
hearts=hearts, lives=lives, time_left=time_left)
|
||||||
|
|
||||||
intro._blit_centered(intro.build_letterboxed(canvas))
|
intro._blit_centered(intro.build_letterboxed(canvas))
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
|
|||||||
Reference in New Issue
Block a user