/
Lekuz
/
FinalQualifyingWork
Обзор
Документация
Войти
/
Lekuz
/
FinalQualifyingWork
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Application.py
2 016 строк
92 KB
Lekuz
Загрузил файлы с исходными кодами
12 июн 2025, 16:08
12 июн 2025, 16:08
d82ac7c
Код
Авторство
О чём код?
import tkinter as tk from tkinter import ttk, filedialog, messagebox import pandas as pd import random from random import randint import os import json import psycopg2 import datetime import collections import re from Inventory import Sword, Wallet, Staff from typing import Dict PARAM_NAMES = [ "Сила", "Тело", "Точность", "Подвижность", "Интеллект", "Сообразительность", "Восприятие", "Фантазия", "Воля", "Харизма", "Внешность" ] SKILL_NAMES = [ "Риторика", "Дипломатия", "Хитрость", "Эмпатия", "Внимательность", "Техника", "Вычисление", "Анализ", "Моделирование", "Искусство", "Исполнение", "Мелкая моторика", "Ремесло", "Лабораторная работа", "Рукопашный бой", "Холодное оружие", "Огнестрельное оружие", "Атлетика", "Акробатика", "Ловкость рук", "Скрытность", "Интуиция", "Медитация", "Ментальный контроль", "Стойкость", "Мыслеобраз" ] RACE_DATA = { "Люди": { "min_params": [1] * 11, "max_params": [5, 5, 5, 5, 5, 6, 5, 6, 5, 5, 5], "param_points": 12, "skill_points": 15, "names": [ "Моусабель", "Тасселандра", "Ната Серпент", "Хукир Наламбар", "Мадислак Улмокина" ] }, "Эльфы": { "min_params": [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2], "max_params": [5, 5, 5, 5, 5, 5, 6, 5, 5, 5, 7], "param_points": 9, "skill_points": 12, "names": [ "Рилеф Селеварун", "Меларю Летний Бриз", "Шиидра Ильфелкиир", "Вин Экорн", "Сейлас Катдейрин" ] }, "Гномы": { "min_params": [2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "max_params": [6, 6, 5, 5, 6, 5, 5, 5, 5, 5, 5], "param_points": 10, "skill_points": 12, "names": [ 'Боддинок', "Эллижобель", 'Глим "Работяга"', "Келлен", "Бимпноттин" ] }, "Орки": { "min_params": [2, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1], "max_params": [7, 6, 5, 5, 5, 5, 5, 4, 7, 4, 4], "param_points": 10, "skill_points": 12, "names": ["Хенк", "Шамп", "Фенг", "Оунка", "Вола"] }, "Нежить": { "min_params": [1] * 11, "max_params": [9, 3, 4, 4, 5, 4, 7, 3, 4, 3, 3], "param_points": 9, "skill_points": 6, "names": [ "Морелл Клемонс", "Маурус Линдсей", "Камрен Кервин", "Джастиш Гнаш", "Берти Арк" ] } } DB_CONFIG_FILE = "db_config.json" APP_INSTANCE = None def load_postgres_config(): """Загружает конфигурацию подключения к PostgreSQL из файла""" default_config = { "host": "127.0.0.1", "user": "postgres", "password": "qweqwe", "database": "dnd_characters", "port": "5433" } if os.path.exists(DB_CONFIG_FILE): try: with open(DB_CONFIG_FILE, 'r') as f: return json.load(f) except Exception as e: print(f"Ошибка чтения {DB_CONFIG_FILE}: {e}") return default_config def save_postgres_config(config): """Сохраняет конфигурацию подключения к PostgreSQL в файл""" try: with open(DB_CONFIG_FILE, 'w') as f: json.dump(config, f, indent=4) except Exception as e: print(f"Ошибка сохранения {DB_CONFIG_FILE}: {e}") POSTGRES_CONFIG = load_postgres_config() def check_postgres_connection(config): """Проверяет соединение с PostgreSQL""" try: conn = psycopg2.connect(**config) conn.close() return True except Exception as e: print(f"Ошибка подключения к PostgreSQL: {e}") return False def center_window(window, width, height): """Центрирует окно на экране""" screen_width = window.winfo_screenwidth() screen_height = window.winfo_screenheight() x = (screen_width - width) // 2 y = (screen_height - height) // 2 window.geometry(f"{width}x{height}+{x}+{y}") window.resizable(False, False) def sanitize_filename(name): """Заменяет в имени файла недопустимые символы на '_'""" return re.sub(r'[<>:"/\\|?*]', '_', name) class Character: def __init__(self, race_name: str): """Создает объект персонажа выбранной расы""" self.race = race_name race_data = RACE_DATA[race_name] self.name = random.choice(race_data["names"]) self.params = {name: race_data["min_params"][i] for i, name in enumerate(PARAM_NAMES)} self.skills = {name: 0 for name in SKILL_NAMES} self.spare_param_points = race_data["param_points"] self.spare_skill_points = race_data["skill_points"] self.active_skills = [] self.inventory = [] def get_min_param(self, param_name: str) -> int: """Возвращает минимальное значение параметра для текущей расы персонажа""" param_index = PARAM_NAMES.index(param_name) return RACE_DATA[self.race]["min_params"][param_index] def get_max_param(self, param_name: str) -> int: """Возвращает максимальное значение параметра для текущей расы персонажа""" param_index = PARAM_NAMES.index(param_name) return RACE_DATA[self.race]["max_params"][param_index] def can_increase_param(self, param_name: str) -> bool: """Проверяет, можно ли увеличить указанный параметр""" return (self.params[param_name] < self.get_max_param(param_name) and self.spare_param_points > 0) def increase_param(self, param_name: str) -> None: """Увеличивает указанный параметр""" if self.can_increase_param(param_name): self.params[param_name] += 1 self.spare_param_points -= 1 def can_decrease_param(self, param_name: str) -> bool: """Проверяет, можно ли уменьшить указанный параметр""" return self.params[param_name] > self.get_min_param(param_name) def decrease_param(self, param_name: str) -> None: """Уменьшает указанный параметр""" if self.can_decrease_param(param_name): self.params[param_name] -= 1 self.spare_param_points += 1 def can_increase_skill(self, skill_name: str) -> bool: """Проверяет, можно ли увеличить указанный навык""" return (self.spare_skill_points > 0 and (skill_name not in self.active_skills or self.skills[skill_name] < 5)) def increase_skill(self, skill_name: str) -> None: """Увеличивает указанный навык""" if self.can_increase_skill(skill_name): if skill_name not in self.active_skills: self.active_skills.append(skill_name) self.skills[skill_name] = 1 else: self.skills[skill_name] += 1 self.spare_skill_points -= 1 def can_decrease_skill(self, skill_name: str) -> bool: """Проверяет, можно ли уменьшить указанный навык""" return skill_name in self.active_skills and self.skills[skill_name] > 1 def decrease_skill(self, skill_name: str) -> None: """Уменьшает указанный навык""" if self.can_decrease_skill(skill_name): self.skills[skill_name] -= 1 self.spare_skill_points += 1 def remove_skill(self, skill_name: str) -> None: """Удаляет выбранный навык и возвращает все потраченные на него очки навыков""" if skill_name in self.active_skills: self.spare_skill_points += self.skills[skill_name] self.active_skills.remove(skill_name) self.skills[skill_name] = 0 def auto_allocate_params(self): """Автоматически распределяет свободные очки параметров персонажа""" param_limits = collections.defaultdict(int) remaining_points = self.spare_param_points while remaining_points > 0: param = random.choice(PARAM_NAMES) if (param_limits[param] < 2 and self.params[param] < self.get_max_param(param)): self.params[param] += 1 param_limits[param] += 1 remaining_points -= 1 self.spare_param_points = remaining_points def auto_allocate_skills(self): """Автоматически распределяет свободные очки навыков персонажа""" skill_limits = collections.defaultdict(int) remaining_points = self.spare_skill_points while remaining_points > 0: skill = random.choice(SKILL_NAMES) if skill not in self.active_skills: self.active_skills.append(skill) self.skills[skill] = 1 skill_limits[skill] = 1 remaining_points -= 1 elif skill_limits[skill] < 3 and self.skills[skill] < 5: self.skills[skill] += 1 skill_limits[skill] += 1 remaining_points -= 1 self.spare_skill_points = remaining_points def to_dataframe(self) -> pd.DataFrame: """Формирует DataFrame pandas для сохранения персонажа в Excel""" df = pd.DataFrame(index=range(33), columns=['A', 'B']) df['A'][0] = self.race df['A'][1:12] = PARAM_NAMES df['A'][12] = 'Свободные очки:' df['A'][14:14 + len(self.active_skills)] = self.active_skills df['A'][14 + len(self.active_skills)] = 'Свободные очки:' df['B'][0] = self.name df['B'][1:12] = [self.params[name] for name in PARAM_NAMES] df['B'][12] = self.spare_param_points df['B'][14:14 + len(self.active_skills)] = [self.skills[name] for name in self.active_skills] df['B'][14 + len(self.active_skills)] = self.spare_skill_points df['A'][14 + len(self.active_skills) + 2] = 'Инвентарь:' for i, item in enumerate(self.inventory): if isinstance(item, Wallet): df['A'][14 + len(self.active_skills) + 3 + i*4] = f"{item.Name} (Кошель)" df['B'][14 + len(self.active_skills) + 3 + i*4] = "Золото:" df['B'][14 + len(self.active_skills) + 4 + i*4] = item.Gold else: item_type = "Меч" if isinstance(item, Sword) else "Жезл" df['A'][14 + len(self.active_skills) + 3 + i*7] = f"{item.Name} ({item_type})" df['B'][14 + len(self.active_skills) + 3 + i*7] = "Уровень:" df['B'][14 + len(self.active_skills) + 4 + i*7] = item.Lvl df['B'][14 + len(self.active_skills) + 5 + i*7] = "Материал:" df['B'][14 + len(self.active_skills) + 6 + i*7] = item.Material df['B'][14 + len(self.active_skills) + 7 + i*7] = "Зачарование:" df['B'][14 + len(self.active_skills) + 8 + i*7] = item.Magic df['B'][14 + len(self.active_skills) + 9 + i*7] = "Урон:" df['B'][14 + len(self.active_skills) + 10 + i*7] = item.Damage if item.MagicDamage > 0: df['B'][14 + len(self.active_skills) + 11 + i*7] = "Маг. урон:" df['B'][14 + len(self.active_skills) + 12 + i*7] = item.MagicDamage return df @classmethod def from_file(cls, file_path: str) -> 'Character': """Загружает персонажа из Excel-файла""" df = pd.read_excel(file_path, header=None) race = df.iloc[0, 0] char = cls(race) char.name = df.iloc[0, 1] for i, param in enumerate(PARAM_NAMES): value = df.iloc[i+1, 1] char.params[param] = int(value) if pd.notna(value) else char.get_min_param(param) spare_params = df.iloc[12, 1] char.spare_param_points = int(spare_params) if pd.notna(spare_params) else 0 skills = [] skill_values = [] i = 14 while i < len(df) and pd.notna(df.iloc[i, 0]) and df.iloc[i, 0] != 'Свободные очки:': skill = df.iloc[i, 0] value = df.iloc[i, 1] if skill in SKILL_NAMES and pd.notna(value): skills.append(skill) skill_values.append(int(value)) i += 1 for skill, value in zip(skills, skill_values): char.active_skills.append(skill) char.skills[skill] = value if i < len(df) and df.iloc[i, 0] == 'Свободные очки:': spare_skills = df.iloc[i, 1] char.spare_skill_points = int(spare_skills) if pd.notna(spare_skills) else 0 i += 2 while i < len(df): item_desc = df.iloc[i, 0] if pd.isna(item_desc): i += 1 continue if "(Кошель)" in str(item_desc): name = str(item_desc).replace(" (Кошель)", "") gold = df.iloc[i+1, 1] wallet = Wallet() wallet.Name = name wallet.Gold = int(gold) if pd.notna(gold) else 0 char.inventory.append(wallet) i += 2 elif "Меч" in str(item_desc) or "Жезл" in str(item_desc): name = str(item_desc).split(" (")[0] if "Меч" in str(item_desc): item = Sword() else: item = Staff() item.Name = name if i+1 < len(df) and df.iloc[i+1, 0] == "Уровень:": item.Lvl = int(df.iloc[i+2, 1]) if pd.notna(df.iloc[i+2, 1]) else 1 if i+3 < len(df) and df.iloc[i+3, 0] == "Материал:": item.Material = str(df.iloc[i+4, 1]) if pd.notna(df.iloc[i+4, 1]) else item.Material if i+5 < len(df) and df.iloc[i+5, 0] == "Зачарование:": item.Magic = str(df.iloc[i+6, 1]) if pd.notna(df.iloc[i+6, 1]) else item.Magic if i+7 < len(df) and df.iloc[i+7, 0] == "Урон:": item.Damage = int(df.iloc[i+8, 1]) if pd.notna(df.iloc[i+8, 1]) else item.Damage if i+9 < len(df) and df.iloc[i+9, 0] == "Маг. урон:": item.MagicDamage = int(df.iloc[i+10, 1]) if pd.notna(df.iloc[i+10, 1]) else 0 i += 12 else: i += 10 char.inventory.append(item) else: i += 1 return char def to_postgres_data(self) -> Dict: """Подготавливает словарь с данными персонажа для сохранения в PostgreSQL""" active_skills_data = { skill: level for skill, level in self.skills.items() if skill in self.active_skills and level > 0 } inventory_data = [] for item in self.inventory: if isinstance(item, Wallet): item_data = { 'type': 'wallet', 'name': item.Name, 'gold': item.Gold } else: item_type = 'sword' if isinstance(item, Sword) else 'staff' item_data = { 'type': item_type, 'name': item.Name, 'level': item.Lvl, 'material': item.Material, 'magic': item.Magic, 'damage': item.Damage, 'magic_damage': item.MagicDamage, 'speed': item.Speed, 'mass': item.Mass } inventory_data.append(item_data) return { "race": self.race, "name": self.name, "params": self.params, "skills": active_skills_data, "spare_param_points": self.spare_param_points, "spare_skill_points": self.spare_skill_points, "inventory": inventory_data } class CharacterTab: def __init__(self, notebook, character: Character, tab_name: str, app=None): """Создает новую вкладку генерации персонажа""" self.notebook = notebook self.character = character self.tab_name = tab_name self.app = app self.saved = False self.initial_name = character.name self.frame = ttk.Frame(notebook) notebook.add(self.frame, text=tab_name) self.param_widgets = {} self.skill_widgets = {} self.create_widgets() self.update_ui() def create_widgets(self): """Создает все виджеты во вкладке генерации персонажа""" self.race_var = tk.StringVar(value=self.character.race) self.race_combobox = ttk.Combobox( self.frame, values=list(RACE_DATA.keys()), textvariable=self.race_var, state="readonly" ) self.race_combobox.bind("<<ComboboxSelected>>", self.on_race_change) self.race_combobox.place(x=150, y=60) tk.Label(self.frame, text="Раса:").place(x=110, y=60) self.name_var = tk.StringVar(value=self.character.name) self.name_entry = tk.Entry(self.frame, textvariable=self.name_var, width=40) self.name_entry.place(x=530, y=60) tk.Label(self.frame, text="Имя:").place(x=490, y=60) tk.Label(self.frame, text="Параметры").place(x=40, y=100) tk.Label(self.frame, text="Значение").place(x=145, y=100) tk.Label(self.frame, text="Мин. знач.").place(x=250, y=100) tk.Label(self.frame, text="Макс. знач.").place(x=325, y=100) for i, param in enumerate(PARAM_NAMES): y_pos = 130 + i * 20 tk.Label(self.frame, text=param, justify='left').place(x=40, y=y_pos) value_label = tk.Label(self.frame, text="", justify='left') value_label.place(x=170, y=y_pos) minus_btn = tk.Button( self.frame, text="-", width=2, height=1, command=lambda p=param: self.on_param_change(p, -1)) minus_btn.place(x=190, y=y_pos) plus_btn = tk.Button( self.frame, text="+", width=2, height=1, command=lambda p=param: self.on_param_change(p, 1)) plus_btn.place(x=220, y=y_pos) min_label = tk.Label(self.frame, text="", justify='left') min_label.place(x=275, y=y_pos) max_label = tk.Label(self.frame, text="", justify='left') max_label.place(x=350, y=y_pos) self.param_widgets[param] = { 'value': value_label, 'minus': minus_btn, 'plus': plus_btn, 'min': min_label, 'max': max_label } self.spare_param_label = tk.Label(self.frame, text="") self.spare_param_label.place(x=40, y=443) tk.Label(self.frame, text="Навыки").place(x=470, y=100) tk.Label(self.frame, text="Значение").place(x=600, y=100) tk.Label(self.frame, text="Мин. знач.").place(x=700, y=100) tk.Label(self.frame, text="Макс. знач.").place(x=780, y=100) self.spare_skill_label = tk.Label(self.frame, text="") self.spare_skill_label.place(x=470, y=443) separator = tk.Frame(self.frame, height=2, bd=1, relief=tk.SUNKEN) separator.place(x=0, y=480, relwidth=1) tk.Label(self.frame, text="Инвентарь", font=('Arial', 10, 'bold')).place(x=400, y=490) self.inv_left_frame = tk.Frame(self.frame, bd=2, relief=tk.GROOVE) self.inv_left_frame.place(x=20, y=520, width=200, height=150) self.combobox_item = ttk.Combobox( self.inv_left_frame, values=['Одноручный меч', 'Кошель', 'Магический жезл'], state='readonly' ) self.combobox_item.pack(pady=5) self.combobox_item.bind("<<ComboboxSelected>>", self.update_item_buttons_state) self.btn_add_item = tk.Button( self.inv_left_frame, text="Создать вручную", command=self.create_inventory_item ) self.btn_add_item.pack(pady=1) self.btn_random_item = tk.Button( self.inv_left_frame, text="Создать случайно", command=self.create_random_item ) self.btn_random_item.pack(pady=1) self.btn_edit_item = tk.Button( self.inv_left_frame, text="Изменить предмет", command=self.edit_inventory_item, state=tk.DISABLED ) self.btn_edit_item.pack(pady=1) self.btn_del_item = tk.Button( self.inv_left_frame, text="Удалить предмет", command=self.delete_inventory_item ) self.btn_del_item.pack(pady=1) self.btn_add_item.config(state=tk.DISABLED) self.btn_random_item.config(state=tk.DISABLED) self.btn_del_item.config(state=tk.DISABLED) self.inv_center_frame = tk.Frame(self.frame, bd=2, relief=tk.GROOVE) self.inv_center_frame.place(x=230, y=520, width=200, height=150) self.listbox_item = tk.Listbox(self.inv_center_frame) self.listbox_item.pack(fill=tk.BOTH, expand=True) self.listbox_item.bind("<<ListboxSelect>>", lambda e: self.btn_del_item.config( state=tk.NORMAL if self.listbox_item.curselection() else tk.DISABLED)) self.inv_right_frame = tk.Frame(self.frame, bd=2, relief=tk.GROOVE) self.inv_right_frame.place(x=440, y=520, width=450, height=150) self.text_details = tk.Text(self.inv_right_frame, wrap=tk.WORD, state=tk.DISABLED) self.text_details.pack(fill=tk.BOTH, expand=True) self.add_skill_btn = tk.Button( self.frame, text="Добавить навык", command=self.add_skill) self.add_skill_btn.place(x=650, y=440) self.random_btn = tk.Button( self.frame, text="Случайный персонаж", command=self.generate_random) self.random_btn.place(x=375, y=10) self.tooltip = tk.Toplevel(self.frame) self.tooltip.withdraw() self.tooltip.overrideredirect(True) tk.Label(self.tooltip, text="При нажатии на кнопку 'Случайный персонаж' раса, имя, параметры и навыки генерируются автоматически", bg="lightyellow", relief="solid", borderwidth=1).pack() self.random_btn.bind("<Enter>", self.show_tooltip) self.random_btn.bind("<Leave>", self.hide_tooltip) self.save_btn = tk.Button( self.frame, text="Сохранить", command=self.save_character) self.save_btn.place(x=190, y=685) self.close_btn = tk.Button( self.frame, text="Закрыть вкладку", command=self.close_tab) self.close_btn.place(x=610, y=685) separator = tk.Frame(self.frame, height=2, bd=1, relief=tk.SUNKEN) separator.place(x=0, y=45, relwidth=1) def update_item_buttons_state(self, event=None): """Обновляет состояние кнопок создания предмета""" if self.combobox_item.get(): self.btn_add_item.config(state=tk.NORMAL) self.btn_random_item.config(state=tk.NORMAL) else: self.btn_add_item.config(state=tk.DISABLED) self.btn_random_item.config(state=tk.DISABLED) def create_inventory_item(self): """Опреденяет тип предмета и создает предмет указанного типа""" item_type = self.combobox_item.get() if not item_type: messagebox.showwarning("Ошибка", "Выберите тип предмета") return if item_type == 'Кошель': self._create_wallet() else: self._create_weapon(item_type) def create_random_item(self): """Опреденяет тип предмета и создает предмет указанного типа случайным образом""" item_type = self.combobox_item.get() if not item_type: messagebox.showwarning("Ошибка", "Выберите тип предмета") return if item_type == 'Одноручный меч': item = Sword() display_type = "Меч" elif item_type == 'Кошель': item = Wallet() display_type = "Кошель" elif item_type == 'Магический жезл': item = Staff() display_type = "Жезл" else: return item.Roll() self.character.inventory.append(item) self.listbox_item.insert(tk.END, f"{item.Name} ({display_type})") messagebox.showinfo("Успех", f"{item_type} создан случайным образом", parent=self.frame.winfo_toplevel()) def _create_wallet(self): """Создает окно для ручного создания кошеля""" window = tk.Toplevel(self.frame) window.title("Создание кошеля") center_window(window, 400, 200) tk.Label(window, text="Название кошеля:").grid(row=0, column=0, padx=10, pady=5, sticky='e') name_entry = tk.Entry(window, width=30) name_entry.grid(row=0, column=1, padx=10, pady=5) tk.Label(window, text="Количество золота:").grid(row=1, column=0, padx=10, pady=5, sticky='e') gold_entry = tk.Entry(window, width=30) gold_entry.grid(row=1, column=1, padx=10, pady=5) def validate(): """Проверяет корректность введенных значений""" name = name_entry.get() gold = gold_entry.get() if not name: messagebox.showwarning("Ошибка", "Введите название кошеля", parent=window) return try: gold = int(gold) if gold < 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Введите корректное количество золота (целое неотрицательное число)", parent=window) return wallet = Wallet() wallet.Name = name wallet.Gold = gold self.character.inventory.append(wallet) self.listbox_item.insert(tk.END, f"{wallet.Name} (Кошель)") window.destroy() button_frame = tk.Frame(window) button_frame.grid(row=2, column=0, columnspan=2, pady=10) tk.Button(button_frame, text="Создать", command=validate).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=window.destroy).pack(side=tk.LEFT, padx=10) def _create_weapon(self, item_type): """Создает окно для ручного создания оружия""" window = tk.Toplevel(self.frame) if item_type == 'Одноручный меч': window.title(f"Создание одноручного меча") else: window.title(f"Создание магического жезла") center_window(window, 400, 250) tk.Label(window, text="Название:").grid(row=0, column=0, padx=10, pady=5, sticky='e') name_entry = tk.Entry(window, width=40) name_entry.grid(row=0, column=1, padx=10, pady=5) tk.Label(window, text="Уровень (1-3):").grid(row=1, column=0, padx=10, pady=5, sticky='e') level_spinbox = tk.Spinbox(window, from_=1, to=3, width=5) level_spinbox.grid(row=1, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Материал:").grid(row=2, column=0, padx=10, pady=5, sticky='e') material_combobox = ttk.Combobox(window, state="readonly") material_combobox.grid(row=2, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Зачарование:").grid(row=3, column=0, padx=10, pady=5, sticky='e') magic_combobox = ttk.Combobox(window, state="readonly") magic_combobox.grid(row=3, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Урон:").grid(row=4, column=0, padx=10, pady=5, sticky='e') damage_entry = tk.Entry(window, width=10) damage_entry.grid(row=4, column=1, padx=10, pady=5, sticky='w') magic_damage_label = tk.Label(window, text="Магический урон:") magic_damage_entry = tk.Entry(window, width=10) def update_magic_damage_visibility(*args): """Обновляет видимость поля 'Магический урон' в зависимости от зачарования.""" magic = magic_combobox.get() if magic in ["урон магией", "похищение жизни"]: magic_damage_label.grid(row=5, column=0, padx=10, pady=5, sticky='e') magic_damage_entry.grid(row=5, column=1, padx=10, pady=5, sticky='w') else: magic_damage_label.grid_remove() magic_damage_entry.grid_remove() magic_combobox.bind("<<ComboboxSelected>>", update_magic_damage_visibility) if item_type == 'Одноручный меч': material_sets = [Sword.MaterialSet1, Sword.MaterialSet2, Sword.MaterialSet3] magic_set = Sword.MagicSet else: material_sets = [Staff.MaterialSet1, Staff.MaterialSet2, Staff.MaterialSet3] magic_set = Staff.MagicSet materials = [] for set_ in material_sets: for mat in set_: materials.append(mat[0]) material_combobox['values'] = materials material_combobox.current(0) magic_combobox['values'] = magic_set magic_combobox.current(0) def validate(): """Проверяет корректность введенных значений""" name = name_entry.get() level = level_spinbox.get() material = material_combobox.get() magic = magic_combobox.get() damage = damage_entry.get() magic_damage = magic_damage_entry.get() if magic in ["урон магией", "похищение жизни"] else 0 if not name: messagebox.showwarning("Ошибка", "Введите название предмета", parent=window) return try: level = int(level) if level < 1 or level > 3: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Уровень должен быть числом от 1 до 3", parent=window) return try: damage = int(damage) if damage <= 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Урон должен быть положительным числом", parent=window) return if magic in ["урон магией", "похищение жизни"]: try: magic_damage = int(magic_damage) if magic_damage <= 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Магический урон должен быть положительным числом", parent=window) return if item_type == 'Одноручный меч': item = Sword() else: item = Staff() item.Name = name item.Lvl = level item.Material = material item.Magic = magic item.Damage = damage if magic in ["урон магией", "похищение жизни"]: item.MagicDamage = magic_damage for set_ in material_sets: for mat in set_: if mat[0] == material: item.Mass = mat[2] item.Speed = mat[3] break self.character.inventory.append(item) display_name = f"{item.Name} (Меч)" if item_type == 'Одноручный меч' else f"{item.Name} (Жезл)" self.listbox_item.insert(tk.END, display_name) window.destroy() button_frame = tk.Frame(window) button_frame.grid(row=6, column=0, columnspan=2, pady=10) tk.Button(button_frame, text="Создать", command=validate).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=window.destroy).pack(side=tk.LEFT, padx=10) def delete_inventory_item(self): """Удаляет выбранный предмет из инвентаря""" selection = self.listbox_item.curselection() if not selection: messagebox.showwarning("Ошибка", "Выберите предмет для удаления") return if messagebox.askyesno("Подтверждение", "Удалить выбранный предмет?", parent=self.frame.winfo_toplevel()): index = selection[0] self.listbox_item.delete(index) del self.character.inventory[index] # Очищаем окно с характеристиками self.text_details.config(state=tk.NORMAL) self.text_details.delete(1.0, tk.END) self.text_details.config(state=tk.DISABLED) self.btn_del_item.config(state=tk.DISABLED) self.btn_edit_item.config(state=tk.DISABLED) def edit_inventory_item(self): """Создает окно для редактирования выбранного предмета""" selection = self.listbox_item.curselection() if not selection: messagebox.showwarning("Ошибка", "Выберите предмет для изменения") return index = selection[0] item = self.character.inventory[index] if isinstance(item, Wallet): self._edit_wallet(item, index) else: item_type = "Одноручный меч" if isinstance(item, Sword) else "Магический жезл" self._edit_weapon(item, index, item_type) def _edit_wallet(self, wallet, index): """Создает окно редактирования кошеля""" window = tk.Toplevel(self.frame) window.title("Изменение кошеля") center_window(window, 400, 200) tk.Label(window, text="Название кошеля:").grid(row=0, column=0, padx=10, pady=5, sticky='e') name_entry = tk.Entry(window, width=30) name_entry.insert(0, wallet.Name) name_entry.grid(row=0, column=1, padx=10, pady=5) tk.Label(window, text="Количество золота:").grid(row=1, column=0, padx=10, pady=5, sticky='e') gold_entry = tk.Entry(window, width=30) gold_entry.insert(0, str(wallet.Gold)) gold_entry.grid(row=1, column=1, padx=10, pady=5) def validate(): """Проверяет корректность введенных значений""" name = name_entry.get() gold = gold_entry.get() if not name: messagebox.showwarning("Ошибка", "Введите название кошеля", parent=window) return try: gold = int(gold) if gold < 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Введите корректное количество золота (целое неотрицательное число)", parent=window) return wallet.Name = name wallet.Gold = gold self.listbox_item.delete(index) self.listbox_item.insert(index, f"{wallet.Name} (Кошель)") self.listbox_item.selection_set(index) window.destroy() button_frame = tk.Frame(window) button_frame.grid(row=2, column=0, columnspan=2, pady=10) tk.Button(button_frame, text="Сохранить", command=validate).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=window.destroy).pack(side=tk.LEFT, padx=10) def _edit_weapon(self, weapon, index, item_type): """Создает окно редактирования оружия""" window = tk.Toplevel(self.frame) if item_type == 'Одноручный меч': window.title(f"Изменение одноручного меча") else: window.title(f"Изменение магического жезла") center_window(window, 400, 250) tk.Label(window, text="Название:").grid(row=0, column=0, padx=10, pady=5, sticky='e') name_entry = tk.Entry(window, width=40) name_entry.insert(0, weapon.Name) name_entry.grid(row=0, column=1, padx=10, pady=5) tk.Label(window, text="Уровень (1-3):").grid(row=1, column=0, padx=10, pady=5, sticky='e') level_spinbox = tk.Spinbox(window, from_=1, to=3, width=5) level_spinbox.delete(0, tk.END) level_spinbox.insert(0, str(weapon.Lvl)) level_spinbox.grid(row=1, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Материал:").grid(row=2, column=0, padx=10, pady=5, sticky='e') material_combobox = ttk.Combobox(window, state="readonly") material_combobox.grid(row=2, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Зачарование:").grid(row=3, column=0, padx=10, pady=5, sticky='e') magic_combobox = ttk.Combobox(window, state="readonly") magic_combobox.grid(row=3, column=1, padx=10, pady=5, sticky='w') tk.Label(window, text="Урон:").grid(row=4, column=0, padx=10, pady=5, sticky='e') damage_entry = tk.Entry(window, width=10) damage_entry.insert(0, str(weapon.Damage)) damage_entry.grid(row=4, column=1, padx=10, pady=5, sticky='w') magic_damage_label = tk.Label(window, text="Магический урон:") magic_damage_entry = tk.Entry(window, width=10) def update_magic_damage_visibility(*args): """Обновляет видимость поля 'Магический урон' в зависимости от зачарования.""" magic = magic_combobox.get() if magic in ["урон магией", "похищение жизни"]: magic_damage_label.grid(row=5, column=0, padx=10, pady=5, sticky='e') magic_damage_entry.grid(row=5, column=1, padx=10, pady=5, sticky='w') if hasattr(weapon, 'MagicDamage'): magic_damage_entry.delete(0, tk.END) magic_damage_entry.insert(0, str(weapon.MagicDamage)) else: magic_damage_label.grid_remove() magic_damage_entry.grid_remove() magic_combobox.bind("<<ComboboxSelected>>", update_magic_damage_visibility) if item_type == 'Одноручный меч': material_sets = [Sword.MaterialSet1, Sword.MaterialSet2, Sword.MaterialSet3] magic_set = Sword.MagicSet else: material_sets = [Staff.MaterialSet1, Staff.MaterialSet2, Staff.MaterialSet3] magic_set = Staff.MagicSet materials = [] for set_ in material_sets: for mat in set_: materials.append(mat[0]) material_combobox['values'] = materials try: material_combobox.set(weapon.Material) except: material_combobox.current(0) magic_combobox['values'] = magic_set try: magic_combobox.set(weapon.Magic) except: magic_combobox.current(0) if weapon.Magic in ["урон магией", "похищение жизни"]: magic_damage_label.grid(row=5, column=0, padx=10, pady=5, sticky='e') magic_damage_entry.grid(row=5, column=1, padx=10, pady=5, sticky='w') magic_damage_entry.insert(0, str(weapon.MagicDamage)) def validate(): """Проверяет корректность введенных значений""" name = name_entry.get() level = level_spinbox.get() material = material_combobox.get() magic = magic_combobox.get() damage = damage_entry.get() magic_damage = magic_damage_entry.get() if magic in ["урон магией", "похищение жизни"] else 0 if not name: messagebox.showwarning("Ошибка", "Введите название предмета", parent=window) return try: level = int(level) if level < 1 or level > 3: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Уровень должен быть числом от 1 до 3", parent=window) return try: damage = int(damage) if damage <= 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Урон должен быть положительным числом", parent=window) return if magic in ["урон магией", "похищение жизни"]: try: magic_damage = int(magic_damage) if magic_damage <= 0: raise ValueError except ValueError: messagebox.showwarning("Ошибка", "Магический урон должен быть положительным числом", parent=window) return weapon.Name = name weapon.Lvl = level weapon.Material = material weapon.Magic = magic weapon.Damage = damage if magic in ["урон магией", "похищение жизни"]: weapon.MagicDamage = magic_damage else: weapon.MagicDamage = 0 for set_ in material_sets: for mat in set_: if mat[0] == material: weapon.Mass = mat[2] weapon.Speed = mat[3] break self.listbox_item.delete(index) display_name = f"{weapon.Name} (Меч)" if item_type == 'Одноручный меч' else f"{weapon.Name} (Жезл)" self.listbox_item.insert(index, display_name) self.listbox_item.selection_set(index) self.show_inventory_details(None) window.destroy() button_frame = tk.Frame(window) button_frame.grid(row=6, column=0, columnspan=2, pady=10) tk.Button(button_frame, text="Сохранить", command=validate).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=window.destroy).pack(side=tk.LEFT, padx=10) def show_inventory_details(self, event): """Отображает характеристики выбранного предмета""" selection = self.listbox_item.curselection() if selection: self.btn_del_item.config(state=tk.NORMAL) self.btn_edit_item.config(state=tk.NORMAL) else: self.btn_del_item.config(state=tk.DISABLED) self.btn_edit_item.config(state=tk.DISABLED) return index = selection[0] if index >= len(self.character.inventory): return item = self.character.inventory[index] self.text_details.config(state=tk.NORMAL) self.text_details.delete(1.0, tk.END) if isinstance(item, Wallet): details = f"""Название: {item.Name} Тип: Кошель Золото: {item.Gold}""" else: item_type = "Меч" if isinstance(item, Sword) else "Жезл" details = ( f"Название: {item.Name}\n" f"Тип: {item_type}\n" f"Уровень: {item.Lvl}\n" f"Материал: {item.Material}\n" f"Зачарование: {item.Magic}\n" f"Урон: {item.Damage}\n" f"Магический урон: {item.MagicDamage}\n" f"Скорость: {item.Speed}\n" f"Масса: {item.Mass}" ) self.text_details.insert(tk.END, details) self.text_details.config(state=tk.DISABLED) def show_tooltip(self, event): """Отображает всплывающую подсказку при наведении на кнопку""" x, y, _, _ = self.random_btn.bbox("insert") x += self.random_btn.winfo_rootx() + 25 y += self.random_btn.winfo_rooty() + 25 self.tooltip.geometry(f"+{x}+{y}") self.tooltip.deiconify() def hide_tooltip(self, event): """Скрывает всплывающую подсказку""" self.tooltip.withdraw() def update_ui(self): """Обновляет интерфейс вкладки генерации персонажа""" self.race_var.set(self.character.race) if self.name_var.get() == self.initial_name: self.name_var.set(self.character.name) self.initial_name = self.character.name for param in PARAM_NAMES: widgets = self.param_widgets[param] widgets['value'].config(text=str(self.character.params[param])) widgets['min'].config(text=str(self.character.get_min_param(param))) widgets['max'].config(text=str(self.character.get_max_param(param))) widgets['minus'].config( state=tk.NORMAL if self.character.can_decrease_param(param) else tk.DISABLED) widgets['plus'].config( state=tk.NORMAL if self.character.can_increase_param(param) else tk.DISABLED) for widget in self.skill_widgets.values(): widget['frame'].destroy() self.skill_widgets = {} y_pos = 130 for skill in self.character.active_skills: skill_frame = tk.Frame(self.frame) skill_frame.place(x=440, y=y_pos, width=450, height=20) del_btn = tk.Button( skill_frame, text="X", width=2, command=lambda s=skill: self.remove_skill(s)) del_btn.pack(side=tk.LEFT) tk.Label(skill_frame, text=skill, width=20, anchor='w').pack(side=tk.LEFT, padx=5) value_label = tk.Label(skill_frame, text=str(self.character.skills[skill])) value_label.pack(side=tk.LEFT, padx=5) minus_btn = tk.Button( skill_frame, text="-", width=2, command=lambda s=skill: self.on_skill_change(s, -1), state=tk.NORMAL if self.character.can_decrease_skill(skill) else tk.DISABLED) minus_btn.pack(side=tk.LEFT, padx=2) plus_btn = tk.Button( skill_frame, text="+", width=2, command=lambda s=skill: self.on_skill_change(s, 1), state=tk.NORMAL if self.character.can_increase_skill(skill) else tk.DISABLED) plus_btn.pack(side=tk.LEFT, padx=2) tk.Label(skill_frame, text="1").pack(side=tk.LEFT, padx=30) tk.Label(skill_frame, text="5").pack(side=tk.LEFT, padx=35) self.skill_widgets[skill] = { 'frame': skill_frame, 'value': value_label, 'minus': minus_btn, 'plus': plus_btn } y_pos += 20 self.spare_param_label.config( text=f"Свободные очки: {self.character.spare_param_points}") self.spare_skill_label.config( text=f"Свободные очки: {self.character.spare_skill_points}") self.add_skill_btn.config( state=tk.NORMAL if self.character.spare_skill_points > 0 else tk.DISABLED) self.listbox_item.delete(0, tk.END) for item in self.character.inventory: if isinstance(item, Wallet): self.listbox_item.insert(tk.END, f"{item.Name} (Кошель)") elif isinstance(item, Sword): self.listbox_item.insert(tk.END, f"{item.Name} (Меч)") elif isinstance(item, Staff): self.listbox_item.insert(tk.END, f"{item.Name} (Жезл)") self.listbox_item.bind("<<ListboxSelect>>", self.show_inventory_details) def check_postgres_connection(): """Проверяет соединение с PostgreSQL""" try: conn = psycopg2.connect(**POSTGRES_CONFIG) conn.close() return True except Exception as e: print(f"Ошибка подключения к PostgreSQL: {e}") return False if not check_postgres_connection(): messagebox.showwarning("Предупреждение", "Не удалось подключиться к PostgreSQL. " "Сохранение в базу данных будет недоступно.") def on_param_change(self, param_name: str, delta: int) -> None: """Обработчик изменения параметров""" if delta > 0: self.character.increase_param(param_name) else: self.character.decrease_param(param_name) self.update_ui() def on_skill_change(self, skill_name: str, delta: int) -> None: """Обработчик изменения навыков""" if delta > 0: self.character.increase_skill(skill_name) else: self.character.decrease_skill(skill_name) self.update_ui() def remove_skill(self, skill_name: str) -> None: """Удаляет навык у персонажа""" self.character.remove_skill(skill_name) self.update_ui() def add_skill(self) -> None: """Создает окно для добавления навыка""" skill_window = tk.Toplevel(self.frame) skill_window.title("Добавление навыка") center_window(skill_window, 300, 220) tk.Label(skill_window, text="Выберите навык из списка:").pack(pady=10) skill_combobox = ttk.Combobox(skill_window, values=SKILL_NAMES, state="readonly") skill_combobox.pack(pady=5) tk.Label(skill_window, text="ИЛИ").pack(pady=5) tk.Label(skill_window, text="Введите название навыка:").pack() skill_entry = tk.Entry(skill_window, width=25) skill_entry.pack(pady=5) def on_continue(): """Добавляет навык с выбранным или введенным вручную названием""" skill_name = skill_entry.get() or skill_combobox.get() if skill_name: if not self.character.can_increase_skill(skill_name): messagebox.showwarning("Ошибка", "Нельзя добавить этот навык") return self.character.increase_skill(skill_name) self.update_ui() skill_window.destroy() button_frame = tk.Frame(skill_window) button_frame.pack(pady=10) tk.Button(button_frame, text="Отмена", command=skill_window.destroy).pack(side=tk.LEFT, padx=5) tk.Button(button_frame, text="Продолжить", command=on_continue).pack(side=tk.LEFT, padx=5) def on_race_change(self, event) -> None: """Обработчик смены расы персонажа""" new_race = self.race_var.get() if new_race != self.character.race: self.character = Character(new_race) self.name_var.set(self.character.name) self.update_ui() def generate_random(self) -> None: """Генерирует случайного персонажа""" has_changes = ( self.name_var.get() != self.initial_name or self.character.spare_param_points < RACE_DATA[self.character.race]["param_points"] or len(self.character.active_skills) > 0 ) if has_changes: answer = messagebox.askyesno( "Подтверждение", "Все внесённые изменения в текущей вкладке удалятся. Вы хотите продолжить?", default=messagebox.NO ) if not answer: return inventory_backup = self.character.inventory new_race = random.choice(list(RACE_DATA.keys())) self.character = Character(new_race) self.character.inventory = inventory_backup self.character.auto_allocate_params() self.character.auto_allocate_skills() self.name_var.set(self.character.name) self.initial_name = self.character.name self.update_ui() def close_tab(self) -> None: """Закрывает вкладку персонажа с проверкой на наличие сохранения""" if not self.saved: answer = messagebox.askyesnocancel( "Сохранение персонажа", "Персонаж не сохранен. Хотите сохранить перед закрытием?", default=messagebox.YES ) if answer is None: return elif answer: self.save_character() if not self.saved: return if self.app: self.app.character_tabs.remove(self) self.notebook.forget(self.frame) self.tooltip.destroy() def save_character(self) -> None: """Создает окно выбора способа сохранения персонажа""" self.character.name = self.name_var.get() choice_window = tk.Toplevel(self.frame) choice_window.title("Способ сохранения") center_window(choice_window, 300, 150) tk.Label(choice_window, text="Выберите способ сохранения:").pack(pady=10) def save_to_excel(): choice_window.destroy() self._save_to_excel() def save_to_postgres(): choice_window.destroy() self._save_to_postgres() button_frame = tk.Frame(choice_window) button_frame.pack(pady=20) tk.Button(button_frame, text="В Excel файл", command=save_to_excel).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="В PostgreSQL", command=save_to_postgres).pack(side=tk.LEFT, padx=10) def _save_to_excel(self): """Сохраняет персонажа в Excel файл""" sanitized_name = sanitize_filename(self.character.name) file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")], initialfile=f"{sanitized_name}.xlsx" ) if file_path: df = self.character.to_dataframe() df.to_excel(file_path, index=False, header=False) self.saved = True update_log_safe(file_path) messagebox.showinfo("Успех", f"Персонаж {self.character.name} сохранен в файл {os.path.basename(file_path)}") def _save_to_postgres(self): """Сохраняет персонажа в базу PostgreSQL""" connection = None try: if not check_postgres_connection(POSTGRES_CONFIG): messagebox.showerror("Ошибка", "Не удалось подключиться к PostgreSQL", parent=self.frame.winfo_toplevel()) return connection = psycopg2.connect(**POSTGRES_CONFIG) connection.autocommit = True char_data = self.character.to_postgres_data() if hasattr(self.character, 'id'): result = [] def on_choice(value): result.append(value) choice_window.destroy() choice_window = tk.Toplevel(self.frame) choice_window.title("Сохранение персонажа") center_window(choice_window, 400, 200) tk.Label(choice_window, text="Вы хотите перезаписать загруженного персонажа или создать новую запись?", wraplength=350, justify="center").pack(pady=20) button_frame = tk.Frame(choice_window) button_frame.pack(pady=20) tk.Button(button_frame, text="Перезаписать", command=lambda: on_choice("overwrite")).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Создать новую запись", command=lambda: on_choice("new")).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=lambda: on_choice("cancel")).pack(side=tk.LEFT, padx=10) def on_close(): on_choice("cancel") choice_window.protocol("WM_DELETE_WINDOW", on_close) choice_window.wait_window() if not result or result[0] == "cancel": return if result[0] == "overwrite": with connection.cursor() as cursor: cursor.execute(""" UPDATE characters SET race = %s, name = %s, params = %s::jsonb, skills = %s::jsonb, spare_param_points = %s, spare_skill_points = %s, inventory = %s::jsonb WHERE id = %s RETURNING id """, ( char_data["race"], char_data["name"], json.dumps(char_data["params"]), json.dumps(char_data["skills"]), char_data["spare_param_points"], char_data["spare_skill_points"], json.dumps(char_data["inventory"]), self.character.id )) char_id = self.character.id log_entry = f"PostgreSQL:{char_id} ({self.character.name})" update_log_safe(log_entry) messagebox.showinfo("Успех", f"Персонаж {self.character.name} обновлен (ID: {char_id})") self.saved = True return with connection.cursor() as cursor: cursor.execute(""" SELECT MIN(t1.id + 1) FROM characters t1 LEFT JOIN characters t2 ON t1.id + 1 = t2.id WHERE t2.id IS NULL """) next_id = cursor.fetchone()[0] or 1 cursor.execute(""" INSERT INTO characters (id, race, name, params, skills, spare_param_points, spare_skill_points, inventory) VALUES (%s, %s, %s, %s::jsonb, %s::jsonb, %s, %s, %s::jsonb) RETURNING id """, ( next_id, char_data["race"], char_data["name"], json.dumps(char_data["params"]), json.dumps(char_data["skills"]), char_data["spare_param_points"], char_data["spare_skill_points"], json.dumps(char_data["inventory"]) )) char_id = cursor.fetchone()[0] self.character.id = char_id log_entry = f"PostgreSQL:{char_id} ({self.character.name})" update_log_safe(log_entry) messagebox.showinfo("Успех", f"Персонаж {self.character.name} сохранен (ID: {char_id})") self.saved = True except Exception as e: messagebox.showerror("Ошибка", f"Ошибка PostgreSQL: {str(e)}") finally: if connection: connection.close() class DnDGeneratorApp: def __init__(self, root): """Создает главное окно приложения""" global APP_INSTANCE APP_INSTANCE = self self.root = root self.root.title("Генератор персонажей DnD") self.root.geometry("900x750") self.root.resizable(False, False) window_width = 900 window_height = 750 screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() x = (screen_width // 2) - (window_width // 2) y = (screen_height // 2) - (window_height // 2) - 45 self.root.geometry(f"{window_width}x{window_height}+{x}+{y}") self.tab_counter = 1 self.recent_files = [] self.character_tabs = [] self.create_widgets() self.refresh_recent_files() self.root.protocol("WM_DELETE_WINDOW", self.on_close) def configure_db(self): """Создает окно настроек подключения к базе данных PostgreSQL""" config_window = tk.Toplevel(self.root) config_window.title("Настройки подключения к PostgreSQL") center_window(config_window, 400, 300) labels = ["host", "port", "database", "user", "password"] entries = {} for i, key in enumerate(labels): tk.Label(config_window, text=key.capitalize() + ":").grid(row=i, column=0, padx=10, pady=5, sticky='e') entry = tk.Entry(config_window, width=30, show="*" if key == "password" else "") entry.insert(0, POSTGRES_CONFIG.get(key, "")) entry.grid(row=i, column=1, padx=10, pady=5) entries[key] = entry def on_save(): global POSTGRES_CONFIG new_config = {k: entries[k].get() for k in labels} POSTGRES_CONFIG = new_config save_postgres_config(new_config) messagebox.showinfo("Успех", "Настройки сохранены") config_window.destroy() tk.Button(config_window, text="Сохранить", command=on_save).grid(row=len(labels), column=0, columnspan=2, pady=20) def on_close(self): """Обработчик выхода из приложения с проверкой на наличие несохраненных персонажей""" unsaved_tabs = [tab for tab in self.character_tabs if not tab.saved] if not unsaved_tabs: self.root.destroy() return for tab in unsaved_tabs: tab.character.name = tab.name_var.get() unsaved_list = "\n".join([f"{tab.tab_name}: {tab.character.name}" for tab in unsaved_tabs]) answer = messagebox.askyesnocancel( "Сохранение персонажей", f"Следующие персонажи не сохранены:\n{unsaved_list}\n\nХотите сохранить их перед выходом?", default=messagebox.YES ) if answer is None: return elif answer: for tab in unsaved_tabs: self.notebook.select(tab.frame) tab.save_character() if not tab.saved: return self.root.destroy() def create_widgets(self): """Создает интерфейс главного окна и меню""" self.notebook = ttk.Notebook(self.root) self.notebook.pack(expand=True, fill='both') self.start_tab = ttk.Frame(self.notebook) self.notebook.add(self.start_tab, text='Главная страница') self.menu = tk.Menu(self.root) self.root.config(menu=self.menu) file_menu = tk.Menu(self.menu, tearoff=0) file_menu.add_command(label="Новый персонаж", command=self.create_new_character) file_menu.add_command(label="Открыть файл", command=self.open_character_file) file_menu.add_command(label="Настройки базы данных", command=self.configure_db) file_menu.add_separator() file_menu.add_command(label="Выход", command=self.on_close) self.menu.add_cascade(label="Файл", menu=file_menu) help_menu = tk.Menu(self.menu, tearoff=0) help_menu.add_command(label="Помощь", command=self.show_help) help_menu.add_command(label="О программе", command=self.show_about) self.menu.add_cascade(label="Справка", menu=help_menu) tk.Button( self.start_tab, text="Создать персонажа", command=self.create_new_character ).place(x=120, y=200) tk.Button( self.start_tab, text="Загрузить", command=self.open_character_file ).place(x=145, y=280) self.delete_btn = tk.Button( self.start_tab, text="Удалить из списка", command=self.delete_from_list, state=tk.DISABLED ) self.delete_btn.place(x=125, y=360) self.db_manage_btn = tk.Button( self.start_tab, text="Управление базой данных", command=self.manage_database ) self.db_manage_btn.place(x=110, y=440) tk.Label( self.start_tab, text="Недавно созданные/открытые файлы" ).place(x=310, y=25) self.recent_files_tree = ttk.Treeview(self.start_tab, show='tree') self.recent_files_tree.place(x=310, y=50, width=500, height=550) self.recent_files_tree.bind("<Double-1>", self.on_file_double_click) self.recent_files_tree.bind("<<TreeviewSelect>>", self.on_tree_select) tk.Label( self.start_tab, text="Кликните дважды по названию файла, чтобы открыть его" ).place(x=310, y=605) def load_recent_files(self): """Загружает лог-файл и обновляет список недавно измененных файлов""" if not os.path.exists('log.txt'): return try: with open('log.txt', 'r') as f: lines = [line.strip() for line in f.readlines() if line.strip()] except Exception as e: print(f"Ошибка чтения log.txt: {e}") return seen = set() self.recent_files = [] for line in reversed(lines): parts = line.split(' [') if len(parts) < 2: continue file_part = parts[0] timestamp = parts[1].rstrip(']') if file_part.startswith('PostgreSQL:'): parts = file_part.split(':') if len(parts) >= 2: char_info = parts[1].split('(') char_id = char_info[0].strip() char_name = char_info[1].rstrip(')') if len(char_info) > 1 else "Неизвестный" display_text = f"PostgreSQL: {char_name} (ID: {char_id}) [{timestamp}]" self.recent_files.append((display_text, file_part)) else: if os.path.exists(file_part): filename = os.path.splitext(os.path.basename(file_part))[0] display_text = f"{filename} [{timestamp}]" self.recent_files.append((display_text, file_part)) self.recent_files_tree.delete(*self.recent_files_tree.get_children()) for display_text, file_path in self.recent_files: self.recent_files_tree.insert("", tk.END, text=display_text, values=(file_path,)) def create_new_character(self): """Создает новую вкладку со случайно созданным персонажем""" tab_name = f"Персонаж {self.tab_counter}" character = Character(random.choice(list(RACE_DATA.keys()))) character.auto_allocate_params() character.auto_allocate_skills() tab = CharacterTab(self.notebook, character, tab_name, app=self) self.character_tabs.append(tab) self.tab_counter += 1 self.notebook.select(len(self.notebook.tabs()) - 1) def open_character_file(self, file_path=None): """Создает окно с выбором варианта загрузки персонажа""" if not file_path: choice_window = tk.Toplevel(self.root) choice_window.title("Источник загрузки") center_window(choice_window, 300, 150) tk.Label(choice_window, text="Выберите источник загрузки:").pack(pady=10) def load_from_excel(): choice_window.destroy() self._load_from_excel() def load_from_postgres(): choice_window.destroy() self._load_from_postgres() button_frame = tk.Frame(choice_window) button_frame.pack(pady=20) tk.Button(button_frame, text="Из Excel", command=load_from_excel).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Из PostgreSQL", command=load_from_postgres).pack(side=tk.LEFT, padx=10) else: self._load_from_excel(file_path) def _load_from_excel(self, file_path=None): """Загружает персонажа из Excel-файла""" if not file_path: file_path = filedialog.askopenfilename( filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")]) if file_path: try: character = Character.from_file(file_path) tab_name = f"Персонаж {self.tab_counter}" tab = CharacterTab(self.notebook, character, tab_name, app=self) self.character_tabs.append(tab) self.tab_counter += 1 self.notebook.select(len(self.notebook.tabs()) - 1) update_log_safe(file_path) self.refresh_recent_files() except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить файл: {str(e)}") def _load_from_postgres(self): """Загружает персонажа из базы данных PostgreSQL""" list_window = tk.Toplevel(self.root) list_window.title("Персонажи в PostgreSQL") center_window(list_window, 850, 500) tree = ttk.Treeview(list_window, columns=('ID', 'Имя', 'Раса', 'Дата'), show='headings') tree.heading('ID', text='ID') tree.heading('Имя', text='Имя') tree.heading('Раса', text='Раса') tree.heading('Дата', text='Дата создания') tree.pack(expand=True, fill='both', padx=10, pady=10) def load_selected(): selected = tree.selection() if selected: char_id = tree.item(selected[0], 'values')[0] list_window.destroy() self._load_character_by_id(char_id) button_frame = tk.Frame(list_window) button_frame.pack(pady=10) tk.Button(button_frame, text="Загрузить", command=load_selected).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=list_window.destroy).pack(side=tk.LEFT, padx=10) try: connection = psycopg2.connect(**POSTGRES_CONFIG) with connection.cursor() as cursor: cursor.execute(""" SELECT id, name, race, to_char(created_at, 'DD.MM.YYYY HH24:MI:SS') FROM characters ORDER BY id ASC """) for row in cursor.fetchall(): tree.insert('', tk.END, values=row) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить список: {str(e)}") finally: if 'connection' in locals() and connection: connection.close() def _load_character_by_id(self, char_id): """Загружает данные персонажа из базы данных PostgreSQL по его ID""" connection = None try: connection = psycopg2.connect(**POSTGRES_CONFIG) with connection.cursor() as cursor: cursor.execute(""" SELECT race, name, params, skills, spare_param_points, spare_skill_points, inventory FROM characters WHERE id = %s """, (char_id,)) result = cursor.fetchone() if not result: messagebox.showerror("Ошибка", f"Персонаж с ID {char_id} не найден") return character = Character(result[0]) character.id = char_id character.name = result[1] character.params = result[2] skills_data = result[3] character.spare_param_points = result[4] character.spare_skill_points = result[5] character.active_skills = [] character.skills = {skill: 0 for skill in SKILL_NAMES} for skill, level in skills_data.items(): if level > 0: character.active_skills.append(skill) character.skills[skill] = level inventory_data = result[6] if result[6] else [] for item_data in inventory_data: if item_data['type'] == 'wallet': wallet = Wallet() wallet.Name = item_data['name'] wallet.Gold = item_data['gold'] character.inventory.append(wallet) elif item_data['type'] == 'sword': sword = Sword() sword.Name = item_data['name'] sword.Lvl = item_data['level'] sword.Material = item_data['material'] sword.Magic = item_data['magic'] sword.Damage = item_data['damage'] sword.MagicDamage = item_data['magic_damage'] sword.Speed = item_data['speed'] sword.Mass = item_data['mass'] character.inventory.append(sword) elif item_data['type'] == 'staff': staff = Staff() staff.Name = item_data['name'] staff.Lvl = item_data['level'] staff.Material = item_data['material'] staff.Magic = item_data['magic'] staff.Damage = item_data['damage'] staff.MagicDamage = item_data['magic_damage'] staff.Speed = item_data['speed'] staff.Mass = item_data['mass'] character.inventory.append(staff) tab_name = f"Персонаж {self.tab_counter} (ID: {char_id})" tab = CharacterTab(self.notebook, character, tab_name, app=self) self.character_tabs.append(tab) self.tab_counter += 1 self.notebook.select(len(self.notebook.tabs()) - 1) tab.listbox_item.delete(0, tk.END) for item in character.inventory: if isinstance(item, Wallet): tab.listbox_item.insert(tk.END, f"{item.Name} (Кошель)") elif isinstance(item, Sword): tab.listbox_item.insert(tk.END, f"{item.Name} (Меч)") elif isinstance(item, Staff): tab.listbox_item.insert(tk.END, f"{item.Name} (Жезл)") tab.listbox_item.bind("<<ListboxSelect>>", tab.show_inventory_details) messagebox.showinfo("Успех", f"Персонаж с ID:{char_id} загружен из базы данных") update_log_safe(f"PostgreSQL:{char_id} ({character.name})") except Exception as e: messagebox.showerror("Ошибка", f"Ошибка загрузки: {str(e)}") finally: if connection: connection.close() def on_file_double_click(self, event): """Обработчик двойного клика по файлу для его загрузки""" selected_item = self.recent_files_tree.selection() if not selected_item: return item_values = self.recent_files_tree.item(selected_item[0], 'values') if not item_values: return file_path = item_values[0] if file_path.startswith('PostgreSQL:'): char_id = file_path.split(':')[1].split()[0] self._load_character_by_id(char_id) else: self._load_from_excel(file_path) def on_tree_select(self, event): """Обработчик выбора элемента в списке недавних файлов""" self.delete_btn.config(state=tk.NORMAL if self.recent_files_tree.selection() else tk.DISABLED) def delete_from_list(self): """Удаляет выбранную запись из списка недавних файлов и лог-файла""" selected = self.recent_files_tree.selection() if not selected: return if messagebox.askyesno("Подтверждение", "Вы действительно хотите удалить выбранный файл из списка?"): item_values = self.recent_files_tree.item(selected[0], 'values') if not item_values: return file_path = item_values[0] if os.path.exists('log.txt'): with open('log.txt', 'r') as f: lines = [line.strip() for line in f.readlines() if line.strip()] filtered_lines = [line for line in lines if not line.startswith(file_path)] with open('log.txt', 'w') as f: f.write('\n'.join(filtered_lines)) self.refresh_recent_files() self.delete_btn.config(state=tk.DISABLED) def manage_database(self): """Создает окно управления базой данных""" manage_window = tk.Toplevel(self.root) manage_window.title("Управление базой данных") center_window(manage_window, 850, 500) tree = ttk.Treeview(manage_window, columns=('ID', 'Имя', 'Раса', 'Дата'), show='headings') tree.heading('ID', text='ID') tree.heading('Имя', text='Имя') tree.heading('Раса', text='Раса') tree.heading('Дата', text='Дата создания') tree.pack(expand=True, fill='both', padx=10, pady=10) def delete_selected(): selected = tree.selection() if not selected: return char_id = tree.item(selected[0], 'values')[0] char_name = tree.item(selected[0], 'values')[1] if messagebox.askyesno("Подтверждение", f"Вы действительно хотите удалить персонажа {char_name} (ID {char_id})?", parent=manage_window): try: connection = psycopg2.connect(**POSTGRES_CONFIG) connection.autocommit = True with connection.cursor() as cursor: cursor.execute("DELETE FROM characters WHERE id = %s", (char_id,)) cursor.execute(""" UPDATE characters SET id = id - 1 WHERE id > %s; """, (char_id,)) if os.path.exists('log.txt'): with open('log.txt', 'r') as f: lines = [line.strip() for line in f.readlines() if line.strip()] filtered_lines = [] for line in lines: if line.startswith(f"PostgreSQL:{char_id}"): continue elif line.startswith("PostgreSQL:"): parts = line.split(':') if len(parts) > 1 and parts[1].split()[0].isdigit(): other_id = int(parts[1].split()[0]) if other_id > int(char_id): new_id = other_id - 1 line = line.replace(f":{other_id}", f":{new_id}") filtered_lines.append(line) with open('log.txt', 'w') as f: f.write('\n'.join(filtered_lines)) messagebox.showinfo("Успех", "Запись удалена, ID персонажей обновлены", parent=manage_window) refresh_list() self.refresh_recent_files() except Exception as e: messagebox.showerror("Ошибка", f"Ошибка при удалении: {str(e)}") finally: if 'connection' in locals() and connection: connection.close() def refresh_list(): """Обновляет список записей в таблице""" tree.delete(*tree.get_children()) try: connection = psycopg2.connect(**POSTGRES_CONFIG) with connection.cursor() as cursor: cursor.execute(""" SELECT id, name, race, to_char(created_at, 'DD.MM.YYYY HH24:MI:SS') FROM characters ORDER BY id """) for row in cursor.fetchall(): tree.insert('', tk.END, values=row) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить список: {str(e)}") finally: if 'connection' in locals() and connection: connection.close() button_frame = tk.Frame(manage_window) button_frame.pack(pady=10) tk.Button(button_frame, text="Удалить из базы", command=delete_selected).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Закрыть", command=manage_window.destroy).pack(side=tk.LEFT, padx=10) refresh_list() def refresh_recent_files(self): """Обновляет список недавних файлов""" self.load_recent_files() def show_help(self): """Создает окно справки""" help_text = ( "Для создания случайного персонажа нажмите кнопку 'Создать персонажа'.\n\n" "Для загрузки персонажа из файла или базы данных нажмите кнопку 'Загрузить'. Далее в появившемся окне выберите вариант " "загрузки: для загрузки из Excel файла нажмите кнопку 'Из Excel' и выберите нужный файл." "для загрузки из базы данных PostgreSQL нажмите кнопку 'Из PostgreSQL'. Далее в появившемся окне нажмите на нужную " "строку и нажмите кнопку 'Загрузить'.\n\n" "Для удаления файла из списка недавно изменённых файлов нажмите один раз на название соответствующего файла в списке " "и затем нажмите кнопку 'Удалить из списка'.\n\n" "Для управления записями в базе данных нажмите кнопку 'Управление базой данных'. Далее в появившемся окне Вы можете " "нажать на нужную строку и затем нажмать кнопку 'Удалить из базы' для удаления записи из базы данных.\n\n" "Для открытия изменённого ранее файла дважды нажмите на его имя в списке.\n\n" "Во вкладке генерации персонажа Вы можете изменять его расу, имя и характеристики, а также изменять предметы в инвентаре " "персонажа." ) messagebox.showinfo("Помощь", help_text) def show_about(self): """Создает окно c информацией о программе""" about_text = ( "Генератор персонажей для настольной игры DnD\n" "Версия 1.0\n" "© 2025" ) messagebox.showinfo("О программе", about_text) def update_log_safe(new_entry): """Обновляет лог-файл и список недавно измененных файлов""" log_file = 'log.txt' existing_lines = [] if os.path.exists(log_file): with open(log_file, 'r') as f: existing_lines = [line.strip() for line in f if line.strip()] if new_entry.startswith("PostgreSQL:"): match = re.match(r"PostgreSQL:(\d+)", new_entry) if match: char_id = match.group(1) existing_lines = [line for line in existing_lines if f"PostgreSQL:{char_id}" not in line] elif os.path.exists(new_entry): existing_lines = [line for line in existing_lines if new_entry not in line] timestamp = datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S") existing_lines.append(f"{new_entry} [{timestamp}]") with open(log_file, 'w') as f: for line in existing_lines: f.write(f"{line}\n") try: app_instance = get_app_instance() if app_instance: app_instance.refresh_recent_files() except: pass def get_app_instance(): """Возвращает текущий экземпляр главного окна приложения""" global APP_INSTANCE return APP_INSTANCE if __name__ == "__main__": root = tk.Tk() app = DnDGeneratorApp(root) root.mainloop()