From 6d121a965299ee69a1a9ca5309457c38a8f62554 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 14:20:04 +0200 Subject: [PATCH] feat(render): hud overlay and audio backend --- game/render/audioBackend.py | 71 +++++++++++++++++++++++++++++++++++++ game/render/hud.py | 51 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 game/render/audioBackend.py create mode 100644 game/render/hud.py diff --git a/game/render/audioBackend.py b/game/render/audioBackend.py new file mode 100644 index 0000000..d0f95df --- /dev/null +++ b/game/render/audioBackend.py @@ -0,0 +1,71 @@ +import array +import math + +import pygame + +toneTable = { + "jump": (660, 90), + "doubleJump": (820, 90), + "dash": (300, 80), + "stomp": (200, 120), + "pickup": (900, 70), + "hurt": (160, 160), + "die": (110, 300), + "shoot": (420, 70), + "checkpoint": (1040, 120), + "levelComplete": (760, 260), + "spark": (520, 40), +} + + +def synthTone(frequency, milliseconds, volume=0.25): + mixerInfo = pygame.mixer.get_init() + if mixerInfo is None: + return None + sampleRate, _, channels = mixerInfo + frames = int(sampleRate * milliseconds / 1000.0) + amplitude = int(32767 * volume) + samples = array.array("h") + for index in range(frames): + envelope = 1.0 - index / frames + value = int(amplitude * envelope * (1.0 if math.sin(2.0 * math.pi * frequency * index / sampleRate) >= 0 else -1.0)) + for _ in range(max(1, channels)): + samples.append(value) + return pygame.mixer.Sound(buffer=samples.tobytes()) + + +class PygameAudio: + def __init__(self): + self.enabled = False + self.sounds = {} + try: + if pygame.mixer.get_init() is None: + pygame.mixer.init() + for name, (frequency, duration) in toneTable.items(): + sound = synthTone(frequency, duration) + if sound is not None: + self.sounds[name] = sound + self.enabled = bool(self.sounds) + except pygame.error: + self.enabled = False + + def sink(self, event): + if not self.enabled: + return + sound = self.sounds.get(event.name) + if sound is not None: + sound.play() + + +class NullAudio: + enabled = False + + def sink(self, event): + pass + + +def buildAudioSink(): + backend = PygameAudio() + if backend.enabled: + return backend.sink + return NullAudio().sink diff --git a/game/render/hud.py b/game/render/hud.py new file mode 100644 index 0000000..195b914 --- /dev/null +++ b/game/render/hud.py @@ -0,0 +1,51 @@ +import pygame + +from .theme import hudTextColor, hudShadowColor, heartFull, heartEmpty + + +class Hud: + def __init__(self): + self.font = pygame.font.Font(None, 22) + self.bigFont = pygame.font.Font(None, 48) + + def drawText(self, surf, text, pos, color=hudTextColor): + shadow = self.font.render(text, True, hudShadowColor) + label = self.font.render(text, True, color) + surf.blit(shadow, (pos[0] + 1, pos[1] + 1)) + surf.blit(label, pos) + + def drawCentered(self, surf, text, y, color=hudTextColor): + label = self.bigFont.render(text, True, color) + shadow = self.bigFont.render(text, True, hudShadowColor) + x = (surf.get_width() - label.get_width()) // 2 + surf.blit(shadow, (x + 2, y + 2)) + surf.blit(label, (x, y)) + + def drawHearts(self, surf, hp, maxHp): + for index in range(maxHp): + color = heartFull if index < hp else heartEmpty + x = 10 + index * 16 + y = 30 + pygame.draw.circle(surf, color, (x + 3, y + 3), 3) + pygame.draw.circle(surf, color, (x + 8, y + 3), 3) + pygame.draw.polygon(surf, color, [(x, y + 4), (x + 11, y + 4), (x + 5, y + 11)]) + + def draw(self, surf, world): + self.drawText(surf, "SCORE %06d" % world.score, (10, 8)) + self.drawText(surf, "COINS x%02d" % world.coins, (surf.get_width() - 130, 8)) + self.drawText(surf, "LIVES %d" % world.lives, (surf.get_width() - 130, 30)) + self.drawHearts(surf, world.player.hp, world.player.maxHp) + + def drawOverlay(self, surf, mode): + veil = pygame.Surface(surf.get_size(), pygame.SRCALPHA) + veil.fill((0, 0, 0, 140)) + surf.blit(veil, (0, 0)) + messages = { + "menu": "PIXEL PLATFORMER", + "paused": "PAUSED", + "gameover": "GAME OVER", + "complete": "LEVEL CLEAR", + "victory": "YOU WIN", + } + self.drawCentered(surf, messages.get(mode, ""), surf.get_height() // 2 - 40) + self.drawCentered(surf, "PRESS ENTER", surf.get_height() // 2 + 12)