Show director summons as info toasts and presence queue labels.

Corridor waiters need a summon-phase byte on presence (v11); EventDef DirectorSummoned is info-only so clocks keep running.
This commit is contained in:
Leonid Pershin
2026-08-21 13:07:47 +03:00
parent d3c2327e23
commit 9e429d0cd7
29 changed files with 453 additions and 28 deletions
@@ -104,9 +104,73 @@ internal static class DirectorSummonSystem
school.DirectorSummons.Add(new DirectorSummonTicket(personId, school.NextSummonOrder++, admitted: false));
PresenceSystem.Enqueue(school, personId);
school.RaiseWorldEvent(new WorldEvent(EventTriggers.DirectorSummon, personId));
return true;
}
/// <summary>
/// Wire tag for the presence frame: going / waiting / hearing. Node + state alone cannot tell
/// a corridor waiter from a passer-by, so this is a dedicated byte (protocol bump).
/// </summary>
public static byte ResolvePresencePhase(School school, string personId)
{
var ticket = school.DirectorSummons.FirstOrDefault(row =>
row.PersonId.Equals(personId, StringComparison.Ordinal));
if (ticket is null)
{
return DirectorSummons.PresenceNone;
}
var walking = false;
var hearing = false;
string? nodeId = null;
school.World.Query(
in PresenceQuery,
(ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity, ref Intent _) =>
{
if (!identity.Id.Equals(personId, StringComparison.Ordinal))
{
return;
}
nodeId = presence.NodeId;
walking = presence.Path.Length > 0 || presence.RemainingMinutes > 0
|| (presence.DestinationId is not null
&& !presence.DestinationId.Equals(presence.NodeId, StringComparison.Ordinal));
hearing = activity.IsActive && ActivitySystem.IsPrincipalHearing(activity.ActionId);
});
if (hearing)
{
return DirectorSummons.PresenceHearing;
}
if (walking)
{
return DirectorSummons.PresenceGoing;
}
var office = DirectorSummons.OfficeNodeId(school.Map!);
if (ticket.Admitted
&& office is not null
&& nodeId is not null
&& nodeId.Equals(office, StringComparison.Ordinal))
{
return DirectorSummons.PresenceHearing;
}
return DirectorSummons.PresenceWaiting;
}
/// <summary>ActionDef name for the person card when the live activity is idle but summoned.</summary>
public static string? PresenceActionId(byte phase) => phase switch
{
DirectorSummons.PresenceGoing => DirectorSummons.GoingAction,
DirectorSummons.PresenceWaiting => DirectorSummons.WaitingAction,
DirectorSummons.PresenceHearing => DirectorSummons.HearingAction,
_ => null,
};
public static void ClearAll(School school)
{
if (school.DirectorSummons.Count == 0)
+14
View File
@@ -426,6 +426,20 @@ public sealed class School : IDisposable
return copy;
}
/// <summary>Queue a fact for the worker's notice board. Simulation never builds UI itself.</summary>
internal void RaiseWorldEvent(in WorldEvent fact) => _worldEvents.Add(fact);
/// <summary>
/// Presence wire tag for auto director summons. Values match <c>DirectorSummons.Presence*</c>
/// and <see cref="Protocol.PresenceSummonPhase"/>.
/// </summary>
public byte DirectorSummonPresencePhase(string personId) =>
DirectorSummonSystem.ResolvePresencePhase(this, personId);
/// <summary>ActionDef id for the person card when summoned but the live activity is idle.</summary>
public string? DirectorSummonPresenceActionId(string personId) =>
DirectorSummonSystem.PresenceActionId(DirectorSummonPresencePhase(personId));
private void RecordWorldEvents(DateTime before, DateTime after)
{
_worldEvents.AddRange(EventSystem.Detect(this, before, after));
+2 -1
View File
@@ -2,5 +2,6 @@ namespace HSchool.Simulation;
/// <summary>
/// A fact the school raised this step. Same seam as <c>AffinityEvent</c>: a list, no UI.
/// <see cref="PersonKey"/> is empty when the fact is school-wide (morning, lesson bell).
/// </summary>
public readonly record struct WorldEvent(string Trigger);
public readonly record struct WorldEvent(string Trigger, string PersonKey = "");