diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index db2fe66..53fc0f3 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -1,6 +1,5 @@
{
- "hud.world": "LittleSim | seed {0} | population {1} | {2} FPS",
- "hud.terrain": "LittleSim | terrain | seed {0} | animals: {1}",
+ "hud.world": "LittleSim | seed {0} | {2} FPS",
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)",
"hud.paused": "PAUSED",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index 4fe9056..f7bed53 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -1,6 +1,5 @@
{
- "hud.world": "LittleSim | сид {0} | население {1} | {2} FPS",
- "hud.terrain": "LittleSim | террейн | сид {0} | животных: {1}",
+ "hud.world": "LittleSim | сид {0} | {2} FPS",
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)",
"hud.paused": "ПАУЗА",
diff --git a/engine b/engine
index ef1111b..5365945 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit ef1111bcb6144af4d633fc45849c5d48b1693fff
+Subproject commit 53659450a6de73a19fd8dd911cd145d01635ab74
diff --git a/src/LittleSim/Scenes/TerrainScene.cs b/src/LittleSim/Scenes/TerrainScene.cs
deleted file mode 100644
index 9dcfca9..0000000
--- a/src/LittleSim/Scenes/TerrainScene.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-using Friflo.Engine.ECS;
-using LittleSim.Content;
-using LittleSim.Sim;
-using Microsoft.Xna.Framework;
-using MrGameEng.Assets;
-using MrGameEng.Collisions;
-using MrGameEng.Core;
-using MrGameEng.DevConsole;
-using MrGameEng.Graphics;
-using MrGameEng.Input;
-using MrGameEng.Tilemaps;
-using MrGameEng.UI;
-using Myra.Graphics2D.UI;
-
-namespace LittleSim.Scenes;
-
-///
-/// Демо-сцена тайлового террейна: карта-Tilemap по дефам рельефа (поверхность из атласа
-/// или тонированный тайл), растительность по скаттеру дефов (деревья — с коллайдером
-/// ствола), животные — дефы вида "animal" с расталкиванием. Команды: "regen", "world".
-///
-public sealed class TerrainScene : Scene
-{
- private const int TerrainWidth = 44;
- private const int TerrainHeight = 28;
- private const int CellSize = 24;
- private const int Population = 14;
-
- private static readonly RectF TerrainBounds = new(
- 0f,
- 0f,
- TerrainWidth * CellSize,
- TerrainHeight * CellSize
- );
-
- private const uint AnimalLayer = 0b01;
- private const uint ObstacleLayer = 0b10;
-
- private readonly int _seed;
-
- public TerrainScene(int seed) => _seed = seed;
-
- protected override void OnLoad()
- {
- var assets = Context.Services.GetOrDefault() ?? Context.UseAssets();
- var content = Context.Services.Get();
- var atlases = Context.Services.Get();
- var device = Context.GraphicsDevice;
- var input = this.UseInput();
- var renderer = this.UseRenderer2D(
- new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
- );
- GameLayers.EnsureRegistered(renderer);
- this.UseTilemaps();
-
- var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
-
- // Тайлсет из дефов: у дефа есть поверхность — текстура из атласа, нет — тонированный тайл.
- var tiles = new TileSet();
- var tileByDef = new Dictionary();
- foreach (var terrain in content.Terrains.All)
- {
- tileByDef[terrain] = terrain.Surface is { } surface
- ? tiles.Add(atlases.GetRegion(device, surface))
- : tiles.Add(white, terrain.Tint);
- }
-
- // Террейн строится кодом: высоты по сиду → деф клетки → id тайла; поверх — скаттер.
- var random = new Random(_seed);
- var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
- var grid = new TileGrid(TerrainWidth, TerrainHeight);
- var landCells = new List();
- for (var x = 0; x < TerrainWidth; x++)
- {
- for (var y = 0; y < TerrainHeight; y++)
- {
- var terrain = content.Terrains.Classify(heights[x, y]);
- grid[x, y] = tileByDef[terrain];
- var cell = new Point(x, y);
- if (terrain.IsLand)
- {
- landCells.Add(cell);
- }
-
- ScatterSpawner.Spawn(
- this,
- content,
- atlases,
- device,
- terrain,
- cell,
- CellSize,
- random,
- obstacleLayer: ObstacleLayer,
- collidesWith: AnimalLayer
- );
- }
- }
-
- Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
-
- // Животные: дефы вида "animal", не наслаиваются (коллайдеры + расталкивание).
- var animals = content.Defs.All().Where(p => p.Kind == PawnDef.KindAnimal).ToList();
- for (var i = 0; i < Population && landCells.Count > 0 && animals.Count > 0; i++)
- {
- var cell = landCells[random.Next(landCells.Count)];
- var animal = animals[random.Next(animals.Count)];
- var body = atlases.GetRegion(device, animal.Texture);
- var sprite = new Sprite(body, GameLayers.Beings);
- sprite.CenterOrigin();
- var collider = Collider.Circle(animal.SizeCells * CellSize * 0.32f);
- collider.Layer = AnimalLayer;
- collider.CollidesWith = AnimalLayer | ObstacleLayer;
- Store.CreateEntity(
- new Transform2D(
- new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
- scale: new Vector2(CellSize * animal.SizeCells / body.Width)
- ),
- sprite,
- new Wander(),
- collider
- );
- }
-
- var camera = Store.CreateEntity(
- new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)
- );
-
- var desktop = this.UseUI();
- var hudLabel = new Label { Left = 10, Top = 8 };
- desktop.Root = hudLabel;
-
- var console = this.UseDevConsole();
- ContentCommands.Register(console, content, atlases);
- console.Register(
- "regen",
- "regen [seed] — regenerate the terrain",
- (c, args) =>
- {
- if (Context.Scenes.IsTransitioning)
- {
- return;
- }
-
- var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
- c.WriteLine($"regenerating terrain, seed {seed}");
- Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
- }
- );
- console.Register(
- "world",
- "world [seed] — switch to the world scene",
- (c, args) =>
- {
- if (Context.Scenes.IsTransitioning)
- {
- return;
- }
-
- var seed = args.Length > 0 ? int.Parse(args[0]) : _seed;
- Context.Scenes.Switch(
- new WorldScene(new App.WorldConfig { Name = "World", Seed = seed }),
- Transition.Fade(0.6f)
- );
- }
- );
-
- UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds));
- var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
- UpdateSystems.Add(new SeparationSystem(collisions));
- UpdateSystems.Add(new GodCameraSystem(camera, input, console));
- UpdateSystems.Add(
- new HudSystem(Context, content.Languages, hudLabel, "hud.terrain", _seed, Population)
- );
-
- Log.Info(
- $"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells, {Population} animals"
- );
- }
-}
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index a879add..772057e 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Friflo.Engine.ECS;
using LittleSim.App;
using LittleSim.Content;
@@ -13,6 +12,7 @@ using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Graphics;
using MrGameEng.Input;
+using MrGameEng.Tilemaps;
using MrGameEng.UI;
using Myra.Graphics2D;
using Myra.Graphics2D.UI;
@@ -20,10 +20,11 @@ using Myra.Graphics2D.UI;
namespace LittleSim.Scenes;
///
-/// Мир LittleSim: рельеф, растения и жители целиком описаны дефами Core-мода. Сцена строится
-/// из (размер/сид/население/сглаживание) и опционально восстанавливает
-/// полное состояние симуляции из . Поверх мира — HUD, полоса скорости
-/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc) и дев-консоль.
+/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
+/// (размер/сид/масштаб деталей) процедурной генерацией движка
+/// (). Каждый тип клетки рисуется текстурой-поверхностью из атласа
+/// (вода — тонированным тайлом). Поверх мира — HUD, полоса скорости (пауза/x1/x3/x6 + горячие
+/// клавиши), меню-пауза (Esc) и дев-консоль. Жителей/растений пока нет — только террейн.
///
public sealed class WorldScene : Scene
{
@@ -41,7 +42,7 @@ public sealed class WorldScene : Scene
public WorldScene(WorldConfig config)
: this(config, null) { }
- /// Мир, восстановленный из сохранения (рельеф — из сида, жители — из снимка).
+ /// Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).
public WorldScene(WorldConfig config, WorldSave? save)
{
_config = config;
@@ -63,47 +64,9 @@ public sealed class WorldScene : Scene
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
);
GameLayers.EnsureRegistered(renderer);
+ this.UseTilemaps();
- var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
-
- // Рельеф детерминирован сидом и параметром сглаживания — и для нового мира, и для загрузки.
- var random = new Random(_config.Seed);
- var heights = WorldGenerator.GenerateHeights(
- _config.Width,
- _config.Height,
- _config.Seed,
- _config.SmoothPasses
- );
- for (var x = 0; x < _config.Width; x++)
- {
- for (var y = 0; y < _config.Height; y++)
- {
- var terrain = content.Terrains.Classify(heights[x, y]);
- Store.CreateEntity(
- Transform2D.At(new Vector2(x * CellSize, y * CellSize)),
- new Sprite(white) { Color = terrain.Tint }
- );
- ScatterSpawner.Spawn(
- this,
- content,
- atlases,
- device,
- terrain,
- new Point(x, y),
- CellSize,
- random
- );
- }
- }
-
- if (_save is null)
- {
- SpawnPopulation(content, atlases, device, heights, random);
- }
- else
- {
- RestorePopulation(content, atlases, device);
- }
+ BuildTerrain(content, atlases, device, assets);
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
@@ -124,21 +87,9 @@ public sealed class WorldScene : Scene
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
- var decisions = new PawnDecisionSystem();
- UpdateSystems.Add(decisions);
- UpdateSystems.Add(new WanderSystem(_config.Seed, _bounds));
- UpdateSystems.Add(new PawnNeedsSystem());
- UpdateSystems.Add(new PawnAppearanceSystem());
UpdateSystems.Add(new GodCameraSystem(camera, input, console, () => _pause.IsOpen));
UpdateSystems.Add(
- new HudSystem(
- Context,
- content.Languages,
- hudLabel,
- "hud.world",
- _config.Seed,
- _config.Population
- )
+ new HudSystem(Context, content.Languages, hudLabel, "hud.world", _config.Seed, 0)
);
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
@@ -148,95 +99,46 @@ public sealed class WorldScene : Scene
);
}
- private void SpawnPopulation(
+ ///
+ /// Строит одну сущность-: тайлсет из дефов рельефа (поверхность из
+ /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации.
+ ///
+ private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
- float[,] heights,
- Random random
+ AssetManager assets
)
{
- var landCells = new List();
+ var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
+
+ // Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
+ var tiles = new TileSet();
+ var tileByDef = new Dictionary();
+ foreach (var terrain in content.Terrains.All)
+ {
+ tileByDef[terrain] = terrain.Surface is { } surface
+ ? tiles.Add(atlases.GetRegion(device, surface))
+ : tiles.Add(white, terrain.Tint);
+ }
+
+ // Рельеф детерминирован сидом и масштабом деталей — и для нового мира, и для загрузки.
+ var heights = WorldGenerator.Generate(
+ _config.Width,
+ _config.Height,
+ _config.Seed,
+ _config.SmoothPasses
+ );
+ var grid = new TileGrid(_config.Width, _config.Height);
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
{
- if (content.Terrains.Classify(heights[x, y]).IsLand)
- {
- landCells.Add(new Point(x, y));
- }
+ grid[x, y] = tileByDef[content.Terrains.Classify(heights[x, y])];
}
}
- var beings = content.Defs.All().Where(p => p.Kind == PawnDef.KindBeing).ToList();
- for (var i = 0; i < _config.Population && landCells.Count > 0 && beings.Count > 0; i++)
- {
- var cell = landCells[random.Next(landCells.Count)];
- var being = beings[random.Next(beings.Count)];
- CreatePawn(
- atlases,
- device,
- being,
- new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
- energy: 0.5f + random.NextSingle() * 0.5f,
- action: PawnAction.Wander,
- decideIn: random.NextSingle() * 0.75f,
- direction: Vector2.Zero,
- changeIn: 0f
- );
- }
- }
-
- private void RestorePopulation(
- GameContent content,
- ModAtlases atlases,
- Microsoft.Xna.Framework.Graphics.GraphicsDevice device
- )
- {
- foreach (var pawn in _save!.Pawns)
- {
- if (!content.Defs.TryGet(pawn.DefName, out var being))
- {
- continue; // деф пропал (мод убрали) — пропускаем жителя
- }
-
- CreatePawn(
- atlases,
- device,
- being,
- new Vector2(pawn.X, pawn.Y),
- energy: pawn.Energy,
- action: (PawnAction)pawn.Action,
- decideIn: pawn.DecideIn,
- direction: new Vector2(pawn.DirX, pawn.DirY),
- changeIn: pawn.ChangeIn
- );
- }
- }
-
- private void CreatePawn(
- ModAtlases atlases,
- Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
- PawnDef being,
- Vector2 position,
- float energy,
- PawnAction action,
- float decideIn,
- Vector2 direction,
- float changeIn
- )
- {
- var body = atlases.GetRegion(device, being.Texture);
- var sprite = new Sprite(body, GameLayers.Beings);
- sprite.CenterOrigin();
- Store.CreateEntity(
- new Transform2D(position, scale: new Vector2(CellSize * being.SizeCells / body.Width)),
- sprite,
- new Wander { Direction = direction, ChangeIn = changeIn },
- new PawnNeeds { Energy = energy },
- new PawnBrain { Action = action, DecideIn = decideIn },
- new PawnId { DefName = being.DefName }
- );
+ Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
}
private HorizontalStackPanel BuildSpeedBar(GameContent content)
@@ -312,6 +214,7 @@ public sealed class WorldScene : Scene
private string SaveWorld()
{
+ // Жителей пока нет — сохраняем только конфиг мира; рельеф воспроизводится из сида.
var save = new WorldSave
{
Name = _config.Name,
@@ -324,38 +227,8 @@ public sealed class WorldScene : Scene
ElapsedSeconds = Context.Clock.TotalTime,
};
- Store
- .Query()
- .ForEachEntity(
- (
- ref PawnId id,
- ref Transform2D transform,
- ref Wander wander,
- ref PawnNeeds needs,
- ref PawnBrain brain,
- Entity _
- ) =>
- {
- save.Pawns.Add(
- new PawnSave
- {
- DefName = id.DefName,
- X = transform.Position.X,
- Y = transform.Position.Y,
- Scale = transform.Scale.X,
- Energy = needs.Energy,
- Action = (int)brain.Action,
- DecideIn = brain.DecideIn,
- DirX = wander.Direction.X,
- DirY = wander.Direction.Y,
- ChangeIn = wander.ChangeIn,
- }
- );
- }
- );
-
new SaveStore().Write(save);
- Log.Info($"World '{_config.Name}' saved ({save.Pawns.Count} pawns)");
+ Log.Info($"World '{_config.Name}' saved");
return _config.Name;
}
@@ -389,20 +262,6 @@ public sealed class WorldScene : Scene
);
}
);
- console.Register(
- "terrain",
- "terrain [seed] — switch to the tile terrain demo scene",
- (c, args) =>
- {
- if (Context.Scenes.IsTransitioning)
- {
- return;
- }
-
- var seed = args.Length > 0 ? int.Parse(args[0]) : _config.Seed;
- Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f));
- }
- );
console.Register(
"menu",
"menu — return to the main menu",
diff --git a/src/LittleSim/Sim/WorldGenerator.cs b/src/LittleSim/Sim/WorldGenerator.cs
index 5935539..8ac67b0 100644
--- a/src/LittleSim/Sim/WorldGenerator.cs
+++ b/src/LittleSim/Sim/WorldGenerator.cs
@@ -1,77 +1,33 @@
+using MrGameEng.WorldGen;
+
namespace LittleSim.Sim;
///
-/// Детерминированная генерация рельефа: случайное поле, сглаженное несколькими проходами.
-/// Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой симуляции.
-/// Классификация высот в типы клеток живёт в дефах ().
+/// Игровой адаптер процедурной генерации: превращает параметры мира в настройки движкового
+/// (шум Перлина → fBm → island-маска) и отдаёт нормализованную
+/// карту высот. Один и тот же сид всегда даёт один и тот же мир — фундамент воспроизводимой
+/// симуляции. Классификация высот в типы клеток живёт в дефах ().
///
public static class WorldGenerator
{
- public static float[,] GenerateHeights(int width, int height, int seed, int passes = 4)
+ ///
+ /// Строит нормализованную карту высот × из
+ /// . (1..8, из меню «масштаб деталей»/сейва)
+ /// задаёт размер «континентов»: больше — крупнее и плавнее суша (ниже частота шума).
+ ///
+ public static Heightmap Generate(int width, int height, int seed, int detail = 4)
{
- var random = new Random(seed);
- var heights = new float[width, height];
- for (var x = 0; x < width; x++)
+ var scale = Math.Clamp(detail, 1, 8);
+ var settings = new HeightmapSettings
{
- for (var y = 0; y < height; y++)
- {
- heights[x, y] = random.NextSingle();
- }
- }
-
- // Сглаживание усреднением окрестности 3×3 — дёшево и даёт связные "континенты".
- // Больше проходов — крупнее и плавнее "континенты".
- for (var pass = 0; pass < passes; pass++)
- {
- var smoothed = new float[width, height];
- for (var x = 0; x < width; x++)
- {
- for (var y = 0; y < height; y++)
- {
- var sum = 0f;
- var count = 0;
- for (var dx = -1; dx <= 1; dx++)
- {
- for (var dy = -1; dy <= 1; dy++)
- {
- var nx = x + dx;
- var ny = y + dy;
- if (nx >= 0 && nx < width && ny >= 0 && ny < height)
- {
- sum += heights[nx, ny];
- count++;
- }
- }
- }
-
- smoothed[x, y] = sum / count;
- }
- }
-
- heights = smoothed;
- }
-
- Normalize(heights, width, height);
- return heights;
- }
-
- private static void Normalize(float[,] heights, int width, int height)
- {
- var min = float.MaxValue;
- var max = float.MinValue;
- foreach (var value in heights)
- {
- min = Math.Min(min, value);
- max = Math.Max(max, value);
- }
-
- var range = Math.Max(0.0001f, max - min);
- for (var x = 0; x < width; x++)
- {
- for (var y = 0; y < height; y++)
- {
- heights[x, y] = (heights[x, y] - min) / range;
- }
- }
+ Seed = seed,
+ Frequency = 6f / scale, // больше «масштаба деталей» → ниже частота → крупнее континенты
+ Octaves = 5,
+ Persistence = 0.5f,
+ Lacunarity = 2f,
+ IslandStrength = 0.55f, // края мира уходят под воду — мир выглядит как континент
+ IslandPower = 3f,
+ };
+ return new HeightmapGenerator(settings).Generate(width, height);
}
}