Add lesson whispers, teacher catch chance, and after-bell teacher talk.
Whispering cuts lesson skill gain from BehaviorDef; a teacher in the room may interrupt into a discipline circle, and pedagogy scales the pupil's opinion of the teacher. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -57,7 +57,12 @@ internal static class LessonLearningSystem
|
||||
in People,
|
||||
(ref PersonIdentity identity, ref PersonSkills skills, ref PersonTraits traits, ref PersonNeeds needs, ref PersonRoles roles, ref Presence presence, ref PersonActivity activity) =>
|
||||
{
|
||||
if (activity.IsActive || !IsStanding(presence))
|
||||
if (!IsStanding(presence))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (activity.IsActive && !TalkActions.IsWhisper(activity.ActionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -98,6 +103,8 @@ internal static class LessonLearningSystem
|
||||
school.TryLogLessonOnce(personId, PersonLogTypes.LessonNoTextbook, lesson.Subject);
|
||||
}
|
||||
|
||||
textbookFactor *= TalkCircles.LessonWhisperFactor(activity.ActionId, rules);
|
||||
|
||||
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
||||
? found
|
||||
: new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
|
||||
@@ -13,6 +13,8 @@ public static class PersonLogTypes
|
||||
public const string ActionStarted = "action-started";
|
||||
public const string ActionEnded = "action-ended";
|
||||
public const string TalkEnded = "talk-ended";
|
||||
public const string Whispered = "whispered";
|
||||
public const string TeacherInterrupted = "teacher-interrupted";
|
||||
public const string ApparelReplaced = "apparel-replaced";
|
||||
public const string ApparelChanged = "apparel-changed";
|
||||
public const string LessonNoTeacher = "lesson-no-teacher";
|
||||
@@ -29,6 +31,16 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
||||
public string Caption(DefCatalog catalog, string locale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
if (Type.Equals(PersonLogTypes.Whispered, StringComparison.Ordinal))
|
||||
{
|
||||
return catalog.Text(locale, "Whispered");
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.TeacherInterrupted, StringComparison.Ordinal))
|
||||
{
|
||||
return catalog.Text(locale, "TeacherInterrupted");
|
||||
}
|
||||
|
||||
if (ThingDef is null)
|
||||
{
|
||||
return Type;
|
||||
|
||||
@@ -333,7 +333,7 @@ internal static class PresenceSystem
|
||||
: slot.Kind == DaySlotKind.Lesson && lessons.Any(lesson => lesson.Period == slot.Index);
|
||||
if (activity.IsActive && TalkActions.IsTalk(activity.ActionId))
|
||||
{
|
||||
if (!bound)
|
||||
if (!bound || TalkActions.IsWhisper(activity.ActionId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -432,7 +432,48 @@ internal static class PresenceSystem
|
||||
presence = PresenceStepper.StartWalk(presence, walks, decision.WalkTo, headingHome: false);
|
||||
}
|
||||
|
||||
return decision.StartAction is not null && !activity.IsActive ? decision.StartAction : null;
|
||||
if (decision.StartAction is not null && !activity.IsActive)
|
||||
{
|
||||
return decision.StartAction;
|
||||
}
|
||||
|
||||
if (!activity.IsActive
|
||||
&& decision.WalkTo is null
|
||||
&& ShouldStartWhisper(school, person, state))
|
||||
{
|
||||
return TalkActions.Whisper;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ShouldStartWhisper(School school, Person person, ActorState state)
|
||||
{
|
||||
if (!state.BoundToLesson
|
||||
|| !person.IsStudent
|
||||
|| state.IsWalking
|
||||
|| state.NodeId is null
|
||||
|| state.DutyRoom is null
|
||||
|| !state.NodeId.Equals(state.DutyRoom, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var location = school.Map?.NodeDef(state.NodeId);
|
||||
if (location is null || !location.Equals("Classroom", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rules = school.Catalog?.BehaviorRules;
|
||||
var chance = (rules?.WhisperStartChance ?? 0.12f) * TalkCircles.Initiative(person.Traits, school.Catalog!);
|
||||
var roll = TalkCircles.Roll01(
|
||||
Seed.Mix(
|
||||
school.PeopleSeed,
|
||||
person.Id,
|
||||
DateOnly.FromDateTime(school.Clock.Time).DayNumber,
|
||||
Seed.WhisperSalt + (school.Clock.Time.Minute / 2)));
|
||||
return TalkCircles.WhisperCaught(roll, chance, 0.5f, 1f);
|
||||
}
|
||||
|
||||
private static Dictionary<(string Node, string Thing), int> SnapshotOccupied(School school, string exceptId)
|
||||
|
||||
@@ -18,6 +18,9 @@ internal sealed class ActiveTalkCircle
|
||||
public required List<string> Members { get; init; }
|
||||
|
||||
public float RemainingMinutes { get; set; }
|
||||
|
||||
/// <summary>Set once a teacher in the room has rolled to notice this whisper.</summary>
|
||||
public bool CatchChecked { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Forms 2–4 person talk circles, applies outcomes, writes topic log lines.</summary>
|
||||
@@ -59,7 +62,7 @@ internal static class TalkCircleSystem
|
||||
}
|
||||
|
||||
var person = school.Roster.People.FirstOrDefault(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
if (person is null)
|
||||
if (person is null || !RoleFits(action, person))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -115,6 +118,11 @@ internal static class TalkCircleSystem
|
||||
{
|
||||
circle.RemainingMinutes -= minutes;
|
||||
SyncActivity(school, circle);
|
||||
if (TalkActions.IsWhisper(circle.ActionId) && TryCatchWhisper(school, circle))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (circle.RemainingMinutes > 0)
|
||||
{
|
||||
continue;
|
||||
@@ -173,7 +181,8 @@ internal static class TalkCircleSystem
|
||||
{
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var max = TalkCircles.MaxSize(action.DefName, rules, initiator.Traits, school.Catalog);
|
||||
var candidates = GatherCandidates(school, initiator.Id, nodeId, requirePhone: false);
|
||||
var studentsOnly = TalkActions.IsWhisper(action.DefName) || TalkActions.IsTeacherTalk(action.DefName);
|
||||
var candidates = GatherCandidates(school, initiator.Id, nodeId, requirePhone: false, studentsOnly);
|
||||
var ranked = TalkCircles.RankInvitees(initiator, candidates, rules);
|
||||
var members = new List<string> { initiator.Id };
|
||||
foreach (var id in ranked)
|
||||
@@ -186,7 +195,7 @@ internal static class TalkCircleSystem
|
||||
members.Add(id);
|
||||
}
|
||||
|
||||
var min = rules?.TalkCircleMin ?? 2;
|
||||
var min = TalkActions.IsTeacherTalk(action.DefName) ? 3 : rules?.TalkCircleMin ?? 2;
|
||||
if (members.Count < min)
|
||||
{
|
||||
return false;
|
||||
@@ -196,8 +205,17 @@ internal static class TalkCircleSystem
|
||||
school.Catalog,
|
||||
initiator,
|
||||
initiator.AgeOn(school.Clock.Time),
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.ApparelSalt + 21))
|
||||
?? school.Catalog.Topics.Values.First(topic => !topic.Abstract).DefName;
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.ApparelSalt + 21),
|
||||
whisperOnLesson: TalkActions.IsWhisper(action.DefName));
|
||||
if (TalkActions.IsTeacherTalk(action.DefName))
|
||||
{
|
||||
topicId = TalkCircles.PickTeacherTopic(
|
||||
school.Catalog,
|
||||
rules,
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.WhisperSalt));
|
||||
}
|
||||
|
||||
topicId ??= school.Catalog.Topics.Values.First(topic => !topic.Abstract).DefName;
|
||||
|
||||
var circle = new ActiveTalkCircle
|
||||
{
|
||||
@@ -216,6 +234,11 @@ internal static class TalkCircleSystem
|
||||
{
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var person = school.Roster!.People.First(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
if ((TalkActions.IsWhisper(action.DefName) || TalkActions.IsTeacherTalk(action.DefName)) && !person.IsStudent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var max = TalkCircles.MaxSize(action.DefName, rules, person.Traits, school.Catalog);
|
||||
if (circle.Members.Count >= max || circle.Members.Contains(personId))
|
||||
{
|
||||
@@ -238,7 +261,7 @@ internal static class TalkCircleSystem
|
||||
&& circle.RemainingMinutes > 0)
|
||||
{
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var max = rules?.TalkCircleMax ?? 4;
|
||||
var max = TalkCircles.MaxSize(actionId, rules, [], school.Catalog);
|
||||
if (circle.Members.Count < max)
|
||||
{
|
||||
return circle;
|
||||
@@ -253,7 +276,8 @@ internal static class TalkCircleSystem
|
||||
School school,
|
||||
string exceptId,
|
||||
string nodeId,
|
||||
bool requirePhone)
|
||||
bool requirePhone,
|
||||
bool studentsOnly = false)
|
||||
{
|
||||
var roster = school.Roster!.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var list = new List<TalkCircles.Candidate>();
|
||||
@@ -268,6 +292,11 @@ internal static class TalkCircleSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (studentsOnly && !roles.IsStudent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!roster.TryGetValue(identity.Id, out var person))
|
||||
{
|
||||
return;
|
||||
@@ -296,6 +325,14 @@ internal static class TalkCircleSystem
|
||||
{
|
||||
school.TalkCircleByPerson[member] = circle.Id;
|
||||
SetActivity(school, member, action.DefName, circle.TopicId, circle.RemainingMinutes);
|
||||
if (TalkActions.IsWhisper(action.DefName))
|
||||
{
|
||||
school.AppendDayLog(new PersonLogEvent(
|
||||
member,
|
||||
school.Clock.Time,
|
||||
PersonLogTypes.Whispered,
|
||||
circle.TopicId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +429,11 @@ internal static class TalkCircleSystem
|
||||
appearance,
|
||||
rules,
|
||||
catalog);
|
||||
if (person.IsStudent && other.IsStaff)
|
||||
{
|
||||
delta = TalkCircles.ScaleByPedagogy(delta, SkillOf(school, other.Id, "Pedagogy"), rules);
|
||||
}
|
||||
|
||||
if (delta == 0)
|
||||
{
|
||||
continue;
|
||||
@@ -417,6 +459,150 @@ internal static class TalkCircleSystem
|
||||
school.TalkCirclesById.Remove(circle.Id);
|
||||
}
|
||||
|
||||
private static bool TryCatchWhisper(School school, ActiveTalkCircle circle)
|
||||
{
|
||||
if (circle.CatchChecked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var teacherId = StaffStandingIn(school, circle.NodeId);
|
||||
if (teacherId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
circle.CatchChecked = true;
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var traits = new List<string>();
|
||||
foreach (var memberId in circle.Members)
|
||||
{
|
||||
var member = school.Roster!.People.FirstOrDefault(person => person.Id.Equals(memberId, StringComparison.Ordinal));
|
||||
if (member is not null)
|
||||
{
|
||||
traits.AddRange(member.Traits);
|
||||
}
|
||||
}
|
||||
|
||||
var roll = TalkCircles.Roll01(
|
||||
Seed.Mix(school.PeopleSeed, circle.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.WhisperSalt));
|
||||
var multiplier = TalkCircles.WhisperCatchMultiplier(traits, school.Catalog);
|
||||
if (!TalkCircles.WhisperCaught(
|
||||
roll,
|
||||
rules?.WhisperCatchChance ?? 0.4f,
|
||||
rules?.WhisperCatchMax ?? 0.85f,
|
||||
multiplier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConvertToReprimand(school, circle, teacherId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ConvertToReprimand(School school, ActiveTalkCircle circle, string teacherId)
|
||||
{
|
||||
var pupils = circle.Members.ToArray();
|
||||
foreach (var member in pupils)
|
||||
{
|
||||
school.AppendDayLog(new PersonLogEvent(
|
||||
member,
|
||||
school.Clock.Time,
|
||||
PersonLogTypes.TeacherInterrupted,
|
||||
circle.TopicId));
|
||||
school.TalkCircleByPerson.Remove(member);
|
||||
ClearActivity(school, member);
|
||||
}
|
||||
|
||||
school.TalkCirclesById.Remove(circle.Id);
|
||||
if (!school.Catalog!.Actions.TryGetValue(TalkActions.TeacherTalk, out var action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var members = new List<string> { teacherId };
|
||||
var max = TalkCircles.MaxSize(TalkActions.TeacherTalk, school.Catalog.BehaviorRules, [], school.Catalog);
|
||||
foreach (var id in pupils.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
if (members.Count >= max)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!members.Contains(id, StringComparer.Ordinal))
|
||||
{
|
||||
members.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (members.Count < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reprimand = new ActiveTalkCircle
|
||||
{
|
||||
Id = $"reprimand-{teacherId}-{circle.NodeId}-{school.Clock.Time.Ticks}",
|
||||
ActionId = TalkActions.TeacherTalk,
|
||||
TopicId = TeacherTopics.Discipline,
|
||||
NodeId = circle.NodeId,
|
||||
Members = members,
|
||||
RemainingMinutes = action.Minutes,
|
||||
CatchChecked = true,
|
||||
};
|
||||
Register(school, reprimand, action);
|
||||
}
|
||||
|
||||
private static string? StaffStandingIn(School school, string nodeId)
|
||||
{
|
||||
string? found = null;
|
||||
school.World.Query(
|
||||
in People,
|
||||
(ref PersonIdentity identity, ref PersonRoles roles, ref Presence presence, ref PersonActivity activity) =>
|
||||
{
|
||||
if (found is not null
|
||||
|| !roles.IsStaff
|
||||
|| presence.NodeId is null
|
||||
|| !presence.NodeId.Equals(nodeId, StringComparison.Ordinal)
|
||||
|| presence.Path.Length > 0
|
||||
|| presence.RemainingMinutes > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
found = identity.Id;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
private static bool RoleFits(ActionDef action, Person person)
|
||||
{
|
||||
if (action.Roles.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var role in action.Roles)
|
||||
{
|
||||
if (role.Equals(HSchool.Content.PersonRoles.Student, StringComparison.OrdinalIgnoreCase) && person.IsStudent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (role.Equals(HSchool.Content.PersonRoles.Staff, StringComparison.OrdinalIgnoreCase) && person.IsStaff)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (role.Equals(HSchool.Content.PersonRoles.Parent, StringComparison.OrdinalIgnoreCase) && person.IsParent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ApparelIssue AppearanceOf(School school, Person person)
|
||||
{
|
||||
var schoolClass = person.ClassId is { } classId
|
||||
|
||||
Reference in New Issue
Block a user