653 lines
25 KiB
C#
653 lines
25 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Friflo.Engine.ECS;
|
||
using LittleSim.App;
|
||
using LittleSim.Content;
|
||
using LittleSim.Sim;
|
||
using LittleSim.UI;
|
||
using Microsoft.Xna.Framework;
|
||
using Microsoft.Xna.Framework.Input;
|
||
using MrGameEng.Assets;
|
||
using MrGameEng.Core;
|
||
using MrGameEng.DevConsole;
|
||
using MrGameEng.Formulas;
|
||
using MrGameEng.Genetics;
|
||
using MrGameEng.Graphics;
|
||
using MrGameEng.Host;
|
||
using MrGameEng.Input;
|
||
using MrGameEng.Inspector;
|
||
using MrGameEng.Lighting;
|
||
using MrGameEng.Tilemaps;
|
||
using MrGameEng.UI;
|
||
using Myra.Graphics2D;
|
||
using Myra.Graphics2D.UI;
|
||
|
||
namespace LittleSim.Scenes;
|
||
|
||
/// <summary>
|
||
/// Мир LittleSim: тайловый рельеф целиком описан дефами Core-мода и строится из
|
||
/// <see cref="WorldConfig"/> (размер/сид/масштаб деталей) процедурной генерацией движка
|
||
/// (<see cref="WorldGenerator"/>). Каждый тип клетки рисуется текстурой-поверхностью из атласа
|
||
/// (вода — тонированным тайлом). Поверх растёт растительность с геномом (рост по свету/температуре/
|
||
/// почве, размножение по Менделю и смерть), климат и день/ночь. UI: HUD, полоса скорости
|
||
/// (пауза/x1/x3/x6 + горячие клавиши), меню-пауза (Esc), дев-консоль и ECS-инспектор (F1).
|
||
/// </summary>
|
||
public sealed class WorldScene : Scene
|
||
{
|
||
public const int CellSize = 16;
|
||
|
||
/// <summary>
|
||
/// Сколько секунд масштабированного времени длится один игровой день. База (x1) — 3 игровые
|
||
/// минуты за 1 реальную секунду: сутки = 1440 мин ÷ 3 = 480 с.
|
||
/// </summary>
|
||
public const float SecondsPerDay = 480f;
|
||
|
||
/// <summary>Максимум растений на клетку — потолок плотности для размножения.</summary>
|
||
public const int DensityCap = 4;
|
||
|
||
private readonly WorldConfig _config;
|
||
private readonly WorldSave? _save;
|
||
private readonly RectF _bounds;
|
||
|
||
private GameSpeed _speed = null!;
|
||
private PauseMenu _pause = null!;
|
||
private Selection _selection = null!;
|
||
private InspectPanel _inspect = null!;
|
||
private readonly List<Action> _speedRefreshers = [];
|
||
|
||
private PlantSet _plants = null!;
|
||
private float[] _cellFertility = [];
|
||
private bool[] _cellLand = [];
|
||
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
|
||
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
|
||
|
||
/// <summary>Новый мир из конфига.</summary>
|
||
public WorldScene(WorldConfig config)
|
||
: this(config, null) { }
|
||
|
||
/// <summary>Мир, восстановленный из сохранения (рельеф детерминирован сидом конфига).</summary>
|
||
public WorldScene(WorldConfig config, WorldSave? save)
|
||
{
|
||
_config = config;
|
||
_save = save;
|
||
_bounds = new RectF(0f, 0f, config.Width * CellSize, config.Height * CellSize);
|
||
}
|
||
|
||
protected override void OnLoad()
|
||
{
|
||
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
||
var content = Context.Services.Get<GameContent>();
|
||
var atlases = Context.Services.Get<ModAtlases>();
|
||
var device = Context.GetGraphicsDevice();
|
||
var input = this.UseInput();
|
||
_speed = Context.Services.Get<GameSpeed>();
|
||
_speed.SetStep(0); // новый мир/загрузка стартуют на x1
|
||
|
||
var renderer = this.UseRenderer2D(
|
||
new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }
|
||
);
|
||
GameLayers.EnsureRegistered(renderer);
|
||
this.UseTilemaps();
|
||
|
||
var calendar = Context.UseCalendar(SecondsPerDay);
|
||
var climate = Context.UseClimate(ClimateSettings.Default);
|
||
var dayNight = new DayNight(calendar, DayNightSettings.Default);
|
||
|
||
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
|
||
_plants = new PlantSet(content, atlases, device);
|
||
var loadingPlants = _save?.Plants is { Count: > 0 };
|
||
BuildTerrain(
|
||
content,
|
||
atlases,
|
||
device,
|
||
assets,
|
||
loadingPlants ? null : new Random(_config.Seed)
|
||
);
|
||
if (loadingPlants)
|
||
{
|
||
RestorePlants(content);
|
||
}
|
||
|
||
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
|
||
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
|
||
var lighting = this.UseLighting(
|
||
renderer,
|
||
dayNight,
|
||
_config.Width,
|
||
_config.Height,
|
||
CellSize,
|
||
Vector2.Zero,
|
||
BuildOccluders
|
||
);
|
||
|
||
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
|
||
|
||
// HUD, полоса скорости и меню-пауза в одной корневой панели.
|
||
var desktop = this.UseUI();
|
||
var hudLabel = new Label { Left = 10, Top = 8 };
|
||
var speedBar = BuildSpeedBar(content);
|
||
_pause = new PauseMenu(
|
||
Context,
|
||
content,
|
||
_speed,
|
||
onSave: SaveWorld,
|
||
onMainMenu: () => Switch(new MainMenuScene()),
|
||
onQuit: () => Context.Services.Get<Game>().Exit()
|
||
);
|
||
_selection = new Selection();
|
||
_inspect = new InspectPanel(Store, _plants, content, climate, _selection);
|
||
desktop.Root = Ui.Screen(
|
||
hudLabel,
|
||
_inspect.Highlight,
|
||
_inspect.Panel,
|
||
speedBar,
|
||
_pause.Root
|
||
);
|
||
|
||
this.UseInspector(renderer);
|
||
var console = this.UseDevConsole();
|
||
RegisterCommands(console, content, atlases);
|
||
console.Register(
|
||
"light",
|
||
"light [radius] — place a point light at the cursor (night shadows demo)",
|
||
(c, args) =>
|
||
{
|
||
var radius =
|
||
args.Length > 0
|
||
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
|
||
: 120f;
|
||
var mouse = Mouse.GetState();
|
||
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
|
||
Store.CreateEntity(
|
||
Transform2D.At(world),
|
||
new PointLight
|
||
{
|
||
Radius = radius,
|
||
Color = Color.White,
|
||
Intensity = 0.9f,
|
||
}
|
||
);
|
||
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
|
||
}
|
||
);
|
||
|
||
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
|
||
UpdateSystems.Add(new PlantFruitingSystem(_plants, calendar, climate));
|
||
UpdateSystems.Add(
|
||
new PlantLifecycleSystem(
|
||
Store,
|
||
_plants,
|
||
calendar,
|
||
climate,
|
||
Context.Clock,
|
||
_config.Width,
|
||
_config.Height,
|
||
CellSize,
|
||
_cellFertility,
|
||
_cellLand,
|
||
DensityCap,
|
||
_config.Seed
|
||
)
|
||
);
|
||
UpdateSystems.Add(
|
||
new GodCameraSystem(camera, input, renderer, console, () => _pause.IsOpen)
|
||
);
|
||
UpdateSystems.Add(
|
||
new SelectionSystem(
|
||
Store,
|
||
input,
|
||
renderer,
|
||
_selection,
|
||
() => _pause.IsOpen,
|
||
p => _inspect.IsOverPanel(p) || speedBar.Bounds.Contains(p)
|
||
)
|
||
);
|
||
UpdateSystems.Add(new CallbackSystem(() => _inspect.Refresh(renderer)));
|
||
UpdateSystems.Add(
|
||
new HudSystem(
|
||
Context,
|
||
content.Languages,
|
||
hudLabel,
|
||
"hud.world",
|
||
_config.Seed,
|
||
calendar,
|
||
climate
|
||
)
|
||
);
|
||
UpdateSystems.Add(new CallbackSystem(() => Hotkeys(input)));
|
||
|
||
Log.Info(
|
||
$"World '{_config.Name}': {_config.Width}x{_config.Height}, seed {_config.Seed}"
|
||
+ (_save is null ? " (new)" : " (loaded)")
|
||
);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Строит сущность-<see cref="Tilemap"/> (тайлсет из дефов рельефа + сетка по карте высот) и
|
||
/// заполняет по-клеточные массивы плодородности/суши для жизненного цикла. Если
|
||
/// <paramref name="scatterRandom"/> задан (новый мир) — рассыпает растительность по дефам;
|
||
/// при загрузке передаётся null (растения восстанавливаются из сейва отдельно).
|
||
/// </summary>
|
||
private void BuildTerrain(
|
||
GameContent content,
|
||
ModAtlases atlases,
|
||
Microsoft.Xna.Framework.Graphics.GraphicsDevice device,
|
||
AssetManager assets,
|
||
Random? scatterRandom
|
||
)
|
||
{
|
||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
||
|
||
// Тайлсет из дефов: есть поверхность — текстура из атласа, нет — тонированный тайл (вода).
|
||
var tiles = new TileSet();
|
||
var tileByDef = new Dictionary<TerrainDef, ushort>();
|
||
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);
|
||
var cells = _config.Width * _config.Height;
|
||
_cellFertility = new float[cells];
|
||
_cellLand = new bool[cells];
|
||
_cellOccluderBase = new bool[cells];
|
||
_cellOccluder = new bool[cells];
|
||
for (var x = 0; x < _config.Width; x++)
|
||
{
|
||
for (var y = 0; y < _config.Height; y++)
|
||
{
|
||
var terrain = content.Terrains.Classify(heights[x, y]);
|
||
grid[x, y] = tileByDef[terrain];
|
||
var cell = y * _config.Width + x;
|
||
_cellFertility[cell] = terrain.Fertility;
|
||
_cellLand[cell] = terrain.IsLand;
|
||
_cellOccluderBase[cell] = terrain.BlocksLight;
|
||
if (scatterRandom is not null)
|
||
{
|
||
ScatterSpawner.Spawn(
|
||
this,
|
||
content,
|
||
_plants,
|
||
terrain,
|
||
new Point(x, y),
|
||
CellSize,
|
||
scatterRandom
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||
}
|
||
|
||
private HorizontalStackPanel BuildSpeedBar(GameContent content)
|
||
{
|
||
var bar = Ui.Row(6);
|
||
bar.HorizontalAlignment = HorizontalAlignment.Center;
|
||
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
||
bar.Margin = new Thickness(0, 0, 0, 12);
|
||
|
||
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
|
||
pause.Click += (_, _) => _speed.Pause();
|
||
bar.Widgets.Add(pause);
|
||
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
||
|
||
for (var i = 0; i < _speed.Steps.Count; i++)
|
||
{
|
||
var index = i;
|
||
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
|
||
button.Click += (_, _) => _speed.SetStep(index);
|
||
bar.Widgets.Add(button);
|
||
_speedRefreshers.Add(() =>
|
||
button.TextColor =
|
||
!_speed.IsPaused && _speed.StepIndex == index ? Ui.Accent : Ui.Muted
|
||
);
|
||
}
|
||
|
||
void Refresh()
|
||
{
|
||
foreach (var refresh in _speedRefreshers)
|
||
{
|
||
refresh();
|
||
}
|
||
}
|
||
|
||
_speed.Changed += Refresh;
|
||
RegisterUnload(() => _speed.Changed -= Refresh);
|
||
Refresh();
|
||
return bar;
|
||
}
|
||
|
||
private void Hotkeys(InputManager input)
|
||
{
|
||
if (input.IsKeyPressed(Keys.Escape))
|
||
{
|
||
// Esc сначала снимает выделение (если есть), и только потом открывает меню-паузу.
|
||
if (_selection.HasSelection)
|
||
{
|
||
_selection.Clear();
|
||
}
|
||
else
|
||
{
|
||
_pause.Toggle();
|
||
}
|
||
}
|
||
|
||
if (_pause.IsOpen)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (input.IsKeyPressed(Keys.Space))
|
||
{
|
||
_speed.TogglePause();
|
||
}
|
||
|
||
if (input.IsKeyPressed(Keys.D1))
|
||
{
|
||
_speed.SetStep(0);
|
||
}
|
||
|
||
if (input.IsKeyPressed(Keys.D2) && _speed.Steps.Count > 1)
|
||
{
|
||
_speed.SetStep(1);
|
||
}
|
||
|
||
if (input.IsKeyPressed(Keys.D3) && _speed.Steps.Count > 2)
|
||
{
|
||
_speed.SetStep(2);
|
||
}
|
||
}
|
||
|
||
private string SaveWorld()
|
||
{
|
||
var save = new WorldSave
|
||
{
|
||
Name = _config.Name,
|
||
Width = _config.Width,
|
||
Height = _config.Height,
|
||
Seed = _config.Seed,
|
||
SmoothPasses = _config.SmoothPasses,
|
||
Population = _config.Population,
|
||
SavedUtc = DateTime.UtcNow,
|
||
ElapsedSeconds = Context.Clock.TotalTime,
|
||
};
|
||
|
||
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
|
||
Store
|
||
.Query<PlantGrowth, PlantOrganism, Transform2D>()
|
||
.ForEachEntity(
|
||
(
|
||
ref PlantGrowth grow,
|
||
ref PlantOrganism org,
|
||
ref Transform2D transform,
|
||
Entity _
|
||
) =>
|
||
{
|
||
save.Plants.Add(
|
||
new PlantSave
|
||
{
|
||
Species = _plants[grow.Species].Def.DefName,
|
||
X = transform.Position.X,
|
||
Y = transform.Position.Y,
|
||
AgeDays = grow.AgeDays,
|
||
Stage = grow.Stage,
|
||
CellFertility = grow.CellFertility,
|
||
Genome = org.Genome.ToDictionary(),
|
||
}
|
||
);
|
||
}
|
||
);
|
||
|
||
new SaveStore().Write(save);
|
||
Log.Info($"World '{_config.Name}' saved ({save.Plants.Count} plants)");
|
||
return _config.Name;
|
||
}
|
||
|
||
private void RestorePlants(GameContent content)
|
||
{
|
||
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
|
||
foreach (var plant in _save!.Plants)
|
||
{
|
||
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
|
||
{
|
||
continue; // вид пропал (мод убрали) — пропускаем растение
|
||
}
|
||
|
||
var index = _plants.IndexOf(def);
|
||
var genome = plant.Genome is { Count: > 0 }
|
||
? new Genome(plant.Genome)
|
||
: _plants[index].Template.Generate(fallback);
|
||
PlantFactory.Create(
|
||
Store,
|
||
_plants,
|
||
index,
|
||
new Vector2(plant.X, plant.Y),
|
||
plant.AgeDays,
|
||
genome,
|
||
plant.CellFertility,
|
||
CellSize
|
||
);
|
||
}
|
||
}
|
||
|
||
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
|
||
private bool[] BuildOccluders()
|
||
{
|
||
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
|
||
Store
|
||
.Query<PlantGrowth, Transform2D>()
|
||
.ForEachEntity(
|
||
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
|
||
{
|
||
var species = _plants[grow.Species];
|
||
if (
|
||
species.Def.TrunkRadiusCells <= 0f
|
||
|| grow.Stage < species.Stages.Length - 1
|
||
)
|
||
{
|
||
return; // затеняют только зрелые деревья (со стволом)
|
||
}
|
||
|
||
var cx = Math.Clamp(
|
||
(int)(transform.Position.X / CellSize),
|
||
0,
|
||
_config.Width - 1
|
||
);
|
||
var cy = Math.Clamp(
|
||
(int)(transform.Position.Y / CellSize),
|
||
0,
|
||
_config.Height - 1
|
||
);
|
||
_cellOccluder[cy * _config.Width + cx] = true;
|
||
}
|
||
);
|
||
return _cellOccluder;
|
||
}
|
||
|
||
private void Switch(Scene scene)
|
||
{
|
||
if (!Context.Scenes.IsTransitioning)
|
||
{
|
||
_speed.Resume();
|
||
Context.Scenes.Switch(scene, Transitions.Fade(0.5f));
|
||
}
|
||
}
|
||
|
||
private void RegisterCommands(DevConsole console, GameContent content, ModAtlases atlases)
|
||
{
|
||
ContentCommands.Register(console, content, atlases);
|
||
console.Register(
|
||
"regen",
|
||
"regen [seed] — regenerate the world with a new seed",
|
||
(c, args) =>
|
||
{
|
||
if (Context.Scenes.IsTransitioning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||
c.WriteLine($"regenerating world, seed {seed}");
|
||
Context.Scenes.Switch(
|
||
new WorldScene(_config with { Seed = seed }),
|
||
Transitions.Fade(0.6f)
|
||
);
|
||
}
|
||
);
|
||
console.Register(
|
||
"formula",
|
||
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
|
||
(c, args) =>
|
||
{
|
||
if (args.Length == 0)
|
||
{
|
||
c.WriteLine(
|
||
"usage: formula <expr> e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)"
|
||
);
|
||
return;
|
||
}
|
||
|
||
var expression = string.Join(' ', args);
|
||
try
|
||
{
|
||
c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}");
|
||
}
|
||
catch (FormulaException e)
|
||
{
|
||
c.WriteLine($"error: {e.Message}");
|
||
}
|
||
}
|
||
);
|
||
console.Register(
|
||
"gene",
|
||
"gene [seed] — generate a genome from Gene defs, show alleles/traits and a bred child",
|
||
(c, args) => RunGeneDemo(c, content, args)
|
||
);
|
||
console.Register(
|
||
"plant",
|
||
"plant <species> [seed] — sample a species genome and show its gene-driven traits/products",
|
||
(c, args) => RunPlantDemo(c, content, args)
|
||
);
|
||
console.Register(
|
||
"menu",
|
||
"menu — return to the main menu",
|
||
(_, _) => Switch(new MainMenuScene())
|
||
);
|
||
}
|
||
|
||
// Демонстрация генной системы (фаза G2): из Gene-дефов генерируем геном, печатаем аллели,
|
||
// выраженные значения и признаки (вычисленные формулами), затем скрещиваем двух особей.
|
||
private static void RunGeneDemo(DevConsole console, GameContent content, string[] args)
|
||
{
|
||
var genes = content.Defs.All<GeneDef>();
|
||
if (genes.Count == 0)
|
||
{
|
||
console.WriteLine("no Gene defs loaded");
|
||
return;
|
||
}
|
||
|
||
var registry = genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
|
||
var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next();
|
||
var random = new Random(seed);
|
||
|
||
console.WriteLine($"genome from {genes.Count} genes, seed {seed}:");
|
||
var parentA = Genome.Generate(genes, random);
|
||
foreach (var gene in genes)
|
||
{
|
||
var allele = parentA[gene.DefName];
|
||
console.WriteLine(
|
||
$" {gene.DefName}: [{allele.A:0.##}, {allele.B:0.##}] -> {parentA.Express(gene):0.###}"
|
||
);
|
||
}
|
||
|
||
console.WriteLine("traits:");
|
||
foreach (var (trait, value) in Phenotype.Compute(parentA, registry).OrderBy(t => t.Key))
|
||
{
|
||
console.WriteLine($" {trait} = {value:0.###}");
|
||
}
|
||
|
||
var parentB = Genome.Generate(genes, random);
|
||
var child = Genome.Breed(parentA, parentB, registry, random);
|
||
var childTraits = Phenotype.Compute(child, registry);
|
||
console.WriteLine(
|
||
$"bred child: {child.Alleles.Count} genes, "
|
||
+ $"vigor={childTraits.GetValueOrDefault("vigor"):0.###}, "
|
||
+ $"lifespan={childTraits.GetValueOrDefault("lifespan"):0.#}, "
|
||
+ $"variant={childTraits.GetValueOrDefault("variant"):0}"
|
||
);
|
||
}
|
||
|
||
// Демонстрация генного контента (фаза G4): по виду берём его набор генов, генерируем особь и
|
||
// печатаем признаки роста/жизни и продукты (плоды/добыча с количеством от генов).
|
||
private void RunPlantDemo(DevConsole console, GameContent content, string[] args)
|
||
{
|
||
var species = content.Defs.NamesOf("Plant");
|
||
if (args.Length == 0)
|
||
{
|
||
console.WriteLine("usage: plant <species> [seed] e.g. plant TreeOakA");
|
||
console.WriteLine($"species: {string.Join(", ", species)}");
|
||
return;
|
||
}
|
||
|
||
if (!content.Defs.TryGet<PlantDef>(args[0], out var def))
|
||
{
|
||
console.WriteLine($"no plant '{args[0]}'; species: {string.Join(", ", species)}");
|
||
return;
|
||
}
|
||
|
||
var index = _plants.IndexOf(def);
|
||
var seed = args.Length > 1 ? int.Parse(args[1]) : Random.Shared.Next();
|
||
var genome = _plants[index].Template.Generate(new Random(seed));
|
||
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, _plants.GeneRegistry));
|
||
|
||
console.WriteLine(
|
||
$"{def.DefName} (seed {seed}){(traits.IsVariant ? " [variant morph]" : "")}:"
|
||
);
|
||
console.WriteLine(
|
||
$" vigor {traits.Vigor:0.##}, lifespan {traits.Lifespan:0} d, "
|
||
+ $"optimalLight {traits.OptimalLight:0.##}, leafHue {traits.LeafHue:0.##}"
|
||
);
|
||
var (tMin, tLow, tHigh, tMax) = traits.TemperatureBand();
|
||
console.WriteLine(
|
||
$" temp: grows {tMin:0.#}..{tMax:0.#}°C, optimal {tLow:0.#}..{tHigh:0.#}°C "
|
||
+ $"(cold {traits.ColdHardiness:0.#}, heat {traits.HeatHardiness:0.#})"
|
||
);
|
||
|
||
if (def.HarvestProduct is { } harvest)
|
||
{
|
||
console.WriteLine(
|
||
$" harvest: {ProductLabel(content, harvest)} x{traits.HarvestAmount:0.#}"
|
||
);
|
||
}
|
||
|
||
if (def.FruitProduct is { } fruit && traits.FruitYield >= 1f)
|
||
{
|
||
var season = content.Languages.Get(
|
||
$"season.{((Season)traits.FruitSeason).ToString().ToLowerInvariant()}"
|
||
);
|
||
console.WriteLine(
|
||
$" fruit: {ProductLabel(content, fruit)} x{traits.FruitYield:0.#} in {season}"
|
||
);
|
||
}
|
||
else
|
||
{
|
||
console.WriteLine(" barren (no fruit)");
|
||
}
|
||
}
|
||
|
||
private static string ProductLabel(GameContent content, string productDefName) =>
|
||
content.Defs.TryGet<ProductDef>(productDefName, out var product)
|
||
? content.Languages.Get(product.Label)
|
||
: productDefName;
|
||
}
|