mirror of
https://github.com/EDeev/pixel_gamble.git
synced 2026-10-07 23:49:29 +03:00
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы - меч, копьё и посох, 11 улучшений между волнами, опыт и уровни - меню, пауза, настройки, итоги партии и рекорд, русский и английский - мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt - асинхронный цикл: та же игра в браузере через pygbag - тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages - README на двух языках; исходная версия — релиз v1.0.0
170 lines
6.8 KiB
Python
170 lines
6.8 KiB
Python
"""Элементы интерфейса в экранных координатах (960×540): текст, панели, меню, сердечки, карточки."""
|
||
|
||
import pygame
|
||
|
||
from . import assets
|
||
from .settings import BLACK, GOLD, GREY, PANEL, PANEL_EDGE, PANEL_LIGHT, SCALE, SCREEN_H, SCREEN_W, WHITE
|
||
|
||
# размеры шрифта: Tiny5 чёткий на кратных 10
|
||
SMALL, MEDIUM, LARGE, HUGE = 20, 30, 40, 60
|
||
|
||
_text_cache = {}
|
||
|
||
|
||
def outlined(value, size, color, outline=(20, 16, 24), face="text"):
|
||
"""Текст с контуром — читается на любом фоне. Результат кэшируется."""
|
||
key = (value, size, color, outline, face)
|
||
image = _text_cache.get(key)
|
||
if image is None:
|
||
font = assets.font(size, face)
|
||
base = font.render(value, False, color)
|
||
edge = font.render(value, False, outline)
|
||
w = max(2, size // 10)
|
||
image = pygame.Surface((base.get_width() + 2 * w, base.get_height() + 2 * w), pygame.SRCALPHA)
|
||
for dx in (0, w, 2 * w):
|
||
for dy in (0, w, 2 * w):
|
||
image.blit(edge, (dx, dy))
|
||
image.blit(base, (w, w))
|
||
if len(_text_cache) > 600:
|
||
_text_cache.clear()
|
||
_text_cache[key] = image
|
||
return image
|
||
|
||
|
||
def text(surface, value, pos, size=SMALL, color=WHITE, anchor="topleft", face="text"):
|
||
image = outlined(str(value), size, color, face=face)
|
||
rect = image.get_rect(**{anchor: pos})
|
||
surface.blit(image, rect)
|
||
return rect
|
||
|
||
|
||
def big(image, factor=SCALE):
|
||
return pygame.transform.scale_by(image, factor)
|
||
|
||
|
||
def panel(surface, rect, color=PANEL, edge=PANEL_EDGE, light=PANEL_LIGHT):
|
||
rect = pygame.Rect(rect)
|
||
pygame.draw.rect(surface, edge, rect, border_radius=6)
|
||
pygame.draw.rect(surface, light, rect.inflate(-4, -4), border_radius=5)
|
||
pygame.draw.rect(surface, color, rect.inflate(-8, -8), border_radius=4)
|
||
|
||
|
||
def dim(surface, alpha=150):
|
||
shade = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
|
||
shade.fill((10, 8, 14, alpha))
|
||
surface.blit(shade, (0, 0))
|
||
|
||
|
||
class Menu:
|
||
"""Вертикальный список кнопок: стрелки/WS, Enter/пробел, мышь."""
|
||
|
||
def __init__(self, items, center, width=300, gap=40):
|
||
self.items = items # [(id, подпись)]
|
||
self.center = center
|
||
self.width, self.gap = width, gap
|
||
self.index = 0
|
||
|
||
def rects(self):
|
||
top = self.center[1] - (len(self.items) * self.gap) // 2
|
||
return [pygame.Rect(self.center[0] - self.width // 2, top + i * self.gap, self.width, self.gap - 8)
|
||
for i in range(len(self.items))]
|
||
|
||
def handle(self, event, audio):
|
||
"""Возвращает id выбранной кнопки или None."""
|
||
if event.type == pygame.KEYDOWN:
|
||
if event.key in (pygame.K_UP, pygame.K_w):
|
||
self.index = (self.index - 1) % len(self.items)
|
||
audio.play("menu_move")
|
||
elif event.key in (pygame.K_DOWN, pygame.K_s):
|
||
self.index = (self.index + 1) % len(self.items)
|
||
audio.play("menu_move")
|
||
elif event.key in (pygame.K_RETURN, pygame.K_SPACE, pygame.K_KP_ENTER):
|
||
audio.play("menu_accept")
|
||
return self.items[self.index][0]
|
||
elif event.type == pygame.MOUSEMOTION:
|
||
for i, rect in enumerate(self.rects()):
|
||
if rect.collidepoint(event.pos) and i != self.index:
|
||
self.index = i
|
||
audio.play("menu_move")
|
||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||
for i, rect in enumerate(self.rects()):
|
||
if rect.collidepoint(event.pos):
|
||
self.index = i
|
||
audio.play("menu_accept")
|
||
return self.items[i][0]
|
||
return None
|
||
|
||
def draw(self, surface):
|
||
for i, (rect, (_, label)) in enumerate(zip(self.rects(), self.items, strict=True)):
|
||
active = i == self.index
|
||
panel(surface, rect, color=(86, 58, 52) if active else PANEL)
|
||
text(surface, label, rect.center, SMALL, GOLD if active else WHITE, "center")
|
||
if active:
|
||
text(surface, ">", (rect.left - 12, rect.centery), SMALL, GOLD, "midright")
|
||
|
||
|
||
_hearts = None
|
||
|
||
|
||
def hearts(surface, player, pos):
|
||
global _hearts
|
||
if _hearts is None:
|
||
_hearts = [big(f) for f in assets.strip("ui/hearts.png", 16)] # пусто, 1/4, 1/2, 3/4, целое
|
||
x, y = pos
|
||
for i in range(player.max_hp // 4):
|
||
quarters = max(0, min(4, player.hp - i * 4))
|
||
surface.blit(_hearts[quarters], (x + i * 26, y))
|
||
|
||
|
||
def bar(surface, rect, ratio, color, back=(40, 30, 40)):
|
||
rect = pygame.Rect(rect)
|
||
pygame.draw.rect(surface, BLACK, rect)
|
||
inner = rect.inflate(-4, -4)
|
||
pygame.draw.rect(surface, back, inner)
|
||
if ratio > 0:
|
||
pygame.draw.rect(surface, color, (inner.x, inner.y, round(inner.width * min(1.0, ratio)), inner.height))
|
||
|
||
|
||
def weapon_slots(surface, player, pos):
|
||
x, y = pos
|
||
for i, weapon in enumerate(player.weapons):
|
||
rect = pygame.Rect(x + i * 44, y, 40, 52)
|
||
active = weapon == player.weapon
|
||
panel(surface, rect, color=(86, 58, 52) if active else PANEL, light=GOLD if active else PANEL_LIGHT)
|
||
icon = big(assets.image(f"items/{weapon}.png"))
|
||
surface.blit(icon, icon.get_rect(center=(rect.centerx, rect.centery - 2)))
|
||
text(surface, i + 1, (rect.right - 3, rect.bottom + 2), SMALL, GREY, "bottomright")
|
||
|
||
|
||
def card_rects(count=3):
|
||
w, h, gap = 232, 240, 24
|
||
left = SCREEN_W // 2 - (count * w + (count - 1) * gap) // 2
|
||
return [pygame.Rect(left + i * (w + gap), SCREEN_H // 2 - h // 2 + 20, w, h) for i in range(count)]
|
||
|
||
|
||
def cards(surface, keys, lang, selected):
|
||
for i, (key, rect) in enumerate(zip(keys, card_rects(len(keys)), strict=True)):
|
||
if i == selected:
|
||
rect = rect.move(0, -6)
|
||
active = i == selected
|
||
panel(surface, rect, color=(86, 58, 52) if active else PANEL, light=GOLD if active else PANEL_LIGHT)
|
||
icon = big(assets.image(f"icons/{key}.png"), 3)
|
||
surface.blit(icon, icon.get_rect(center=(rect.centerx, rect.top + 64)))
|
||
title, desc = lang.upgrade(key)
|
||
text(surface, title, (rect.centerx, rect.top + 118), SMALL, GOLD, "midtop")
|
||
for j, line in enumerate(wrap(desc, 20)):
|
||
text(surface, line, (rect.centerx, rect.top + 152 + j * 22), SMALL, WHITE, "midtop")
|
||
text(surface, i + 1, (rect.centerx, rect.bottom - 8), SMALL, GREY, "midbottom")
|
||
|
||
|
||
def wrap(value, width):
|
||
words, lines, line = value.split(), [], ""
|
||
for word in words:
|
||
if len(line) + len(word) + 1 > width and line:
|
||
lines.append(line)
|
||
line = word
|
||
else:
|
||
line = f"{line} {word}".strip()
|
||
if line:
|
||
lines.append(line)
|
||
return lines
|