commit d225612d2efa0f11090a97346faefc98cca22e47 Author: Данила Копылов Date: Sun May 18 18:12:16 2025 +0300 Практика сделана diff --git a/__pycache__/climate_zones_map.cpython-313.pyc b/__pycache__/climate_zones_map.cpython-313.pyc new file mode 100644 index 0000000..06088df Binary files /dev/null and b/__pycache__/climate_zones_map.cpython-313.pyc differ diff --git a/__pycache__/frontend.cpython-313.pyc b/__pycache__/frontend.cpython-313.pyc new file mode 100644 index 0000000..d0b3a56 Binary files /dev/null and b/__pycache__/frontend.cpython-313.pyc differ diff --git a/__pycache__/generation_heightmap.cpython-313.pyc b/__pycache__/generation_heightmap.cpython-313.pyc new file mode 100644 index 0000000..2981b1b Binary files /dev/null and b/__pycache__/generation_heightmap.cpython-313.pyc differ diff --git a/__pycache__/generation_landwater.cpython-313.pyc b/__pycache__/generation_landwater.cpython-313.pyc new file mode 100644 index 0000000..4ad1725 Binary files /dev/null and b/__pycache__/generation_landwater.cpython-313.pyc differ diff --git a/__pycache__/map_style_processor.cpython-313.pyc b/__pycache__/map_style_processor.cpython-313.pyc new file mode 100644 index 0000000..433b32e Binary files /dev/null and b/__pycache__/map_style_processor.cpython-313.pyc differ diff --git a/__pycache__/map_styler.cpython-313.pyc b/__pycache__/map_styler.cpython-313.pyc new file mode 100644 index 0000000..5697cd4 Binary files /dev/null and b/__pycache__/map_styler.cpython-313.pyc differ diff --git a/__pycache__/settings.cpython-313.pyc b/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..db355d3 Binary files /dev/null and b/__pycache__/settings.cpython-313.pyc differ diff --git a/climate_zones_artistic.py b/climate_zones_artistic.py new file mode 100644 index 0000000..9574fa0 --- /dev/null +++ b/climate_zones_artistic.py @@ -0,0 +1,49 @@ +import numpy as np +import random +from settings import WorldSettings + +class ArtisticClimateGenerator: + def __init__(self, height_map: np.ndarray, settings: WorldSettings): + self.height_map = height_map + self.settings = settings + random.seed(settings.get_valid_seed()) + + # Оригинальные цвета из ClimateGenerator + self.biome_colors = { + 'water': (0.2, 0.4, 0.8, 1.0), + 'ice': (0.95, 0.95, 0.98, 0.8), + 'tundra': (0.4, 0.6, 0.9, 0.9), + 'taiga': (0.1, 0.3, 0.15, 1.0), + 'forest': (0.3, 0.6, 0.3, 1.0), + 'desert': (0.9, 0.6, 0.2, 1.0), + 'steppe': (0.8, 0.75, 0.5, 1.0), + 'swamp': (0.4, 0.35, 0.2, 0.9), + 'jungle': (0.1, 0.5, 0.1, 1.0), + 'savanna': (0.8, 0.7, 0.3, 1.0) + } + + def generate_biome_map(self, enabled_biomes=None) -> np.ndarray: + """Полностью случайное распределение биомов""" + if enabled_biomes is None: + enabled_biomes = list(self.biome_colors.keys()) + + height, width = self.height_map.shape + biome_map = np.zeros((height, width, 4), dtype=np.float32) + + # Включаем воду, если она есть в списке + water_included = 'water' in enabled_biomes + water_level = 0.05 + + for y in range(height): + for x in range(width): + # Если это вода - рисуем воду и переходим к следующей точке + if water_included and self.height_map[y,x] < water_level: + biome_map[y,x] = self.biome_colors['water'] + continue + + # Случайный выбор любого биома + available_biomes = [b for b in enabled_biomes if b != 'water'] or ['forest'] + random_biome = random.choice(available_biomes) + biome_map[y,x] = self.biome_colors[random_biome] + + return biome_map \ No newline at end of file diff --git a/climate_zones_map.py b/climate_zones_map.py new file mode 100644 index 0000000..fd2b02a --- /dev/null +++ b/climate_zones_map.py @@ -0,0 +1,117 @@ +import numpy as np +from opensimplex import OpenSimplex +from settings import WorldSettings +from numba import njit + +class ClimateGenerator: + def __init__(self, height_map: np.ndarray, settings: WorldSettings): + self.height_map = height_map + self.settings = settings + self.noise = OpenSimplex(seed=settings.get_valid_seed()) + + # Палитра биомов + self.biome_colors = { + 'water': (0.2, 0.4, 0.8, 1.0), + 'ice': (0.95, 0.95, 0.98, 0.8), + 'tundra': (0.4, 0.6, 0.9, 0.9), + 'taiga': (0.1, 0.3, 0.15, 1.0), + 'forest': (0.3, 0.6, 0.3, 1.0), + 'desert': (0.9, 0.6, 0.2, 1.0), + 'steppe': (0.8, 0.75, 0.5, 1.0), + 'swamp': (0.4, 0.35, 0.2, 0.9), + 'jungle': (0.1, 0.5, 0.1, 1.0), + 'savanna': (0.8, 0.7, 0.3, 1.0) + } + + def generate_biome_map(self, enabled_biomes=None) -> np.ndarray: + """Генерация карты биомов с учетом выбранных биомов""" + if enabled_biomes is None: + enabled_biomes = list(self.biome_colors.keys()) + + height, width = self.height_map.shape + + # Генерация шумов + temp_noise = self._generate_noise(width, height, 40) + moist_noise = self._generate_noise(width, height, 50) + + biome_map = np.zeros((height, width, 4), dtype=np.float32) + self._fill_biome_map( + biome_map, + self.height_map, + temp_noise, + moist_noise, + enabled_biomes + ) + return biome_map + + def _generate_noise(self, width, height, scale): + """Генерация многооктавного шума""" + noise = np.zeros((height, width)) + for octave in [1.0, 0.5, 0.25]: + for y in range(height): + for x in range(width): + nx = x / (scale * octave) + ny = y / (scale * octave) + noise[y, x] += self.noise.noise2(nx, ny) * octave + return (noise - noise.min()) / (noise.max() - noise.min()) + + @staticmethod + @njit + def _fill_biome_map(biome_map, height_map, temp_noise, moist_noise, enabled_biomes): + height, width = height_map.shape + water_level = 0.05 + + for y in range(height): + latitude = abs(y/height - 0.5) * 2 # 0 на экваторе, 1 на полюсах + + for x in range(width): + h = height_map[y,x] + + # Вода (всегда включена, без прозрачности) + if h < water_level: + biome_map[y,x] = (0.2, 0.4, 0.8, 1.0) + continue + + # Климатические параметры + temp = (0.7 - latitude * 0.5) * (1.0 - h * 0.5) * (0.8 + temp_noise[y,x] * 0.4) + moist = moist_noise[y,x] + + # Расчёт прозрачности: сильнее зависит от высоты + alpha = 1.0 - h**2 * 0.7 # h^2 делает прозрачность резче на вершинах + + # Выбор биома + if temp < 0.3: # Холодные зоны + if h > 0.8 and 'ice' in enabled_biomes: + biome = (0.95, 0.95, 0.98, alpha * 0.7) # Лёд прозрачнее + elif moist > 0.65 and 'tundra' in enabled_biomes: + biome = (0.4, 0.6, 0.9, alpha) + elif 'taiga' in enabled_biomes: + biome = (0.1, 0.3, 0.15, alpha) + else: + biome = (0.3, 0.6, 0.3, alpha) + + elif temp < 0.6: # Умеренные зоны + if moist > 0.7 and 'swamp' in enabled_biomes: + biome = (0.4, 0.35, 0.2, alpha) + elif moist > 0.5 and 'forest' in enabled_biomes: + biome = (0.3, 0.6, 0.3, alpha) + elif moist > 0.3 and 'steppe' in enabled_biomes: + biome = (0.8, 0.75, 0.5, alpha) + elif 'desert' in enabled_biomes: + biome = (0.9, 0.6, 0.2, alpha) + else: + biome = (0.3, 0.6, 0.3, alpha) + + else: # Теплые зоны + if moist < 0.3 and 'desert' in enabled_biomes: + biome = (0.9, 0.6, 0.2, alpha) + elif moist < 0.5 and 'savanna' in enabled_biomes: + biome = (0.8, 0.7, 0.3, alpha) + elif moist > 0.7 and 'jungle' in enabled_biomes: + biome = (0.1, 0.5, 0.1, alpha) + elif 'forest' in enabled_biomes: + biome = (0.3, 0.6, 0.3, alpha) + else: + biome = (0.8, 0.75, 0.5, alpha) + + biome_map[y,x] = biome \ No newline at end of file diff --git a/frontend.py b/frontend.py new file mode 100644 index 0000000..e67b805 --- /dev/null +++ b/frontend.py @@ -0,0 +1,391 @@ +import tkinter as tk +from tkinter import ttk +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from generation_landwater import WorldGenerator +from generation_heightmap import HeightmapGenerator +from climate_zones_map import ClimateGenerator +from settings import WorldSettings +from map_styler import MapStyler +import threading +import tempfile +import os +from PIL import Image, ImageTk + +class WorldGeneratorApp: + def __init__(self, root): + self.root = root + self.settings = WorldSettings() + self._init_variables() + self._setup_ui() + self.root.protocol("WM_DELETE_WINDOW", self._on_close) + + def _init_variables(self): + """Инициализация переменных""" + self.width_var = tk.IntVar(value=self.settings.width) + self.height_var = tk.IntVar(value=self.settings.height) + self.mode_var = tk.StringVar(value=self.settings.mode) + + # Параметры рельефа + self.terrain_rough_var = tk.DoubleVar(value=self.settings.terrain_roughness) + self.continent_size_var = tk.DoubleVar(value=self.settings.continent_size) + self.continent_rough_var = tk.DoubleVar(value=self.settings.continent_roughness) + self.islands_dens_var = tk.DoubleVar(value=self.settings.islands_density) + self.islands_rough_var = tk.DoubleVar(value=self.settings.islands_roughness) + self.island_size_var = tk.DoubleVar(value=self.settings.island_size) + + # Биомы + self.biome_vars = { + 'ice': tk.BooleanVar(value=True), + 'tundra': tk.BooleanVar(value=True), + 'taiga': tk.BooleanVar(value=True), + 'forest': tk.BooleanVar(value=True), + 'desert': tk.BooleanVar(value=True), + 'steppe': tk.BooleanVar(value=True), + 'swamp': tk.BooleanVar(value=True), + 'jungle': tk.BooleanVar(value=True), + 'savanna': tk.BooleanVar(value=True) + } + + # Данные карт + self.height_map = None + self.biome_map = None + self.map_window = None + self.canvas = None + + def _setup_ui(self): + """Настройка интерфейса""" + self.root.title("Генератор мира") + self.root.geometry("900x650") + + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Панель управления + control_frame = ttk.LabelFrame(main_frame, text="Управление") + control_frame.pack(fill=tk.X, pady=5) + + # Первый ряд кнопок + btn_row1 = ttk.Frame(control_frame) + btn_row1.pack(fill=tk.X) + + buttons_row1 = [ + ("Сгенерировать землю", self.generate_land), + ("Сгенерировать высоты", self.generate_height), + ("Сгенерировать климат", self.generate_climate), + ("Показать карту", self.show_map) + ] + + for i, (text, cmd) in enumerate(buttons_row1): + ttk.Button(btn_row1, text=text, command=cmd).grid( + row=0, column=i, padx=5, pady=5, sticky="ew") + btn_row1.grid_columnconfigure(i, weight=1) + + # Второй ряд кнопок + btn_row2 = ttk.Frame(control_frame) + btn_row2.pack(fill=tk.X, pady=5) + + buttons_row2 = [ + ("АВТО", self.auto_generate), + ("Стилизовать под свиток", self.style_map), + ("Настройки", self.open_settings) + ] + + for i, (text, cmd) in enumerate(buttons_row2): + ttk.Button(btn_row2, text=text, command=cmd).grid( + row=0, column=i, padx=5, pady=5, sticky="ew") + btn_row2.grid_columnconfigure(i, weight=1) + + # Статус бар + self.status_var = tk.StringVar(value="Готов к работе") + ttk.Label(main_frame, textvariable=self.status_var, + relief=tk.SUNKEN, anchor=tk.W).pack(fill=tk.X, pady=5) + + def _on_close(self): + """Корректное закрытие приложения""" + if self.map_window: + self.map_window.destroy() + plt.close('all') + self.root.destroy() + + + def auto_generate(self): + """Автоматическая генерация без стилизации""" + def _generate(): + try: + self.status_var.set("Автогенерация: создание земли...") + self.generate_land() + + self.status_var.set("Автогенерация: создание высот...") + self.generate_height() + + self.status_var.set("Автогенерация: создание климата...") + self.generate_climate() + + self.status_var.set("Автогенерация завершена!") + self.show_map() + except Exception as e: + self.status_var.set(f"Ошибка: {str(e)}") + + threading.Thread(target=_generate, daemon=True).start() + + def open_settings(self): + settings_win = tk.Toplevel(self.root) + settings_win.title("Настройки генерации") + settings_win.geometry("600x500") + + notebook = ttk.Notebook(settings_win) + + # Основные настройки + basic_frame = ttk.Frame(notebook) + self.create_basic_settings(basic_frame) + notebook.add(basic_frame, text="Основные") + + # Настройки биомов + biome_frame = ttk.Frame(notebook) + self.create_biome_settings(biome_frame) + notebook.add(biome_frame, text="Биомы") + + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + + # Кнопки управления + btn_frame = ttk.Frame(settings_win) + ttk.Button(btn_frame, text="Применить", + command=lambda: [self.save_settings(), settings_win.destroy()]).pack(side=tk.LEFT, padx=5) + ttk.Button(btn_frame, text="Отмена", + command=settings_win.destroy).pack(side=tk.LEFT, padx=5) + btn_frame.pack(pady=10) + + def style_map(self): + """Стилизация текущей карты под старинный свиток""" + if not hasattr(self, 'biome_map') and not hasattr(self, 'height_map'): + self.status_var.set("Ошибка: нет данных для стилизации") + return + + try: + self.status_var.set("Стилизация...") + self.root.update_idletasks() + + # Подготовка данных + if hasattr(self, 'biome_map') and self.biome_map is not None: + img_data = (self.biome_map[:, :, :3] * 255).astype(np.uint8) + else: + norm_height = (self.height_map - self.height_map.min()) / (self.height_map.max() - self.height_map.min()) + img_data = (plt.cm.terrain(norm_height)[:, :, :3] * 255).astype(np.uint8) + + # Создаем PIL Image и применяем стилизацию + pil_img = Image.fromarray(img_data, 'RGB') + styled_img = MapStyler.apply_parchment_effect(pil_img) + + # Отображаем результат + self.preview_window = tk.Toplevel(self.root) + self.preview_window.title("Стилизованная карта") + + img_tk = ImageTk.PhotoImage(styled_img) + label = ttk.Label(self.preview_window, image=img_tk) + label.image = img_tk + label.pack() + + self.status_var.set("Стилизация завершена!") + + except Exception as e: + self.status_var.set(f"Ошибка стилизации: {str(e)}") + + def create_basic_settings(self, frame): + """Настройки размера и типа карты""" + # Размер карты + size_frame = ttk.LabelFrame(frame, text="Размер карты") + size_frame.pack(fill=tk.X, padx=5, pady=5) + + ttk.Label(size_frame, text="Ширина:").grid(row=0, column=0, sticky="e") + ttk.Spinbox(size_frame, from_=100, to=1000, textvariable=self.width_var).grid( + row=0, column=1, sticky="ew", padx=5) + + ttk.Label(size_frame, text="Высота:").grid(row=1, column=0, sticky="e") + ttk.Spinbox(size_frame, from_=100, to=1000, textvariable=self.height_var).grid( + row=1, column=1, sticky="ew", padx=5) + + # Тип генерации + type_frame = ttk.LabelFrame(frame, text="Тип ландшафта") + type_frame.pack(fill=tk.X, padx=5, pady=5) + + types = ["land_only", "continent", "islands"] + texts = ["Только суша", "Материк", "Острова"] + for t, text in zip(types, texts): + ttk.Radiobutton(type_frame, text=text, variable=self.mode_var, value=t).pack( + anchor=tk.W) + + # Параметры рельефа + terrain_frame = ttk.LabelFrame(frame, text="Параметры рельефа") + terrain_frame.pack(fill=tk.X, padx=5, pady=5) + + params = [ + ("Шероховатость:", self.terrain_rough_var), + ("Размер материка:", self.continent_size_var), + ("Шероховатость материка:", self.continent_rough_var), + ("Плотность островов:", self.islands_dens_var), + ("Шероховатость островов:", self.islands_rough_var), + ("Размер островов:", self.island_size_var) + ] + + for i, (text, var) in enumerate(params): + ttk.Label(terrain_frame, text=text).grid(row=i, column=0, sticky="e", padx=5) + ttk.Scale(terrain_frame, from_=0.1, to=1.0, variable=var, + orient=tk.HORIZONTAL).grid(row=i, column=1, sticky="ew", padx=5) + ttk.Label(terrain_frame, textvariable=var).grid(row=i, column=2, padx=5) + + # Режим климата + climate_frame = ttk.LabelFrame(frame, text="Режим климата") + climate_frame.pack(fill=tk.X, padx=5, pady=5) + + if not hasattr(self.settings, 'climate_mode'): + self.settings.climate_mode = 'realistic' + + ttk.Radiobutton(climate_frame, text="Реалистичный", + variable=self.settings.climate_mode, + value="realistic").pack(anchor=tk.W) + ttk.Radiobutton(climate_frame, text="Художественный", + variable=self.settings.climate_mode, + value="artistic").pack(anchor=tk.W) + + def create_biome_settings(self, frame): + """Настройки биомов через чекбоксы""" + biome_frame = ttk.LabelFrame(frame, text="Выбор биомов") + biome_frame.pack(fill=tk.BOTH, padx=5, pady=5, expand=True) + + for i, (biome, var) in enumerate(self.biome_vars.items()): + col = i % 3 + row = i // 3 + ttk.Checkbutton( + biome_frame, + text=biome.capitalize(), + variable=var + ).grid(row=row, column=col, sticky="w", padx=5, pady=2) + + def save_settings(self): + """Сохраняет все настройки""" + try: + self.settings.width = self.width_var.get() + self.settings.height = self.height_var.get() + self.settings.mode = self.mode_var.get() + self.settings.terrain_roughness = self.terrain_rough_var.get() + self.settings.continent_size = self.continent_size_var.get() + self.settings.continent_roughness = self.continent_rough_var.get() + self.settings.islands_density = self.islands_dens_var.get() + self.settings.islands_roughness = self.islands_rough_var.get() + self.settings.island_size = self.island_size_var.get() + + self.status_var.set("Настройки сохранены") + except Exception as e: + self.status_var.set(f"Ошибка сохранения: {str(e)}") + + def generate_land(self): + """Генерация карты земли/воды""" + try: + self.land_generator = WorldGenerator( + width=self.settings.width, + height=self.settings.height, + seed=self.settings.get_valid_seed() + ) + + if self.settings.mode == 'land_only': + self.height_map = self.land_generator.generate_land_only() + elif self.settings.mode == 'continent': + self.height_map = self.land_generator.generate_continent( + size=self.settings.continent_size, + roughness=self.settings.continent_roughness + ) + else: + self.height_map = self.land_generator.generate_islands( + density=self.settings.islands_density, + island_size=self.settings.island_size, + roughness=self.settings.islands_roughness + ) + + self.biome_map = None + self.styled_map_path = None + self.status_var.set("Карта земли сгенерирована") + except Exception as e: + self.status_var.set(f"Ошибка генерации земли: {str(e)}") + + def generate_height(self): + """Генерация карты высот""" + if not hasattr(self, 'height_map') or self.height_map is None: + self.status_var.set("Ошибка: сначала сгенерируйте карту земли") + return + + try: + self.height_generator = HeightmapGenerator( + width=self.settings.width, + height=self.settings.height, + seed=self.settings.get_valid_seed() + ) + + self.height_map = self.height_generator.generate_heightmap( + self.height_map, + roughness=self.settings.terrain_roughness + ) + + self.biome_map = None + self.styled_map_path = None + self.status_var.set("Карта высот сгенерирована") + except Exception as e: + self.status_var.set(f"Ошибка генерации высот: {str(e)}") + + def generate_climate(self): + if not hasattr(self, 'height_map'): + self.status_var.set("Ошибка: сначала сгенерируйте карту высот") + return + + try: + enabled_biomes = [b for b, var in self.biome_vars.items() if var.get()] + + if self.settings.climate_mode == 'realistic': + from climate_zones_map import ClimateGenerator + self.climate_gen = ClimateGenerator(self.height_map, self.settings) + else: + from climate_zones_artistic import ArtisticClimateGenerator + self.climate_gen = ArtisticClimateGenerator(self.height_map, self.settings) + + self.biome_map = self.climate_gen.generate_biome_map(['water'] + enabled_biomes) + self.status_var.set(f"Карта климата ({self.settings.climate_mode}) сгенерирована") + except Exception as e: + self.status_var.set(f"Ошибка генерации климата: {str(e)}") + + def show_map(self): + """Отображение карты во встроенном окне""" + if not hasattr(self, 'biome_map') and not hasattr(self, 'height_map'): + self.status_var.set("Нет данных для отображения") + return + + if self.map_window: + self.map_window.destroy() + + self.map_window = tk.Toplevel(self.root) + self.map_window.title("Просмотр карты") + + fig = plt.Figure(figsize=(8, 6)) + ax = fig.add_subplot(111) + + if hasattr(self, 'biome_map') and self.biome_map is not None: + ax.imshow(self.biome_map) + ax.set_title("Карта биомов") + else: + ax.imshow(self.height_map, cmap='terrain') + ax.set_title("Карта высот") + + ax.axis('off') + + self.canvas = FigureCanvasTkAgg(fig, master=self.map_window) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Кнопка закрытия + ttk.Button(self.map_window, text="Закрыть", + command=self.map_window.destroy).pack(pady=5) + +if __name__ == "__main__": + root = tk.Tk() + app = WorldGeneratorApp(root) + root.mainloop() \ No newline at end of file diff --git a/generation_heightmap.py b/generation_heightmap.py new file mode 100644 index 0000000..e804e39 --- /dev/null +++ b/generation_heightmap.py @@ -0,0 +1,40 @@ +import numpy as np +from opensimplex import OpenSimplex + +class HeightmapGenerator: + def __init__(self, width=200, height=200, seed=None): + self.width = width + self.height = height + self.seed = seed if seed is not None else np.random.randint(0, 1000000) + self.noise = OpenSimplex(seed=self.seed) + + def generate_heightmap(self, land_water_map, roughness=0.5): + heightmap = np.zeros((self.height, self.width)) + scale = 30 / (roughness + 0.1) + + base_noise = np.zeros((self.height, self.width)) + for y in range(self.height): + for x in range(self.width): + nx = x / scale + ny = y / scale + base_noise[y,x] = self.noise.noise2(nx, ny) + + base_noise = (base_noise - base_noise.min()) / (base_noise.max() - base_noise.min()) + + detail_scale = scale * 0.3 + detail_noise = np.zeros((self.height, self.width)) + for y in range(self.height): + for x in range(self.width): + nx = x / detail_scale + ny = y / detail_scale + detail_noise[y,x] = self.noise.noise2(nx, ny) * 0.3 + + combined = base_noise + detail_noise + combined = np.where(land_water_map == 1, combined, 0) + combined = (combined - combined.min()) / (combined.max() - combined.min()) + + heightmap = np.where(land_water_map == 1, + np.round(combined * 9 + 1) / 10, + 0) + + return heightmap \ No newline at end of file diff --git a/generation_landwater.py b/generation_landwater.py new file mode 100644 index 0000000..e29d221 --- /dev/null +++ b/generation_landwater.py @@ -0,0 +1,79 @@ +import numpy as np +import opensimplex +import random + +class WorldGenerator: + def __init__(self, width=200, height=200, seed=None): + self.width = width + self.height = height + self.seed = seed if seed is not None else random.randint(0, 1000000) + self.noise = opensimplex.OpenSimplex(seed=self.seed) + random.seed(self.seed) + + def generate_land_only(self): + return np.ones((self.height, self.width)) + + def generate_continent(self, size=0.7, roughness=0.5): + world = np.zeros((self.height, self.width)) + center_x, center_y = self.width//2, self.height//2 + + for y in range(self.height): + for x in range(self.width): + nx = (x - center_x) / (self.width * 0.8) + ny = (y - center_y) / (self.height * 0.8) + dist = np.sqrt(nx**2 + ny**2) * (1.0/size) + noise_val = self.noise.noise2(x * roughness/10, y * roughness/10) + world[y][x] = 1 if (1 - dist) + noise_val * 0.5 > 0.4 else 0 + return world + + def generate_islands(self, density=0.6, island_size=0.3, roughness=0.5): + world = np.zeros((self.height, self.width)) + scale = 20 / (island_size + 0.1) + + temp_map = np.zeros((self.height, self.width)) + for y in range(self.height): + for x in range(self.width): + nx = x / scale * (1 + roughness) + ny = y / scale * (1 + roughness) + noise_val = self.noise.noise2(nx, ny) + threshold = 0.5 - density*0.4 + if noise_val > threshold: + temp_map[y][x] = 1 + + islands = [] + visited = np.zeros_like(temp_map) + for y in range(self.height): + for x in range(self.width): + if temp_map[y][x] == 1 and visited[y][x] == 0: + island = [] + queue = [(y, x)] + visited[y][x] = 1 + + min_y, max_y = y, y + min_x, max_x = x, x + + while queue: + cy, cx = queue.pop() + island.append((cy, cx)) + + min_y = min(min_y, cy) + max_y = max(max_y, cy) + min_x = min(min_x, cx) + max_x = max(max_x, cx) + + for dy, dx in [(-1,0),(1,0),(0,-1),(0,1)]: + ny, nx = cy+dy, cx+dx + if (0 <= ny < self.height and 0 <= nx < self.width and + temp_map[ny][nx] == 1 and visited[ny][nx] == 0): + visited[ny][nx] = 1 + queue.append((ny, nx)) + + if (min_y > 0 and max_y < self.height-1 and + min_x > 0 and max_x < self.width-1): + islands.append(island) + + for island in islands: + for y, x in island: + world[y][x] = 1 + + return world \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..077abd3 --- /dev/null +++ b/main.py @@ -0,0 +1,10 @@ +from frontend import WorldGeneratorApp +import tkinter as tk + +def main(): + root = tk.Tk() + app = WorldGeneratorApp(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/map_styler.py b/map_styler.py new file mode 100644 index 0000000..bc099b4 --- /dev/null +++ b/map_styler.py @@ -0,0 +1,82 @@ +import numpy as np +from PIL import Image, ImageOps, ImageFilter, ImageDraw, ImageEnhance +import random + +class MapStyler: + @staticmethod + def apply_parchment_effect(pil_image, output_path=None): + """ + Применяет эффект старинного свитка к изображению PIL + Args: + pil_image: PIL Image object + output_path: необязательный путь для сохранения + Returns: + PIL Image с эффектом + """ + # Текстура пергамента + width, height = pil_image.size + parchment = MapStyler._create_parchment_texture(width, height) + + # Эффект старения + result_img = Image.blend(parchment, pil_image, 0.6) + + # Эффекты старения + result_img = result_img.filter(ImageFilter.SMOOTH_MORE) + result_img = ImageEnhance.Contrast(result_img).enhance(1.2) + result_img = ImageEnhance.Color(result_img).enhance(0.9) + + # Декоративные элементы + result_img = MapStyler._add_decorations(result_img) + + if output_path: + result_img.save(output_path, quality=95) + + return result_img + + @staticmethod + def _create_parchment_texture(width, height): + """Создает текстуру пергамента""" + # Цвет пергамента + base = Image.new('RGB', (width, height), color=(240, 230, 200)) + + # Добавляем шум и неравномерность + noise = np.random.normal(0, 0.1, (height, width, 3)) * 255 + noise = np.clip(noise, -30, 30) + noise_img = Image.fromarray(noise.astype(np.uint8), 'RGB') + + return Image.blend(base, noise_img, 0.3) + + @staticmethod + def _add_decorations(img): + """Добавляет декоративные элементы""" + draw = ImageDraw.Draw(img) + width, height = img.size + + # Рамка в стиле старинной книги + border_color = (139, 69, 19) # Коричневый + border_width = max(width, height) // 100 + draw.rectangle([(0, 0), (width-1, height-1)], outline=border_color, width=border_width) + + # Угловые узоры + + corner_size = min(width, height) // 8 + corners = [(0, 0, 180, 270),(width-corner_size, 0, 270, 360),(0, height-corner_size, 90, 180),(width-corner_size, height-corner_size, 0, 90)] + for x, y, start, end in corners: + draw.arc([x, y, x+corner_size, y+corner_size], start, end, fill=border_color, width=2) + + # Эффект потертости по краям + mask = Image.new('L', (width, height), 255) + edge_width = min(width, height) // 15 + for i in range(edge_width): + alpha = int(255 * (i / edge_width) ** 0.5) + for side in ['top', 'bottom', 'left', 'right']: + if side in ['top', 'bottom']: + box = [0, i if side == 'top' else height-1-i, + width, i+1 if side == 'top' else height-i] + else: + box = [i if side == 'left' else width-1-i, 0, + i+1 if side == 'left' else width-i, height] + mask_draw = ImageDraw.Draw(mask) + mask_draw.rectangle(box, fill=alpha) + + return Image.composite(img, Image.new('RGB', img.size, (220, 210, 180)), mask) \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..885db01 --- /dev/null +++ b/settings.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +import numpy as np + +@dataclass +class WorldSettings: + width: int = 400 + height: int = 300 + seed: int = None + mode: str = 'continent' # 'land_only', 'continent', 'islands' + + # Параметры рельефа + terrain_roughness: float = 0.5 + continent_size: float = 0.7 + continent_roughness: float = 0.5 + islands_density: float = 0.6 + islands_roughness: float = 0.5 + island_size: float = 0.5 + climate_mode: str = 'realistic' # 'realistic' или 'artistic' + + def get_valid_seed(self): + return self.seed if self.seed is not None else np.random.randint(0, 1000000) \ No newline at end of file