Events jump-to-creature, UI effects, sane mating & mushroom habitat
- Event log: double-click an entry to focus camera + select the responsible creature; hover highlight, fade-in, hint line. - UI effects: fade-in for events/perf overlays, hover on inspect tabs and speed buttons, breathing selection bracket. - Mating: same-species only (no boar x chicken); event strings reworded gender/case-neutral (fixes 'кабан спарилась с курица'). - Mushrooms: shade plants no longer scattered into bright cells (no canopy -> they don't appear), matching the low-light niche. - Bump engine: lighter night floor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
010d401b75
commit
9f5f5e2ca9
@@ -210,30 +210,31 @@
|
|||||||
"hud.perf": "Perf",
|
"hud.perf": "Perf",
|
||||||
"events.title": "Event log",
|
"events.title": "Event log",
|
||||||
"events.empty": "Nothing has happened yet",
|
"events.empty": "Nothing has happened yet",
|
||||||
|
"events.hint": "Double-click — jump to the creature",
|
||||||
"perf.title": "Performance",
|
"perf.title": "Performance",
|
||||||
"perf.fps": "{0} FPS · {1:0.0} ms/frame",
|
"perf.fps": "{0} FPS · {1:0.0} ms/frame",
|
||||||
"perf.root": "{0}: {1:0.000} ms · systems {2}",
|
"perf.root": "{0}: {1:0.000} ms · systems {2}",
|
||||||
"perf.systemline": "{0} {1:0.000} ms {2:0}%",
|
"perf.systemline": "{0} {1:0.000} ms {2:0}%",
|
||||||
"perf.hint": "Refreshes ~5/s · costliest systems shown",
|
"perf.hint": "Refreshes ~5/s · costliest systems shown",
|
||||||
"event.mated": "{0} mated with {1}",
|
"event.mated": "Pair: {0} + {1}",
|
||||||
"event.pregnant": "{0} became pregnant",
|
"event.pregnant": "{0}: pregnant",
|
||||||
"event.birth": "{0} gave birth ×{1}",
|
"event.birth": "{0}: birth ×{1}",
|
||||||
"event.hatch": "{0}: eggs hatched ×{1}",
|
"event.hatch": "{0}: hatched ×{1}",
|
||||||
"event.hunt": "{0} started hunting: target — {1}",
|
"event.hunt": "Hunt: {0} → {1}",
|
||||||
"event.injured": "{0} wounded: {1}",
|
"event.injured": "Wound: {0} → {1}",
|
||||||
"event.struck": "{0} struck {1} (dmg {2:0})",
|
"event.struck": "Hit: {0} → {1} ({2:0})",
|
||||||
"event.tookhit": "{0}: hit by {1} (dmg {2:0})",
|
"event.tookhit": "Hit: {1} → {0} ({2:0})",
|
||||||
"event.killed": "{0} killed prey: {1}",
|
"event.killed": "Kill: {0} → {1}",
|
||||||
"event.ate": "{0} ate",
|
"event.ate": "{0} is feeding",
|
||||||
"event.drank": "{0} drank",
|
"event.drank": "{0} is drinking",
|
||||||
"event.rest": "{0} is resting",
|
"event.rest": "{0} is resting",
|
||||||
"event.dig": "{0} digs for food",
|
"event.dig": "{0} digs for food",
|
||||||
"event.wallow": "{0} wallows in mud",
|
"event.wallow": "{0} wallows in mud",
|
||||||
"event.fled": "{0} is fleeing",
|
"event.fled": "{0} is fleeing",
|
||||||
"event.grew": "{0}: new stage — {1}",
|
"event.grew": "{0}: new stage — {1}",
|
||||||
"event.death.starve": "{0} died of hunger/thirst",
|
"event.death.starve": "Death: {0} (hunger/thirst)",
|
||||||
"event.death.age": "{0} died of old age",
|
"event.death.age": "Death: {0} (old age)",
|
||||||
"event.death.disease": "{0} died of disease",
|
"event.death.disease": "Death: {0} (disease)",
|
||||||
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
"net.hud": "Multiplayer: {0} | beings: {1} | Esc — back to menu",
|
||||||
"net.connecting": "connecting…",
|
"net.connecting": "connecting…",
|
||||||
"net.connected": "connected",
|
"net.connected": "connected",
|
||||||
|
|||||||
@@ -210,30 +210,31 @@
|
|||||||
"hud.perf": "Произв.",
|
"hud.perf": "Произв.",
|
||||||
"events.title": "Журнал событий",
|
"events.title": "Журнал событий",
|
||||||
"events.empty": "Пока ничего не произошло",
|
"events.empty": "Пока ничего не произошло",
|
||||||
|
"events.hint": "Двойной клик — перейти к существу",
|
||||||
"perf.title": "Производительность",
|
"perf.title": "Производительность",
|
||||||
"perf.fps": "{0} FPS · {1:0.0} мс/кадр",
|
"perf.fps": "{0} FPS · {1:0.0} мс/кадр",
|
||||||
"perf.root": "{0}: {1:0.000} мс · систем {2}",
|
"perf.root": "{0}: {1:0.000} мс · систем {2}",
|
||||||
"perf.systemline": "{0} {1:0.000} мс {2:0}%",
|
"perf.systemline": "{0} {1:0.000} мс {2:0}%",
|
||||||
"perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы",
|
"perf.hint": "Обновляется ~5 раз/с · показаны самые дорогие системы",
|
||||||
"event.mated": "{0} спарилась с {1}",
|
"event.mated": "Пара: {0} + {1}",
|
||||||
"event.pregnant": "{0} забеременела",
|
"event.pregnant": "{0}: беременность",
|
||||||
"event.birth": "{0} принесла потомство ×{1}",
|
"event.birth": "{0}: роды ×{1}",
|
||||||
"event.hatch": "{0}: вылупились детёныши ×{1}",
|
"event.hatch": "{0}: вылупление ×{1}",
|
||||||
"event.hunt": "{0} начала охоту: цель — {1}",
|
"event.hunt": "Охота: {0} → {1}",
|
||||||
"event.injured": "{0} ранила: {1}",
|
"event.injured": "Ранение: {0} → {1}",
|
||||||
"event.struck": "{0} ударил {1} (урон {2:0})",
|
"event.struck": "Удар: {0} → {1} ({2:0})",
|
||||||
"event.tookhit": "{0}: удар от {1} (урон {2:0})",
|
"event.tookhit": "Удар: {1} → {0} ({2:0})",
|
||||||
"event.killed": "{0} убила добычу: {1}",
|
"event.killed": "Добыча: {0} → {1}",
|
||||||
"event.ate": "{0} поела",
|
"event.ate": "{0} кормится",
|
||||||
"event.drank": "{0} попила",
|
"event.drank": "{0} пьёт воду",
|
||||||
"event.rest": "{0} отдыхает",
|
"event.rest": "{0} отдыхает",
|
||||||
"event.dig": "{0} роет землю в поисках корма",
|
"event.dig": "{0} роет землю в поисках корма",
|
||||||
"event.wallow": "{0} валяется в грязи",
|
"event.wallow": "{0} валяется в грязи",
|
||||||
"event.fled": "{0} спасается бегством",
|
"event.fled": "{0} спасается бегством",
|
||||||
"event.grew": "{0}: новая стадия — {1}",
|
"event.grew": "{0}: новая стадия — {1}",
|
||||||
"event.death.starve": "{0} погибла от голода/жажды",
|
"event.death.starve": "Гибель: {0} (голод/жажда)",
|
||||||
"event.death.age": "{0} умерла от старости",
|
"event.death.age": "Гибель: {0} (старость)",
|
||||||
"event.death.disease": "{0} погибла от болезни",
|
"event.death.disease": "Гибель: {0} (болезнь)",
|
||||||
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
"net.hud": "Мультиплеер: {0} | жителей: {1} | Esc — в меню",
|
||||||
"net.connecting": "подключение…",
|
"net.connecting": "подключение…",
|
||||||
"net.connected": "подключено",
|
"net.connected": "подключено",
|
||||||
|
|||||||
+1
-1
Submodule engine updated: 96e19c7c61...46f3931f85
@@ -2,20 +2,27 @@ using Friflo.Engine.ECS;
|
|||||||
using LittleSim.Content;
|
using LittleSim.Content;
|
||||||
using LittleSim.Sim;
|
using LittleSim.Sim;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.AI;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.DevConsole;
|
using MrGameEng.DevConsole;
|
||||||
using MrGameEng.Graphics;
|
using MrGameEng.Graphics;
|
||||||
|
using MrGameEng.Lighting;
|
||||||
|
|
||||||
namespace LittleSim.Scenes;
|
namespace LittleSim.Scenes;
|
||||||
|
|
||||||
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
/// <summary>Спавн растительности по скаттеру дефов террейна.</summary>
|
||||||
internal static class ScatterSpawner
|
internal static class ScatterSpawner
|
||||||
{
|
{
|
||||||
|
// Ниже этой световой пригодности (при полном дневном свете клетки) растение не сеем: теневому виду
|
||||||
|
// (грибы, optimalLight≈0.15) в открытой клетке не место — без полога крон ему тут не выжить, поэтому
|
||||||
|
// он и не должен появляться, пока такой ниши нет. Солнечные виды (≥0.7 пригодности) проходят свободно.
|
||||||
|
private const float ShadeSpawnThreshold = 0.15f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
/// Бросает каждую запись скаттера клетки независимо; при успехе ставит одно растение со
|
||||||
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
/// случайным смещением и случайной (детерминированной сидом) зрелостью — мир после генерации
|
||||||
/// разновозрастный. Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением
|
/// разновозрастный. Теневые виды не сеются в ярко освещённые клетки (см. <see cref="ShadeSpawnThreshold"/>).
|
||||||
/// управляет <see cref="PlantGrowthSystem"/>.
|
/// Стадия и спрайт берутся из <paramref name="plants"/>, дальше растением управляет <see cref="PlantGrowthSystem"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Spawn(
|
public static void Spawn(
|
||||||
Scene scene,
|
Scene scene,
|
||||||
@@ -27,6 +34,10 @@ internal static class ScatterSpawner
|
|||||||
Random random
|
Random random
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
// Статичная освещённость клетки в полдень: затенена только светонепроницаемым террейном (полог
|
||||||
|
// крон сейчас не учитывается). По ней решаем, годится ли клетка теневому виду.
|
||||||
|
var exposure = terrain.BlocksLight ? LightmapBuilder.OccluderShade : 1f;
|
||||||
|
|
||||||
foreach (var entry in terrain.Scatter)
|
foreach (var entry in terrain.Scatter)
|
||||||
{
|
{
|
||||||
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
if (entry.Options.Count == 0 || random.NextSingle() >= entry.Chance)
|
||||||
@@ -35,6 +46,14 @@ internal static class ScatterSpawner
|
|||||||
}
|
}
|
||||||
|
|
||||||
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
var def = content.Defs.Get<PlantDef>(entry.Options[random.Next(entry.Options.Count)]);
|
||||||
|
if (
|
||||||
|
Suitability.Gaussian(exposure, def.Genome.OptimalLight, def.Genome.LightTolerance)
|
||||||
|
< ShadeSpawnThreshold
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue; // теневому виду в ярко освещённой клетке не место
|
||||||
|
}
|
||||||
|
|
||||||
var index = plants.IndexOf(def);
|
var index = plants.IndexOf(def);
|
||||||
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
var px = (cell.X + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
var py = (cell.Y + 0.3f + random.NextSingle() * 0.4f) * cellSize;
|
||||||
|
|||||||
@@ -247,8 +247,14 @@ public sealed class WorldScene : Scene
|
|||||||
hudChildren.Add(spawner.Panel);
|
hudChildren.Add(spawner.Panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Журнал событий — доступен всем (кнопка «События»).
|
// Журнал событий — доступен всем (кнопка «События»). Двойной клик по записи ведёт камеру к
|
||||||
_eventsPanel = new EventsPanel(content.Languages, _events);
|
// «ответственному» и выбирает его (если он ещё жив).
|
||||||
|
_eventsPanel = new EventsPanel(
|
||||||
|
content.Languages,
|
||||||
|
_events,
|
||||||
|
Context.Clock,
|
||||||
|
id => FocusEntity(camera, id)
|
||||||
|
);
|
||||||
hudChildren.Add(_eventsPanel.Panel);
|
hudChildren.Add(_eventsPanel.Panel);
|
||||||
hudButtons.Widgets.Add(
|
hudButtons.Widgets.Add(
|
||||||
HudToggle(
|
HudToggle(
|
||||||
@@ -555,6 +561,25 @@ public sealed class WorldScene : Scene
|
|||||||
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
Store.CreateEntity(new Tilemap(grid, tiles, CellSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Переход к «ответственному» за событие (двойной клик в журнале): выбирает сущность и центрирует на
|
||||||
|
// ней камеру, если она ещё существует (актёр мог умереть). Камера зажимается к границам при отрисовке.
|
||||||
|
private void FocusEntity(Entity camera, int id)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
id < 0
|
||||||
|
|| !Store.TryGetEntityById(id, out var target)
|
||||||
|
|| target.IsNull
|
||||||
|
|| !target.HasComponent<Transform2D>()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_selection.Select(id);
|
||||||
|
ref var cam = ref camera.GetComponent<Camera>();
|
||||||
|
cam.Position = target.GetComponent<Transform2D>().Position;
|
||||||
|
}
|
||||||
|
|
||||||
// Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт.
|
// Кнопка-переключатель оверлея HUD: подсвечивается как активная, пока оверлей открыт.
|
||||||
private static TextButton HudToggle(string text, Action onClick, Func<bool> isOpen)
|
private static TextButton HudToggle(string text, Action onClick, Func<bool> isOpen)
|
||||||
{
|
{
|
||||||
@@ -575,7 +600,13 @@ public sealed class WorldScene : Scene
|
|||||||
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
bar.VerticalAlignment = VerticalAlignment.Bottom;
|
||||||
bar.Margin = new Thickness(0, 0, 0, 12);
|
bar.Margin = new Thickness(0, 0, 0, 12);
|
||||||
|
|
||||||
var pause = new TextButton { Text = content.Languages.Get("speed.pause") };
|
var hover = new SolidBrush(new Color(40, 54, 72)); // подсветка кнопок скорости при наведении
|
||||||
|
var pause = new TextButton
|
||||||
|
{
|
||||||
|
Text = content.Languages.Get("speed.pause"),
|
||||||
|
Padding = new Thickness(8, 4),
|
||||||
|
OverBackground = hover,
|
||||||
|
};
|
||||||
pause.Click += (_, _) => _speed.Pause();
|
pause.Click += (_, _) => _speed.Pause();
|
||||||
bar.Widgets.Add(pause);
|
bar.Widgets.Add(pause);
|
||||||
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
_speedRefreshers.Add(() => pause.TextColor = _speed.IsPaused ? Ui.Accent : Ui.Muted);
|
||||||
@@ -583,7 +614,12 @@ public sealed class WorldScene : Scene
|
|||||||
for (var i = 0; i < _speed.Steps.Count; i++)
|
for (var i = 0; i < _speed.Steps.Count; i++)
|
||||||
{
|
{
|
||||||
var index = i;
|
var index = i;
|
||||||
var button = new TextButton { Text = $"x{_speed.Steps[i]:0}" };
|
var button = new TextButton
|
||||||
|
{
|
||||||
|
Text = $"x{_speed.Steps[i]:0}",
|
||||||
|
Padding = new Thickness(8, 4),
|
||||||
|
OverBackground = hover,
|
||||||
|
};
|
||||||
button.Click += (_, _) => _speed.SetStep(index);
|
button.Click += (_, _) => _speed.SetStep(index);
|
||||||
bar.Widgets.Add(button);
|
bar.Widgets.Add(button);
|
||||||
_speedRefreshers.Add(() =>
|
_speedRefreshers.Add(() =>
|
||||||
|
|||||||
@@ -638,6 +638,7 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
&& TryFindMate(
|
&& TryFindMate(
|
||||||
pos,
|
pos,
|
||||||
o[i].IsMale,
|
o[i].IsMale,
|
||||||
|
o[i].Species,
|
||||||
seeRadius,
|
seeRadius,
|
||||||
out var mateId,
|
out var mateId,
|
||||||
out var matePos
|
out var matePos
|
||||||
@@ -1333,10 +1334,11 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ближайший подходящий партнёр в радиусе зрения: взрослый, противоположного пола, с влечением > 0.5.
|
// Ближайший подходящий партнёр в радиусе зрения: ТОГО ЖЕ вида, взрослый, противоположного пола.
|
||||||
private bool TryFindMate(
|
private bool TryFindMate(
|
||||||
Vector2 from,
|
Vector2 from,
|
||||||
bool selfMale,
|
bool selfMale,
|
||||||
|
int selfSpecies,
|
||||||
float radius,
|
float radius,
|
||||||
out int mateId,
|
out int mateId,
|
||||||
out Vector2 position
|
out Vector2 position
|
||||||
@@ -1348,13 +1350,13 @@ public sealed class AnimalDecisionSystem : BaseSystem
|
|||||||
// _animalBuf уже собран в DecideIn на макс. радиусе чувств — фильтруем по радиусу спаривания.
|
// _animalBuf уже собран в DecideIn на макс. радиусе чувств — фильтруем по радиусу спаривания.
|
||||||
foreach (var e in _animalBuf)
|
foreach (var e in _animalBuf)
|
||||||
{
|
{
|
||||||
if (e.IsMale == selfMale || !e.IsAdult)
|
if (e.Species != selfSpecies || e.IsMale == selfMale || !e.IsAdult)
|
||||||
{
|
{
|
||||||
continue; // тот же пол / не взрослый (self отсеивается по полу)
|
continue; // другой вид / тот же пол / не взрослый (self отсеивается по полу)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Влечение партнёра не проверяется (учёт придёт с половым отбором); вид не фильтруется —
|
// Влечение партнёра не проверяется (учёт придёт с половым отбором). self отсекается совпадением
|
||||||
// межвидовые пары возможны, как и раньше. self отсекается совпадением пола и sq≈0.
|
// пола и sq≈0; межвидовые пары исключены (раньше волк/курица могли «спариться»).
|
||||||
var sq = Vector2.DistanceSquared(from, e.Pos);
|
var sq = Vector2.DistanceSquared(from, e.Pos);
|
||||||
if (sq > 0.01f && (sq < bestSq || (sq == bestSq && e.Id < mateId)))
|
if (sq > 0.01f && (sq < bestSq || (sq == bestSq && e.Id < mateId)))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ public enum EventCategory : byte
|
|||||||
Routine,
|
Routine,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Одна запись журнала: готовая (локализованная) строка, категория и отметка времени календаря.</summary>
|
/// <summary>
|
||||||
|
/// Одна запись журнала: готовая (локализованная) строка, категория, отметка времени календаря и id
|
||||||
|
/// «ответственной» сущности (для перехода камерой/выбора по двойному клику); -1 — не к кому переходить
|
||||||
|
/// (напр. смерть — особь исчезла).
|
||||||
|
/// </summary>
|
||||||
public readonly record struct LogEntry(
|
public readonly record struct LogEntry(
|
||||||
string Text,
|
string Text,
|
||||||
EventCategory Category,
|
EventCategory Category,
|
||||||
int Day,
|
int Day,
|
||||||
int Hour,
|
int Hour,
|
||||||
int Minute
|
int Minute,
|
||||||
|
int SubjectId
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -72,7 +77,7 @@ public sealed class WorldEvents
|
|||||||
public void Mated(Entity a, int aSpecies, Entity b, int bSpecies)
|
public void Mated(Entity a, int aSpecies, Entity b, int bSpecies)
|
||||||
{
|
{
|
||||||
var text = _lang.Format("event.mated", Label(aSpecies), Label(bSpecies));
|
var text = _lang.Format("event.mated", Label(aSpecies), Label(bSpecies));
|
||||||
Global(EventCategory.Reproduction, text);
|
Global(EventCategory.Reproduction, text, a.Id);
|
||||||
Creature(a, EventCategory.Reproduction, text);
|
Creature(a, EventCategory.Reproduction, text);
|
||||||
Creature(
|
Creature(
|
||||||
b,
|
b,
|
||||||
@@ -84,32 +89,32 @@ public sealed class WorldEvents
|
|||||||
public void Pregnant(Entity mother, int species)
|
public void Pregnant(Entity mother, int species)
|
||||||
{
|
{
|
||||||
var text = _lang.Format("event.pregnant", Label(species));
|
var text = _lang.Format("event.pregnant", Label(species));
|
||||||
Global(EventCategory.Reproduction, text);
|
Global(EventCategory.Reproduction, text, mother.Id);
|
||||||
Creature(mother, EventCategory.Reproduction, text);
|
Creature(mother, EventCategory.Reproduction, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Born(Entity mother, int species, int litter)
|
public void Born(Entity mother, int species, int litter)
|
||||||
{
|
{
|
||||||
var text = _lang.Format("event.birth", Label(species), litter);
|
var text = _lang.Format("event.birth", Label(species), litter);
|
||||||
Global(EventCategory.Reproduction, text);
|
Global(EventCategory.Reproduction, text, mother.Id);
|
||||||
Creature(mother, EventCategory.Reproduction, text);
|
Creature(mother, EventCategory.Reproduction, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Hatched(int species, int count) =>
|
public void Hatched(int species, int count) =>
|
||||||
Global(EventCategory.Reproduction, _lang.Format("event.hatch", Label(species), count));
|
Global(EventCategory.Reproduction, _lang.Format("event.hatch", Label(species), count), -1);
|
||||||
|
|
||||||
// — Охота и бой —
|
// — Охота и бой —
|
||||||
public void HuntStarted(Entity hunter, int hunterSpecies, int preySpecies)
|
public void HuntStarted(Entity hunter, int hunterSpecies, int preySpecies)
|
||||||
{
|
{
|
||||||
var text = _lang.Format("event.hunt", Label(hunterSpecies), Label(preySpecies));
|
var text = _lang.Format("event.hunt", Label(hunterSpecies), Label(preySpecies));
|
||||||
Global(EventCategory.Combat, text);
|
Global(EventCategory.Combat, text, hunter.Id);
|
||||||
Creature(hunter, EventCategory.Combat, text);
|
Creature(hunter, EventCategory.Combat, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Killed(Entity hunter, int hunterSpecies, int preySpecies)
|
public void Killed(Entity hunter, int hunterSpecies, int preySpecies)
|
||||||
{
|
{
|
||||||
var text = _lang.Format("event.killed", Label(hunterSpecies), Label(preySpecies));
|
var text = _lang.Format("event.killed", Label(hunterSpecies), Label(preySpecies));
|
||||||
Global(EventCategory.Combat, text);
|
Global(EventCategory.Combat, text, hunter.Id);
|
||||||
Creature(hunter, EventCategory.Combat, text);
|
Creature(hunter, EventCategory.Combat, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +143,7 @@ public sealed class WorldEvents
|
|||||||
|
|
||||||
// — Смерть (умершая особь исчезает, её хроника — тоже; пишем только в глобальный журнал) —
|
// — Смерть (умершая особь исчезает, её хроника — тоже; пишем только в глобальный журнал) —
|
||||||
public void Died(int species, string causeKey) =>
|
public void Died(int species, string causeKey) =>
|
||||||
Global(EventCategory.Death, _lang.Format(causeKey, Label(species)));
|
Global(EventCategory.Death, _lang.Format(causeKey, Label(species)), -1); // особь исчезла
|
||||||
|
|
||||||
// — Бытовое (только личная хроника особи, чтобы не засорять глобальный журнал) —
|
// — Бытовое (только личная хроника особи, чтобы не засорять глобальный журнал) —
|
||||||
public void Ate(Entity e, int species) =>
|
public void Ate(Entity e, int species) =>
|
||||||
@@ -167,12 +172,13 @@ public sealed class WorldEvents
|
|||||||
|
|
||||||
private string Label(int species) => _lang.Get(_animals[species].Def.Label);
|
private string Label(int species) => _lang.Get(_animals[species].Def.Label);
|
||||||
|
|
||||||
private LogEntry Make(EventCategory category, string text) =>
|
private LogEntry Make(EventCategory category, string text, int subjectId) =>
|
||||||
new(text, category, _calendar.Day, _calendar.Hour, _calendar.Minute);
|
new(text, category, _calendar.Day, _calendar.Hour, _calendar.Minute, subjectId);
|
||||||
|
|
||||||
private void Global(EventCategory category, string text)
|
// subjectId — кого выбирать/куда вести камеру по двойному клику (-1 — не к кому, напр. смерть).
|
||||||
|
private void Global(EventCategory category, string text, int subjectId)
|
||||||
{
|
{
|
||||||
_global.Add(Make(category, text));
|
_global.Add(Make(category, text, subjectId));
|
||||||
if (_global.Count > _capacity)
|
if (_global.Count > _capacity)
|
||||||
{
|
{
|
||||||
_global.RemoveAt(0); // вытесняем старейшее
|
_global.RemoveAt(0); // вытесняем старейшее
|
||||||
@@ -188,7 +194,7 @@ public sealed class WorldEvents
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Entries.Add(Make(category, text));
|
log.Entries.Add(Make(category, text, e.Id)); // субъект хроники — сама особь
|
||||||
if (log.Entries.Count > log.Capacity)
|
if (log.Entries.Count > log.Capacity)
|
||||||
{
|
{
|
||||||
log.Entries.RemoveAt(0);
|
log.Entries.RemoveAt(0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using LittleSim.Sim;
|
using LittleSim.Sim;
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Core;
|
||||||
using MrGameEng.Mods;
|
using MrGameEng.Mods;
|
||||||
using Myra.Graphics2D;
|
using Myra.Graphics2D;
|
||||||
using Myra.Graphics2D.Brushes;
|
using Myra.Graphics2D.Brushes;
|
||||||
@@ -11,31 +12,44 @@ namespace LittleSim.UI;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Оверлей глобального журнала событий мира (фича UI «События»): последние заметные события (спаривание,
|
/// Оверлей глобального журнала событий мира (фича UI «События»): последние заметные события (спаривание,
|
||||||
/// роды/вылупление, начало охоты, убийство, смерти) новыми сверху, цвет строки — по категории. Источник —
|
/// роды/вылупление, начало охоты, убийство, смерти) новыми сверху, цвет строки — по категории. Источник —
|
||||||
/// <see cref="WorldEvents"/>; тело перестраивается лишь при изменении версии журнала (новое событие),
|
/// <see cref="WorldEvents"/>; тело перестраивается лишь при изменении версии журнала (новое событие).
|
||||||
/// поэтому открытое окно почти ничего не стоит на паузе. Переключается кнопкой HUD.
|
/// Кликабельные записи подсвечиваются при наведении, по двойному клику ведут камеру к «ответственному».
|
||||||
|
/// Окно плавно проявляется при открытии. Переключается кнопкой HUD.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class EventsPanel
|
internal sealed class EventsPanel
|
||||||
{
|
{
|
||||||
private const int MaxShown = 60; // сколько последних записей показывать
|
private const int MaxShown = 60; // сколько последних записей показывать
|
||||||
|
private const float FadeSeconds = 0.16f; // длительность плавного появления
|
||||||
|
|
||||||
// Цвета строк по категории (приглушённые — читаемо на тёмном фоне).
|
// Цвета строк по категории (приглушённые — читаемо на тёмном фоне).
|
||||||
private static readonly Color Reproduction = new(120, 186, 120);
|
private static readonly Color Reproduction = new(120, 186, 120);
|
||||||
private static readonly Color Combat = new(214, 150, 86);
|
private static readonly Color Combat = new(214, 150, 86);
|
||||||
private static readonly Color Death = new(196, 92, 86);
|
private static readonly Color Death = new(196, 92, 86);
|
||||||
private static readonly Color Routine = new(150, 160, 175);
|
private static readonly Color Routine = new(150, 160, 175);
|
||||||
|
private static readonly IBrush RowHover = new SolidBrush(new Color(96, 134, 168, 60));
|
||||||
|
|
||||||
private readonly LanguageManager _lang;
|
private readonly LanguageManager _lang;
|
||||||
private readonly WorldEvents _events;
|
private readonly WorldEvents _events;
|
||||||
|
private readonly GameClock _clock;
|
||||||
|
private readonly Action<int> _onFocus; // двойной клик по записи: перейти/выбрать «ответственного»
|
||||||
private readonly VerticalStackPanel _panel;
|
private readonly VerticalStackPanel _panel;
|
||||||
private readonly VerticalStackPanel _body;
|
private readonly VerticalStackPanel _body;
|
||||||
private int _builtVersion = -1;
|
private int _builtVersion = -1;
|
||||||
|
private float _opacity;
|
||||||
|
|
||||||
public EventsPanel(LanguageManager lang, WorldEvents events)
|
public EventsPanel(
|
||||||
|
LanguageManager lang,
|
||||||
|
WorldEvents events,
|
||||||
|
GameClock clock,
|
||||||
|
Action<int> onFocus
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_lang = lang;
|
_lang = lang;
|
||||||
_events = events;
|
_events = events;
|
||||||
|
_clock = clock;
|
||||||
|
_onFocus = onFocus;
|
||||||
|
|
||||||
_body = new VerticalStackPanel { Spacing = 2 };
|
_body = new VerticalStackPanel { Spacing = 1 };
|
||||||
var scroll = new ScrollViewer
|
var scroll = new ScrollViewer
|
||||||
{
|
{
|
||||||
Content = _body,
|
Content = _body,
|
||||||
@@ -59,6 +73,8 @@ internal sealed class EventsPanel
|
|||||||
_panel.Widgets.Add(new Label { Text = _lang.Get("events.title"), TextColor = Ui.Accent });
|
_panel.Widgets.Add(new Label { Text = _lang.Get("events.title"), TextColor = Ui.Accent });
|
||||||
_panel.Widgets.Add(new HorizontalSeparator());
|
_panel.Widgets.Add(new HorizontalSeparator());
|
||||||
_panel.Widgets.Add(scroll);
|
_panel.Widgets.Add(scroll);
|
||||||
|
_panel.Widgets.Add(new HorizontalSeparator());
|
||||||
|
_panel.Widgets.Add(new Label { Text = _lang.Get("events.hint"), TextColor = Ui.Muted });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Корневой виджет — добавляется в HUD.</summary>
|
/// <summary>Корневой виджет — добавляется в HUD.</summary>
|
||||||
@@ -67,20 +83,33 @@ internal sealed class EventsPanel
|
|||||||
/// <summary>Открыт ли журнал.</summary>
|
/// <summary>Открыт ли журнал.</summary>
|
||||||
public bool IsOpen => _panel.Visible;
|
public bool IsOpen => _panel.Visible;
|
||||||
|
|
||||||
/// <summary>Показать/скрыть; при открытии форсирует пересборку тела.</summary>
|
/// <summary>Показать/скрыть; при открытии запускает плавное появление и форсирует пересборку тела.</summary>
|
||||||
public void Toggle()
|
public void Toggle()
|
||||||
{
|
{
|
||||||
_panel.Visible = !_panel.Visible;
|
_panel.Visible = !_panel.Visible;
|
||||||
if (_panel.Visible)
|
if (_panel.Visible)
|
||||||
{
|
{
|
||||||
|
_opacity = 0f;
|
||||||
|
_panel.Opacity = 0f;
|
||||||
_builtVersion = -1; // пересобрать при следующем Refresh
|
_builtVersion = -1; // пересобрать при следующем Refresh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Вызывать каждый кадр: перестраивает тело лишь при новом событии (по версии журнала).</summary>
|
/// <summary>Вызывать каждый кадр: плавное появление + пересборка тела при новом событии (по версии).</summary>
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
if (!_panel.Visible || _events.Version == _builtVersion)
|
if (!_panel.Visible)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_opacity < 1f)
|
||||||
|
{
|
||||||
|
_opacity = MathF.Min(1f, _opacity + _clock.UnscaledDeltaTime / FadeSeconds);
|
||||||
|
_panel.Opacity = _opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_events.Version == _builtVersion)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,14 +129,33 @@ internal sealed class EventsPanel
|
|||||||
{
|
{
|
||||||
var entry = entries[entries.Count - 1 - i];
|
var entry = entries[entries.Count - 1 - i];
|
||||||
var stamp = _lang.Format("log.stamp", entry.Day, entry.Hour, entry.Minute);
|
var stamp = _lang.Format("log.stamp", entry.Day, entry.Hour, entry.Minute);
|
||||||
_body.Widgets.Add(
|
var color = ColorOf(entry.Category);
|
||||||
new Label
|
var row = new Label
|
||||||
{
|
{
|
||||||
Text = stamp + " " + entry.Text,
|
Text = stamp + " " + entry.Text,
|
||||||
TextColor = ColorOf(entry.Category),
|
TextColor = color,
|
||||||
Wrap = true,
|
Wrap = true,
|
||||||
|
Padding = new Thickness(4, 2),
|
||||||
|
};
|
||||||
|
// Запись с «ответственным» — кликабельна: подсветка при наведении + переход по двойному клику.
|
||||||
|
var subjectId = entry.SubjectId;
|
||||||
|
if (subjectId >= 0)
|
||||||
|
{
|
||||||
|
var baseColor = color;
|
||||||
|
row.MouseEntered += (_, _) =>
|
||||||
|
{
|
||||||
|
row.Background = RowHover;
|
||||||
|
row.TextColor = Color.Lerp(baseColor, Color.White, 0.45f);
|
||||||
|
};
|
||||||
|
row.MouseLeft += (_, _) =>
|
||||||
|
{
|
||||||
|
row.Background = null;
|
||||||
|
row.TextColor = baseColor;
|
||||||
|
};
|
||||||
|
row.TouchDoubleClick += (_, _) => _onFocus(subjectId);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
_body.Widgets.Add(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ internal sealed class InspectPanel
|
|||||||
region,
|
region,
|
||||||
sprite.Origin
|
sprite.Origin
|
||||||
);
|
);
|
||||||
_brackets.Show(center, radius); // позиция брекета — каждый кадр (дёшево, следит за движением)
|
// Лёгкая «дышащая» пульсация рамки выделения (живее статичной скобки), по реальному времени.
|
||||||
|
var pulse = 1f + 0.06f * MathF.Sin((float)_clock.UnscaledTotalTime * 5f);
|
||||||
|
_brackets.Show(center, radius * pulse); // позиция/размер каждый кадр (дёшево, следит за движением)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тело вкладки пересобираем только при смене выбора/вкладки или по таймеру (значения нужд/HP
|
// Тело вкладки пересобираем только при смене выбора/вкладки или по таймеру (значения нужд/HP
|
||||||
@@ -241,6 +243,7 @@ internal sealed class InspectPanel
|
|||||||
{
|
{
|
||||||
Text = _content.Languages.Get(labelKey),
|
Text = _content.Languages.Get(labelKey),
|
||||||
Padding = new Thickness(6, 3),
|
Padding = new Thickness(6, 3),
|
||||||
|
OverBackground = new SolidBrush(new Color(40, 54, 72)), // подсветка при наведении
|
||||||
};
|
};
|
||||||
button.Click += (_, _) => _tab = tab;
|
button.Click += (_, _) => _tab = tab;
|
||||||
bar.Widgets.Add(button);
|
bar.Widgets.Add(button);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ internal sealed class PerfPanel
|
|||||||
{
|
{
|
||||||
private const int TopSystems = 12; // сколько самых дорогих систем показывать на корень
|
private const int TopSystems = 12; // сколько самых дорогих систем показывать на корень
|
||||||
private const double RefreshInterval = 0.2; // сек реального времени между пересборками тела (5 Гц)
|
private const double RefreshInterval = 0.2; // сек реального времени между пересборками тела (5 Гц)
|
||||||
|
private const float FadeSeconds = 0.16f; // длительность плавного появления
|
||||||
|
|
||||||
private readonly LanguageManager _lang;
|
private readonly LanguageManager _lang;
|
||||||
private readonly GameClock _clock;
|
private readonly GameClock _clock;
|
||||||
@@ -35,6 +36,7 @@ internal sealed class PerfPanel
|
|||||||
private float _accum; // накоплено реального времени для окна усреднения FPS
|
private float _accum; // накоплено реального времени для окна усреднения FPS
|
||||||
private int _frames;
|
private int _frames;
|
||||||
private double _nextBuildAt;
|
private double _nextBuildAt;
|
||||||
|
private float _opacity;
|
||||||
|
|
||||||
public PerfPanel(LanguageManager lang, GameClock clock, params SystemRoot[] roots)
|
public PerfPanel(LanguageManager lang, GameClock clock, params SystemRoot[] roots)
|
||||||
{
|
{
|
||||||
@@ -92,6 +94,8 @@ internal sealed class PerfPanel
|
|||||||
_panel.Visible = !_panel.Visible;
|
_panel.Visible = !_panel.Visible;
|
||||||
if (_panel.Visible)
|
if (_panel.Visible)
|
||||||
{
|
{
|
||||||
|
_opacity = 0f;
|
||||||
|
_panel.Opacity = 0f; // плавно проявим в Refresh
|
||||||
PerfReport.EnableMonitoring(_roots);
|
PerfReport.EnableMonitoring(_roots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,14 @@ internal sealed class PerfPanel
|
|||||||
/// <summary>Вызывать каждый кадр: считает FPS (всегда) и перестраивает тело по троттлингу (если открыто).</summary>
|
/// <summary>Вызывать каждый кадр: считает FPS (всегда) и перестраивает тело по троттлингу (если открыто).</summary>
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
_accum += _clock.UnscaledDeltaTime;
|
var dt = _clock.UnscaledDeltaTime;
|
||||||
|
if (_panel.Visible && _opacity < 1f)
|
||||||
|
{
|
||||||
|
_opacity = MathF.Min(1f, _opacity + dt / FadeSeconds);
|
||||||
|
_panel.Opacity = _opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
_accum += dt;
|
||||||
_frames++;
|
_frames++;
|
||||||
var now = _clock.UnscaledTotalTime;
|
var now = _clock.UnscaledTotalTime;
|
||||||
if (now < _nextBuildAt)
|
if (now < _nextBuildAt)
|
||||||
|
|||||||
Reference in New Issue
Block a user