Enhance school simulation and management by integrating roster functionality, allowing for the installation and persistence of student and staff data. Update the school architecture to include a roster alongside existing components, ensuring proper validation against map layouts. Revise room definitions to support homeroom designations and update related tests to validate new functionalities and ensure robustness in roster handling.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>Identity, the one layer every person has.</summary>
|
||||
public readonly record struct PersonIdentity(
|
||||
string Id,
|
||||
string FamilyId,
|
||||
bool Female,
|
||||
DateTime BirthDate,
|
||||
PersonName Name);
|
||||
|
||||
/// <summary>Height, weight and categorical body attributes including derived <c>Build</c>.</summary>
|
||||
public readonly record struct PersonBody(
|
||||
IReadOnlyDictionary<string, int> Numbers,
|
||||
IReadOnlyDictionary<string, string> Choices);
|
||||
|
||||
public readonly record struct PersonSkills(IReadOnlyDictionary<string, int> Values);
|
||||
|
||||
public readonly record struct PersonTraits(IReadOnlyList<string> Ids);
|
||||
|
||||
/// <summary>Live need values. The dictionary is mutated in place as the clock advances.</summary>
|
||||
public readonly record struct PersonNeeds(Dictionary<string, float> Values);
|
||||
|
||||
public readonly record struct PersonRoles(
|
||||
bool IsStudent,
|
||||
bool IsStaff,
|
||||
bool IsParent,
|
||||
string? ClassId,
|
||||
string? Position,
|
||||
string? WorkplaceRoomId);
|
||||
|
||||
/// <summary>A roster class: year and letter live here, the room is the homeroom they occupy.</summary>
|
||||
public readonly record struct ClassIdentity(
|
||||
string Id,
|
||||
int Year,
|
||||
string Letter,
|
||||
string RoomId,
|
||||
int Capacity);
|
||||
@@ -54,14 +54,16 @@ public sealed class GameClock
|
||||
/// Advances the calendar by one fixed step of <paramref name="realSeconds"/>, scaled by the
|
||||
/// base rate and the current speed. Does nothing while paused.
|
||||
/// </summary>
|
||||
public void Advance(double realSeconds, double gameMinutesPerRealSecond)
|
||||
/// <returns>Game minutes actually added; zero while paused.</returns>
|
||||
public double Advance(double realSeconds, double gameMinutesPerRealSecond)
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var gameMinutes = realSeconds * gameMinutesPerRealSecond * Multiplier;
|
||||
Time = Time.AddMinutes(gameMinutes);
|
||||
return gameMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Arch" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Arch" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HSchool.Content\HSchool.Content.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HSchool.Content\HSchool.Content.csproj" />
|
||||
<ProjectReference Include="..\HSchool.People\HSchool.People.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Drains needs by <see cref="NeedDef.DecayPerHour"/> × simulated hours. Core ships decay at zero
|
||||
/// so this is a no-op until something exists that can refill them.
|
||||
/// </summary>
|
||||
public static class NeedDecay
|
||||
{
|
||||
private static readonly QueryDescription PeopleWithNeeds = new QueryDescription().WithAll<PersonNeeds>();
|
||||
|
||||
public static void Apply(World world, DefCatalog catalog, double gameMinutes)
|
||||
{
|
||||
if (gameMinutes <= 0 || catalog.Needs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hours = gameMinutes / 60d;
|
||||
world.Query(in PeopleWithNeeds, (ref PersonNeeds needs) =>
|
||||
{
|
||||
foreach (var def in catalog.Needs.Values)
|
||||
{
|
||||
if (def.Abstract || !needs.Values.TryGetValue(def.DefName, out var current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = current - (float)(def.DecayPerHour * hours);
|
||||
needs.Values[def.DefName] = Math.Clamp(next, def.Min, def.Max);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Arch.Core;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>Turns roster records into Arch entities. Parents have no map location — by design.</summary>
|
||||
public static class RosterSpawner
|
||||
{
|
||||
public static void Spawn(World world, Roster roster)
|
||||
{
|
||||
foreach (var schoolClass in roster.Classes)
|
||||
{
|
||||
world.Create(
|
||||
new ClassIdentity(
|
||||
schoolClass.Id,
|
||||
schoolClass.Year,
|
||||
schoolClass.Letter,
|
||||
schoolClass.RoomId,
|
||||
schoolClass.Capacity));
|
||||
}
|
||||
|
||||
foreach (var person in roster.People)
|
||||
{
|
||||
world.Create(
|
||||
new PersonIdentity(person.Id, person.FamilyId, person.Female, person.BirthDate, person.Name),
|
||||
new PersonBody(person.Numbers, person.Choices),
|
||||
new PersonSkills(person.Skills),
|
||||
new PersonTraits(person.Traits),
|
||||
new PersonNeeds(new Dictionary<string, float>(person.Needs, StringComparer.Ordinal)),
|
||||
new PersonRoles(
|
||||
person.IsStudent,
|
||||
person.IsStaff,
|
||||
person.IsParent,
|
||||
person.ClassId,
|
||||
person.Position,
|
||||
person.WorkplaceRoomId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// One save: a name, a calendar, a frozen def catalog, a map instance, and the ECS world that will
|
||||
/// hold everything the school is made of. The world is empty for now — pupils, rooms and staff
|
||||
/// land in it as the game grows — but it is created and destroyed with the school so ownership is
|
||||
/// never in question.
|
||||
/// One save: a name, a calendar, a frozen def catalog, a map instance, the ECS world, and — once
|
||||
/// people exist — the roster those entities were built from.
|
||||
/// </summary>
|
||||
public sealed class School : IDisposable
|
||||
{
|
||||
@@ -66,12 +65,34 @@ public sealed class School : IDisposable
|
||||
/// <summary>The Arch world backing this school. Only this school's worker thread may touch it.</summary>
|
||||
public World World { get; }
|
||||
|
||||
/// <summary>Runs one fixed step of the school. Today that is only the calendar.</summary>
|
||||
/// <summary>Composition snapshot. Null in clock-only tests or before <see cref="InstallPeople"/>.</summary>
|
||||
public Roster? Roster { get; private set; }
|
||||
|
||||
public int PeopleSeed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Installs a roster that already matches the map. Spawns entities; does not write to disk.
|
||||
/// </summary>
|
||||
public void InstallPeople(Roster roster, int seed)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(roster);
|
||||
|
||||
Roster = roster;
|
||||
PeopleSeed = seed;
|
||||
RosterSpawner.Spawn(World, roster);
|
||||
}
|
||||
|
||||
/// <summary>Runs one fixed step of the school: calendar, then need decay.</summary>
|
||||
public void Tick(double deltaTime, double gameMinutesPerRealSecond)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
Clock.Advance(deltaTime, gameMinutesPerRealSecond);
|
||||
var gameMinutes = Clock.Advance(deltaTime, gameMinutesPerRealSecond);
|
||||
if (gameMinutes > 0 && Catalog is not null)
|
||||
{
|
||||
NeedDecay.Apply(World, Catalog, gameMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
Reference in New Issue
Block a user