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