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
284 lines
11 KiB
Python
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()
|