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); } }