This commit is contained in:
sShemet
2025-12-22 14:03:10 +05:00
commit ade2833df7
74 changed files with 42924 additions and 0 deletions

View File

@@ -0,0 +1,410 @@
import pygame
import sys
import os
import re
import random
# Инициализация Pygame
pygame.init()
# Настройки окна
WIDTH, HEIGHT = 1280, 720
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Визуальная новелла")
# Цвета
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
LIGHT_BLUE = (173, 216, 230)
DARK_BLUE = (0, 0, 139)
CHOICE_BG = (240, 240, 255)
# Шрифты
font = pygame.font.Font(None, 36)
character_font = pygame.font.Font(None, 32)
choice_font = pygame.font.Font(None, 30)
small_font = pygame.font.Font(None, 24)
class NovelEngine:
def __init__(self):
self.script = []
self.current_line = 0
self.running = True
self.background = None
self.characters = {}
self.variables = {}
self.choices = []
self.waiting_for_choice = False
self.call_stack = [] # Стек для хранения позиций возврата
self.text_box_rect = pygame.Rect(50, HEIGHT - 250, WIDTH - 100, 200)
self.choice_box_rect = pygame.Rect(WIDTH//2 - 300, HEIGHT//2 - 150, 600, 300)
def load_script(self, filename):
"""Загрузка скрипта из файла"""
self.script = []
script_path = os.path.join(os.path.dirname(__file__), filename)
if not os.path.exists(script_path):
print(f"Ошибка: файл {script_path} не найден!")
return False
with open(script_path, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
if line and not line.startswith('#'):
self.script.append(line)
return True
def parse_character(self, command):
"""Обработка описания персонажа с дополнительными атрибутами"""
if command.startswith("char "):
parts = command[5:].split("|")
char_id = parts[0].strip().split(":")[0]
char_data = {
"name": parts[0].split(":")[1].strip(),
"appearance": parts[1].strip() if len(parts) > 1 else "",
"details": parts[2].strip() if len(parts) > 2 else "",
"scent": parts[3].strip() if len(parts) > 3 else ""
}
self.characters[char_id] = char_data
return True
return False
def parse_command(self, command):
"""Обработка команд скрипта"""
# Случайный переход в подпрограмму
if command.startswith("random_gosub "):
parts = command[12:].split()
if len(parts) >= 2:
probability = float(parts[0])
if random.random() < probability:
# Сохраняем текущую позицию для возврата
self.call_stack.append(self.current_line + 1)
# Переходим к случайной подпрограмме
sub_label = random.choice(parts[1:])
self.jump_to_label(sub_label)
return True
# Возврат из подпрограммы
elif command == "return":
if self.call_stack:
self.current_line = self.call_stack.pop()
else:
print("Ошибка: стек вызовов пуст!")
return True
# Установка переменной
elif command.startswith("set "):
parts = command[4:].split("|")
for part in parts:
if "=" in part:
var, val = part.split("=", 1)
self.variables[var.strip()] = val.strip()
return True
# Условие
elif command.startswith("if "):
match = re.match(r'if (\w+)([=!<>]+)(.+?) then goto (\w+)', command[3:])
if match:
var_name, op, value, label = match.groups()
current_value = self.variables.get(var_name, "")
condition_met = False
if op == "==":
condition_met = str(current_value) == value
elif op == "!=":
condition_met = str(current_value) != value
elif op == ">":
condition_met = float(current_value) > float(value)
elif op == "<":
condition_met = float(current_value) < float(value)
if condition_met:
self.jump_to_label(label)
return True
# Метка
elif command.startswith("label "):
return True
# Подпрограмма
elif command.startswith("sub_"):
return True
# Переход
elif command.startswith("goto "):
label = command[5:].strip()
self.jump_to_label(label)
return True
# Фон
elif command.startswith("bg "):
bg_color = command[3:].strip().lower()
if bg_color == "black":
self.background = BLACK
elif bg_color == "white":
self.background = WHITE
elif bg_color == "blue":
self.background = LIGHT_BLUE
elif bg_color == "sunset":
self.background = (255, 153, 51) # Оранжевый для заката
else:
self.background = GRAY
return True
# Персонаж
elif command.startswith("char "):
return self.parse_character(command)
return False
def draw_descriptions(self, text, x, y, font_obj, color):
"""Отрисовка текста с учётом звёздочек"""
if text.startswith("*") and text.endswith("*"):
# Рисуем курсивом для описаний
italic_font = pygame.font.Font(None, 32)
italic_font.set_italic(True)
self.draw_text(text[1:-1], x, y, italic_font, color)
else:
self.draw_text(text, x, y, font_obj, color)
def jump_to_label(self, label):
"""Переход к метке в скрипте"""
for i, line in enumerate(self.script):
if line.startswith(f"@label {label}") or line.startswith(f"@{label}"):
self.current_line = i
return
print(f"Метка '{label}' не найдена!")
def process_choices(self, line):
"""Обработка строки с выбором"""
if line.startswith("choice "):
choices_text = line[7:].split("|")
self.choices = []
for choice_text in choices_text:
parts = choice_text.split("=>")
if len(parts) == 2:
choice_display = parts[0].strip()
choice_action = parts[1].strip()
self.choices.append((choice_display, choice_action))
self.waiting_for_choice = True
return True
return False
def substitute_variables(self, text):
"""Подстановка переменных в текст"""
for var_name, var_value in self.variables.items():
text = text.replace(f"{{{var_name}}}", str(var_value))
return text
def get_character_info(self, char_id):
"""Возвращает полное описание персонажа"""
if char_id not in self.characters:
return f"Персонаж {char_id}", ""
char = self.characters.get(char_id)
if isinstance(char, str):
return char, ""
details = []
if char["appearance"]: details.append(char['appearance'])
if char["details"]: details.append(char['details'])
if char["scent"]: details.append(char['scent'])
return char["name"], " | ".join(details)
def process_line(self):
if self.current_line >= len(self.script):
self.running = False
return None
line = self.script[self.current_line]
line = self.substitute_variables(line)
# Обработка команд
if line.startswith("@"):
if self.parse_command(line[1:]):
self.current_line += 1
return self.process_line()
return None
# Обработка выбора
if self.process_choices(line):
self.current_line += 1
return None
# Обработка диалога с проверкой разделителя
if ":" in line and not line.startswith("*"):
parts = line.split(":", 1)
if len(parts) == 2:
char_id = parts[0].strip()
text = parts[1].strip()
return char_id, text
# Обработка описания или простого текста
return None, line
def make_choice(self, choice_index):
"""Обработка выбора игрока"""
if 0 <= choice_index < len(self.choices):
choice_action = self.choices[choice_index][1]
if choice_action.startswith("goto "):
label = choice_action[5:].strip()
self.jump_to_label(label)
elif "+=" in choice_action:
var, value = choice_action.split("+=", 1)
var = var.strip()
self.variables[var] = str(int(self.variables.get(var, 0)) + int(value.strip()))
self.current_line += 1
elif "-=" in choice_action:
var, value = choice_action.split("-=", 1)
var = var.strip()
self.variables[var] = str(int(self.variables.get(var, 0)) - int(value.strip()))
self.current_line += 1
elif "=" in choice_action:
var, value = choice_action.split("=", 1)
self.variables[var.strip()] = value.strip()
self.current_line += 1
self.choices = []
self.waiting_for_choice = False
def next_line(self):
"""Переход к следующей строке"""
self.current_line += 1
if self.current_line >= len(self.script):
self.running = False
def draw_text_box(self):
"""Отрисовка текстового поля"""
pygame.draw.rect(screen, WHITE, self.text_box_rect)
pygame.draw.rect(screen, DARK_BLUE, self.text_box_rect, 3)
def draw_choice_box(self):
"""Отрисовка поля выбора"""
pygame.draw.rect(screen, CHOICE_BG, self.choice_box_rect)
pygame.draw.rect(screen, DARK_BLUE, self.choice_box_rect, 3)
def draw_text(self, text, x, y, font_obj, color=BLACK, max_width=None):
"""Отрисовка текста с переносом строк"""
if max_width is None:
max_width = self.text_box_rect.width - 40
words = text.split(' ')
space_width = font_obj.size(' ')[0]
line_height = font_obj.get_height()
current_line = []
current_width = 0
for word in words:
word_surface = font_obj.render(word, True, color)
word_width = word_surface.get_width()
if current_width + word_width <= max_width:
current_line.append(word)
current_width += word_width + space_width
else:
line_text = ' '.join(current_line)
line_surface = font_obj.render(line_text, True, color)
screen.blit(line_surface, (x, y))
y += line_height
current_line = [word]
current_width = word_width + space_width
if current_line:
line_text = ' '.join(current_line)
line_surface = font_obj.render(line_text, True, color)
screen.blit(line_surface, (x, y))
def draw(self):
"""Отрисовка текущего состояния"""
# Фон
bg_color = self.background if self.background else LIGHT_BLUE
screen.fill(bg_color)
# Текстовое поле
self.draw_text_box()
# Текст
line_data = self.process_line()
x = self.text_box_rect.x + 20
y = self.text_box_rect.y + 20
if line_data and not self.waiting_for_choice:
if isinstance(line_data, tuple): # Диалог с персонажем
char_id, text = line_data
name, details = self.get_character_info(char_id)
# Отрисовка имени персонажа
name_surface = character_font.render(name + ":", True, DARK_BLUE)
screen.blit(name_surface, (x, y))
y += 40
# Отрисовка текста персонажа
self.draw_text(text, x, y, font, BLACK)
y += 60
# Отрисовка деталей персонажа (если есть)
if details:
details_surface = small_font.render(details, True, (100, 100, 100))
screen.blit(details_surface, (x, y))
else: # Описание или простой текст
_, text = line_data
self.draw_descriptions(text, x, y, font, BLACK)
# Варианты выбора
if self.waiting_for_choice and self.choices:
self.draw_choice_box()
choice_y = self.choice_box_rect.y + 30
for i, (choice_text, _) in enumerate(self.choices):
choice_surface = choice_font.render(f"{i+1}. {choice_text}", True, BLACK)
screen.blit(choice_surface, (self.choice_box_rect.x + 30, choice_y))
choice_y += 40
# Индикатор продолжения
if self.running and not self.waiting_for_choice and line_data:
indicator = font.render("> Нажмите ПРОБЕЛ", True, DARK_BLUE)
screen.blit(indicator, (WIDTH - 200, HEIGHT - 50))
pygame.display.flip()
def run(self):
"""Основной цикл"""
clock = pygame.time.Clock()
while self.running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and not self.waiting_for_choice:
self.next_line()
elif event.key == pygame.K_1 and self.waiting_for_choice and len(self.choices) >= 1:
self.make_choice(0)
elif event.key == pygame.K_2 and self.waiting_for_choice and len(self.choices) >= 2:
self.make_choice(1)
elif event.key == pygame.K_3 and self.waiting_for_choice and len(self.choices) >= 3:
self.make_choice(2)
elif event.key == pygame.K_4 and self.waiting_for_choice and len(self.choices) >= 4:
self.make_choice(3)
elif event.key == pygame.K_ESCAPE:
self.running = False
self.draw()
clock.tick(60)
# Создаем экземпляр движка и запускаем
if __name__ == "__main__":
engine = NovelEngine()
if engine.load_script("island_script.txt"):
engine.run()
pygame.quit()
sys.exit()