1
0
Fork 0
mirror of https://github.com/EDeev/pixel_gamble.git synced 2026-10-07 23:49:29 +03:00
pixel_gamble/game/pixelgamble/app.py
Egor Deev 4ecb9257c8 Pixel Gamble 2.0: законченная игра на ресурсах Ninja Adventure
- 10 волн, 5 видов врагов со своим поведением и босс в три фазы
- меч, копьё и посох, 11 улучшений между волнами, опыт и уровни
- меню, пауза, настройки, итоги партии и рекорд, русский и английский
- мир 480×270 с масштабированием, интерфейс в 960×540, скорости от dt
- асинхронный цикл: та же игра в браузере через pygbag
- тесты без окна, автоигрок, CI, релизы exe/Linux, GitHub Pages
- README на двух языках; исходная версия — релиз v1.0.0
2026-10-07 12:29:41 +00:00

284 lines
11 KiB
Python

"""Окно, главный цикл и экраны: меню, настройки, управление, итоги партии."""
import math
import pygame
from . import assets, save, ui
from .game import Game
from .i18n import Lang
from .settings import FPS, GOLD, GREY, HEIGHT, RED, SCREEN_H, SCREEN_W, TITLE, VERSION, WAVES_TOTAL, WEB, WHITE, WIDTH
from .world import World
class Backdrop:
"""Арена, медленно проплывающая за меню."""
def __init__(self):
self.world = World()
self.t = 0.0
def draw(self, view, dt):
self.t += dt
max_x, max_y = self.world.width - WIDTH, self.world.height - HEIGHT
ox = round(max_x / 2 + math.sin(self.t * 0.07) * max_x / 2)
oy = round(max_y / 2 + math.sin(self.t * 0.05) * max_y / 2)
view.blit(self.world.floor, (-ox, -oy))
for prop in sorted(self.world.props, key=lambda p: p.sort_y):
view.blit(prop.image, prop.rect.move(-ox, -oy))
ui.dim(view, 110)
class Scene:
"""Экран: мир рисуется в маленький view, интерфейс — поверх на screen."""
def __init__(self, app):
self.app = app
def handle(self, event):
pass
def update(self, dt):
pass
def draw_world(self, view, dt):
self.app.backdrop.draw(view, dt)
def draw_ui(self, screen):
pass
class MenuScene(Scene):
def __init__(self, app):
super().__init__(app)
items = [("play", app.lang("play")), ("controls", app.lang("controls")), ("settings", app.lang("settings"))]
if not WEB:
items.append(("quit", app.lang("quit")))
self.menu = ui.Menu(items, (SCREEN_W // 2, 344))
app.audio.music("menu")
def handle(self, event):
choice = self.menu.handle(event, self.app.audio)
if choice == "play":
self.app.new_game()
elif choice == "controls":
self.app.scene = ControlsScene(self.app)
elif choice == "settings":
self.app.scene = SettingsScene(self.app)
elif choice == "quit":
self.app.running = False
def draw_ui(self, screen):
lang = self.app.lang
bob = round(math.sin(self.app.backdrop.t * 2) * 4)
title = ui.text(screen, "PIXEL GAMBLE", (SCREEN_W // 2, 112 + bob), 64, GOLD, "center", face="title")
ui.text(screen, lang("subtitle"), (SCREEN_W // 2, 176), ui.SMALL, WHITE, "center")
hero = ui.big(assets.image("chars/player_face.png"))
screen.blit(hero, hero.get_rect(midright=(title.left - 24, 112 - bob)))
boss = ui.big(assets.image("chars/boss_face.png"))
screen.blit(boss, boss.get_rect(midleft=(title.right + 24, 112 + bob)))
if self.app.data["best"]:
ui.text(screen, lang("best", score=self.app.data["best"]), (SCREEN_W // 2, 216), ui.SMALL, GOLD, "center")
self.menu.draw(screen)
ui.text(screen, lang("credits"), (SCREEN_W // 2, SCREEN_H - 8), ui.SMALL, GREY, "midbottom")
ui.text(screen, f"v{VERSION}", (SCREEN_W - 8, SCREEN_H - 8), ui.SMALL, GREY, "bottomright")
class ControlsScene(Scene):
def __init__(self, app):
super().__init__(app)
self.menu = ui.Menu([("back", app.lang("back"))], (SCREEN_W // 2, SCREEN_H - 56))
def handle(self, event):
back = event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
if self.menu.handle(event, self.app.audio) == "back" or back:
self.app.scene = MenuScene(self.app)
def draw_ui(self, screen):
rect = pygame.Rect(140, 36, SCREEN_W - 280, SCREEN_H - 120)
ui.panel(screen, rect)
ui.text(screen, self.app.lang("controls"), (SCREEN_W // 2, 70), ui.LARGE, GOLD, "center")
for i, line in enumerate(self.app.lang("controls_text")):
ui.text(screen, line, (rect.left + 36, 104 + i * 30), ui.SMALL, WHITE)
self.menu.draw(screen)
class SettingsScene(Scene):
def __init__(self, app):
super().__init__(app)
self.build()
def build(self, index=0):
lang, data = self.app.lang, self.app.data
items = [("music", lang("music", value=round(data["music"] * 100))),
("sounds", lang("sounds", value=round(data["sounds"] * 100))),
("lang", lang("language"))]
if not WEB:
items.append(("fullscreen", lang("fullscreen", value=lang("on") if data["fullscreen"] else lang("off"))))
items.append(("back", lang("back")))
self.menu = ui.Menu(items, (SCREEN_W // 2, 300), width=380)
self.menu.index = index
def change(self, key, step):
data = self.app.data
if key in ("music", "sounds"):
data[key] = round(min(1.0, max(0.0, data[key] + 0.1 * step)), 1)
self.app.audio.set_volumes(data["music"], data["sounds"])
self.app.audio.play("menu_move")
elif key == "lang":
self.app.lang.toggle()
data["lang"] = self.app.lang.code
elif key == "fullscreen":
self.app.toggle_fullscreen()
save.store(data)
self.build(self.menu.index)
def handle(self, event):
key = self.menu.items[self.menu.index][0]
if event.type == pygame.KEYDOWN and event.key in (pygame.K_LEFT, pygame.K_a, pygame.K_RIGHT, pygame.K_d):
if key != "back":
self.change(key, -1 if event.key in (pygame.K_LEFT, pygame.K_a) else 1)
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
self.app.scene = MenuScene(self.app)
return
choice = self.menu.handle(event, self.app.audio)
if choice == "back":
self.app.scene = MenuScene(self.app)
elif choice in ("music", "sounds"):
# Enter/клик по громкости — шаг вверх по кругу 0…100%
if self.app.data[choice] >= 1.0:
self.app.data[choice] = -0.1
self.change(choice, 1)
elif choice:
self.change(choice, 1)
def draw_ui(self, screen):
ui.text(screen, self.app.lang("settings"), (SCREEN_W // 2, 140), ui.LARGE, GOLD, "center")
self.menu.draw(screen)
ui.text(screen, "< >", (SCREEN_W // 2, SCREEN_H - 60), ui.SMALL, GREY, "center")
class PlayScene(Scene):
def __init__(self, app, game):
super().__init__(app)
self.game = game
def handle(self, event):
self.game.handle(event)
def update(self, dt):
self.game.update(dt, self.app.bot.controls(self.game) if self.app.bot else None)
def draw_world(self, view, dt):
self.game.draw_world(view, dt)
def draw_ui(self, screen):
self.game.draw_ui(screen)
class ResultsScene(Scene):
def __init__(self, app, game):
super().__init__(app)
self.victory = game.result == "victory"
self.record = game.score > app.data["best"]
if self.record:
app.data["best"] = game.score
save.store(app.data)
minutes, seconds = divmod(int(game.time), 60)
lang = app.lang
self.rows = [(lang("stat_wave"), f"{game.wave}/{WAVES_TOTAL}"), (lang("stat_kills"), game.kills),
(lang("stat_level"), game.player.level), (lang("stat_time"), f"{minutes}:{seconds:02d}"),
(lang("stat_score"), game.score)]
self.menu = ui.Menu([("again", lang("again")), ("to_menu", lang("to_menu"))], (SCREEN_W // 2, 444))
app.audio.music("menu")
def handle(self, event):
choice = self.menu.handle(event, self.app.audio)
if choice == "again":
self.app.new_game()
elif choice == "to_menu":
self.app.to_menu()
def draw_ui(self, screen):
lang = self.app.lang
title = lang("victory") if self.victory else lang("game_over")
ui.text(screen, title, (SCREEN_W // 2, 72), ui.HUGE, GOLD if self.victory else RED, "center")
rect = pygame.Rect(SCREEN_W // 2 - 220, 124, 440, 240)
ui.panel(screen, rect)
for i, (label, value) in enumerate(self.rows):
y = rect.top + 24 + i * 40
ui.text(screen, label, (rect.left + 28, y), ui.SMALL, WHITE)
ui.text(screen, value, (rect.right - 28, y), ui.SMALL, GOLD, "topright")
if self.record:
ui.text(screen, lang("new_record"), (SCREEN_W // 2, rect.bottom + 20), ui.SMALL, GOLD, "center")
self.menu.draw(screen)
class App:
def __init__(self, bot=None):
pygame.init()
try:
pygame.mixer.init()
except (pygame.error, NotImplementedError): # нет звуковой карты или SDL_mixer
pass
self.data = save.load()
self.bot = bot
flags = 0 if WEB else pygame.SCALED | pygame.RESIZABLE
try:
self.screen = pygame.display.set_mode((SCREEN_W, SCREEN_H), flags)
except pygame.error:
self.screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
self.view = pygame.Surface((WIDTH, HEIGHT)).convert()
pygame.display.set_caption(TITLE)
pygame.display.set_icon(assets.image("chars/player_face.png"))
if self.data["fullscreen"] and not WEB and not bot:
pygame.display.toggle_fullscreen()
self.clock = pygame.time.Clock()
self.audio = assets.Audio(self.data["music"], self.data["sounds"])
self.lang = Lang(self.data["lang"])
self.backdrop = Backdrop()
self.running = True
self.scene = MenuScene(self)
self.game = None
def new_game(self):
self.game = Game(self, rng=self.bot.rng if self.bot else None)
self.scene = PlayScene(self, self.game)
def to_menu(self):
self.game = None
self.scene = MenuScene(self)
def show_results(self, game):
self.scene = ResultsScene(self, game)
def toggle_fullscreen(self):
if WEB:
return
try:
pygame.display.toggle_fullscreen()
self.data["fullscreen"] = not self.data["fullscreen"]
except pygame.error:
pass
def frame(self, dt=None):
if dt is None:
dt = min(self.clock.tick(FPS) / 1000, 1 / 20)
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN and event.key == pygame.K_F11:
self.toggle_fullscreen()
save.store(self.data)
elif event.type in (pygame.WINDOWFOCUSLOST,) and isinstance(self.scene, PlayScene) and not self.bot:
self.game.paused = self.game.paused or not self.game.result
else:
self.scene.handle(event)
if self.bot:
self.bot.drive(self)
self.scene.update(dt)
self.scene.draw_world(self.view, dt)
pygame.transform.scale(self.view, self.screen.get_size(), self.screen)
self.scene.draw_ui(self.screen)
pygame.display.flip()