Echte GAMEBAR.PCC als HUD, pixelgenaue Slopes, Step-up, Wasser-Grund

Stefans dritte Feedback-Runde:
- HUD nutzt jetzt die ECHTE GAMEBAR.PCC aus PCKELL.DAT (Stefans Fund):
  Kellogg's-Leiste, Boxrahmen und Uhr-":" kommen aus der Original-Grafik;
  weisse Box-Innenflaechen werden beim Laden dunkelrot gefuellt (macht die
  Original-Engine zur Laufzeit ebenso). Inhalte an den gemessenen Box-
  Positionen; HUD_H=25 (Grafikhoehe), VIEW_H=175.
- Slopes PIXELGENAU: Laufflaeche = oberster Nicht-Himmel-Pixel der Tile-
  Grafik pro Spalte (die Haenge sind ~1:2, nicht 45 Grad -- Ursache des
  restlichen Schwebens). Sky-Index vom haeufigsten Luft-Tile. Reine
  Himmel-Spalten tragen nicht.
- STEP_UP=6: Mini-Stufen (Plateau-Kante ueber Hangende) werden beim Laufen
  ueberwunden; hohe Waende blocken weiterhin.
- Unterste Map-Zeile immer solide (Grafik-Bodenstreifen ohne ctypes) --
  kein Steckenbleiben mehr beim Tieftauchen.
- Smacks-Steh-Frames 12/13 statt 0/1 (Anhalte-Sprite-Bug).

Verifiziert: Slope-Drop exakt auf Grafikkante, Plateau-Aufstieg, Wand-
Integritaet, Smacks-Idle, --once exit 0, E2E rc=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 11:39:21 +02:00
co-authored by Claude Fable 5
parent 7eb7f6f3b7
commit 6c3d6ea7a7
2 changed files with 126 additions and 64 deletions
+30
View File
@@ -1505,3 +1505,33 @@ Stefans Live-Test (5 Screenshots), alles umgesetzt:
Verifiziert per Physik-Simulationen (Wade-Ebene 686=water_y+14 + Sprung
vy=-345 von dort; Smacks-Ausstiegssprung vy=-360 an offener Wasserstelle;
Smacks unter der Bruecke stoesst korrekt an die Decke) + E2E rc=0 + --once ok.
## 25.07.2026 (5) -- dritte Feedback-Runde: echte GAMEBAR, Pixel-Slopes, Diverses
1. **GAMEBAR.PCC ist die echte HUD-Grafik** (320x25, aus PCKELL.DAT --
Stefans Fund via preview/png_out/GAMEBAR.png): Kellogg's-Leiste, alle 6
Boxrahmen, sogar der Uhr-":". Die weissen Box-Innenflaechen fuellt die
Engine zur Laufzeit dunkelrot -> wir ersetzen sie beim Laden (Pixel-Loop
rechts der Kellogg's-Leiste). HUD zeichnet nur noch Inhalte an die
gemessenen Box-Positionen (57/105/160/216/254/286). HUD_H jetzt 25,
VIEW_H 175. (Mit W1-Palette statt eigener wird die Grafik magenta --
getestet und verworfen.)
2. **Slopes jetzt PIXELGENAU**: pro Slope-Tile (ctype 16) und Spalte wird
der oberste Nicht-Himmel-Pixel der Tile-Grafik als Laufflaeche genommen
(slope_profiles; Sky-Index = haeufigster Pixel des haeufigsten Luft-Tiles
-- NICHT der ersten Luft-Zelle, das war Tile 0/Randfueller). Die Haenge
sind tatsaechlich ~1:2 geneigt, nicht 45 Grad -- daher das restliche
"Schweben" beim Drueberlaufen. Tiles mit reiner Himmel-Spalte tragen
nicht (die oberen der gestapelten Hang-Tiles inkl. Tile 1+ctype16).
Drop-Test: Landung exakt auf Grafikkante (626.0 = Kante 626).
3. **STEP_UP=6**: die untersten 6px der Hitbox blocken nicht an Waenden --
der 4px-Rest an der Plateau-Kante wird hochgestept (hohe Waende blocken
weiter ueber die oberen Hitbox-Zeilen, verifiziert).
4. **Wasser-Grund**: unterste Map-Zeile ist jetzt immer solide (der gruene
Bodenstreifen hat keine ctypes -- Smacks hing beim Tieftauchen in der
Grafik). Smacks-Grund jetzt 736 = Streifen-Oberkante.
5. **Smacks-Steh-Frames 12/13** (0/1 sind Schrittphasen -- "beim Anhalten
lief die Animation weiter").
Verifiziert: Slope-Drop pixelgenau, Plateau-Aufstieg + Weiterlauf, Wand-
Integritaet, Smacks idle=12, --once exit 0, E2E rc=0.
+96 -64
View File
@@ -28,8 +28,8 @@ import pygame
from . import intro
from .formats import parse_map, parse_ico, parse_bob, get_world_palette, get_bob_palette
VIEW_W, VIEW_H = 320, 176
HUD_H = 24
VIEW_W, VIEW_H = 320, 175
HUD_H = 25 # GAMEBAR.PCC ist exakt 320x25
TILE = 16
WALK_SPEED = 110.0
@@ -71,7 +71,9 @@ CHAR_DEFS = {
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,
# 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
@@ -160,8 +162,39 @@ class Level:
self.surface = self.surface.convert()
self.palette = palette
# PIXELGENAUE Slope-Laufflaechen (Stefans "steht in der Luft"-Bug,
# 2. Runde): statt einer 45-Grad-Annahme wird pro Slope-Tile (ctype
# 16) und Spalte der oberste Nicht-Himmel-Pixel der TILE-GRAFIK als
# Bodenhoehe genommen -- die Figur laeuft exakt auf der Graskante.
# Spalten, die komplett Himmel sind (z.B. im oberen von zwei
# gestapelten Hang-Tiles), tragen nicht (Profil=TILE -> kein Boden).
# Sky-Index: haeufigster Pixelwert des HAEUFIGSTEN Luft-Tiles (nicht
# der ersten Luft-Zelle -- die kann ein Randfueller-Tile sein).
from collections import Counter
air_tiles = Counter(t for t, ct in self.cells if ct == 0 and t < len(tiles))
sky = None
if air_tiles:
common_tile = air_tiles.most_common(1)[0][0]
sky = Counter(tiles[common_tile]).most_common(1)[0][0]
self.slope_profiles = {}
for tile, ct in self.cells:
if ct == SLOPE_CT and tile < len(tiles) and tile not in self.slope_profiles:
grid = tiles[tile]
prof = []
for x in range(TILE):
top = TILE
for y in range(TILE):
if grid[y * TILE + x] != sky:
top = y
break
prof.append(top)
self.slope_profiles[tile] = prof
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 - 1:
# Rand ist solide; die UNTERSTE Zeile ebenfalls (dort liegt der
# Grafik-Bodenstreifen ohne eigene ctype-Werte -- Stefans Bug
# "beim Tieftauchen haengt die Figur im Boden").
return SOLID_BIT
return self.cells[ty * self.width + tx][1]
@@ -169,19 +202,20 @@ class Level:
return bool(self.ctype(tx, ty) & SOLID_BIT)
def slope_surface(self, px, ty):
'''Pixelgenaue Bodenhoehe im Slope-Tile (ctype 16) an Pixel-x px --
oberster Nicht-Himmel-Pixel der Tile-Grafik (siehe slope_profiles).
None, wenn dort kein Slope-Tile liegt oder die Spalte reiner Himmel
ist (dann traegt erst ein tieferes Tile).'''
tx = int(px // TILE)
if self.ctype(tx, ty) != SLOPE_CT:
return None
fx = px - tx * TILE
rising_right = (self.ctype(tx + 1, ty - 1) == SLOPE_CT or
self.solid(tx + 1, ty - 1) or self.solid(tx + 1, ty))
rising_left = (self.ctype(tx - 1, ty - 1) == SLOPE_CT or
self.solid(tx - 1, ty - 1) or self.solid(tx - 1, ty))
if rising_right and not rising_left:
return (ty + 1) * TILE - 1 - fx
if rising_left and not rising_right:
return ty * TILE + fx
return (ty + 1) * TILE - 1 - fx
prof = self.slope_profiles.get(self.cells[ty * self.width + tx][0])
if prof is None:
return (ty + 1) * TILE - 1
top = prof[min(TILE - 1, max(0, int(px - tx * TILE)))]
if top >= TILE:
return None
return ty * TILE + top
def in_water(self, py):
wy = self.info.get('water_y')
@@ -278,16 +312,20 @@ class Player:
self.idle_t = 0.0
self.vy = min(self.vy + GRAVITY * dt, 480.0)
# X + Wandkollision
# X + Wandkollision. STEP_UP: die untersten Pixel der Hitbox blocken
# nicht -- kleine Stufen (z.B. der 4px-Rest an der Plateau-Kante ueber
# einem Hang) werden durchlaufen und vom Boden-Code hochgehoben.
STEP_UP = 6
nx = self.x + self.vx * dt
r = self.rect()
wall_bottom = (r.bottom - 1 - (STEP_UP if self.on_ground else 0)) // TILE
if self.vx > 0:
tx = int((nx + self.HITBOX_W / 2) // 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, wall_bottom + 1)):
nx = tx * TILE - self.HITBOX_W / 2 - 0.01
elif self.vx < 0:
tx = int((nx - self.HITBOX_W / 2) // 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, wall_bottom + 1)):
nx = (tx + 1) * TILE + self.HITBOX_W / 2 + 0.01
self.x = nx
@@ -377,18 +415,15 @@ class Player:
class Hud:
'''HUD-Balken nach Stefans Referenz-Screenshot: Kellogg's-Leiste,
Keks-Box MIT Item-Icon (ITE0-3 je Charakter), Item-Box, Score-Box,
Herz-Box (2x2-GRID), Charakter-Kopf (WECH, zeigt IMMER die aktuell
gewaehlte Figur) + Leben, Uhr.'''
'''HUD auf Basis der ECHTEN GAMEBAR.PCC (320x25, liegt im PCKELL.DAT --
Stefans Fund). Die Grafik liefert Kellogg's-Leiste, Boxrahmen und sogar
den ":" der Uhr; die weissen Box-Innenflaechen fuellt die Original-Engine
zur Laufzeit dunkelrot (wir ebenso, per Pixel-Ersetzung rechts der
Kellogg's-Leiste). Inhalte an den gemessenen Box-Positionen:
[57-99 Icon+Kekse][105-154 Item][160-210 Score][216-248 Herzen 2x2]
[254-280 Kopf+Leben][286-315 Uhr].'''
# Farben nach Stefans Original-HUD-Screenshot: HELLE (creme-weisse)
# Boxrahmen auf dunkelrotem Grund -- nicht das satte Gold der ersten
# Version.
FRAME = (244, 232, 200)
RED = (146, 10, 10)
DARK = (80, 4, 4)
WHITE = (248, 248, 248)
BOX_FILL = (146, 10, 10)
def __init__(self, assets):
from .menu import BitmapFont, _mapping_from_rows
@@ -397,13 +432,25 @@ class Hud:
palette = get_bob_palette(container.entries, 'GAMEBAR')
self.icons = _frames_to_surfaces(frames, palette)
self.heart_full = self.icons[10]
self.heart_empty = self.icons[11]
self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7,
_mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']),
spacing=8)
logo = assets.pcc_surface('KELLOGGS').subsurface((36, 26, 252, 66))
self.kellogg = pygame.transform.smoothscale(logo, (50, 13))
# Item-Icons (Cerealien) + Charakter-Koepfe klein
# GAMEBAR.PCC vorbereiten: Innenflaechen der 6 Boxen dunkelrot,
# der Grafik-":" (Uhr-Box) gelb wie die Ziffern.
bar = assets.pcc_surface('GAMEBAR').convert()
import pygame.surfarray as sa
arr = sa.pixels3d(bar)
w = arr.shape[0]
for x in range(56, w):
col = arr[x]
for y in range(col.shape[0]):
r, g, b = col[y]
if r > 200 and g > 200 and b > 200:
col[y] = self.BOX_FILL
elif 292 <= x <= 302 and r > 150 and g < 100:
col[y] = (255, 222, 0) # ":"-Punkte der Uhr
del arr
self.bar = bar
self.item_icons = {n: intro.make_border_bg_transparent(assets.pcc_surface(n))
for n in ('ITE0', 'ITE1', 'ITE2', 'ITE3')}
self.heads_small = []
@@ -411,47 +458,32 @@ class Hud:
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):
pygame.draw.rect(canvas, self.FRAME, (x - 1, y - 1, w + 2, h + 2),
border_radius=3)
pygame.draw.rect(canvas, self.RED, (x, y, w, h), border_radius=2)
def _text(self, canvas, x, y, text):
canvas.blit(self.font.render(text, outline=(0, 0, 0)), (x, y))
def draw(self, canvas, y0, char_def, cookies, score, hearts, lives, time_left):
'''Layout nach Stefans Original-HUD-Screenshot (Positionen /2 aufs
320er-Raster gemessen): [Kellogg's][Icon+Keks][Item][Score][Herz 2x2]
[Kopf+Leben][Uhr], Boxen fast HUD-hoch mit hellem Rahmen.'''
pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H))
box_y, box_h = y0 + 3, 18
canvas.blit(self.bar, (0, y0))
text_y = y0 + 9
# Kellogg's-Leiste (weiss, abgerundet)
pygame.draw.rect(canvas, self.WHITE, (2, y0 + 2, 60, HUD_H - 4), border_radius=7)
canvas.blit(self.kellogg, (7, y0 + 6))
# Keks-Box: Cerealien-Icon (16x16, volle Groesse) + Zaehler
self._box(canvas, 66, box_y, 48, box_h)
# Box 1: Cerealien-Icon des aktiven Charakters + Keks-Zaehler
icon = self.item_icons.get(char_def['item_icon'])
if icon:
canvas.blit(icon, (68, y0 + 4))
self._text(canvas, 87, text_y, f'{cookies:03d}')
# Item-Box (leer)
self._box(canvas, 118, box_y, 38, box_h)
# Score
self._box(canvas, 160, box_y, 56, box_h)
self._text(canvas, 164, text_y, f'{score:06d}')
# Herz-Box: 2x2-Grid; leere Plaetze bleiben dunkel (wie im Original)
self._box(canvas, 220, box_y, 24, box_h)
canvas.blit(icon, (59, y0 + 4))
self._text(canvas, 76, text_y, f'{cookies:03d}')
# Box 2 (105-154): Item -- noch leer
# Box 3: Score
self._text(canvas, 161, text_y, f'{score:06d}')
# Box 4: Herzen 2x2 (leere Plaetze bleiben dunkel)
for i in range(min(hearts, 4)):
canvas.blit(self.heart_full, (222 + (i % 2) * 10, y0 + 4 + (i // 2) * 9))
# Charakter-Kopf (zeigt IMMER die gewaehlte Figur) + Leben
self._box(canvas, 248, box_y, 32, box_h)
canvas.blit(self.heads_small[char_def['head_index']], (249, y0 + 4))
self._text(canvas, 269, text_y, str(lives))
# Uhr
canvas.blit(self.heart_full, (221 + (i % 2) * 11, y0 + 4 + (i // 2) * 9))
# Box 5: Charakter-Kopf (immer die gewaehlte Figur) + Leben
canvas.blit(self.heads_small[char_def['head_index']], (254, y0 + 4))
self._text(canvas, 272, text_y, str(lives))
# Box 6: Uhr (der Grafik-":" ist rot auf dunkelrot kaum sichtbar --
# eigener gelber ":" per Font drueber)
m, s = divmod(max(0, int(time_left)), 60)
self._box(canvas, 284, box_y, 34, box_h)
self._text(canvas, 287, text_y, f'{m}:{s:02d}')
self._text(canvas, 288, text_y, str(m))
self._text(canvas, 294, text_y, ':')
self._text(canvas, 301, text_y, f'{s:02d}')
def run_character_select(clock, assets, available, current):