nice working and testing
This commit is contained in:
6847
clannad_parser/00414.scr.txt
Normal file
6847
clannad_parser/00414.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
9314
clannad_parser/00416.scr.txt
Normal file
9314
clannad_parser/00416.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
11225
clannad_parser/00417.scr.txt
Normal file
11225
clannad_parser/00417.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
10127
clannad_parser/00418.scr.txt
Normal file
10127
clannad_parser/00418.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
6947
clannad_parser/00419.scr.txt
Normal file
6947
clannad_parser/00419.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
7963
clannad_parser/00420.scr.txt
Normal file
7963
clannad_parser/00420.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
10245
clannad_parser/00421.scr.txt
Normal file
10245
clannad_parser/00421.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
16973
clannad_parser/00422.scr.txt
Normal file
16973
clannad_parser/00422.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
16291
clannad_parser/00423.scr.txt
Normal file
16291
clannad_parser/00423.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
14209
clannad_parser/00424.scr.txt
Normal file
14209
clannad_parser/00424.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
9637
clannad_parser/00425.scr.txt
Normal file
9637
clannad_parser/00425.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
7545
clannad_parser/00429.scr.txt
Normal file
7545
clannad_parser/00429.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
12701
clannad_parser/02426.scr.txt
Normal file
12701
clannad_parser/02426.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
10268
clannad_parser/02509.scr.txt
Normal file
10268
clannad_parser/02509.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
13121
clannad_parser/04425.scr.txt
Normal file
13121
clannad_parser/04425.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
8831
clannad_parser/04503.scr.txt
Normal file
8831
clannad_parser/04503.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
9506
clannad_parser/04508.scr.txt
Normal file
9506
clannad_parser/04508.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
7885
clannad_parser/05424.scr.txt
Normal file
7885
clannad_parser/05424.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
10889
clannad_parser/05430.scr.txt
Normal file
10889
clannad_parser/05430.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
13128
clannad_parser/07400.scr.txt
Normal file
13128
clannad_parser/07400.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
12176
clannad_parser/07401.scr.txt
Normal file
12176
clannad_parser/07401.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
14175
clannad_parser/07500.scr.txt
Normal file
14175
clannad_parser/07500.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
13041
clannad_parser/07600.scr.txt
Normal file
13041
clannad_parser/07600.scr.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
def parse_text_command(data, pos):
|
def parse_text_command(data, pos):
|
||||||
# Читаем параметры в Little Endian
|
# Читаем параметры в Little Endian
|
||||||
@@ -17,18 +18,33 @@ def parse_text_command(data, pos):
|
|||||||
|
|
||||||
# Декодируем UTF-16LE текст
|
# Декодируем UTF-16LE текст
|
||||||
text = text_bytes.decode('utf-16le', errors='replace')
|
text = text_bytes.decode('utf-16le', errors='replace')
|
||||||
total_size = 8 + len(text_bytes) + 2 # 8 байт заголовка + текст + 0000
|
total_size = 8 + len(text_bytes) # 8 байт заголовка + текст + 0000
|
||||||
return f"TextID: {text_id}, VoiceID: {voice_id}, Text: {text}", total_size
|
return f"TextID: {text_id}, VoiceID: {voice_id}, Text: {text}", total_size
|
||||||
|
|
||||||
# База команд (идентификаторы в Big Endian)
|
# База команд (идентификаторы в Big Endian)
|
||||||
command_db = {
|
command_db = {
|
||||||
|
0x0001: {
|
||||||
|
"name": "Sprite",
|
||||||
|
"size": 14,
|
||||||
|
"handler": lambda data, pos: (f"ID: {int.from_bytes(data[pos+2:pos+4], 'little')} | " \
|
||||||
|
+ f"MODE: {int.from_bytes(data[pos+4:pos+6], 'little')} | " \
|
||||||
|
+ f"X: {int.from_bytes(data[pos+6:pos+8], 'big')} | " \
|
||||||
|
+ f"Y: {int.from_bytes(data[pos+8:pos+10], 'big')} | " \
|
||||||
|
+ f"fade: {int.from_bytes(data[pos+10:pos+12], 'big')}ms" \
|
||||||
|
, 12)
|
||||||
|
},
|
||||||
0x1401: {
|
0x1401: {
|
||||||
"name": "PlayBGM",
|
"name": "BGM",
|
||||||
"size": 2,
|
"size": 2,
|
||||||
"handler": lambda data, pos: (f"BGM Track: {int.from_bytes(data[pos:pos+2], 'little')}", 2)
|
"handler": lambda data, pos: (f"BGM Track: {int.from_bytes(data[pos:pos+2], 'little')}", 2)
|
||||||
},
|
},
|
||||||
|
0x1600: {
|
||||||
|
"name": "Pause",
|
||||||
|
"size": 2,
|
||||||
|
"handler": lambda data, pos: (f"{int.from_bytes(data[pos:pos+2], 'little')}ms", 2)
|
||||||
|
},
|
||||||
0x0A00: {
|
0x0A00: {
|
||||||
"name": "ShowText",
|
"name": "Text",
|
||||||
"size": 8,
|
"size": 8,
|
||||||
"handler": parse_text_command
|
"handler": parse_text_command
|
||||||
}
|
}
|
||||||
@@ -55,20 +71,51 @@ def parse_script(file_path):
|
|||||||
while pos % 4 != 0:
|
while pos % 4 != 0:
|
||||||
pos += 1
|
pos += 1
|
||||||
else:
|
else:
|
||||||
# output.append(f"[0x{pos:08X}][0x{cmd:04X}] UNKNOWN COMMAND")
|
output.append(f"[0x{pos:08X}][0x{cmd:04X}] UNKNOWN COMMAND")
|
||||||
pos += 2
|
pos += 2
|
||||||
|
|
||||||
# Сохраняем в файл
|
# Сохраняем в файл
|
||||||
with open("script" + file_path + ".txt", "w", encoding="utf-8") as f:
|
output_file = file_path + ".txt"
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(output))
|
f.write("\n".join(output))
|
||||||
|
|
||||||
|
print(f"Обработан: {file_path} -> {output_file}")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def batch_process_scr_files():
|
||||||
if len(sys.argv) < 2:
|
# Получаем текущий каталог
|
||||||
print("Usage: python parser.py <script.bin>")
|
current_dir = os.getcwd()
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
results = parse_script(sys.argv[1])
|
# Ищем все файлы с расширением .scr
|
||||||
for line in results:
|
scr_files = []
|
||||||
print(line)
|
for file_name in os.listdir(current_dir):
|
||||||
|
if file_name.lower().endswith('.scr'):
|
||||||
|
scr_files.append(file_name)
|
||||||
|
|
||||||
|
if not scr_files:
|
||||||
|
print("Файлы с расширением .scr не найдены в текущем каталоге.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(f"Найдено {len(scr_files)} файлов .scr для обработки:")
|
||||||
|
for file_name in scr_files:
|
||||||
|
print(f" - {file_name}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for file_name in scr_files:
|
||||||
|
try:
|
||||||
|
file_results = parse_script(file_name)
|
||||||
|
results.extend(file_results)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при обработке файла {file_name}: {e}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
# Если указан аргумент командной строки, обрабатываем указанный файл
|
||||||
|
results = parse_script(sys.argv[1])
|
||||||
|
for line in results:
|
||||||
|
print(line)
|
||||||
|
else:
|
||||||
|
# Если аргументов нет, сканируем текущий каталог
|
||||||
|
batch_process_scr_files()
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,8 @@ video_id = y2fwatM1oOg
|
|||||||
enabled = 0
|
enabled = 0
|
||||||
|
|
||||||
[Twitch]
|
[Twitch]
|
||||||
channel = coulthardf1
|
channel = sergeyshemet
|
||||||
|
; channel = nuke73
|
||||||
enabled = 1
|
enabled = 1
|
||||||
|
|
||||||
[Alerts]
|
[Alerts]
|
||||||
|
|||||||
@@ -232,11 +232,11 @@ def make_json_object():
|
|||||||
|
|
||||||
# Добавляем приветственное сообщение каждые 10 минут
|
# Добавляем приветственное сообщение каждые 10 минут
|
||||||
current_second = int(time.time())
|
current_second = int(time.time())
|
||||||
if current_second % 600 <= 5:
|
if current_second % 1000 <= 5:
|
||||||
# Проверяем, есть ли приветственное сообщение от Eikichi-bot в последних 20 сообщениях
|
# Проверяем, есть ли приветственное сообщение от Eikichi-bot в последних 20 сообщениях
|
||||||
has_recent_hello = any(
|
has_recent_hello = any(
|
||||||
comment.get('sendr') == 'Eikichi-bot' and comment.get('type') == 'hello'
|
comment.get('sendr') == 'Eikichi-bot' and comment.get('type') == 'hello'
|
||||||
for comment in all_comments[-20:] # Последние 20 сообщений
|
for comment in all_comments[-40:] # Последние 20 сообщений
|
||||||
)
|
)
|
||||||
|
|
||||||
if not has_recent_hello:
|
if not has_recent_hello:
|
||||||
|
|||||||
15
index.html
15
index.html
@@ -18,7 +18,7 @@ body {
|
|||||||
|
|
||||||
|
|
||||||
font-family: "Roboto Condensed", sans-serif;
|
font-family: "Roboto Condensed", sans-serif;
|
||||||
font-size: 1em;
|
font-size: 1.1em;
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
color: lighter;
|
color: lighter;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@@ -29,8 +29,9 @@ body {
|
|||||||
#chatwin {
|
#chatwin {
|
||||||
/* color: white; */
|
/* color: white; */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 150px;
|
width: 100%;
|
||||||
max-width: 450px;
|
/* min-width: 150px; */
|
||||||
|
max-width: 510px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ body {
|
|||||||
.nameline {
|
.nameline {
|
||||||
color: white;
|
color: white;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
width: 25%;
|
width: 27%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -70,9 +71,9 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
vertical-align: auto;
|
vertical-align: auto;
|
||||||
|
|
||||||
flex: 0 0 25%; /* Фиксируем ширину - не растягивается и не сжимается */
|
flex: 0 0 27%; /* Фиксируем ширину - не растягивается и не сжимается */
|
||||||
min-width: 0; /* Важно для работы переноса текста в flex-контейнере */
|
min-width: 0; /* Важно для работы переноса текста в flex-контейнере */
|
||||||
max-width: 25%; /* Ограничиваем максимальную ширину */
|
max-width: 27%; /* Ограничиваем максимальную ширину */
|
||||||
word-wrap: break-word; /* Перенос длинных слов */
|
word-wrap: break-word; /* Перенос длинных слов */
|
||||||
overflow-wrap: break-word; /* Современная версия */
|
overflow-wrap: break-word; /* Современная версия */
|
||||||
word-break: break-word; /* Разрыв слов если нужно */
|
word-break: break-word; /* Разрыв слов если нужно */
|
||||||
@@ -123,7 +124,7 @@ body {
|
|||||||
|
|
||||||
.msgline {
|
.msgline {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
width: 75%;
|
width: 73%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
flex: 1; /* Занимает всё оставшееся пространство */
|
flex: 1; /* Занимает всё оставшееся пространство */
|
||||||
|
|||||||
390
jrpg/girl.py
Normal file
390
jrpg/girl.py
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import pygame
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
W, H = 600, 750
|
||||||
|
screen = pygame.display.set_mode((W, H))
|
||||||
|
pygame.display.set_caption("Anime Girl - Blue Pink Black")
|
||||||
|
|
||||||
|
# Color palette
|
||||||
|
BLACK = (5, 5, 15)
|
||||||
|
DARK = (10, 10, 30)
|
||||||
|
PINK = (255, 100, 180)
|
||||||
|
PINK2 = (255, 160, 210)
|
||||||
|
PINK3 = (200, 60, 130)
|
||||||
|
BLUE = (80, 180, 255)
|
||||||
|
BLUE2 = (40, 120, 220)
|
||||||
|
BLUE3 = (130, 210, 255)
|
||||||
|
CYAN = (0, 220, 255)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
SKIN = (255, 220, 210)
|
||||||
|
SKIN2 = (240, 195, 185)
|
||||||
|
PURPLE = (160, 80, 220)
|
||||||
|
GLITCH = (0, 255, 200)
|
||||||
|
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
t = 0
|
||||||
|
|
||||||
|
def draw_gradient_bg(surf):
|
||||||
|
for y in range(H):
|
||||||
|
ratio = y / H
|
||||||
|
r = int(5 + 15 * ratio)
|
||||||
|
g = int(5 + 10 * ratio)
|
||||||
|
b = int(15 + 40 * ratio)
|
||||||
|
pygame.draw.line(surf, (r, g, b), (0, y), (W, y))
|
||||||
|
|
||||||
|
def draw_glow(surf, color, pos, radius, alpha=80):
|
||||||
|
glow = pygame.Surface((radius*2, radius*2), pygame.SRCALPHA)
|
||||||
|
for i in range(radius, 0, -4):
|
||||||
|
a = int(alpha * (i / radius) ** 2)
|
||||||
|
r, g, b = color
|
||||||
|
pygame.draw.circle(glow, (r, g, b, a), (radius, radius), i)
|
||||||
|
surf.blit(glow, (pos[0]-radius, pos[1]-radius))
|
||||||
|
|
||||||
|
def draw_stars(surf, t):
|
||||||
|
import random
|
||||||
|
random.seed(42)
|
||||||
|
for i in range(60):
|
||||||
|
x = random.randint(0, W)
|
||||||
|
y = random.randint(0, H//2)
|
||||||
|
size = random.uniform(1, 2.5)
|
||||||
|
flicker = 0.5 + 0.5 * math.sin(t * 2 + i)
|
||||||
|
c = int(150 + 105 * flicker)
|
||||||
|
pygame.draw.circle(surf, (c, c, min(255, c+50)), (x, y), int(size))
|
||||||
|
|
||||||
|
def draw_scanlines(surf):
|
||||||
|
s = pygame.Surface((W, H), pygame.SRCALPHA)
|
||||||
|
for y in range(0, H, 4):
|
||||||
|
pygame.draw.line(s, (0, 0, 0, 30), (0, y), (W, y))
|
||||||
|
surf.blit(s, (0, 0))
|
||||||
|
|
||||||
|
def draw_girl(surf, t):
|
||||||
|
# floating bob
|
||||||
|
bob = math.sin(t * 1.5) * 5
|
||||||
|
cx = W // 2
|
||||||
|
base_y = 480 + bob
|
||||||
|
|
||||||
|
# --- HAIR BACK LAYER ---
|
||||||
|
# Long flowing hair behind body
|
||||||
|
hair_pts_back = [
|
||||||
|
(cx - 120, base_y - 320),
|
||||||
|
(cx - 160, base_y - 200),
|
||||||
|
(cx - 190, base_y - 50),
|
||||||
|
(cx - 170, base_y + 100),
|
||||||
|
(cx - 120, base_y + 200),
|
||||||
|
(cx - 60, base_y + 280),
|
||||||
|
(cx, base_y + 300),
|
||||||
|
(cx + 60, base_y + 280),
|
||||||
|
(cx + 120, base_y + 200),
|
||||||
|
(cx + 170, base_y + 100),
|
||||||
|
(cx + 190, base_y - 50),
|
||||||
|
(cx + 160, base_y - 200),
|
||||||
|
(cx + 120, base_y - 320),
|
||||||
|
]
|
||||||
|
pygame.draw.polygon(surf, BLUE2, hair_pts_back)
|
||||||
|
pygame.draw.polygon(surf, (20, 60, 120), hair_pts_back, 2)
|
||||||
|
|
||||||
|
# --- BODY / TORSO ---
|
||||||
|
# Dress / top
|
||||||
|
body_rect = pygame.Rect(cx - 70, base_y - 100, 140, 200)
|
||||||
|
pygame.draw.ellipse(surf, DARK, pygame.Rect(cx-65, base_y-95, 130, 180))
|
||||||
|
|
||||||
|
# Dress gradient effect - layered
|
||||||
|
for i, col in enumerate([(30, 10, 60), (50, 20, 90), (70, 30, 110)]):
|
||||||
|
pygame.draw.ellipse(surf, col,
|
||||||
|
pygame.Rect(cx-60+i*3, base_y-90+i*2, 120-i*6, 170-i*4))
|
||||||
|
|
||||||
|
# Collar / ribbon
|
||||||
|
ribbon_pts = [(cx-40, base_y-90), (cx, base_y-70), (cx+40, base_y-90),
|
||||||
|
(cx+20, base_y-60), (cx, base_y-50), (cx-20, base_y-60)]
|
||||||
|
pygame.draw.polygon(surf, PINK3, ribbon_pts)
|
||||||
|
|
||||||
|
# Bow on collar
|
||||||
|
bow_left = [(cx-35, base_y-78), (cx-8, base_y-65), (cx-8, base_y-75)]
|
||||||
|
bow_right = [(cx+35, base_y-78), (cx+8, base_y-65), (cx+8, base_y-75)]
|
||||||
|
pygame.draw.polygon(surf, PINK, bow_left)
|
||||||
|
pygame.draw.polygon(surf, PINK, bow_right)
|
||||||
|
pygame.draw.circle(surf, PINK2, (cx, base_y-72), 5)
|
||||||
|
|
||||||
|
# Glowing trim
|
||||||
|
pygame.draw.arc(surf, CYAN,
|
||||||
|
pygame.Rect(cx-62, base_y-92, 124, 40), 0, math.pi, 2)
|
||||||
|
|
||||||
|
# --- NECK ---
|
||||||
|
pygame.draw.rect(surf, SKIN, pygame.Rect(cx-18, base_y-140, 36, 55), border_radius=8)
|
||||||
|
|
||||||
|
# --- HEAD ---
|
||||||
|
head_cy = base_y - 200
|
||||||
|
# Face
|
||||||
|
pygame.draw.ellipse(surf, SKIN, pygame.Rect(cx-65, head_cy-75, 130, 155))
|
||||||
|
# Cheek blush
|
||||||
|
blush_alpha = pygame.Surface((60, 25), pygame.SRCALPHA)
|
||||||
|
pygame.draw.ellipse(blush_alpha, (255, 100, 130, 80), (0, 0, 60, 25))
|
||||||
|
surf.blit(blush_alpha, (cx-85, head_cy+10))
|
||||||
|
surf.blit(blush_alpha, (cx+25, head_cy+10))
|
||||||
|
|
||||||
|
# --- HAIR FRONT ---
|
||||||
|
# Top of head / bangs
|
||||||
|
hair_top = [
|
||||||
|
(cx-70, head_cy-70),
|
||||||
|
(cx-90, head_cy-100),
|
||||||
|
(cx-70, head_cy-130),
|
||||||
|
(cx-30, head_cy-155),
|
||||||
|
(cx, head_cy-165),
|
||||||
|
(cx+30, head_cy-155),
|
||||||
|
(cx+70, head_cy-130),
|
||||||
|
(cx+90, head_cy-100),
|
||||||
|
(cx+70, head_cy-70),
|
||||||
|
(cx+50, head_cy-40),
|
||||||
|
(cx, head_cy-30),
|
||||||
|
(cx-50, head_cy-40),
|
||||||
|
]
|
||||||
|
pygame.draw.polygon(surf, BLUE, hair_top)
|
||||||
|
|
||||||
|
# Bangs (front)
|
||||||
|
bang1 = [(cx-70, head_cy-70), (cx-95, head_cy-10), (cx-65, head_cy+20),
|
||||||
|
(cx-40, head_cy-10), (cx-50, head_cy-50)]
|
||||||
|
bang2 = [(cx-30, head_cy-75), (cx-45, head_cy-5), (cx-20, head_cy+10),
|
||||||
|
(cx+5, head_cy-10), (cx-5, head_cy-60)]
|
||||||
|
bang3 = [(cx+30, head_cy-75), (cx+10, head_cy-5), (cx+30, head_cy+15),
|
||||||
|
(cx+55, head_cy-5), (cx+45, head_cy-60)]
|
||||||
|
bang4 = [(cx+70, head_cy-70), (cx+50, head_cy-50), (cx+55, head_cy+20),
|
||||||
|
(cx+80, head_cy-10), (cx+95, head_cy-30)]
|
||||||
|
for bang in [bang1, bang2, bang3, bang4]:
|
||||||
|
pygame.draw.polygon(surf, BLUE, bang)
|
||||||
|
|
||||||
|
# Hair highlight
|
||||||
|
highlight_pts = [(cx-20, head_cy-155), (cx+20, head_cy-155),
|
||||||
|
(cx+10, head_cy-110), (cx-10, head_cy-110)]
|
||||||
|
pygame.draw.polygon(surf, BLUE3, highlight_pts)
|
||||||
|
|
||||||
|
# Hair shine line
|
||||||
|
pygame.draw.arc(surf, (200, 240, 255),
|
||||||
|
pygame.Rect(cx-30, head_cy-140, 60, 30), 0.2, math.pi-0.2, 2)
|
||||||
|
|
||||||
|
# --- EYES ---
|
||||||
|
eye_y = head_cy + 5
|
||||||
|
eye_offset = math.sin(t * 0.8) * 1 # subtle sway
|
||||||
|
|
||||||
|
for side in [-1, 1]:
|
||||||
|
ex = cx + side * 28
|
||||||
|
ey = eye_y
|
||||||
|
|
||||||
|
# Eye shadow
|
||||||
|
eye_sh = pygame.Surface((50, 30), pygame.SRCALPHA)
|
||||||
|
pygame.draw.ellipse(eye_sh, (80, 0, 60, 100), (0, 0, 50, 30))
|
||||||
|
surf.blit(eye_sh, (ex-25, ey-10))
|
||||||
|
|
||||||
|
# White of eye
|
||||||
|
pygame.draw.ellipse(surf, WHITE, pygame.Rect(ex-18, ey-12, 36, 28))
|
||||||
|
|
||||||
|
# Iris - blue gradient effect
|
||||||
|
pygame.draw.ellipse(surf, BLUE2, pygame.Rect(ex-13, ey-10, 26, 25))
|
||||||
|
pygame.draw.ellipse(surf, BLUE, pygame.Rect(ex-10, ey-7, 20, 19))
|
||||||
|
pygame.draw.ellipse(surf, CYAN, pygame.Rect(ex-6, ey-4, 12, 12))
|
||||||
|
|
||||||
|
# Pupil
|
||||||
|
pygame.draw.ellipse(surf, BLACK, pygame.Rect(ex-5, ey-3, 10, 11))
|
||||||
|
|
||||||
|
# Eye shine
|
||||||
|
pygame.draw.circle(surf, WHITE, (ex-4, ey-1), 3)
|
||||||
|
pygame.draw.circle(surf, WHITE, (ex+3, ey+3), 2)
|
||||||
|
|
||||||
|
# Eyelashes - upper lid
|
||||||
|
pygame.draw.arc(surf, BLACK,
|
||||||
|
pygame.Rect(ex-19, ey-14, 38, 22), 0.1, math.pi-0.1, 3)
|
||||||
|
# Lash lines
|
||||||
|
for lx_off, angle in [(-18, 2.4), (-10, 2.0), (-2, 1.7), (6, 1.5), (14, 1.8)]:
|
||||||
|
lx = ex + lx_off
|
||||||
|
pygame.draw.line(surf, BLACK,
|
||||||
|
(lx, ey - int(12 * math.sin(angle))),
|
||||||
|
(lx - side*2, ey - int(12 * math.sin(angle)) - 5), 2)
|
||||||
|
|
||||||
|
# Lower lash
|
||||||
|
pygame.draw.arc(surf, SKIN2,
|
||||||
|
pygame.Rect(ex-16, ey+2, 32, 14), math.pi+0.2, 2*math.pi-0.2, 1)
|
||||||
|
|
||||||
|
# --- NOSE (subtle) ---
|
||||||
|
pygame.draw.arc(surf, SKIN2,
|
||||||
|
pygame.Rect(cx-6, head_cy+28, 12, 8), math.pi+0.3, 2*math.pi-0.3, 1)
|
||||||
|
|
||||||
|
# --- MOUTH ---
|
||||||
|
mouth_y = head_cy + 50
|
||||||
|
# Smile
|
||||||
|
pygame.draw.arc(surf, PINK3,
|
||||||
|
pygame.Rect(cx-18, mouth_y, 36, 16), math.pi+0.3, 2*math.pi-0.3, 2)
|
||||||
|
# Lips
|
||||||
|
pygame.draw.ellipse(surf, PINK2, pygame.Rect(cx-14, mouth_y+1, 28, 9))
|
||||||
|
pygame.draw.arc(surf, PINK3,
|
||||||
|
pygame.Rect(cx-12, mouth_y, 24, 12), math.pi+0.4, 2*math.pi-0.4, 1)
|
||||||
|
|
||||||
|
# --- EARS ---
|
||||||
|
for side in [-1, 1]:
|
||||||
|
ex = cx + side * 63
|
||||||
|
pygame.draw.ellipse(surf, SKIN, pygame.Rect(ex - 10, head_cy - 5, 20, 28))
|
||||||
|
pygame.draw.ellipse(surf, SKIN2, pygame.Rect(ex - 6, head_cy, 12, 18))
|
||||||
|
|
||||||
|
# --- HAIRPIN / ACCESSORIES ---
|
||||||
|
# Star hairpin left
|
||||||
|
star_x, star_y = cx - 50, head_cy - 110
|
||||||
|
for i in range(5):
|
||||||
|
a1 = math.radians(i * 72 - 90)
|
||||||
|
a2 = math.radians(i * 72 + 36 - 90)
|
||||||
|
p1 = (star_x + 12 * math.cos(a1), star_y + 12 * math.sin(a1))
|
||||||
|
p2 = (star_x + 5 * math.cos(a2), star_y + 5 * math.sin(a2))
|
||||||
|
pygame.draw.line(surf, PINK, (int(p1[0]), int(p1[1])),
|
||||||
|
(int(p2[0]), int(p2[1])), 2)
|
||||||
|
pygame.draw.circle(surf, PINK, (star_x, star_y), 4)
|
||||||
|
|
||||||
|
# Glowing dot accessories in hair
|
||||||
|
for gx, gy, gc in [(cx+40, head_cy-130, CYAN),
|
||||||
|
(cx+70, head_cy-90, PINK),
|
||||||
|
(cx-60, head_cy-95, BLUE3)]:
|
||||||
|
draw_glow(surf, gc, (gx, gy), 15, 60)
|
||||||
|
pygame.draw.circle(surf, gc, (gx, gy), 4)
|
||||||
|
pygame.draw.circle(surf, WHITE, (gx, gy), 2)
|
||||||
|
|
||||||
|
# --- ARMS ---
|
||||||
|
# Left arm
|
||||||
|
arm_wave = math.sin(t * 1.2) * 8
|
||||||
|
arm_l_pts = [(cx-70, base_y-80), (cx-100, base_y-30+arm_wave),
|
||||||
|
(cx-115, base_y+60+arm_wave), (cx-95, base_y+65+arm_wave),
|
||||||
|
(cx-80, base_y-20+arm_wave), (cx-55, base_y-70)]
|
||||||
|
pygame.draw.polygon(surf, (40, 20, 80), arm_l_pts)
|
||||||
|
|
||||||
|
# Right arm
|
||||||
|
arm_r_pts = [(cx+70, base_y-80), (cx+100, base_y-30-arm_wave),
|
||||||
|
(cx+115, base_y+60-arm_wave), (cx+95, base_y+65-arm_wave),
|
||||||
|
(cx+80, base_y-20-arm_wave), (cx+55, base_y-70)]
|
||||||
|
pygame.draw.polygon(surf, (40, 20, 80), arm_r_pts)
|
||||||
|
|
||||||
|
# Hands
|
||||||
|
pygame.draw.circle(surf, SKIN, (cx-108, base_y+62+arm_wave), 14)
|
||||||
|
pygame.draw.circle(surf, SKIN, (cx+108, base_y+62-arm_wave), 14)
|
||||||
|
|
||||||
|
# Arm glow trim
|
||||||
|
pygame.draw.arc(surf, CYAN,
|
||||||
|
pygame.Rect(cx-118, base_y+46+arm_wave, 20, 20), 0, math.pi*2, 1)
|
||||||
|
pygame.draw.arc(surf, PINK,
|
||||||
|
pygame.Rect(cx+98, base_y+46-arm_wave, 20, 20), 0, math.pi*2, 1)
|
||||||
|
|
||||||
|
# --- SKIRT ---
|
||||||
|
skirt_top = base_y + 90
|
||||||
|
skirt_pts = [
|
||||||
|
(cx - 70, skirt_top),
|
||||||
|
(cx - 130, skirt_top + 100),
|
||||||
|
(cx - 110, skirt_top + 180),
|
||||||
|
(cx, skirt_top + 200),
|
||||||
|
(cx + 110, skirt_top + 180),
|
||||||
|
(cx + 130, skirt_top + 100),
|
||||||
|
(cx + 70, skirt_top),
|
||||||
|
]
|
||||||
|
pygame.draw.polygon(surf, (50, 20, 90), skirt_pts)
|
||||||
|
|
||||||
|
# Skirt details - ruffles
|
||||||
|
for i, col in enumerate([PURPLE, PINK3, PINK]):
|
||||||
|
offset = i * 15
|
||||||
|
s_pts = [
|
||||||
|
(cx - 70 - offset//2, skirt_top + offset*3),
|
||||||
|
(cx - 130 - offset, skirt_top + 100 + offset*2),
|
||||||
|
(cx + 130 + offset, skirt_top + 100 + offset*2),
|
||||||
|
(cx + 70 + offset//2, skirt_top + offset*3),
|
||||||
|
]
|
||||||
|
pygame.draw.lines(surf, col, False, s_pts, 2)
|
||||||
|
|
||||||
|
# Skirt sparkles
|
||||||
|
skirt_wave = math.sin(t * 2) * 3
|
||||||
|
for sx, sy in [(cx-80, skirt_top+40), (cx+60, skirt_top+60),
|
||||||
|
(cx-30, skirt_top+120), (cx+100, skirt_top+140)]:
|
||||||
|
pygame.draw.circle(surf, CYAN, (sx, sy+int(skirt_wave)), 2)
|
||||||
|
pygame.draw.circle(surf, PINK2, (sx+5, sy-5+int(skirt_wave)), 1)
|
||||||
|
|
||||||
|
# --- LEGS ---
|
||||||
|
leg_h = skirt_top + 180
|
||||||
|
pygame.draw.rect(surf, SKIN, pygame.Rect(cx-40, leg_h, 30, 80), border_radius=8)
|
||||||
|
pygame.draw.rect(surf, SKIN, pygame.Rect(cx+10, leg_h, 30, 80), border_radius=8)
|
||||||
|
|
||||||
|
# Stockings / boots
|
||||||
|
pygame.draw.rect(surf, (20, 20, 50), pygame.Rect(cx-42, leg_h+50, 34, 40), border_radius=5)
|
||||||
|
pygame.draw.rect(surf, (20, 20, 50), pygame.Rect(cx+8, leg_h+50, 34, 40), border_radius=5)
|
||||||
|
pygame.draw.line(surf, BLUE, (cx-42, leg_h+50), (cx-8, leg_h+50), 2)
|
||||||
|
pygame.draw.line(surf, BLUE, (cx+8, leg_h+50), (cx+42, leg_h+50), 2)
|
||||||
|
|
||||||
|
def draw_particles(surf, t):
|
||||||
|
for i in range(20):
|
||||||
|
px = int((W/2 + 220 * math.cos(t*0.3 + i * 0.9)) % W)
|
||||||
|
py = int((300 + 200 * math.sin(t*0.4 + i * 1.1) + i * 20) % H)
|
||||||
|
size = 1 + int(2 * abs(math.sin(t + i)))
|
||||||
|
colors = [PINK, CYAN, BLUE3, PINK2]
|
||||||
|
c = colors[i % len(colors)]
|
||||||
|
alpha_surf = pygame.Surface((size*4, size*4), pygame.SRCALPHA)
|
||||||
|
pygame.draw.circle(alpha_surf, (*c, 120), (size*2, size*2), size*2)
|
||||||
|
surf.blit(alpha_surf, (px-size*2, py-size*2))
|
||||||
|
pygame.draw.circle(surf, c, (px, py), size)
|
||||||
|
|
||||||
|
def draw_frame_decoration(surf, t):
|
||||||
|
# Corner decorations
|
||||||
|
corner_glow = abs(math.sin(t)) * 100 + 100
|
||||||
|
corners = [(20, 20), (W-20, 20), (20, H-20), (W-20, H-20)]
|
||||||
|
for cx, cy in corners:
|
||||||
|
pygame.draw.circle(surf, PINK, (cx, cy), 3)
|
||||||
|
draw_glow(surf, PINK, (cx, cy), 20, int(corner_glow*0.5))
|
||||||
|
|
||||||
|
# Animated border
|
||||||
|
border_col = (
|
||||||
|
int(60 + 40 * math.sin(t)),
|
||||||
|
int(20 + 20 * math.sin(t + 1)),
|
||||||
|
int(120 + 60 * math.sin(t + 2))
|
||||||
|
)
|
||||||
|
pygame.draw.rect(surf, border_col, (5, 5, W-10, H-10), 2, border_radius=8)
|
||||||
|
|
||||||
|
# Title text
|
||||||
|
try:
|
||||||
|
font = pygame.font.SysFont("Arial", 22, bold=True)
|
||||||
|
font_sm = pygame.font.SysFont("Arial", 13)
|
||||||
|
except:
|
||||||
|
font = pygame.font.Font(None, 28)
|
||||||
|
font_sm = pygame.font.Font(None, 18)
|
||||||
|
|
||||||
|
title = font.render("夢の少女", True, CYAN)
|
||||||
|
sub = font_sm.render("DREAM GIRL ✦ ANIME STYLE", True, PINK2)
|
||||||
|
# Glow effect for title
|
||||||
|
draw_glow(surf, CYAN, (W//2, H-38), 60, 40)
|
||||||
|
surf.blit(title, title.get_rect(center=(W//2, H-38)))
|
||||||
|
surf.blit(sub, sub.get_rect(center=(W//2, H-18)))
|
||||||
|
|
||||||
|
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:
|
||||||
|
running = False
|
||||||
|
|
||||||
|
t += 0.03
|
||||||
|
|
||||||
|
# Background
|
||||||
|
draw_gradient_bg(screen)
|
||||||
|
draw_stars(screen, t)
|
||||||
|
|
||||||
|
# Ambient glow behind character
|
||||||
|
draw_glow(screen, BLUE2, (W//2, 400), 200, 30)
|
||||||
|
draw_glow(screen, PINK3, (W//2, 300), 150, 20)
|
||||||
|
|
||||||
|
# Particles
|
||||||
|
draw_particles(screen, t)
|
||||||
|
|
||||||
|
# Character
|
||||||
|
draw_girl(screen, t)
|
||||||
|
|
||||||
|
# Post effects
|
||||||
|
draw_scanlines(screen)
|
||||||
|
draw_frame_decoration(screen, t)
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
clock.tick(60)
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit()
|
||||||
546
jrpg/island_novel.py
Normal file
546
jrpg/island_novel.py
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
"""
|
||||||
|
🏝️ ОСТРОВ ДВУХ СЕРДЕЦ — визуальная новелла
|
||||||
|
Запуск: pip install pygame && python island_novel.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
pygame.init()
|
||||||
|
|
||||||
|
W, H = 1024, 600
|
||||||
|
screen = pygame.display.set_mode((W, H), pygame.SCALED | pygame.RESIZABLE)
|
||||||
|
pygame.display.set_caption("🏝️ Остров двух сердец")
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
# ── Шрифты ──────────────────────────────────────────────────────────────────
|
||||||
|
def load_font(size, bold=False):
|
||||||
|
for name in ["Segoe UI", "Arial Unicode MS", "DejaVu Sans", "FreeSans", "Liberation Sans"]:
|
||||||
|
try:
|
||||||
|
return pygame.font.SysFont(name, size, bold=bold)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return pygame.font.Font(None, size)
|
||||||
|
|
||||||
|
FONT_BIG = load_font(30, bold=True)
|
||||||
|
FONT_MED = load_font(22)
|
||||||
|
FONT_SMALL = load_font(18)
|
||||||
|
FONT_NAME = load_font(24, bold=True)
|
||||||
|
|
||||||
|
# ── Цвета ────────────────────────────────────────────────────────────────────
|
||||||
|
SKY_DAWN = [(255,180,100), (255,120,80), (180,80,120)]
|
||||||
|
SKY_DAY = [(100,180,255), (50,130,230), (30,80,180)]
|
||||||
|
SKY_DUSK = [(255,120,60), (200,80,120), (100,40,100)]
|
||||||
|
SKY_NIGHT = [(10,10,40), (20,20,80), (5,5,20)]
|
||||||
|
|
||||||
|
SAND_COL = (240, 220, 160)
|
||||||
|
WATER_COL = (60, 160, 220)
|
||||||
|
PALM_TRUNK = (120, 80, 40)
|
||||||
|
PALM_LEAF = (40, 160, 60)
|
||||||
|
|
||||||
|
# ── Рисование фонов ──────────────────────────────────────────────────────────
|
||||||
|
def draw_gradient(surface, colors, rect=None):
|
||||||
|
if rect is None:
|
||||||
|
rect = surface.get_rect()
|
||||||
|
x, y, w, h = rect
|
||||||
|
c1, c2, c3 = colors
|
||||||
|
for i in range(h):
|
||||||
|
t = i / h
|
||||||
|
if t < 0.5:
|
||||||
|
r = c1[0] + (c2[0]-c1[0]) * t*2
|
||||||
|
g = c1[1] + (c2[1]-c1[1]) * t*2
|
||||||
|
b = c1[2] + (c2[2]-c1[2]) * t*2
|
||||||
|
else:
|
||||||
|
r = c2[0] + (c3[0]-c2[0]) * (t-0.5)*2
|
||||||
|
g = c2[1] + (c3[1]-c2[1]) * (t-0.5)*2
|
||||||
|
b = c2[2] + (c3[2]-c2[2]) * (t-0.5)*2
|
||||||
|
pygame.draw.line(surface, (int(r),int(g),int(b)), (x,y+i),(x+w,y+i))
|
||||||
|
|
||||||
|
def draw_stars(surface, n=80):
|
||||||
|
rng = random.Random(42)
|
||||||
|
for _ in range(n):
|
||||||
|
x = rng.randint(0, W)
|
||||||
|
y = rng.randint(0, H//2)
|
||||||
|
r = rng.randint(1,2)
|
||||||
|
pygame.draw.circle(surface, (255,255,220), (x,y), r)
|
||||||
|
|
||||||
|
def draw_sun(surface, time_of_day):
|
||||||
|
if time_of_day == "morning":
|
||||||
|
pos, col = (150, 120), (255,230,100)
|
||||||
|
elif time_of_day == "day":
|
||||||
|
pos, col = (512, 80), (255,255,180)
|
||||||
|
elif time_of_day == "evening":
|
||||||
|
pos, col = (870, 130), (255,160,60)
|
||||||
|
else:
|
||||||
|
pos, col = None, None
|
||||||
|
if pos:
|
||||||
|
pygame.draw.circle(surface, col, pos, 45)
|
||||||
|
pygame.draw.circle(surface, (255,255,255,80), pos, 55, 3)
|
||||||
|
|
||||||
|
def draw_moon(surface):
|
||||||
|
pygame.draw.circle(surface, (230,230,200), (800,100), 35)
|
||||||
|
pygame.draw.circle(surface, (10,10,40), (815,90), 30)
|
||||||
|
|
||||||
|
def draw_palm(surface, x, y, seed=0):
|
||||||
|
rng = random.Random(seed)
|
||||||
|
# ствол
|
||||||
|
points = [(x,y),(x-8,y-40),(x-5,y-90),(x,y-150),(x+5,y-180)]
|
||||||
|
pygame.draw.lines(surface, PALM_TRUNK, False, points, 10)
|
||||||
|
# листья
|
||||||
|
for angle in range(0, 360, 60):
|
||||||
|
rad = math.radians(angle + rng.randint(-20,20))
|
||||||
|
lx = x + int(80*math.cos(rad))
|
||||||
|
ly = y - 185 + int(50*math.sin(rad))
|
||||||
|
pygame.draw.line(surface, PALM_LEAF, (x, y-185), (lx,ly), 6)
|
||||||
|
# скругление
|
||||||
|
pygame.draw.circle(surface, PALM_LEAF, (lx,ly), 8)
|
||||||
|
|
||||||
|
def draw_waves(surface, t):
|
||||||
|
for i in range(5):
|
||||||
|
oy = 30*i
|
||||||
|
for x in range(0, W, 4):
|
||||||
|
y = int(H*0.62 + 8 + oy + 6*math.sin((x/60 + t*0.5 + i)*1.2))
|
||||||
|
pygame.draw.circle(surface, (100,200,240,120), (x,y), 2)
|
||||||
|
|
||||||
|
def build_background(time_of_day):
|
||||||
|
surf = pygame.Surface((W,H))
|
||||||
|
# небо
|
||||||
|
sky = {"morning":SKY_DAWN,"day":SKY_DAY,"evening":SKY_DUSK,"night":SKY_NIGHT}[time_of_day]
|
||||||
|
draw_gradient(surf, sky)
|
||||||
|
if time_of_day == "night":
|
||||||
|
draw_stars(surf)
|
||||||
|
draw_moon(surf)
|
||||||
|
else:
|
||||||
|
draw_sun(surf, time_of_day)
|
||||||
|
# вода
|
||||||
|
pygame.draw.rect(surf, WATER_COL, (0, int(H*0.6), W, int(H*0.4)))
|
||||||
|
# песок
|
||||||
|
pygame.draw.ellipse(surf, SAND_COL, (50, int(H*0.55), W-100, int(H*0.35)))
|
||||||
|
# пальмы
|
||||||
|
draw_palm(surf, 120, int(H*0.75), seed=1)
|
||||||
|
draw_palm(surf, W-180, int(H*0.73), seed=2)
|
||||||
|
draw_palm(surf, 400, int(H*0.78), seed=3)
|
||||||
|
return surf
|
||||||
|
|
||||||
|
# Кэшируем фоны
|
||||||
|
BG = {tod: build_background(tod) for tod in ["morning","day","evening","night"]}
|
||||||
|
|
||||||
|
# ── Персонажи ────────────────────────────────────────────────────────────────
|
||||||
|
def draw_anime_girl(surface, x, y, palette, name, emotion="neutral", flip=False):
|
||||||
|
"""Рисует простую анимешную девочку программно."""
|
||||||
|
s = pygame.Surface((160,300), pygame.SRCALPHA)
|
||||||
|
|
||||||
|
hair_col, dress_col, skin_col, eye_col = palette
|
||||||
|
|
||||||
|
# тело / платье
|
||||||
|
pygame.draw.ellipse(s, dress_col, (35,130,90,140))
|
||||||
|
# руки
|
||||||
|
pygame.draw.ellipse(s, skin_col, (10,140,30,70))
|
||||||
|
pygame.draw.ellipse(s, skin_col, (120,140,30,70))
|
||||||
|
# шея
|
||||||
|
pygame.draw.rect(s, skin_col, (67,105,26,35))
|
||||||
|
# голова
|
||||||
|
pygame.draw.ellipse(s, skin_col, (30,20,100,100))
|
||||||
|
# волосы
|
||||||
|
pygame.draw.ellipse(s, hair_col, (25,10,110,70))
|
||||||
|
pygame.draw.ellipse(s, hair_col, (15,30,40,80)) # левая прядь
|
||||||
|
pygame.draw.ellipse(s, hair_col, (105,30,40,80)) # правая прядь
|
||||||
|
pygame.draw.rect(s, hair_col, (25,10,110,40))
|
||||||
|
# глаза
|
||||||
|
eye_y = 65
|
||||||
|
for ex in [55,95]:
|
||||||
|
pygame.draw.ellipse(s, (255,255,255), (ex-12,eye_y,24,16))
|
||||||
|
pygame.draw.ellipse(s, eye_col, (ex-8,eye_y+2,16,12))
|
||||||
|
pygame.draw.circle(s, (0,0,0), (ex,eye_y+8), 5)
|
||||||
|
pygame.draw.circle(s, (255,255,255), (ex+3,eye_y+4), 2)
|
||||||
|
# брови
|
||||||
|
brow_y = eye_y - 8
|
||||||
|
if emotion == "happy":
|
||||||
|
pygame.draw.arc(s, hair_col, (45,brow_y,20,10), 0, math.pi, 2)
|
||||||
|
pygame.draw.arc(s, hair_col, (90,brow_y,20,10), 0, math.pi, 2)
|
||||||
|
elif emotion == "sad":
|
||||||
|
pygame.draw.arc(s, hair_col, (45,brow_y,20,10), math.pi, 2*math.pi, 2)
|
||||||
|
pygame.draw.arc(s, hair_col, (90,brow_y,20,10), math.pi, 2*math.pi, 2)
|
||||||
|
else:
|
||||||
|
pygame.draw.line(s, hair_col, (45,brow_y),(65,brow_y+2), 2)
|
||||||
|
pygame.draw.line(s, hair_col, (90,brow_y),(110,brow_y+2), 2)
|
||||||
|
# рот
|
||||||
|
mouth_y = 95
|
||||||
|
if emotion == "happy":
|
||||||
|
pygame.draw.arc(s, (200,80,80), (60,mouth_y,30,16), math.pi, 2*math.pi, 2)
|
||||||
|
elif emotion == "sad":
|
||||||
|
pygame.draw.arc(s, (200,80,80), (60,mouth_y,30,16), 0, math.pi, 2)
|
||||||
|
else:
|
||||||
|
pygame.draw.line(s, (200,80,80), (65,mouth_y+8),(90,mouth_y+8), 2)
|
||||||
|
# ушки
|
||||||
|
pygame.draw.circle(s, skin_col, (30,75), 10)
|
||||||
|
pygame.draw.circle(s, skin_col, (130,75), 10)
|
||||||
|
# ноги
|
||||||
|
pygame.draw.rect(s, skin_col, (55,255,20,40))
|
||||||
|
pygame.draw.rect(s, skin_col, (90,255,20,40))
|
||||||
|
|
||||||
|
if flip:
|
||||||
|
s = pygame.transform.flip(s, True, False)
|
||||||
|
surface.blit(s, (x-80, y-280))
|
||||||
|
|
||||||
|
# Палитры персонажей
|
||||||
|
SAKURA_PALETTE = (
|
||||||
|
(255,150,180), # розовые волосы
|
||||||
|
(220,160,220), # фиолетовое платье
|
||||||
|
(255,220,195), # кожа
|
||||||
|
(255,100,180), # розовые глаза
|
||||||
|
)
|
||||||
|
YUKI_PALETTE = (
|
||||||
|
(100,180,255), # голубые волосы
|
||||||
|
(180,220,255), # голубое платье
|
||||||
|
(255,220,195),
|
||||||
|
(60,140,255), # синие глаза
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Диалоги ──────────────────────────────────────────────────────────────────
|
||||||
|
STORY = [
|
||||||
|
# (day, time_of_day, speaker, emotion, text, points_sakura, points_yuki)
|
||||||
|
# ДЕНЬ 1
|
||||||
|
("День 1","morning","narrator","neutral",
|
||||||
|
"Волны выбросили тебя на песчаный берег. Рядом — две незнакомки.",0,0),
|
||||||
|
("День 1","morning","Сакура","happy",
|
||||||
|
"Эй! Ты жив? Я Сакура! Не переживай, я умею разводить костёр!",0,0),
|
||||||
|
("День 1","morning","Юки","neutral",
|
||||||
|
"Я Юки. Надо составить план выживания. Паниковать бесполезно.",0,0),
|
||||||
|
("День 1","morning","narrator","neutral",
|
||||||
|
"Ты решаешь, кому помочь первой: собрать дрова или найти пресную воду.",0,0),
|
||||||
|
# выбор
|
||||||
|
("День 1","morning","CHOICE","neutral","[1] Помочь Сакуре с костром / [2] Помочь Юки с водой",0,0),
|
||||||
|
|
||||||
|
("День 1","day","narrator","neutral",
|
||||||
|
"Полдень. Солнце жжёт нещадно. Вы нашли кокосы.",0,0),
|
||||||
|
("День 1","day","Сакура","happy",
|
||||||
|
"УРА! Кокосы! Давай устроим пикник прямо здесь! Я так рада!",0,0),
|
||||||
|
("День 1","day","Юки","neutral",
|
||||||
|
"Кокосовое молоко восполнит электролиты. Рационально разделим поровну.",0,0),
|
||||||
|
("День 1","day","CHOICE","neutral","[1] Потанцевать с Сакурой / [2] Вместе составить карту острова с Юки",0,0),
|
||||||
|
|
||||||
|
("День 1","evening","narrator","neutral",
|
||||||
|
"Вечер. Костёр потрескивает. Звёзды зажигаются одна за другой.",0,0),
|
||||||
|
("День 1","evening","Сакура","happy",
|
||||||
|
"Смотри, как красиво! Знаешь... я рада, что ты рядом.",0,0),
|
||||||
|
("День 1","evening","Юки","neutral",
|
||||||
|
"По созвездиям можно определить стороны света. Это пригодится.",0,0),
|
||||||
|
("День 1","evening","CHOICE","neutral","[1] Смотреть на звёзды с Сакурой / [2] Учиться навигации у Юки",0,0),
|
||||||
|
|
||||||
|
("День 1","night","narrator","neutral",
|
||||||
|
"Ночь. Все устали. Ты засыпаешь под шум волн...",0,0),
|
||||||
|
|
||||||
|
# ДЕНЬ 2
|
||||||
|
("День 2","morning","narrator","neutral",
|
||||||
|
"Утро второго дня. Тебя будит Сакура — она уже приготовила фрукты!",0,0),
|
||||||
|
("День 2","morning","Сакура","happy",
|
||||||
|
"Доброе утро! Я нашла манго и бананы! Ешь, пожалуйста!",0,0),
|
||||||
|
("День 2","morning","Юки","neutral",
|
||||||
|
"Я слышала шум вертолёта ночью. Нужно сделать сигнальный знак на пляже.",0,0),
|
||||||
|
("День 2","morning","CHOICE","neutral","[1] Помочь Сакуре украсить лагерь / [2] Помочь Юки сделать SOS-знак",0,0),
|
||||||
|
|
||||||
|
("День 2","day","narrator","neutral",
|
||||||
|
"День. Начался дождь. Пришлось укрываться вместе в пальмовом шалаше.",0,0),
|
||||||
|
("День 2","day","Сакура","happy",
|
||||||
|
"Ахаха, так тесно! Это как приключение в аниме! Мне нравится!",0,0),
|
||||||
|
("День 2","day","Юки","neutral",
|
||||||
|
"Дождевую воду можно собирать. Я уже поставила листья как воронки.",0,0),
|
||||||
|
("День 2","day","CHOICE","neutral","[1] Рассказать Сакуре смешную историю / [2] Помочь Юки с системой сбора воды",0,0),
|
||||||
|
|
||||||
|
("День 2","evening","narrator","neutral",
|
||||||
|
"Вечер. Дождь стих. Все вышли на берег смотреть на закат.",0,0),
|
||||||
|
("День 2","evening","Сакура","happy",
|
||||||
|
"Правда ведь... я хотела бы остаться здесь чуть подольше...",0,0),
|
||||||
|
("День 2","evening","Юки","neutral",
|
||||||
|
"Завтра мы должны попробовать подать сигнал. Но сегодня... спасибо, что ты здесь.",0,0),
|
||||||
|
("День 2","evening","CHOICE","neutral","[1] Взять Сакуру за руку / [2] Сесть рядом с Юки",0,0),
|
||||||
|
|
||||||
|
("День 2","night","narrator","neutral",
|
||||||
|
"Ночь. Ты долго смотришь в звёздное небо, думая о них обеих...",0,0),
|
||||||
|
|
||||||
|
# ДЕНЬ 3
|
||||||
|
("День 3","morning","narrator","neutral",
|
||||||
|
"Третье утро. Вдали виден силуэт корабля!",0,0),
|
||||||
|
("День 3","morning","Юки","happy",
|
||||||
|
"Корабль! Мой сигнальный знак сработал! Нас спасут!",0,0),
|
||||||
|
("День 3","morning","Сакура","sad",
|
||||||
|
"Ура... хотя мне немного грустно. Это были лучшие дни в моей жизни.",0,0),
|
||||||
|
("День 3","morning","CHOICE","neutral","[1] Обнять Сакуру / [2] Сказать Юки, что она умница",0,0),
|
||||||
|
|
||||||
|
("День 3","day","narrator","neutral",
|
||||||
|
"Корабль приближается. Осталось несколько часов. Вы готовите плот.",0,0),
|
||||||
|
("День 3","day","Сакура","neutral",
|
||||||
|
"Эй... что будет, когда мы вернёмся? Мы ведь увидимся снова?",0,0),
|
||||||
|
("День 3","day","Юки","neutral",
|
||||||
|
"Я хочу сказать тебе кое-что важное, пока есть время...",0,0),
|
||||||
|
("День 3","day","CHOICE","neutral","[1] Пообещать Сакуре встретиться / [2] Выслушать Юки",0,0),
|
||||||
|
|
||||||
|
("День 3","evening","narrator","neutral",
|
||||||
|
"Корабль у берега. Это последний вечер на острове.",0,0),
|
||||||
|
("День 3","evening","CHOICE","neutral",
|
||||||
|
"[1] Попрощаться со всеми / [2] Остаться с Сакурой / [3] Остаться с Юки",0,0),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Очки за каждый выбор: (delta_sakura, delta_yuki)
|
||||||
|
CHOICE_POINTS = {
|
||||||
|
# day1 morning
|
||||||
|
0: {1:(3,0), 2:(0,3)},
|
||||||
|
# day1 day
|
||||||
|
1: {1:(3,0), 2:(0,3)},
|
||||||
|
# day1 evening
|
||||||
|
2: {1:(3,0), 2:(0,3)},
|
||||||
|
# day2 morning
|
||||||
|
3: {1:(3,0), 2:(0,3)},
|
||||||
|
# day2 day
|
||||||
|
4: {1:(3,0), 2:(0,3)},
|
||||||
|
# day2 evening
|
||||||
|
5: {1:(3,0), 2:(0,3)},
|
||||||
|
# day3 morning
|
||||||
|
6: {1:(3,0), 2:(0,3)},
|
||||||
|
# day3 day
|
||||||
|
7: {1:(3,0), 2:(0,3)},
|
||||||
|
# day3 evening — финальный выбор
|
||||||
|
8: {1:(0,0), 2:(5,0), 3:(0,5)},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Текстовая обёртка ─────────────────────────────────────────────────────────
|
||||||
|
def wrap_text(text, font, max_w):
|
||||||
|
words = text.split()
|
||||||
|
lines, line = [], ""
|
||||||
|
for w in words:
|
||||||
|
test = (line + " " + w).strip()
|
||||||
|
if font.size(test)[0] <= max_w:
|
||||||
|
line = test
|
||||||
|
else:
|
||||||
|
if line: lines.append(line)
|
||||||
|
line = w
|
||||||
|
if line: lines.append(line)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
# ── UI-компоненты ─────────────────────────────────────────────────────────────
|
||||||
|
def draw_textbox(surface, speaker, text, palette=None):
|
||||||
|
box = pygame.Rect(30, H-170, W-60, 150)
|
||||||
|
s = pygame.Surface((box.w, box.h), pygame.SRCALPHA)
|
||||||
|
s.fill((0,0,0,180))
|
||||||
|
surface.blit(s, (box.x, box.y))
|
||||||
|
pygame.draw.rect(surface, (200,200,200), box, 2, border_radius=8)
|
||||||
|
|
||||||
|
name_col = (255,200,100)
|
||||||
|
if speaker == "Сакура":
|
||||||
|
name_col = SAKURA_PALETTE[3]
|
||||||
|
elif speaker == "Юки":
|
||||||
|
name_col = YUKI_PALETTE[3]
|
||||||
|
elif speaker == "narrator":
|
||||||
|
name_col = (180,180,180)
|
||||||
|
|
||||||
|
disp_name = "" if speaker in ("narrator","CHOICE") else speaker
|
||||||
|
if disp_name:
|
||||||
|
ns = FONT_NAME.render(disp_name, True, name_col)
|
||||||
|
surface.blit(ns, (box.x+16, box.y+8))
|
||||||
|
|
||||||
|
lines = wrap_text(text, FONT_MED, box.w-30)
|
||||||
|
for i,l in enumerate(lines[:5]):
|
||||||
|
ts = FONT_MED.render(l, True, (255,255,255))
|
||||||
|
surface.blit(ts, (box.x+16, box.y+38+i*26))
|
||||||
|
|
||||||
|
def draw_hud(surface, ps, py):
|
||||||
|
# полоски очков
|
||||||
|
pygame.draw.rect(surface, (0,0,0,120), (10,10,220,60), border_radius=6)
|
||||||
|
pygame.draw.rect(surface, SAKURA_PALETTE[3], (80,18, min(ps*8,140),18), border_radius=4)
|
||||||
|
pygame.draw.rect(surface, YUKI_PALETTE[3], (80,42, min(py*8,140),18), border_radius=4)
|
||||||
|
surface.blit(FONT_SMALL.render(f"Сакура {ps:2d}", True, SAKURA_PALETTE[3]), (12,18))
|
||||||
|
surface.blit(FONT_SMALL.render(f"Юки {py:2d}", True, YUKI_PALETTE[3]), (12,42))
|
||||||
|
|
||||||
|
def draw_choice_buttons(surface, choices, hover):
|
||||||
|
bw, bh = 360, 44
|
||||||
|
start_y = H - 170 + 50
|
||||||
|
rects = []
|
||||||
|
for i, ch in enumerate(choices):
|
||||||
|
bx = W//2 - bw//2
|
||||||
|
by = start_y + i*(bh+10)
|
||||||
|
col = (80,130,200) if hover==i else (40,60,100)
|
||||||
|
pygame.draw.rect(surface, col, (bx,by,bw,bh), border_radius=8)
|
||||||
|
pygame.draw.rect(surface, (200,200,220),(bx,by,bw,bh), 2, border_radius=8)
|
||||||
|
ts = FONT_MED.render(ch, True, (255,255,255))
|
||||||
|
surface.blit(ts, (bx + bw//2 - ts.get_width()//2, by+bh//2-ts.get_height()//2))
|
||||||
|
rects.append(pygame.Rect(bx,by,bw,bh))
|
||||||
|
return rects
|
||||||
|
|
||||||
|
# ── Экран концовки ────────────────────────────────────────────────────────────
|
||||||
|
def ending_screen(surface, ps, py, last_choice):
|
||||||
|
surface.blit(BG["day"], (0,0))
|
||||||
|
|
||||||
|
if last_choice == 1: # уплываем одни
|
||||||
|
title = "Конец: Одиночное плавание"
|
||||||
|
desc = ("Ты помахал им рукой с борта корабля. Остров скрылся за горизонтом.\n"
|
||||||
|
"В душе — тёплые воспоминания о трёх необычных днях.\n"
|
||||||
|
"Может, судьба ещё сведёт вас снова...")
|
||||||
|
col = (200,200,220)
|
||||||
|
elif last_choice == 2 or (ps > py):
|
||||||
|
title = "Конец: Сердце Сакуры 🌸"
|
||||||
|
desc = ("Сакура прыгнула тебе на шею прямо на пирсе!\n"
|
||||||
|
"«Я знала! Я знала, что ты выберешь меня!»\n"
|
||||||
|
"Юки улыбнулась и тихо пожелала вам счастья.")
|
||||||
|
col = SAKURA_PALETTE[3]
|
||||||
|
draw_anime_girl(surface, 300, 420, SAKURA_PALETTE, "Сакура", "happy")
|
||||||
|
elif last_choice == 3 or (py > ps):
|
||||||
|
title = "Конец: Звезда Юки ❄️"
|
||||||
|
desc = ("Юки остановила тебя у трапа и тихо сказала:\n"
|
||||||
|
"«Я... хотела бы видеть тебя снова. Очень.»\n"
|
||||||
|
"Её щёки стали розовыми. Это было дороже всех слов.")
|
||||||
|
col = YUKI_PALETTE[3]
|
||||||
|
draw_anime_girl(surface, 700, 420, YUKI_PALETTE, "Юки", "happy", flip=True)
|
||||||
|
else:
|
||||||
|
title = "Конец: Три сердца"
|
||||||
|
desc = ("Вы все трое не смогли расстаться.\n"
|
||||||
|
"Решили вместе путешествовать на арендованной яхте.\n"
|
||||||
|
"Остров остался позади, а впереди — бесконечное море.")
|
||||||
|
col = (255,255,200)
|
||||||
|
draw_anime_girl(surface, 280, 420, SAKURA_PALETTE, "Сакура", "happy")
|
||||||
|
draw_anime_girl(surface, 720, 420, YUKI_PALETTE, "Юки", "happy", flip=True)
|
||||||
|
|
||||||
|
overlay = pygame.Surface((W, H), pygame.SRCALPHA)
|
||||||
|
overlay.fill((0,0,0,100))
|
||||||
|
surface.blit(overlay, (0,0))
|
||||||
|
|
||||||
|
ts = FONT_BIG.render(title, True, col)
|
||||||
|
surface.blit(ts, (W//2 - ts.get_width()//2, 60))
|
||||||
|
|
||||||
|
for i, line in enumerate(desc.split("\n")):
|
||||||
|
ls = FONT_MED.render(line, True, (255,255,255))
|
||||||
|
surface.blit(ls, (W//2 - ls.get_width()//2, 150 + i*35))
|
||||||
|
|
||||||
|
hint = FONT_SMALL.render("Нажми любую клавишу для выхода", True, (180,180,180))
|
||||||
|
surface.blit(hint, (W//2 - hint.get_width()//2, H-40))
|
||||||
|
|
||||||
|
score_s = FONT_SMALL.render(f"Очки: Сакура {ps} | Юки {py}", True, (200,200,200))
|
||||||
|
surface.blit(score_s, (W//2 - score_s.get_width()//2, H-65))
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
waiting = True
|
||||||
|
while waiting:
|
||||||
|
for e in pygame.event.get():
|
||||||
|
if e.type == pygame.QUIT: pygame.quit(); sys.exit()
|
||||||
|
if e.type in (pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN):
|
||||||
|
waiting = False
|
||||||
|
|
||||||
|
# ── Главный цикл ──────────────────────────────────────────────────────────────
|
||||||
|
def main():
|
||||||
|
ps, py = 0, 0 # очки
|
||||||
|
scene_idx = 0
|
||||||
|
choice_idx = 0 # счётчик выборов
|
||||||
|
wave_t = 0
|
||||||
|
hover = -1
|
||||||
|
last_final_choice = 1
|
||||||
|
|
||||||
|
# фильтруем сцены по индексу, собираем выборы
|
||||||
|
scene_list = []
|
||||||
|
for entry in STORY:
|
||||||
|
scene_list.append(entry)
|
||||||
|
|
||||||
|
current = 0
|
||||||
|
choice_mode = False
|
||||||
|
choice_options = []
|
||||||
|
choice_rects = []
|
||||||
|
|
||||||
|
def get_bg(tod):
|
||||||
|
return BG.get(tod, BG["day"])
|
||||||
|
|
||||||
|
def parse_choices(text):
|
||||||
|
parts = [p.strip() for p in text.split("/")]
|
||||||
|
result = []
|
||||||
|
for p in parts:
|
||||||
|
if p.startswith("["):
|
||||||
|
bracket_end = p.index("]")
|
||||||
|
result.append(p[bracket_end+1:].strip())
|
||||||
|
else:
|
||||||
|
result.append(p)
|
||||||
|
return result
|
||||||
|
|
||||||
|
running = True
|
||||||
|
while running and current < len(scene_list):
|
||||||
|
dt = clock.tick(60) / 1000
|
||||||
|
wave_t += dt
|
||||||
|
scene = scene_list[current]
|
||||||
|
day_label, tod, speaker, emotion, text, _, _ = scene
|
||||||
|
|
||||||
|
bg = get_bg(tod)
|
||||||
|
|
||||||
|
# events
|
||||||
|
for e in pygame.event.get():
|
||||||
|
if e.type == pygame.QUIT:
|
||||||
|
pygame.quit(); sys.exit()
|
||||||
|
if e.type == pygame.MOUSEMOTION and choice_mode:
|
||||||
|
hover = -1
|
||||||
|
for i,r in enumerate(choice_rects):
|
||||||
|
if r.collidepoint(e.pos): hover = i
|
||||||
|
if e.type == pygame.MOUSEBUTTONDOWN and e.button==1:
|
||||||
|
if choice_mode:
|
||||||
|
for i,r in enumerate(choice_rects):
|
||||||
|
if r.collidepoint(e.pos):
|
||||||
|
chosen = i+1
|
||||||
|
if choice_idx in CHOICE_POINTS:
|
||||||
|
dp = CHOICE_POINTS[choice_idx].get(chosen,(0,0))
|
||||||
|
ps += dp[0]; py += dp[1]
|
||||||
|
# для финального выбора запомним
|
||||||
|
if choice_idx == 8:
|
||||||
|
last_final_choice = chosen
|
||||||
|
choice_idx += 1
|
||||||
|
choice_mode = False
|
||||||
|
current += 1
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
current += 1
|
||||||
|
if e.type == pygame.KEYDOWN:
|
||||||
|
if not choice_mode:
|
||||||
|
current += 1
|
||||||
|
|
||||||
|
# draw
|
||||||
|
screen.blit(bg, (0,0))
|
||||||
|
# анимация волн поверх воды
|
||||||
|
draw_waves(screen, wave_t)
|
||||||
|
|
||||||
|
# персонажи
|
||||||
|
if speaker != "narrator" and speaker != "CHOICE":
|
||||||
|
if speaker == "Сакура":
|
||||||
|
draw_anime_girl(screen, 280, H-175, SAKURA_PALETTE, "Сакура", emotion)
|
||||||
|
elif speaker == "Юки":
|
||||||
|
draw_anime_girl(screen, W-280, H-175, YUKI_PALETTE, "Юки", emotion, flip=True)
|
||||||
|
|
||||||
|
# всегда рисуем обоих в фоне (маленьких)
|
||||||
|
if speaker == "narrator" or speaker == "CHOICE":
|
||||||
|
# рисуем обоих немного прозрачно
|
||||||
|
tmp = pygame.Surface((W,H), pygame.SRCALPHA)
|
||||||
|
draw_anime_girl(tmp, 220, H-175, SAKURA_PALETTE, "Сакура", "neutral")
|
||||||
|
draw_anime_girl(tmp, W-220, H-175, YUKI_PALETTE, "Юки", "neutral", flip=True)
|
||||||
|
tmp.set_alpha(120)
|
||||||
|
screen.blit(tmp, (0,0))
|
||||||
|
|
||||||
|
# день-лейбл
|
||||||
|
day_surf = FONT_BIG.render(day_label, True, (255,255,220))
|
||||||
|
screen.blit(day_surf, (W//2 - day_surf.get_width()//2, 12))
|
||||||
|
|
||||||
|
# HUD
|
||||||
|
draw_hud(screen, ps, py)
|
||||||
|
|
||||||
|
if speaker == "CHOICE":
|
||||||
|
choice_mode = True
|
||||||
|
choice_options = parse_choices(text)
|
||||||
|
choice_rects = draw_choice_buttons(screen, choice_options, hover)
|
||||||
|
else:
|
||||||
|
draw_textbox(screen, speaker, text)
|
||||||
|
hint = FONT_SMALL.render("[ Кликни или нажми любую клавишу ]", True, (160,160,160))
|
||||||
|
screen.blit(hint, (W//2-hint.get_width()//2, H-22))
|
||||||
|
choice_mode = False
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
# Концовка
|
||||||
|
ending_screen(screen, ps, py, last_final_choice)
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
970
jrpg/jrpg_game.py
Normal file
970
jrpg/jrpg_game.py
Normal file
@@ -0,0 +1,970 @@
|
|||||||
|
"""
|
||||||
|
Пиксельная деревня - 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()
|
||||||
1017
jrpg/jrpg_game_en.py
Normal file
1017
jrpg/jrpg_game_en.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user