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()