971 lines
39 KiB
Python
971 lines
39 KiB
Python
"""
|
||
Пиксельная деревня - JRPG
|
||
Уютная маленькая JRPG с деревней, лесом и пошаговыми боями!
|
||
|
||
Требования: pip install pygame
|
||
Запуск: python jrpg_game.py
|
||
|
||
Управление:
|
||
Стрелки - Движение
|
||
E / Пробел - Взаимодействие / Подтвердить
|
||
Escape - Отмена / Назад
|
||
"""
|
||
|
||
import pygame
|
||
import sys
|
||
import random
|
||
import math
|
||
|
||
pygame.init()
|
||
|
||
# ── Константы ──────────────────────────────────────────────────────────────
|
||
W, H = 640, 480
|
||
TILE = 32
|
||
FPS = 60
|
||
|
||
# Яркая пиксельная палитра
|
||
C = {
|
||
"bg": (20, 20, 35),
|
||
"sky": (100, 180, 255),
|
||
"grass": (80, 200, 80),
|
||
"dark_grass":(60, 160, 60),
|
||
"path": (210, 180, 130),
|
||
"water": (60, 160, 220),
|
||
"tree": (40, 140, 40),
|
||
"tree_top": (50, 200, 60),
|
||
"trunk": (140, 90, 40),
|
||
"house_w": (240, 220, 200),
|
||
"house_r": (220, 60, 60),
|
||
"store_w": (200, 220, 255),
|
||
"store_r": (60, 100, 220),
|
||
"door": (140, 80, 30),
|
||
"window": (180, 230, 255),
|
||
"sign": (240, 200, 100),
|
||
"white": (255, 255, 255),
|
||
"black": (0, 0, 0),
|
||
"gray": (160, 160, 160),
|
||
"dark": (30, 30, 50),
|
||
"panel": (20, 20, 50),
|
||
"panel2": (40, 40, 80),
|
||
"border": (120, 100, 200),
|
||
"gold": (255, 210, 50),
|
||
"red": (240, 60, 60),
|
||
"green": (80, 220, 100),
|
||
"blue": (80, 160, 240),
|
||
"pink": (255, 140, 200),
|
||
"purple": (180, 100, 255),
|
||
"yellow": (255, 240, 80),
|
||
"orange": (255, 160, 40),
|
||
"hp_red": (220, 50, 50),
|
||
"mp_blue": (50, 100, 220),
|
||
"xp_green": (50, 200, 80),
|
||
}
|
||
|
||
screen = pygame.display.set_mode((W, H), pygame.SCALED | pygame.RESIZABLE)
|
||
pygame.display.set_caption("Пиксельная деревня")
|
||
clock = pygame.time.Clock()
|
||
|
||
# Шрифты с поддержкой кириллицы
|
||
FONT_PATHS = [
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
|
||
"/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf",
|
||
"/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf",
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||
]
|
||
FONT_PATHS_REGULAR = [
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||
"/usr/share/fonts/truetype/freefont/FreeMono.ttf",
|
||
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
|
||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||
]
|
||
|
||
def load_font(paths, size):
|
||
for p in paths:
|
||
try:
|
||
return pygame.font.Font(p, size)
|
||
except:
|
||
pass
|
||
return pygame.font.SysFont("dejavusansmono", size, bold=True)
|
||
|
||
font_lg = load_font(FONT_PATHS, 20)
|
||
font_md = load_font(FONT_PATHS, 15)
|
||
font_sm = load_font(FONT_PATHS_REGULAR, 12)
|
||
|
||
|
||
# ── Вспомогательные функции ────────────────────────────────────────────────
|
||
def txt(surface, text, x, y, color, fnt=None):
|
||
fnt = fnt or font_md
|
||
surface.blit(fnt.render(str(text), True, color), (x, y))
|
||
|
||
def draw_bar(surf, x, y, w, h, val, mx, color, bg=(60,60,60)):
|
||
pygame.draw.rect(surf, bg, (x, y, w, h))
|
||
fill = int(w * max(0, val) / max(1, mx))
|
||
pygame.draw.rect(surf, color, (x, y, fill, h))
|
||
pygame.draw.rect(surf, C["white"], (x, y, w, h), 1)
|
||
|
||
def panel(surf, x, y, w, h, border=True):
|
||
pygame.draw.rect(surf, C["panel"], (x, y, w, h))
|
||
if border:
|
||
pygame.draw.rect(surf, C["border"], (x, y, w, h), 2)
|
||
|
||
def flash_color(base, t, amp=40):
|
||
v = int(amp * abs(math.sin(t * 4)))
|
||
return tuple(min(255, c + v) for c in base)
|
||
|
||
|
||
# ── Рисование пиксель-арта ──────────────────────────────────────────────────
|
||
def draw_tree(surf, x, y):
|
||
pygame.draw.rect(surf, C["trunk"], (x+12, y+28, 8, 12))
|
||
pygame.draw.polygon(surf, C["tree"], [(x+16,y),(x+2,y+28),(x+30,y+28)])
|
||
pygame.draw.polygon(surf, C["tree_top"], [(x+16,y+4),(x+4,y+24),(x+28,y+24)])
|
||
|
||
def draw_house(surf, x, y, color_w=None, color_r=None):
|
||
cw = color_w or C["house_w"]
|
||
cr = color_r or C["house_r"]
|
||
pygame.draw.rect(surf, cw, (x, y+16, 48, 32))
|
||
pygame.draw.polygon(surf, cr, [(x,y+16),(x+24,y),(x+48,y+16)])
|
||
pygame.draw.rect(surf, C["door"], (x+18, y+32, 12, 16))
|
||
pygame.draw.rect(surf, C["window"], (x+4, y+22, 10, 10))
|
||
pygame.draw.rect(surf, C["window"], (x+34, y+22, 10, 10))
|
||
|
||
def draw_store(surf, x, y):
|
||
draw_house(surf, x, y, C["store_w"], C["store_r"])
|
||
pygame.draw.rect(surf, C["sign"], (x+4, y+12, 40, 9))
|
||
txt(surf, "ЛАВКА", x+5, y+11, C["dark"], font_sm)
|
||
|
||
def draw_player(surf, x, y, frame=0, facing=0):
|
||
bob = int(math.sin(frame * 0.3) * 2)
|
||
bx, by = x, y + bob
|
||
pygame.draw.rect(surf, C["blue"], (bx+5, by+10, 12, 12))
|
||
pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12))
|
||
pygame.draw.rect(surf, C["orange"], (bx+4, by+2, 14, 5))
|
||
ex = bx+7 if facing != 2 else bx+11
|
||
pygame.draw.rect(surf, C["dark"], (ex, by+6, 2, 2))
|
||
pygame.draw.rect(surf, C["dark"], (ex+4, by+6, 2, 2))
|
||
lx = bx+5 + (2 if frame % 20 < 10 else -1)
|
||
pygame.draw.rect(surf, C["dark"], (lx, by+22, 4, 6))
|
||
pygame.draw.rect(surf, C["dark"], (lx+6, by+22, 4, 6))
|
||
|
||
def draw_npc_mitsuha(surf, x, y, frame=0):
|
||
bob = int(math.sin(frame * 0.2) * 1.5)
|
||
bx, by = x, y + bob
|
||
pygame.draw.rect(surf, C["pink"], (bx+5, by+10, 12, 12))
|
||
pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12))
|
||
pygame.draw.rect(surf, C["purple"], (bx+4, by+2, 14, 5))
|
||
pygame.draw.rect(surf, C["purple"], (bx+14, by+4, 4, 10))
|
||
pygame.draw.rect(surf, C["dark"], (bx+7, by+6, 2, 2))
|
||
pygame.draw.rect(surf, C["dark"], (bx+11, by+6, 2, 2))
|
||
pygame.draw.rect(surf, C["pink"], (bx+5, by+22, 4, 6))
|
||
pygame.draw.rect(surf, C["pink"], (bx+11, by+22, 4, 6))
|
||
|
||
def draw_slime(surf, x, y, frame=0, color=None):
|
||
c = color or C["green"]
|
||
bounce = int(abs(math.sin(frame * 0.15)) * 3)
|
||
bx, by = x, y - bounce
|
||
pygame.draw.ellipse(surf, c, (bx+2, by+10, 20, 16))
|
||
pygame.draw.ellipse(surf, tuple(min(255,v+40) for v in c), (bx+4, by+10, 16, 10))
|
||
pygame.draw.rect(surf, C["white"], (bx+6, by+13, 4, 4))
|
||
pygame.draw.rect(surf, C["white"], (bx+14, by+13, 4, 4))
|
||
pygame.draw.rect(surf, C["dark"], (bx+7, by+14, 2, 2))
|
||
pygame.draw.rect(surf, C["dark"], (bx+15, by+14, 2, 2))
|
||
|
||
def draw_fairy(surf, x, y, frame=0):
|
||
bob = int(math.sin(frame * 0.2) * 4)
|
||
bx, by = x, y + bob
|
||
wing_surf = pygame.Surface((24, 14), pygame.SRCALPHA)
|
||
pygame.draw.ellipse(wing_surf, (200,220,255,160), (0,0,10,10))
|
||
pygame.draw.ellipse(wing_surf, (200,220,255,160), (14,2,10,8))
|
||
surf.blit(wing_surf, (bx+4, by+8))
|
||
pygame.draw.circle(surf, C["yellow"], (bx+12, by+12), 7)
|
||
pygame.draw.circle(surf, C["white"], (bx+12, by+12), 5)
|
||
pygame.draw.rect(surf, C["dark"], (bx+10, by+10, 2, 2))
|
||
pygame.draw.rect(surf, C["dark"], (bx+13, by+10, 2, 2))
|
||
|
||
|
||
# ── Данные игры ────────────────────────────────────────────────────────────
|
||
ITEMS = {
|
||
"Зелье": {"desc": "Восст. 30 ОЗ", "price": 50, "effect": ("hp", 30)},
|
||
"Хай-зелье": {"desc": "Восст. 80 ОЗ", "price": 120, "effect": ("hp", 80)},
|
||
"Эфир": {"desc": "Восст. 20 ОМ", "price": 60, "effect": ("mp", 20)},
|
||
"Эликсир": {"desc": "Полное ОЗ", "price": 300, "effect": ("hp", 9999)},
|
||
}
|
||
|
||
MONSTERS = [
|
||
{"name": "Зел. слизень", "hp": 20, "mp": 0, "atk": 5, "def": 2, "xp": 10, "gold": 8, "color": C["green"]},
|
||
{"name": "Син. слизень", "hp": 28, "mp": 0, "atk": 6, "def": 3, "xp": 14, "gold": 10, "color": C["blue"]},
|
||
{"name": "Розовая фея", "hp": 18, "mp": 15, "atk": 8, "def": 1, "xp": 18, "gold": 14, "color": C["pink"]},
|
||
{"name": "Лесной огонёк","hp": 35, "mp": 20, "atk": 10, "def": 4, "xp": 25, "gold": 20, "color": C["purple"]},
|
||
]
|
||
|
||
MITSUHA_LINES = [
|
||
"Привет! Какой чудесный день, правда?",
|
||
"Говорят, в лесу живут\nмиленькие монстрики!",
|
||
"Будь осторожен, ладно?\nНо и повеселись тоже!",
|
||
"В лавке продают отличные зелья~",
|
||
"Как я люблю нашу деревню!",
|
||
"О! Ты сегодня выглядишь сильнее!",
|
||
]
|
||
|
||
HOME_LINES = [
|
||
"Уютный домик. Ты хорошо отдохнул!",
|
||
"Сладкие сны ждут тебя~",
|
||
"Дома и стены помогают!",
|
||
]
|
||
|
||
# ── Игрок ──────────────────────────────────────────────────────────────────
|
||
class Player:
|
||
def __init__(self):
|
||
self.name = "Герой"
|
||
self.x, self.y = 5, 6
|
||
self.hp, self.max_hp = 80, 80
|
||
self.mp, self.max_mp = 30, 30
|
||
self.atk = 15
|
||
self.dfn = 5
|
||
self.xp, self.xp_next = 0, 50
|
||
self.level = 1
|
||
self.gold = 100
|
||
self.inventory = {"Зелье": 2}
|
||
self.frame = 0
|
||
self.facing = 1
|
||
self.px = self.x * TILE
|
||
self.py = self.y * TILE
|
||
|
||
def gain_xp(self, amount):
|
||
msgs = []
|
||
self.xp += amount
|
||
while self.xp >= self.xp_next:
|
||
self.xp -= self.xp_next
|
||
self.level += 1
|
||
self.xp_next = int(self.xp_next * 1.4)
|
||
self.max_hp += 15
|
||
self.hp = self.max_hp
|
||
self.max_mp += 5
|
||
self.mp = self.max_mp
|
||
self.atk += 3
|
||
self.dfn += 1
|
||
msgs.append(f"Новый уровень! Ур.{self.level}!")
|
||
return msgs
|
||
|
||
def add_item(self, item):
|
||
self.inventory[item] = self.inventory.get(item, 0) + 1
|
||
|
||
def use_item(self, item):
|
||
if self.inventory.get(item, 0) <= 0:
|
||
return False, "Предметов нет!"
|
||
eff = ITEMS[item]["effect"]
|
||
self.inventory[item] -= 1
|
||
if self.inventory[item] == 0:
|
||
del self.inventory[item]
|
||
if eff[0] == "hp":
|
||
healed = min(eff[1], self.max_hp - self.hp)
|
||
self.hp = min(self.max_hp, self.hp + eff[1])
|
||
return True, f"Восст. {healed} ОЗ!"
|
||
elif eff[0] == "mp":
|
||
self.mp = min(self.max_mp, self.mp + eff[1])
|
||
return True, f"Восст. {eff[1]} ОМ!"
|
||
return False, "Ничего не произошло."
|
||
|
||
|
||
# ── Карты ──────────────────────────────────────────────────────────────────
|
||
TOWN_MAP = [
|
||
"WWWWWWWWWWWWWWWWWWWW",
|
||
"W..................W",
|
||
"W..H.......S.....tW",
|
||
"W..................W",
|
||
"W...........ttt...W",
|
||
"W....t............W",
|
||
"W..................W",
|
||
"W....P.....M......W",
|
||
"W..................W",
|
||
"W..........t......W",
|
||
"W...##.....t......W",
|
||
"W...##............W",
|
||
"W.................W",
|
||
"WWWWWWWWWFWWWWWWWWW",
|
||
]
|
||
FOREST_MAP = [
|
||
"TTTTTTTTTTTTTTTTTTTT",
|
||
"T.t....t....t.....T",
|
||
"T....t.....t......T",
|
||
"T.t........t....t.T",
|
||
"T.....t...........T",
|
||
"T....t...t........T",
|
||
"T.........t.......T",
|
||
"T...t.............T",
|
||
"T....t....t.......T",
|
||
"T.................T",
|
||
"T.....t...........T",
|
||
"T.....t...........T",
|
||
"T.................T",
|
||
"TTTTTTTTTTTTTTTTTTTT",
|
||
]
|
||
|
||
|
||
# ── Камера ─────────────────────────────────────────────────────────────────
|
||
class Camera:
|
||
def __init__(self):
|
||
self.ox = 0
|
||
self.oy = 0
|
||
|
||
def update(self, px, py, map_w, map_h):
|
||
self.ox = px - W // 2 + TILE // 2
|
||
self.oy = py - H // 2 + TILE // 2
|
||
self.ox = max(0, min(self.ox, map_w * TILE - W))
|
||
self.oy = max(0, min(self.oy, map_h * TILE - H))
|
||
|
||
def world_to_screen(self, wx, wy):
|
||
return wx - self.ox, wy - self.oy
|
||
|
||
|
||
# ── Система боя ────────────────────────────────────────────────────────────
|
||
class Battle:
|
||
def __init__(self, player, monster_data):
|
||
self.player = player
|
||
self.mon = dict(monster_data)
|
||
self.mon_hp = self.mon["hp"]
|
||
self.mon_max_hp = self.mon["hp"]
|
||
self.state = "player_turn"
|
||
self.log = [f"Появился {self.mon['name']}!"]
|
||
self.frame = 0
|
||
self.selected = 0
|
||
self.menu = "main"
|
||
self.item_list = []
|
||
self.shake_player = 0
|
||
self.shake_enemy = 0
|
||
self.result_msgs = []
|
||
|
||
def add_log(self, msg):
|
||
self.log.append(msg)
|
||
if len(self.log) > 4:
|
||
self.log.pop(0)
|
||
|
||
def player_attack(self):
|
||
dmg = max(1, self.player.atk - self.mon["def"] + random.randint(-3, 3))
|
||
self.mon_hp -= dmg
|
||
self.add_log(f"Ты нанёс {dmg} урона!")
|
||
self.shake_enemy = 10
|
||
if self.mon_hp <= 0:
|
||
self.mon_hp = 0
|
||
self.end_battle(True)
|
||
else:
|
||
self.state = "enemy_turn"
|
||
|
||
def player_magic(self):
|
||
if self.player.mp < 8:
|
||
self.add_log("Мало маны!")
|
||
return
|
||
self.player.mp -= 8
|
||
dmg = max(1, self.player.atk * 2 - self.mon["def"] + random.randint(-2, 5))
|
||
self.mon_hp -= dmg
|
||
self.add_log(f"Магия! {dmg} урона! (-8 ОМ)")
|
||
self.shake_enemy = 12
|
||
if self.mon_hp <= 0:
|
||
self.mon_hp = 0
|
||
self.end_battle(True)
|
||
else:
|
||
self.state = "enemy_turn"
|
||
|
||
def player_defend(self):
|
||
self.add_log("Ты принял защитную стойку!")
|
||
self.player.dfn += 5
|
||
self._defending = True
|
||
self.state = "enemy_turn"
|
||
|
||
def enemy_turn(self):
|
||
if self.state != "enemy_turn":
|
||
return
|
||
dmg = max(1, self.mon["atk"] - self.player.dfn + random.randint(-2, 3))
|
||
self.player.hp -= dmg
|
||
self.add_log(f"{self.mon['name']} нанёс {dmg} урона!")
|
||
self.shake_player = 10
|
||
if hasattr(self, "_defending"):
|
||
self.player.dfn -= 5
|
||
del self._defending
|
||
if self.player.hp <= 0:
|
||
self.player.hp = 0
|
||
self.end_battle(False)
|
||
else:
|
||
self.state = "player_turn"
|
||
|
||
def end_battle(self, won):
|
||
if won:
|
||
xp = self.mon["xp"]
|
||
gold = self.mon["gold"]
|
||
self.player.gold += gold
|
||
lvl_msgs = self.player.gain_xp(xp)
|
||
self.add_log(f"Победа! +{xp} ОП, +{gold} зол.")
|
||
for m in lvl_msgs:
|
||
self.add_log(m)
|
||
self.result_msgs = ["Победа!", f"+{xp} ОП +{gold} золота"] + lvl_msgs
|
||
self.state = "win"
|
||
else:
|
||
self.player.hp = self.player.max_hp // 4
|
||
self.add_log("Ты потерпел поражение...")
|
||
self.result_msgs = ["Ты упал в обморок!", "Очнулся дома."]
|
||
self.state = "lose"
|
||
|
||
def draw(self, surf):
|
||
self.frame += 1
|
||
for yy in range(H):
|
||
ratio = yy / H
|
||
r = int(20 + ratio * 20)
|
||
g = int(10 + ratio * 20)
|
||
b = int(40 + ratio * 30)
|
||
pygame.draw.line(surf, (r, g, b), (0, yy), (W, yy))
|
||
|
||
random.seed(42)
|
||
for _ in range(40):
|
||
sx = random.randint(0, W)
|
||
sy = random.randint(0, H // 2)
|
||
br = 150 + int(50 * math.sin(self.frame * 0.05 + sx))
|
||
pygame.draw.rect(surf, (br, br, br), (sx, sy, 2, 2))
|
||
random.seed()
|
||
|
||
pygame.draw.rect(surf, (60, 160, 60), (0, H-80, W, 80))
|
||
pygame.draw.rect(surf, (40, 120, 40), (0, H-82, W, 4))
|
||
|
||
ex = W * 3 // 4 - 24
|
||
ey = H // 2 - 40
|
||
esx = int(math.sin(self.shake_enemy * 0.8) * 5) if self.shake_enemy > 0 else 0
|
||
self.shake_enemy = max(0, self.shake_enemy - 1)
|
||
|
||
mon_name = self.mon["name"]
|
||
if "слизень" in mon_name:
|
||
c = self.mon["color"]
|
||
for _ in range(3):
|
||
bc = int(abs(math.sin(self.frame * 0.1 + _)) * 3)
|
||
pygame.draw.ellipse(surf, c, (ex+esx+10, ey+bc+20, 60, 48))
|
||
pygame.draw.ellipse(surf, tuple(min(255,v+60) for v in c), (ex+esx+16, ey+24, 46, 30))
|
||
pygame.draw.rect(surf, C["white"], (ex+esx+22, ey+32, 10, 10))
|
||
pygame.draw.rect(surf, C["white"], (ex+esx+42, ey+32, 10, 10))
|
||
pygame.draw.rect(surf, C["dark"], (ex+esx+24, ey+34, 6, 6))
|
||
pygame.draw.rect(surf, C["dark"], (ex+esx+44, ey+34, 6, 6))
|
||
elif "фея" in mon_name:
|
||
bob = int(math.sin(self.frame * 0.2) * 6)
|
||
pygame.draw.ellipse(surf, (200, 230, 255), (ex+esx-10, ey+bob+30, 30, 20))
|
||
pygame.draw.ellipse(surf, (200, 230, 255), (ex+esx+60, ey+bob+30, 30, 20))
|
||
pygame.draw.circle(surf, C["yellow"], (ex+esx+40, ey+bob+40), 22)
|
||
pygame.draw.circle(surf, C["white"], (ex+esx+40, ey+bob+40), 16)
|
||
pygame.draw.rect(surf, C["dark"], (ex+esx+32, ey+bob+34, 6, 6))
|
||
pygame.draw.rect(surf, C["dark"], (ex+esx+44, ey+bob+34, 6, 6))
|
||
elif "огонёк" in mon_name:
|
||
for ring in range(3):
|
||
r2 = 25 + ring * 10 + int(math.sin(self.frame * 0.1 + ring) * 4)
|
||
pygame.draw.circle(surf, C["purple"], (ex+esx+40, ey+40), r2, 2)
|
||
pygame.draw.circle(surf, C["purple"], (ex+esx+40, ey+40), 18)
|
||
pygame.draw.circle(surf, C["white"], (ex+esx+40, ey+40), 10)
|
||
|
||
draw_bar(surf, ex+esx+5, ey-12, 70, 8, self.mon_hp, self.mon_max_hp, C["hp_red"])
|
||
txt(surf, mon_name, ex+esx-20, ey-28, C["white"], font_sm)
|
||
|
||
psx = W // 4 - 12
|
||
psy = H // 2
|
||
pshake = int(math.sin(self.shake_player * 0.8) * 5) if self.shake_player > 0 else 0
|
||
self.shake_player = max(0, self.shake_player - 1)
|
||
draw_player(surf, psx + pshake, psy, self.frame, 3)
|
||
|
||
# Панель характеристик игрока
|
||
panel(surf, 10, H-130, 195, 120)
|
||
txt(surf, f"Ур.{self.player.level} {self.player.name}", 18, H-126, C["gold"])
|
||
draw_bar(surf, 18, H-108, 172, 10, self.player.hp, self.player.max_hp, C["hp_red"])
|
||
txt(surf, f"ОЗ {self.player.hp}/{self.player.max_hp}", 18, H-96, C["white"], font_sm)
|
||
draw_bar(surf, 18, H-82, 172, 10, self.player.mp, self.player.max_mp, C["mp_blue"])
|
||
txt(surf, f"ОМ {self.player.mp}/{self.player.max_mp}", 18, H-70, C["white"], font_sm)
|
||
draw_bar(surf, 18, H-56, 172, 8, self.player.xp, self.player.xp_next, C["xp_green"])
|
||
txt(surf, f"ОП {self.player.xp}/{self.player.xp_next}", 18, H-44, C["white"], font_sm)
|
||
|
||
# Журнал боя
|
||
panel(surf, W//2-160, H-130, 320, 70)
|
||
for i, line in enumerate(self.log[-3:]):
|
||
txt(surf, line, W//2-152, H-126 + i*22, C["white"], font_sm)
|
||
|
||
# Меню / результат
|
||
if self.state in ("win", "lose"):
|
||
panel(surf, W//2-130, H//2-60, 260, 130)
|
||
color = C["gold"] if self.state == "win" else C["red"]
|
||
txt(surf, self.result_msgs[0], W//2-90, H//2-50, color)
|
||
for i, m in enumerate(self.result_msgs[1:]):
|
||
txt(surf, m, W//2-110, H//2-20 + i*22, C["white"], font_sm)
|
||
txt(surf, "Нажми E чтобы продолжить", W//2-110, H//2+62, C["gray"], font_sm)
|
||
|
||
elif self.state == "player_turn":
|
||
if self.menu == "main":
|
||
panel(surf, W-215, H-130, 205, 120)
|
||
options = ["Атаковать", "Магия (8ОМ)", "Защититься", "Предметы"]
|
||
for i, opt in enumerate(options):
|
||
color = C["yellow"] if i == self.selected else C["white"]
|
||
prefix = "> " if i == self.selected else " "
|
||
txt(surf, prefix + opt, W-210, H-122 + i*26, color)
|
||
elif self.menu == "items":
|
||
panel(surf, W-230, H-150, 220, 140)
|
||
txt(surf, "Предметы:", W-222, H-146, C["gold"])
|
||
if not self.item_list:
|
||
txt(surf, "Нет предметов!", W-210, H-120, C["gray"])
|
||
else:
|
||
for i, (name, qty) in enumerate(self.item_list):
|
||
color = C["yellow"] if i == self.selected else C["white"]
|
||
prefix = "> " if i == self.selected else " "
|
||
txt(surf, f"{prefix}{name} x{qty}", W-222, H-120 + i*24, color, font_sm)
|
||
txt(surf, "ESC=Назад", W-222, H-18, C["gray"], font_sm)
|
||
|
||
def handle_input(self, event):
|
||
if event.type != pygame.KEYDOWN:
|
||
return None
|
||
|
||
if self.state in ("win", "lose"):
|
||
if event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
return self.state
|
||
return None
|
||
|
||
if self.state != "player_turn":
|
||
return None
|
||
|
||
if self.menu == "main":
|
||
if event.key == pygame.K_UP:
|
||
self.selected = (self.selected - 1) % 4
|
||
elif event.key == pygame.K_DOWN:
|
||
self.selected = (self.selected + 1) % 4
|
||
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
if self.selected == 0:
|
||
self.player_attack()
|
||
elif self.selected == 1:
|
||
self.player_magic()
|
||
elif self.selected == 2:
|
||
self.player_defend()
|
||
pygame.time.wait(400)
|
||
self.enemy_turn()
|
||
elif self.selected == 3:
|
||
self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS]
|
||
self.selected = 0
|
||
self.menu = "items"
|
||
elif self.menu == "items":
|
||
if event.key == pygame.K_ESCAPE:
|
||
self.menu = "main"
|
||
self.selected = 0
|
||
elif event.key == pygame.K_UP:
|
||
self.selected = max(0, self.selected - 1)
|
||
elif event.key == pygame.K_DOWN:
|
||
self.selected = min(len(self.item_list)-1, self.selected + 1)
|
||
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
if self.item_list:
|
||
name, _ = self.item_list[self.selected]
|
||
ok, msg = self.player.use_item(name)
|
||
self.add_log(msg)
|
||
self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS]
|
||
if ok:
|
||
self.menu = "main"
|
||
self.selected = 0
|
||
self.state = "enemy_turn"
|
||
|
||
if self.state == "enemy_turn":
|
||
pygame.time.wait(300)
|
||
self.enemy_turn()
|
||
|
||
return None
|
||
|
||
|
||
# ── Лавка ──────────────────────────────────────────────────────────────────
|
||
class Shop:
|
||
def __init__(self, player):
|
||
self.player = player
|
||
self.items = list(ITEMS.keys())
|
||
self.selected = 0
|
||
|
||
def draw(self, surf):
|
||
panel(surf, W//2-190, 55, 380, 370)
|
||
txt(surf, "= ЛАВКА ПРЕДМЕТОВ =", W//2-130, 65, C["gold"])
|
||
txt(surf, f"Золото: {self.player.gold}", W//2+80, 65, C["yellow"], font_sm)
|
||
pygame.draw.line(surf, C["border"], (W//2-185, 90), (W//2+185, 90), 1)
|
||
|
||
for i, name in enumerate(self.items):
|
||
info = ITEMS[name]
|
||
y = 100 + i * 58
|
||
color = C["yellow"] if i == self.selected else C["white"]
|
||
prefix = "> " if i == self.selected else " "
|
||
txt(surf, prefix + name, W//2-180, y, color)
|
||
txt(surf, info["desc"], W//2-165, y+22, C["gray"], font_sm)
|
||
txt(surf, f"{info['price']} зол.", W//2+90, y, C["gold"], font_sm)
|
||
|
||
txt(surf, "E=Купить ESC=Выйти", W//2-100, 382, C["gray"], font_sm)
|
||
|
||
def handle_input(self, event):
|
||
if event.type != pygame.KEYDOWN:
|
||
return False
|
||
if event.key == pygame.K_ESCAPE:
|
||
return True
|
||
if event.key == pygame.K_UP:
|
||
self.selected = (self.selected - 1) % len(self.items)
|
||
elif event.key == pygame.K_DOWN:
|
||
self.selected = (self.selected + 1) % len(self.items)
|
||
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
name = self.items[self.selected]
|
||
price = ITEMS[name]["price"]
|
||
if self.player.gold >= price:
|
||
self.player.gold -= price
|
||
self.player.add_item(name)
|
||
return False
|
||
|
||
|
||
# ── Диалог ─────────────────────────────────────────────────────────────────
|
||
class Dialogue:
|
||
def __init__(self, lines):
|
||
self.lines = lines if isinstance(lines, list) else [lines]
|
||
self.idx = 0
|
||
|
||
@property
|
||
def done(self):
|
||
return self.idx >= len(self.lines)
|
||
|
||
def advance(self):
|
||
self.idx += 1
|
||
|
||
def draw(self, surf):
|
||
if self.done:
|
||
return
|
||
panel(surf, 30, H-110, W-60, 100)
|
||
line = self.lines[self.idx]
|
||
for i, l in enumerate(line.split("\n")):
|
||
txt(surf, l, 46, H-100 + i*22, C["white"])
|
||
txt(surf, "V E", W-70, H-24, C["gray"], font_sm)
|
||
|
||
|
||
# ── Отрисовка карты ────────────────────────────────────────────────────────
|
||
def tile_walkable(tile):
|
||
return tile not in ("W", "t", "#", "T")
|
||
|
||
def render_map(surf, map_data, cam, frame):
|
||
rows = len(map_data)
|
||
for row in range(rows):
|
||
cols = len(map_data[row])
|
||
for col in range(cols):
|
||
tile = map_data[row][col]
|
||
wx = col * TILE
|
||
wy = row * TILE
|
||
sx, sy = cam.world_to_screen(wx, wy)
|
||
if sx > W+TILE or sy > H+TILE or sx < -TILE or sy < -TILE:
|
||
continue
|
||
|
||
grass_c = C["dark_grass"] if (row + col) % 2 == 0 else C["grass"]
|
||
pygame.draw.rect(surf, grass_c, (sx, sy, TILE, TILE))
|
||
|
||
if tile == "W":
|
||
pygame.draw.rect(surf, (100, 80, 60), (sx, sy, TILE, TILE))
|
||
pygame.draw.rect(surf, (80, 60, 40), (sx, sy, TILE, TILE), 2)
|
||
elif tile == "#":
|
||
wo = int(math.sin(frame * 0.05 + col * 0.5) * 2)
|
||
pygame.draw.rect(surf, C["water"], (sx, sy+wo, TILE, TILE))
|
||
pygame.draw.rect(surf, (100, 200, 255), (sx+4, sy+4+wo, TILE-8, 4), 1)
|
||
elif tile == "t":
|
||
draw_tree(surf, sx, sy - 8)
|
||
elif tile == "H":
|
||
draw_house(surf, sx-8, sy-16)
|
||
elif tile == "S":
|
||
draw_store(surf, sx-8, sy-16)
|
||
elif tile == "F":
|
||
pygame.draw.rect(surf, C["dark_grass"], (sx, sy, TILE, TILE))
|
||
pygame.draw.rect(surf, C["green"], (sx+4, sy+4, TILE-8, TILE-8), 2)
|
||
elif tile == "T":
|
||
pygame.draw.rect(surf, C["path"], (sx, sy, TILE, TILE))
|
||
|
||
|
||
# ── Основная игра ──────────────────────────────────────────────────────────
|
||
class Game:
|
||
TOWN = "town"
|
||
FOREST = "forest"
|
||
|
||
def __init__(self):
|
||
self.player = Player()
|
||
self.cam = Camera()
|
||
self.scene = self.TOWN
|
||
self.current_map = TOWN_MAP
|
||
|
||
self.dialogue = None
|
||
self.shop = None
|
||
self.battle = None
|
||
self.frame = 0
|
||
|
||
self.npc_frame = 0
|
||
self.message = None
|
||
|
||
self.mitsuha_dialogue_idx = 0
|
||
self.sparks = []
|
||
|
||
def show_message(self, text, duration=120):
|
||
self.message = (text, duration)
|
||
|
||
def add_spark(self, x, y, color=None):
|
||
c = color or random.choice([C["yellow"], C["white"], C["gold"], C["pink"]])
|
||
self.sparks.append({
|
||
"x": x, "y": y,
|
||
"vx": random.uniform(-1.5, 1.5),
|
||
"vy": random.uniform(-3, -0.5),
|
||
"life": 40, "color": c
|
||
})
|
||
|
||
def get_tile(self, tx, ty):
|
||
m = self.current_map
|
||
if ty < 0 or ty >= len(m) or tx < 0 or tx >= len(m[ty]):
|
||
return "W"
|
||
return m[ty][tx]
|
||
|
||
def try_move(self, dx, dy):
|
||
nx = self.player.x + dx
|
||
ny = self.player.y + dy
|
||
if not tile_walkable(self.get_tile(nx, ny)):
|
||
return
|
||
|
||
if self.scene == self.TOWN and self.get_tile(nx, ny) == "F":
|
||
self.scene = self.FOREST
|
||
self.current_map = FOREST_MAP
|
||
self.player.x, self.player.y = 10, 12
|
||
self.player.px = self.player.x * TILE
|
||
self.player.py = self.player.y * TILE
|
||
self.show_message("Ты вошёл в лес!")
|
||
return
|
||
if self.scene == self.FOREST and self.get_tile(nx, ny) == "T":
|
||
self.scene = self.TOWN
|
||
self.current_map = TOWN_MAP
|
||
self.player.x, self.player.y = 9, 12
|
||
self.player.px = self.player.x * TILE
|
||
self.player.py = self.player.y * TILE
|
||
self.show_message("Ты вернулся в деревню!")
|
||
return
|
||
|
||
self.player.x = nx
|
||
self.player.y = ny
|
||
self.player.px = nx * TILE
|
||
self.player.py = ny * TILE
|
||
|
||
if dx > 0: self.player.facing = 3
|
||
elif dx < 0: self.player.facing = 2
|
||
elif dy > 0: self.player.facing = 1
|
||
else: self.player.facing = 0
|
||
|
||
if self.scene == self.FOREST:
|
||
if random.random() < 0.2:
|
||
mon = random.choice(MONSTERS)
|
||
self.battle = Battle(self.player, mon)
|
||
|
||
def try_interact(self):
|
||
fx, fy = self.player.x, self.player.y
|
||
facing = self.player.facing
|
||
if facing == 0: fy -= 1
|
||
elif facing == 1: fy += 1
|
||
elif facing == 2: fx -= 1
|
||
elif facing == 3: fx += 1
|
||
|
||
tile = self.get_tile(fx, fy)
|
||
|
||
if self.scene == self.TOWN:
|
||
for row_i, row in enumerate(TOWN_MAP):
|
||
if "M" in row:
|
||
mx, my = row.index("M"), row_i
|
||
if abs(mx - self.player.x) + abs(my - self.player.y) <= 1:
|
||
line = MITSUHA_LINES[self.mitsuha_dialogue_idx % len(MITSUHA_LINES)]
|
||
self.mitsuha_dialogue_idx += 1
|
||
self.dialogue = Dialogue(line)
|
||
return
|
||
|
||
if tile == "H":
|
||
self.player.hp = self.player.max_hp
|
||
self.player.mp = self.player.max_mp
|
||
self.dialogue = Dialogue(random.choice(HOME_LINES) + "\n(ОЗ и ОМ восстановлены!)")
|
||
for _ in range(12):
|
||
self.add_spark(self.player.px + random.randint(-20, 20),
|
||
self.player.py + random.randint(-30, 10), C["gold"])
|
||
elif tile == "S":
|
||
self.shop = Shop(self.player)
|
||
|
||
def update(self):
|
||
self.frame += 1
|
||
self.npc_frame += 1
|
||
self.player.frame += 1
|
||
|
||
for s in self.sparks[:]:
|
||
s["x"] += s["vx"]
|
||
s["y"] += s["vy"]
|
||
s["vy"] += 0.1
|
||
s["life"] -= 1
|
||
if s["life"] <= 0:
|
||
self.sparks.remove(s)
|
||
|
||
if self.message:
|
||
text, timer = self.message
|
||
self.message = (text, timer - 1) if timer > 0 else None
|
||
|
||
def draw_world(self, surf):
|
||
surf.fill(C["sky"])
|
||
render_map(surf, self.current_map, self.cam, self.frame)
|
||
|
||
if self.scene == self.TOWN:
|
||
for row_i, row in enumerate(TOWN_MAP):
|
||
if "M" in row:
|
||
mx, my = row.index("M"), row_i
|
||
sx, sy = self.cam.world_to_screen(mx * TILE, my * TILE)
|
||
draw_npc_mitsuha(surf, sx, sy, self.npc_frame)
|
||
panel(surf, sx-4, sy-22, 64, 16)
|
||
txt(surf, "Мицуха", sx-2, sy-21, C["pink"], font_sm)
|
||
|
||
px, py = self.cam.world_to_screen(self.player.px, self.player.py)
|
||
draw_player(surf, px, py, self.player.frame, self.player.facing)
|
||
|
||
for s in self.sparks:
|
||
c = tuple(min(255, v) for v in s["color"])
|
||
pygame.draw.rect(surf, c, (int(s["x"]) - self.cam.ox, int(s["y"]) - self.cam.oy, 3, 3))
|
||
|
||
def draw_hud(self, surf):
|
||
panel(surf, 8, 8, 210, 68)
|
||
txt(surf, f"Ур.{self.player.level} {self.player.name}", 14, 12, C["gold"])
|
||
draw_bar(surf, 14, 32, 190, 8, self.player.hp, self.player.max_hp, C["hp_red"])
|
||
txt(surf, f"ОЗ {self.player.hp}/{self.player.max_hp}", 14, 42, C["white"], font_sm)
|
||
draw_bar(surf, 14, 54, 190, 8, self.player.mp, self.player.max_mp, C["mp_blue"])
|
||
txt(surf, f"ОМ {self.player.mp}/{self.player.max_mp}", 14, 64, C["white"], font_sm)
|
||
|
||
panel(surf, W-165, 8, 157, 26)
|
||
txt(surf, f"Золото: {self.player.gold}", W-159, 12, C["gold"], font_sm)
|
||
|
||
loc = "** Деревня **" if self.scene == self.TOWN else "~~ Лес ~~"
|
||
lw = font_sm.size(loc)[0] + 16
|
||
panel(surf, W//2 - lw//2, 8, lw, 26)
|
||
txt(surf, loc, W//2 - lw//2 + 8, 12, C["white"], font_sm)
|
||
|
||
txt(surf, "Стрелки: движение E: действие", 10, H-18, C["gray"], font_sm)
|
||
|
||
if self.message:
|
||
text, timer = self.message
|
||
tw = font_md.size(text)[0] + 24
|
||
panel(surf, W//2 - tw//2, H//2-20, tw, 40)
|
||
fc = flash_color(C["white"], self.frame * 0.02)
|
||
txt(surf, text, W//2 - tw//2 + 12, H//2-12, fc)
|
||
|
||
def run(self):
|
||
running = True
|
||
while running:
|
||
for event in pygame.event.get():
|
||
if event.type == pygame.QUIT:
|
||
running = False
|
||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
||
if self.dialogue:
|
||
self.dialogue = None
|
||
elif self.shop:
|
||
self.shop = None
|
||
elif self.battle:
|
||
pass
|
||
else:
|
||
running = False
|
||
|
||
if self.battle:
|
||
result = self.battle.handle_input(event)
|
||
if result in ("win", "lose"):
|
||
if result == "lose":
|
||
self.scene = self.TOWN
|
||
self.current_map = TOWN_MAP
|
||
self.player.x, self.player.y = 5, 6
|
||
self.player.px = self.player.x * TILE
|
||
self.player.py = self.player.y * TILE
|
||
self.battle = None
|
||
continue
|
||
|
||
if self.shop:
|
||
if self.shop.handle_input(event):
|
||
self.shop = None
|
||
continue
|
||
|
||
if self.dialogue:
|
||
if event.type == pygame.KEYDOWN and event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
self.dialogue.advance()
|
||
if self.dialogue.done:
|
||
self.dialogue = None
|
||
continue
|
||
|
||
if event.type == pygame.KEYDOWN:
|
||
if event.key == pygame.K_UP:
|
||
self.try_move(0, -1)
|
||
elif event.key == pygame.K_DOWN:
|
||
self.try_move(0, 1)
|
||
elif event.key == pygame.K_LEFT:
|
||
self.try_move(-1, 0)
|
||
elif event.key == pygame.K_RIGHT:
|
||
self.try_move(1, 0)
|
||
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
|
||
self.try_interact()
|
||
|
||
self.update()
|
||
self.cam.update(self.player.px, self.player.py,
|
||
len(self.current_map[0]), len(self.current_map))
|
||
|
||
if self.battle:
|
||
self.battle.draw(screen)
|
||
else:
|
||
self.draw_world(screen)
|
||
self.draw_hud(screen)
|
||
if self.shop:
|
||
self.shop.draw(screen)
|
||
if self.dialogue:
|
||
self.dialogue.draw(screen)
|
||
|
||
pygame.display.flip()
|
||
clock.tick(FPS)
|
||
|
||
pygame.quit()
|
||
sys.exit()
|
||
|
||
|
||
# ── Заставка ───────────────────────────────────────────────────────────────
|
||
def title_screen():
|
||
frame = 0
|
||
while True:
|
||
for event in pygame.event.get():
|
||
if event.type == pygame.QUIT:
|
||
pygame.quit(); sys.exit()
|
||
if event.type == pygame.KEYDOWN:
|
||
return
|
||
|
||
frame += 1
|
||
screen.fill(C["bg"])
|
||
|
||
random.seed(7)
|
||
for _ in range(60):
|
||
sx = random.randint(0, W)
|
||
sy = random.randint(0, H)
|
||
br = 100 + int(80 * math.sin(frame * 0.03 + sx * 0.1))
|
||
pygame.draw.rect(screen, (br, br, int(br*0.8)), (sx, sy, 2, 2))
|
||
random.seed()
|
||
|
||
tc = flash_color(C["gold"], frame * 0.04, 30)
|
||
title = font_lg.render(">> Пиксельная деревня <<", True, tc)
|
||
screen.blit(title, (W//2 - title.get_width()//2, 90))
|
||
|
||
sub = font_md.render("~ Уютная маленькая JRPG ~", True, C["pink"])
|
||
screen.blit(sub, (W//2 - sub.get_width()//2, 128))
|
||
|
||
draw_player(screen, W//2-80, 220, frame)
|
||
draw_npc_mitsuha(screen, W//2-20, 220, frame)
|
||
draw_slime(screen, W//2+60, 230, frame, C["green"])
|
||
draw_fairy(screen, W//2+100, 220, frame)
|
||
|
||
features = [
|
||
"[Д] Деревня: дом, лавка и пруд",
|
||
"[?] Поговори с соседкой Мицухой",
|
||
"[Л] Лес с милыми монстриками",
|
||
"[!] Пошаговые бои",
|
||
"[*] Повышай уровень героя!",
|
||
]
|
||
for i, f in enumerate(features):
|
||
fc = C["white"] if i % 2 == 0 else C["gray"]
|
||
fs = font_sm.render(f, True, fc)
|
||
screen.blit(fs, (W//2 - fs.get_width()//2, 300 + i*22))
|
||
|
||
if int(frame / 30) % 2 == 0:
|
||
ps = font_md.render("Нажми любую клавишу!", True, C["yellow"])
|
||
screen.blit(ps, (W//2 - ps.get_width()//2, 420))
|
||
|
||
pygame.display.flip()
|
||
clock.tick(FPS)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
title_screen()
|
||
game = Game()
|
||
game.run()
|