Практика сделана
This commit is contained in:
commit
d225612d2e
BIN
__pycache__/climate_zones_map.cpython-313.pyc
Normal file
BIN
__pycache__/climate_zones_map.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/frontend.cpython-313.pyc
Normal file
BIN
__pycache__/frontend.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/generation_heightmap.cpython-313.pyc
Normal file
BIN
__pycache__/generation_heightmap.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/generation_landwater.cpython-313.pyc
Normal file
BIN
__pycache__/generation_landwater.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/map_style_processor.cpython-313.pyc
Normal file
BIN
__pycache__/map_style_processor.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/map_styler.cpython-313.pyc
Normal file
BIN
__pycache__/map_styler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/settings.cpython-313.pyc
Normal file
BIN
__pycache__/settings.cpython-313.pyc
Normal file
Binary file not shown.
49
climate_zones_artistic.py
Normal file
49
climate_zones_artistic.py
Normal file
@ -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
|
117
climate_zones_map.py
Normal file
117
climate_zones_map.py
Normal file
@ -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
|
391
frontend.py
Normal file
391
frontend.py
Normal file
@ -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()
|
40
generation_heightmap.py
Normal file
40
generation_heightmap.py
Normal file
@ -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
|
79
generation_landwater.py
Normal file
79
generation_landwater.py
Normal file
@ -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
|
10
main.py
Normal file
10
main.py
Normal file
@ -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()
|
82
map_styler.py
Normal file
82
map_styler.py
Normal file
@ -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)
|
21
settings.py
Normal file
21
settings.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user