Auto-summon pupils to the principal after fight breaks, with a corridor FIFO queue.

This commit is contained in:
Leonid Pershin
2026-08-21 11:49:06 +03:00
parent 8f1a82f42d
commit e40f130248
17 changed files with 839 additions and 18 deletions
+105
View File
@@ -0,0 +1,105 @@
using HSchool.Content;
using HSchool.People;
namespace HSchool.Ai;
/// <summary>
/// Pure map / roster helpers for auto director summons. Queue state lives on the school worker.
/// </summary>
public static class DirectorSummons
{
public const string HearingAction = "PrincipalHearing";
public const string OfficeRoomDef = "PrincipalsOffice";
public const string CorridorRoomDef = "Corridor";
public static string? OfficeNodeId(MapLayout map)
{
ArgumentNullException.ThrowIfNull(map);
return map.Rooms
.Where(room => room.Def.Equals(OfficeRoomDef, StringComparison.Ordinal))
.Select(room => room.Id)
.OrderBy(id => id, StringComparer.Ordinal)
.FirstOrDefault();
}
/// <summary>
/// Corridor (or other neighbour) where the queue waits — never inside the office.
/// Prefer a <see cref="CorridorRoomDef"/> link; tie-break by ordinal id.
/// </summary>
public static string? WaitNodeId(MapLayout map, string? officeNodeId = null)
{
ArgumentNullException.ThrowIfNull(map);
officeNodeId ??= OfficeNodeId(map);
if (officeNodeId is null)
{
return null;
}
string? bestCorridor = null;
string? bestAny = null;
foreach (var link in map.Links)
{
var other = OtherEnd(link, officeNodeId);
if (other is null)
{
continue;
}
bestAny = PickOrdinal(bestAny, other);
if (map.NodeDef(other) is { } def
&& def.Equals(CorridorRoomDef, StringComparison.Ordinal))
{
bestCorridor = PickOrdinal(bestCorridor, other);
}
}
return bestCorridor ?? bestAny;
}
public static bool HasHiredPrincipal(Roster roster) =>
roster.People.Any(person =>
person.IsStaff
&& Staffing.PrincipalPosition.Equals(person.Position, StringComparison.Ordinal));
public static string? PrincipalId(Roster roster) =>
roster.People
.Where(person =>
person.IsStaff
&& Staffing.PrincipalPosition.Equals(person.Position, StringComparison.Ordinal))
.Select(person => person.Id)
.OrderBy(id => id, StringComparer.Ordinal)
.FirstOrDefault();
public static bool CanStart(Roster roster, MapLayout map) =>
HasHiredPrincipal(roster) && OfficeNodeId(map) is not null && WaitNodeId(map) is not null;
public static bool RollSummon(float chance, int seed) =>
chance >= 1f || (chance > 0f && TalkCircles.Roll01(seed) < chance);
private static string? OtherEnd(MapLink link, string nodeId)
{
if (link.A.Equals(nodeId, StringComparison.Ordinal))
{
return link.B;
}
if (link.B.Equals(nodeId, StringComparison.Ordinal))
{
return link.A;
}
return null;
}
private static string PickOrdinal(string? current, string candidate)
{
if (current is null)
{
return candidate;
}
return string.CompareOrdinal(candidate, current) < 0 ? candidate : current;
}
}