Files
UltraChat/tet/novel.py
sShemet ade2833df7 init
2025-12-22 14:03:10 +05:00

621 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pygame
import sys
import os
import re
import random
# Инициализация Pygame
pygame.init()
# Настройки окна
WIDTH, HEIGHT = 1920, 1080
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)
NIGHT_BLUE = (5, 5, 30)
MORNING_YELLOW = (255, 220, 100)
MORNING_GREEN = (100, 180, 80)
MORNING_DARK_BLUE = (20, 40, 100)
DAY_GREEN = (100, 200, 80)
DAY_BLUE = (100, 200, 255)
EVENING_ORANGE = (255, 120, 50)
EVENING_DARK_GREEN = (30, 80, 40)
# Шрифты
try:
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)
italic_font = pygame.font.Font(None, 32)
italic_font.set_italic(True)
except:
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)
italic_font = pygame.font.Font(None, 32)
italic_font.set_italic(True)
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)
self.special_background = None
self.background_textures = {} # Текстуры для сложных фонов
self.init_background_textures() # Инициализируем текстуры
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 init_background_textures(self):
"""Создаем текстуры для сложных фонов один раз при инициализации"""
# Ночное небо
night_surface = pygame.Surface((WIDTH, HEIGHT))
night_surface.fill(NIGHT_BLUE)
for _ in range(100):
x = random.randint(0, WIDTH)
y = random.randint(0, HEIGHT // 2)
size = random.randint(1, 3)
brightness = random.randint(200, 255)
pygame.draw.circle(night_surface, (brightness, brightness, brightness), (x, y), size)
self.background_textures["night"] = night_surface
morning_surface = pygame.Surface((WIDTH, HEIGHT))
for y in range(HEIGHT):
ratio = y / HEIGHT
# Верхняя часть (темно-синяя)
if ratio < 0.3:
r = int(MORNING_DARK_BLUE[0])
g = int(MORNING_DARK_BLUE[1])
b = int(MORNING_DARK_BLUE[2])
# Средняя часть (переход)
elif ratio < 0.7:
r = int(MORNING_DARK_BLUE[0] + (MORNING_YELLOW[0] - MORNING_DARK_BLUE[0]) * (ratio - 0.3) / 0.4)
g = int(MORNING_DARK_BLUE[1] + (MORNING_YELLOW[1] - MORNING_DARK_BLUE[1]) * (ratio - 0.3) / 0.4)
b = int(MORNING_DARK_BLUE[2] + (MORNING_YELLOW[2] - MORNING_DARK_BLUE[2]) * (ratio - 0.3) / 0.4)
# Нижняя часть (зеленая)
else:
r = int(MORNING_GREEN[0])
g = int(MORNING_GREEN[1])
b = int(MORNING_GREEN[2])
pygame.draw.line(morning_surface, (r, g, b), (0, y), (WIDTH, y))
pygame.draw.circle(morning_surface, MORNING_YELLOW, (WIDTH // 2, HEIGHT // 3), 50)
self.background_textures["morning"] = morning_surface
day_surface = pygame.Surface((WIDTH, HEIGHT))
# Градиент от голубого к зеленому
for y in range(HEIGHT):
ratio = y / HEIGHT
if ratio < 0.8:
r = int(DAY_BLUE[0])
g = int(DAY_BLUE[1])
b = int(DAY_BLUE[2])
else:
r = int(DAY_GREEN[0])
g = int(DAY_GREEN[1])
b = int(DAY_GREEN[2])
pygame.draw.line(day_surface, (r, g, b), (0, y), (WIDTH, y))
# Рисуем облака
for _ in range(5):
x = random.randint(0, WIDTH)
y = random.randint(50, HEIGHT // 3)
size = random.randint(30, 70)
pygame.draw.circle(day_surface, WHITE, (x, y), size)
pygame.draw.circle(day_surface, WHITE, (x + size//2, y - size//3), size//2)
pygame.draw.circle(day_surface, WHITE, (x - size//2, y - size//4), size//2)
self.background_textures["day"] = day_surface
evening_surface = pygame.Surface((WIDTH, HEIGHT))
for y in range(HEIGHT):
ratio = y / HEIGHT
if ratio < 0.5:
r = int(EVENING_ORANGE[0])
g = int(EVENING_ORANGE[1])
b = int(EVENING_ORANGE[2])
else:
r = int(EVENING_ORANGE[0] + (EVENING_DARK_GREEN[0] - EVENING_ORANGE[0]) * (ratio - 0.5) / 0.5)
g = int(EVENING_ORANGE[1] + (EVENING_DARK_GREEN[1] - EVENING_ORANGE[1]) * (ratio - 0.5) / 0.5)
b = int(EVENING_ORANGE[2] + (EVENING_DARK_GREEN[2] - EVENING_ORANGE[2]) * (ratio - 0.5) / 0.5)
pygame.draw.line(evening_surface, (r, g, b), (0, y), (WIDTH, y))
# Рисуем заходящее солнце
pygame.draw.circle(evening_surface, (255, 200, 100), (WIDTH // 2, HEIGHT // 2), 60)
pygame.draw.rect(evening_surface, EVENING_DARK_GREEN, (0, HEIGHT // 2, WIDTH, HEIGHT // 2))
self.background_textures["evening"] = evening_surface
def draw_night_sky(self):
"""Рисует ночное небо со звездами"""
screen.blit(self.background_textures["night"], (0, 0))
def draw_morning_sky(self):
"""Рисует утреннее небо с градиентом"""
screen.blit(self.background_textures["morning"], (0, 0))
def draw_day_sky(self):
"""Рисует дневное небо с облаками"""
screen.blit(self.background_textures["day"], (0, 0))
def draw_evening_sky(self):
"""Рисует вечернее небо с закатом"""
screen.blit(self.background_textures["evening"], (0, 0))
def parse_command(self, command):
"""Обработка команд скрипта"""
# Случайный переход в подпрограмму
if command.startswith("random_gosub "):
parts = command[12:].split()
if len(parts) >= 2:
# Создаем список вариантов с их вероятностями
options = []
probabilities = []
labels = []
# Разбираем части на вероятности и метки
i = 0
while i < len(parts):
try:
prob = float(parts[i])
label = parts[i+1]
probabilities.append(prob)
labels.append(label)
options.append((prob, label))
i += 2
except (ValueError, IndexError):
break
if options:
# Нормализуем вероятности (на случай, если они не суммируются в 1)
total = sum(prob for prob, label in options)
if total > 0:
rand = random.random() * total
cumulative = 0
for prob, label in options:
cumulative += prob
if rand <= cumulative:
# Сохраняем текущую позицию для возврата
self.call_stack.append(self.current_line + 1)
# Переходим к выбранной подпрограмме
self.jump_to_label(label)
break
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:
part = part.strip()
if "+=" in part:
var, val = part.split("+=", 1)
var = var.strip()
current = int(self.variables.get(var, 0))
self.variables[var] = str(current + int(val.strip()))
elif "-=" in part:
var, val = part.split("-=", 1)
var = var.strip()
current = int(self.variables.get(var, 0))
self.variables[var] = str(current - int(val.strip()))
elif "=" in part:
var, val = part.split("=", 1)
var = var.strip()
val = val.strip()
# Обработка инкремента (++var)
if val.startswith("++"):
var_to_inc = val[2:]
self.variables[var] = str(int(self.variables.get(var_to_inc, 0)) + 1)
# Обработка декремента (--var)
elif val.startswith("--"):
var_to_dec = val[2:]
self.variables[var] = str(int(self.variables.get(var_to_dec, 0)) - 1)
# Обычное присваивание
else:
self.variables[var] = val
print(f"После выполнения '{command}': {self.variables}") # Отладочный вывод
return True
# Условие
elif command.startswith("if "):
# ИСПРАВЛЕННЫЙ regex с обработкой пробелов
match = re.match(r'if\s+(\w+)\s*([=!<>]+)\s*(.+?)\s+then\s+goto\s+(\w+)', command)
if match:
var_name, op, value, label = match.groups()
current_value = self.variables.get(var_name, "0")
print(f"Отладка: if {var_name}({current_value}) {op} {value} then goto {label}") # Отладка
# Пробуем численное сравнение
try:
current_num = float(current_value)
value_num = float(value)
if op == ">": condition_met = current_num > value_num
elif op == "<": condition_met = current_num < value_num
elif op == ">=": condition_met = current_num >= value_num
elif op == "<=": condition_met = current_num <= value_num
elif op == "==": condition_met = current_num == value_num
elif op == "!=": condition_met = current_num != value_num
else: condition_met = False
except ValueError:
# Строковое сравнение
if op == ">": condition_met = current_value > value
elif op == "<": condition_met = current_value < value
elif op == ">=": condition_met = current_value >= value
elif op == "<=": condition_met = current_value <= value
elif op == "==": condition_met = current_value == value
elif op == "!=": condition_met = current_value != value
else: condition_met = False
if condition_met:
print(f"Условие выполнено, переход к {label}") # Отладка
self.jump_to_label(label)
else:
print("Условие не выполнено") # Отладка
return True
# Метка
elif command.startswith("label "):
return True
# Подпрограмма
elif command.startswith("sub_"):
if not self.call_stack or self.call_stack[-1] != self.current_line:
self.call_stack.append(self.current_line)
return True
# Возврат из подпрограммы
elif command == "return":
if not self.call_stack:
print("Предупреждение: return без вызова подпрограммы, пропускаем")
self.current_line += 1
else:
self.current_line = self.call_stack.pop() + 1 # Возвращаемся на следующую строку
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
self.special_background = None
elif bg_color == "white":
self.background = WHITE
self.special_background = None
elif bg_color == "blue":
self.background = LIGHT_BLUE
self.special_background = None
elif bg_color == "night":
self.special_background = "night"
elif bg_color == "morning":
self.special_background = "morning"
elif bg_color == "day":
self.special_background = "day"
elif bg_color == "evening" or bg_color == "sunset":
self.special_background = "evening"
else:
self.background = GRAY
self.special_background = None
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("*"):
# Удаляем звёздочки и рисуем курсивом
self.draw_text(text[1:-1], x, y, italic_font, (150, 150, 150))
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):
"""Возвращает полное описание персонажа"""
# Убираем вывод "Персонаж none" для несуществующих ID
if char_id not in self.characters:
return "", "" # Возвращаем пустые строки
char = self.characters.get(char_id)
if isinstance(char, str):
return char, ""
details = []
if char.get("appearance"): details.append(char['appearance'])
if char.get("details"): details.append(char['details'])
if char.get("scent"): details.append(char['scent'])
return char.get("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):
"""Отрисовка поля выбора"""
# Создаем полупрозрачную поверхность
s = pygame.Surface((600, 300), pygame.SRCALPHA)
s.fill((240, 240, 255, 230)) # Последний параметр - прозрачность
screen.blit(s, (self.choice_box_rect.x, self.choice_box_rect.y))
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):
"""Отрисовка текущего состояния"""
# Фон
if self.special_background and self.special_background in self.background_textures:
screen.blit(self.background_textures[self.special_background], (0, 0))
elif self.background:
screen.fill(self.background)
else:
screen.fill(LIGHT_BLUE)
# Текстовое поле (убираем очистку перед выбором)
if not self.waiting_for_choice:
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)
# Отрисовываем имя только если оно есть
if name:
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()