Add speech-topic policy from tomorrow and a morning family talk.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,12 @@ internal static class MorningDress
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (isWorkday && school.SpeechRules.HasPending)
|
||||
{
|
||||
school.SpeechRules = school.SpeechRules.ApplyPending();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var offCampus = OffCampusIds(school);
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
@@ -25,6 +26,11 @@ internal static class MorningOpinions
|
||||
changed |= DriftPerson(school.Roster, rules, peopleById, person, rules.OpinionDriftPerMorning);
|
||||
}
|
||||
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
changed |= FamilyMorning(school, peopleById, person, rules);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -63,4 +69,87 @@ internal static class MorningOpinions
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One family topic per person. Parents stay off the map; this only moves the stored opinion.
|
||||
/// Runs after drift so a parent already at the family basis still visibly shifts.
|
||||
/// </summary>
|
||||
private static bool FamilyMorning(
|
||||
School school,
|
||||
IReadOnlyDictionary<string, Person> peopleById,
|
||||
Person person,
|
||||
BehaviorDef rules)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
var targetId = PickHomePartner(school.Roster!, person, peopleById);
|
||||
if (targetId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var topicId = TalkCircles.PickTaggedTopic(
|
||||
catalog,
|
||||
TopicTags.Family,
|
||||
Seed.Mix(
|
||||
school.PeopleSeed,
|
||||
person.Id,
|
||||
DateOnly.FromDateTime(school.Clock.Time).DayNumber,
|
||||
Seed.HomeSalt));
|
||||
if (topicId is null || !catalog.Topics.TryGetValue(topicId, out var topic))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var raw = topic.OpinionShift * rules.HomeTalkOpinionScale;
|
||||
var delta = (int)Math.Round(raw, MidpointRounding.AwayFromZero);
|
||||
if (delta == 0 && topic.OpinionShift != 0)
|
||||
{
|
||||
delta = topic.OpinionShift > 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
if (delta == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = OpinionStore.Get(person, targetId) ?? 0;
|
||||
var changed = OpinionStore.Set(person, targetId, current + delta);
|
||||
school.AppendDayLog(new PersonLogEvent(
|
||||
person.Id,
|
||||
school.Clock.Time,
|
||||
PersonLogTypes.HomeTalk,
|
||||
topicId));
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static string? PickHomePartner(
|
||||
Roster roster,
|
||||
Person person,
|
||||
IReadOnlyDictionary<string, Person> peopleById)
|
||||
{
|
||||
var family = roster.Families.FirstOrDefault(candidate =>
|
||||
candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
|
||||
if (family is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var id in family.ParentIds.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
if (!id.Equals(person.Id, StringComparison.Ordinal) && peopleById.ContainsKey(id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in family.ChildIds.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
if (!id.Equals(person.Id, StringComparison.Ordinal) && peopleById.ContainsKey(id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 HomeTalk = "home-talk";
|
||||
public const string Whispered = "whispered";
|
||||
public const string TeacherInterrupted = "teacher-interrupted";
|
||||
public const string Quarreled = "quarreled";
|
||||
@@ -114,12 +115,14 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal))
|
||||
if (Type.Equals(PersonLogTypes.TalkEnded, StringComparison.Ordinal)
|
||||
|| Type.Equals(PersonLogTypes.HomeTalk, StringComparison.Ordinal))
|
||||
{
|
||||
var topic = catalog.Topics.TryGetValue(ThingDef ?? "", out var def)
|
||||
? catalog.Label(locale, def)
|
||||
: ThingDef ?? Type;
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "TalkEnded"), topic);
|
||||
var key = Type.Equals(PersonLogTypes.HomeTalk, StringComparison.Ordinal) ? "HomeTalk" : "TalkEnded";
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), topic);
|
||||
}
|
||||
|
||||
var localeKey = TypeToLocaleKey(Type);
|
||||
|
||||
@@ -89,6 +89,9 @@ public sealed class School : IDisposable
|
||||
/// <summary>Student and staff dress rules. Pending pair applies on the next work morning.</summary>
|
||||
public SchoolDressRules DressRules { get; set; } = new();
|
||||
|
||||
/// <summary>Student and staff speech-topic rules. Pending ids apply on the next work morning.</summary>
|
||||
public SchoolSpeechRules SpeechRules { get; set; } = new();
|
||||
|
||||
/// <summary>Skill everyone generated for this school speaks natively.</summary>
|
||||
public string? NativeLanguage { get; private set; }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class ActiveTalkCircle
|
||||
|
||||
public required string ActionId { get; init; }
|
||||
|
||||
public required string TopicId { get; init; }
|
||||
public required string TopicId { get; set; }
|
||||
|
||||
public required string NodeId { get; init; }
|
||||
|
||||
@@ -227,8 +227,12 @@ 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 + 20))
|
||||
?? school.Catalog!.Topics.Values.First(topic => !topic.Abstract).DefName;
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.ApparelSalt + 20),
|
||||
speechPolicy: school.SpeechRules.For(initiator.IsStudent));
|
||||
if (topicId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var circle = new ActiveTalkCircle
|
||||
{
|
||||
@@ -272,7 +276,8 @@ internal static class TalkCircleSystem
|
||||
initiator,
|
||||
initiator.AgeOn(school.Clock.Time),
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.ApparelSalt + 21),
|
||||
whisperOnLesson: TalkActions.IsWhisper(action.DefName));
|
||||
whisperOnLesson: TalkActions.IsWhisper(action.DefName),
|
||||
speechPolicy: school.SpeechRules.For(initiator.IsStudent));
|
||||
if (TalkActions.IsTeacherTalk(action.DefName))
|
||||
{
|
||||
topicId = TalkCircles.PickTeacherTopic(
|
||||
@@ -281,7 +286,10 @@ internal static class TalkCircleSystem
|
||||
Seed.Mix(school.PeopleSeed, initiator.Id, DateOnly.FromDateTime(school.Clock.Time).DayNumber, Seed.WhisperSalt));
|
||||
}
|
||||
|
||||
topicId ??= school.Catalog.Topics.Values.First(topic => !topic.Abstract).DefName;
|
||||
if (topicId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var circle = new ActiveTalkCircle
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user