From d225612d2efa0f11090a97346faefc98cca22e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D0=BB=D0=B0=20=D0=9A=D0=BE=D0=BF?= =?UTF-8?q?=D1=8B=D0=BB=D0=BE=D0=B2?= Date: Sun, 18 May 2025 18:12:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/climate_zones_map.cpython-313.pyc | Bin 0 -> 4890 bytes __pycache__/frontend.cpython-313.pyc | Bin 0 -> 26122 bytes .../generation_heightmap.cpython-313.pyc | Bin 0 -> 2804 bytes .../generation_landwater.cpython-313.pyc | Bin 0 -> 4906 bytes .../map_style_processor.cpython-313.pyc | Bin 0 -> 3189 bytes __pycache__/map_styler.cpython-313.pyc | Bin 0 -> 4592 bytes __pycache__/settings.cpython-313.pyc | Bin 0 -> 1352 bytes climate_zones_artistic.py | 49 +++ climate_zones_map.py | 117 ++++++ frontend.py | 391 ++++++++++++++++++ generation_heightmap.py | 40 ++ generation_landwater.py | 79 ++++ main.py | 10 + map_styler.py | 82 ++++ settings.py | 21 + 15 files changed, 789 insertions(+) create mode 100644 __pycache__/climate_zones_map.cpython-313.pyc create mode 100644 __pycache__/frontend.cpython-313.pyc create mode 100644 __pycache__/generation_heightmap.cpython-313.pyc create mode 100644 __pycache__/generation_landwater.cpython-313.pyc create mode 100644 __pycache__/map_style_processor.cpython-313.pyc create mode 100644 __pycache__/map_styler.cpython-313.pyc create mode 100644 __pycache__/settings.cpython-313.pyc create mode 100644 climate_zones_artistic.py create mode 100644 climate_zones_map.py create mode 100644 frontend.py create mode 100644 generation_heightmap.py create mode 100644 generation_landwater.py create mode 100644 main.py create mode 100644 map_styler.py create mode 100644 settings.py diff --git a/__pycache__/climate_zones_map.cpython-313.pyc b/__pycache__/climate_zones_map.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06088df80d42eb13f96a45e7802c8d176a47a40c GIT binary patch literal 4890 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 P|6?-@v+G+#AYJ}{Xsn~3 literal 0 HcmV?d00001 diff --git a/__pycache__/frontend.cpython-313.pyc b/__pycache__/frontend.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0b3a566970a8f33fa330cbaf3a39032bea70ba6 GIT binary patch literal 26122 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�n58ab1$^$YlKJ#?+N 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 literal 0 HcmV?d00001 diff --git a/__pycache__/map_style_processor.cpython-313.pyc b/__pycache__/map_style_processor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..433b32e0015804a28bf32b288b3661aa2a5678f5 GIT binary patch literal 3189 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 literal 0 HcmV?d00001 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