diff --git a/Mods/Core/Defs/plants.json b/Mods/Core/Defs/plants.json
index dd3bd6c..0124e0c 100644
--- a/Mods/Core/Defs/plants.json
+++ b/Mods/Core/Defs/plants.json
@@ -1,15 +1,28 @@
{
"type": "Plant",
"defs": [
- { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28 },
+ { "defName": "BaseTree", "abstract": true, "sizeCells": 2.0, "trunkRadiusCells": 0.28,
+ "stages": [
+ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 4, "label": "plant.stage.seedling" },
+ { "sizeCells": 1.2, "growDays": 9, "label": "plant.stage.sapling" },
+ { "sizeCells": 2.0, "label": "plant.stage.mature" }
+ ] },
{ "defName": "TreeOakA", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakA" },
{ "defName": "TreeOakB", "parent": "BaseTree", "label": "plant.oak", "texture": "things/plant/treeoak/TreeOakB" },
{ "defName": "TreeBirchA", "parent": "BaseTree", "label": "plant.birch", "texture": "things/plant/treebirch/TreeBirchA" },
{ "defName": "TreeGrayPineA", "parent": "BaseTree", "label": "plant.pine", "texture": "things/plant/treegraypine/TreeGrayPineA" },
- { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2 },
+ { "defName": "GrassA", "label": "plant.grass", "texture": "things/plant/grass/grassa", "sizeCells": 1.2,
+ "stages": [
+ { "texture": "things/plant/seed_default", "sizeCells": 0.5, "growDays": 2, "label": "plant.stage.sprout" },
+ { "sizeCells": 1.2, "label": "plant.stage.mature" }
+ ] },
- { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4 },
+ { "defName": "BaseBush", "abstract": true, "label": "plant.bush", "sizeCells": 1.4,
+ "stages": [
+ { "texture": "things/plant/seed_default", "sizeCells": 0.6, "growDays": 3, "label": "plant.stage.sprout" },
+ { "sizeCells": 1.4, "label": "plant.stage.mature" }
+ ] },
{ "defName": "BushA", "parent": "BaseBush", "texture": "things/plant/bush/BushA" },
{ "defName": "BushB", "parent": "BaseBush", "texture": "things/plant/bush/BushB" },
{ "defName": "BushC", "parent": "BaseBush", "texture": "things/plant/bush/BushC" }
diff --git a/Mods/Core/Languages/en/ui.json b/Mods/Core/Languages/en/ui.json
index 53fc0f3..437bc32 100644
--- a/Mods/Core/Languages/en/ui.json
+++ b/Mods/Core/Languages/en/ui.json
@@ -1,5 +1,5 @@
{
- "hud.world": "LittleSim | seed {0} | {2} FPS",
+ "hud.world": "LittleSim | seed {0} | day {1} | {2} FPS",
"hud.controls": "WASD — camera, wheel — zoom, ` — console ('help' lists commands)",
"hud.paused": "PAUSED",
@@ -67,6 +67,10 @@
"plant.pine": "pine",
"plant.grass": "grass tuft",
"plant.bush": "bush",
+ "plant.stage.seedling": "seedling",
+ "plant.stage.sprout": "sprout",
+ "plant.stage.sapling": "sapling",
+ "plant.stage.mature": "mature",
"pawn.being": "being",
"pawn.bear": "bear",
diff --git a/Mods/Core/Languages/ru/ui.json b/Mods/Core/Languages/ru/ui.json
index f7bed53..4ac8cfd 100644
--- a/Mods/Core/Languages/ru/ui.json
+++ b/Mods/Core/Languages/ru/ui.json
@@ -1,5 +1,5 @@
{
- "hud.world": "LittleSim | сид {0} | {2} FPS",
+ "hud.world": "LittleSim | сид {0} | день {1} | {2} FPS",
"hud.controls": "WASD — камера, колесо — зум, ` — консоль ('help' — список команд)",
"hud.paused": "ПАУЗА",
@@ -67,6 +67,10 @@
"plant.pine": "сосна",
"plant.grass": "пучок травы",
"plant.bush": "куст",
+ "plant.stage.seedling": "росток",
+ "plant.stage.sprout": "всходы",
+ "plant.stage.sapling": "саженец",
+ "plant.stage.mature": "взрослое",
"pawn.being": "житель",
"pawn.bear": "медведь",
diff --git a/engine b/engine
index 5365945..f791ee6 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit 53659450a6de73a19fd8dd911cd145d01635ab74
+Subproject commit f791ee6c95be1ac215fb0cc4183f5461b0d42160
diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs
index 96c0af2..1bd2207 100644
--- a/src/LittleSim/Content/GameDefs.cs
+++ b/src/LittleSim/Content/GameDefs.cs
@@ -37,17 +37,40 @@ public sealed class TerrainDef : Def
public Color Tint => Rgb is [var r, var g, var b, ..] ? new(r, g, b) : Color.Magenta;
}
-/// Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие.
+///
+/// Одна стадия роста растения (вложенный объект ): свой спрайт и
+/// размер, и сколько игровых дней растение проводит на этой стадии до перехода к следующей.
+/// Растёт на месте — последняя стадия терминальная (её игнорируется).
+///
+public sealed class PlantGrowthStage
+{
+ /// Ключ текстуры стадии; null — берётся базовая .
+ public string? Texture { get; init; }
+
+ /// Размер спрайта в клетках на этой стадии; 0 — берётся базовый .
+ public float SizeCells { get; init; }
+
+ /// Сколько игровых дней расти на этой стадии до перехода к следующей.
+ public float GrowDays { get; init; }
+
+ /// Ключ локализации названия стадии (опционально, для будущих тултипов).
+ public string? Label { get; init; }
+}
+
+/// Растение (Defs/plants.json): текстура, размер, опциональный ствол-препятствие, стадии роста.
public sealed class PlantDef : Def
{
- /// Ключ текстуры (путь региона в атласах).
+ /// Ключ текстуры (путь региона в атласах); база для стадий без своей текстуры.
public string Texture { get; init; } = "";
- /// Размер спрайта в клетках.
+ /// Базовый размер спрайта в клетках; база для стадий без своего размера.
public float SizeCells { get; init; } = 1f;
/// Радиус коллайдера ствола в клетках; 0 — без коллайдера (трава, кусты).
public float TrunkRadiusCells { get; init; }
+
+ /// Стадии роста по порядку; пусто — одна терминальная стадия из базовой текстуры.
+ public List Stages { get; init; } = [];
}
///
diff --git a/src/LittleSim/Content/PlantSet.cs b/src/LittleSim/Content/PlantSet.cs
new file mode 100644
index 0000000..74872cb
--- /dev/null
+++ b/src/LittleSim/Content/PlantSet.cs
@@ -0,0 +1,89 @@
+using Microsoft.Xna.Framework.Graphics;
+using MrGameEng.Graphics;
+
+namespace LittleSim.Content;
+
+///
+/// Готовая (resolved) таблица растений для сцены: по каждому — массив
+/// стадий с уже загруженными регионами атласа, размерами и порогами входа в днях. Строится один
+/// раз при загрузке сцены (как , потому что нужны атлас и
+/// устройство). индексируется в неё без словарей и аллокаций.
+///
+public sealed class PlantSet
+{
+ /// Разрешённая стадия: спрайт, размер в клетках и день, с которого стадия активна.
+ public readonly record struct Stage(Texture2DRegion Region, float SizeCells, float EnterDay);
+
+ /// Вид растения: его деф и стадии роста по возрастанию .
+ public sealed class Species
+ {
+ /// Исходный деф растения.
+ public required PlantDef Def { get; init; }
+
+ /// Стадии роста по порядку; последняя — терминальная.
+ public required Stage[] Stages { get; init; }
+
+ /// День, с которого растение достигает последней (зрелой) стадии.
+ public float MaturityDays => Stages[^1].EnterDay;
+
+ /// Индекс стадии для данного возраста в днях.
+ public int StageAt(float ageDays)
+ {
+ var stage = 0;
+ while (stage + 1 < Stages.Length && ageDays >= Stages[stage + 1].EnterDay)
+ {
+ stage++;
+ }
+
+ return stage;
+ }
+ }
+
+ private readonly Species[] _species;
+ private readonly Dictionary _index = new();
+
+ /// Строит таблицу из всех (не-abstract) мода.
+ public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
+ {
+ var defs = content.Defs.All().ToArray();
+ _species = new Species[defs.Length];
+ for (var i = 0; i < defs.Length; i++)
+ {
+ var def = defs[i];
+ _species[i] = new Species { Def = def, Stages = BuildStages(def, atlases, device) };
+ _index[def] = i;
+ }
+ }
+
+ /// Число видов в таблице.
+ public int Count => _species.Length;
+
+ /// Вид по индексу.
+ public Species this[int index] => _species[index];
+
+ /// Индекс вида по дефу.
+ public int IndexOf(PlantDef def) => _index[def];
+
+ private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
+ {
+ // Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
+ if (def.Stages.Count == 0)
+ {
+ var region = atlases.GetRegion(device, def.Texture);
+ return [new Stage(region, def.SizeCells, 0f)];
+ }
+
+ var stages = new Stage[def.Stages.Count];
+ var enterDay = 0f;
+ for (var i = 0; i < def.Stages.Count; i++)
+ {
+ var stage = def.Stages[i];
+ var region = atlases.GetRegion(device, stage.Texture ?? def.Texture);
+ var size = stage.SizeCells > 0f ? stage.SizeCells : def.SizeCells;
+ stages[i] = new Stage(region, size, enterDay);
+ enterDay += stage.GrowDays; // следующая стадия входит после GrowDays этой
+ }
+
+ return stages;
+ }
+}
diff --git a/src/LittleSim/Scenes/ContentHelpers.cs b/src/LittleSim/Scenes/ContentHelpers.cs
index 74da80b..c737641 100644
--- a/src/LittleSim/Scenes/ContentHelpers.cs
+++ b/src/LittleSim/Scenes/ContentHelpers.cs
@@ -1,32 +1,30 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
+using LittleSim.Sim;
using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Core;
using MrGameEng.DevConsole;
using MrGameEng.Graphics;
namespace LittleSim.Scenes;
-/// Спавн растительности по скаттеру дефов террейна (общий для обеих сцен).
+/// Спавн растительности по скаттеру дефов террейна.
internal static class ScatterSpawner
{
///
- /// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение
- /// со случайным смещением. Деревья (ствол > 0) получают коллайдер-препятствие,
- /// когда заданы слои коллизий.
+ /// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
+ /// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
+ /// разновозрастный. Стадия и спрайт берутся из , дальше растением
+ /// управляет .
///
public static void Spawn(
Scene scene,
GameContent content,
- ModAtlases atlases,
- GraphicsDevice device,
+ PlantSet plants,
TerrainDef terrain,
Point cell,
int cellSize,
- Random random,
- uint obstacleLayer = 0,
- uint collidesWith = 0
+ Random random
)
{
foreach (var entry in terrain.Scatter)
@@ -36,28 +34,30 @@ internal static class ScatterSpawner
continue;
}
- var plant = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]);
- var region = atlases.GetRegion(device, plant.Texture);
- var sprite = new Sprite(region, GameLayers.Beings);
- sprite.CenterOrigin();
- var entity = scene.Store.CreateEntity(
- new Transform2D(
- new Vector2(
- (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize,
- (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize
- ),
- scale: new Vector2(cellSize * plant.SizeCells / region.Width)
- ),
- sprite
- );
+ var def = content.Defs.Get(entry.Options[random.Next(entry.Options.Count)]);
+ var index = plants.IndexOf(def);
+ var species = plants[index];
+ var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
+ var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
+ var age = random.NextSingle() * species.MaturityDays;
+ var stage = species.StageAt(age);
+ var resolved = species.Stages[stage];
- if (obstacleLayer != 0 && plant.TrunkRadiusCells > 0f)
- {
- var trunk = MrGameEng.Collisions.Collider.Circle(cellSize * plant.TrunkRadiusCells);
- trunk.Layer = obstacleLayer;
- trunk.CollidesWith = collidesWith;
- entity.AddComponent(trunk);
- }
+ var sprite = new Sprite(resolved.Region, GameLayers.Beings);
+ sprite.CenterOrigin();
+ scene.Store.CreateEntity(
+ new Transform2D(
+ new Vector2(px, py),
+ scale: new Vector2(cellSize * resolved.SizeCells / resolved.Region.Width)
+ ),
+ sprite,
+ new PlantGrowth
+ {
+ Species = index,
+ Stage = stage,
+ AgeDays = age,
+ }
+ );
}
}
}
diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs
index 772057e..779b150 100644
--- a/src/LittleSim/Scenes/WorldScene.cs
+++ b/src/LittleSim/Scenes/WorldScene.cs
@@ -30,6 +30,9 @@ public sealed class WorldScene : Scene
{
public const int CellSize = 16;
+ /// Сколько секунд масштабированного времени длится один игровой день.
+ public const float SecondsPerDay = 30f;
+
private readonly WorldConfig _config;
private readonly WorldSave? _save;
private readonly RectF _bounds;
@@ -66,7 +69,12 @@ public sealed class WorldScene : Scene
GameLayers.EnsureRegistered(renderer);
this.UseTilemaps();
- BuildTerrain(content, atlases, device, assets);
+ var calendar = Context.UseCalendar(SecondsPerDay);
+
+ // Рельеф и расстановка растений детерминированы сидом мира (независимые потоки seed).
+ var plants = new PlantSet(content, atlases, device);
+ var random = new Random(_config.Seed);
+ BuildTerrain(content, atlases, device, assets, plants, random);
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
@@ -87,9 +95,10 @@ public sealed class WorldScene : Scene
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
+ UpdateSystems.Add(new PlantGrowthSystem(plants, calendar, CellSize));
UpdateSystems.Add(new GodCameraSystem(camera, input, console, () => _pause.IsOpen));
UpdateSystems.Add(
- new HudSystem(Context, content.Languages, hudLabel, "hud.world", _config.Seed, 0)
+ new HudSystem(Context, content.Languages, hudLabel, "hud.world", _config.Seed, calendar)
);
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
@@ -101,13 +110,16 @@ public sealed class WorldScene : Scene
///
/// Строит одну сущность-: тайлсет из дефов рельефа (поверхность из
- /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации.
+ /// атласа либо тонированный тайл) и сетка тайлов по карте высот процедурной генерации, а
+ /// поверх — растительность по скаттеру дефов (с компонентом роста, разной зрелости).
///
private void BuildTerrain(
GameContent content,
ModAtlases atlases,
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
- AssetManager assets
+ AssetManager assets,
+ PlantSet plants,
+ Random random
)
{
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
@@ -134,7 +146,17 @@ public sealed class WorldScene : Scene
{
for (var y = 0; y < _config.Height; y++)
{
- grid[x, y] = tileByDef[content.Terrains.Classify(heights[x, y])];
+ var terrain = content.Terrains.Classify(heights[x, y]);
+ grid[x, y] = tileByDef[terrain];
+ ScatterSpawner.Spawn(
+ this,
+ content,
+ plants,
+ terrain,
+ new Point(x, y),
+ CellSize,
+ random
+ );
}
}
diff --git a/src/LittleSim/Sim/PlantGrowth.cs b/src/LittleSim/Sim/PlantGrowth.cs
new file mode 100644
index 0000000..2ed7d43
--- /dev/null
+++ b/src/LittleSim/Sim/PlantGrowth.cs
@@ -0,0 +1,20 @@
+using Friflo.Engine.ECS;
+
+namespace LittleSim.Sim;
+
+///
+/// Состояние роста растения: индекс вида в , текущая стадия и
+/// накопленный возраст в игровых днях. Плоские данные — переход между стадиями и смену спрайта
+/// выполняет , читая пороги стадий из таблицы вида.
+///
+public struct PlantGrowth : IComponent
+{
+ /// Индекс вида растения в .
+ public int Species;
+
+ /// Текущая стадия роста (индекс в стадиях вида).
+ public int Stage;
+
+ /// Накопленный возраст в игровых днях.
+ public float AgeDays;
+}
diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs
new file mode 100644
index 0000000..7ef16bf
--- /dev/null
+++ b/src/LittleSim/Sim/PlantGrowthSystem.cs
@@ -0,0 +1,65 @@
+using Friflo.Engine.ECS.Systems;
+using LittleSim.Content;
+using Microsoft.Xna.Framework;
+using MrGameEng.Core;
+using MrGameEng.Graphics;
+
+namespace LittleSim.Sim;
+
+///
+/// Двигает рост растений: копит возраст по игровому календарю и при пересечении порога стадии
+/// переключает спрайт на текстуру новой стадии, переотцентровывает и масштабирует его. Время
+/// идёт по (пауза/скорость учитываются через Tick.deltaTime),
+/// данные стадий — из . Терминальную стадию пропускает (рост окончен).
+///
+public sealed class PlantGrowthSystem(PlantSet plants, Calendar calendar, int cellSize)
+ : QuerySystem
+{
+ protected override void OnUpdate()
+ {
+ var deltaDays = Tick.deltaTime / calendar.SecondsPerDay;
+ if (deltaDays <= 0f)
+ {
+ return; // пауза — рост стоит
+ }
+
+ foreach (var (growths, sprites, transforms, _) in Query.Chunks)
+ {
+ var g = growths.Span;
+ var s = sprites.Span;
+ var t = transforms.Span;
+ for (var i = 0; i < g.Length; i++)
+ {
+ ref var grow = ref g[i];
+ var stages = plants[grow.Species].Stages;
+ if (grow.Stage >= stages.Length - 1)
+ {
+ continue; // уже зрелое — рост окончен
+ }
+
+ grow.AgeDays += deltaDays;
+ var changed = false;
+ while (
+ grow.Stage < stages.Length - 1
+ && grow.AgeDays >= stages[grow.Stage + 1].EnterDay
+ )
+ {
+ grow.Stage++;
+ changed = true;
+ }
+
+ if (changed)
+ {
+ Apply(ref s[i], ref t[i], stages[grow.Stage]);
+ }
+ }
+ }
+ }
+
+ private void Apply(ref Sprite sprite, ref Transform2D transform, in PlantSet.Stage stage)
+ {
+ sprite.Region = stage.Region;
+ sprite.Origin = new Vector2(stage.Region.Width / 2f, stage.Region.Height / 2f);
+ transform.Scale = new Vector2(cellSize * stage.SizeCells / stage.Region.Width);
+ }
+}
diff --git a/src/LittleSim/Sim/SimSystems.cs b/src/LittleSim/Sim/SimSystems.cs
index 8870ce0..5ac8537 100644
--- a/src/LittleSim/Sim/SimSystems.cs
+++ b/src/LittleSim/Sim/SimSystems.cs
@@ -161,7 +161,7 @@ public sealed class GodCameraSystem(
}
///
-/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, населением и FPS)
+/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, днём календаря и FPS)
/// и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама.
///
public sealed class HudSystem(
@@ -170,7 +170,7 @@ public sealed class HudSystem(
Myra.Graphics2D.UI.Label label,
string titleKey,
int seed,
- int population
+ MrGameEng.Core.Calendar? calendar = null
) : BaseSystem
{
private float _accumulated;
@@ -189,7 +189,7 @@ public sealed class HudSystem(
_accumulated = 0f;
_frames = 0;
label.Text =
- languages.Format(titleKey, seed, population, fps)
+ languages.Format(titleKey, seed, calendar?.Day ?? 0, fps)
+ "\n"
+ languages.Get("hud.controls");
}