Merge branch 'phase/42-talk-circles'
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # docs/phases/README.md # src/HSchool.Simulation/PersonLog.cs
This commit is contained in:
@@ -21,6 +21,11 @@ internal static class ActivitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TalkActions.IsTalk(actionId))
|
||||
{
|
||||
return TalkCircleSystem.TryStart(school, personId, actionId);
|
||||
}
|
||||
|
||||
if (!school.Catalog.Actions.TryGetValue(actionId, out var action) || action.Abstract)
|
||||
{
|
||||
return false;
|
||||
@@ -114,6 +119,11 @@ internal static class ActivitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (TalkActions.IsTalk(activity.ActionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var location = school.Map?.NodeDef(presence.NodeId ?? "");
|
||||
if (location is null || !location.Equals(action.Room, StringComparison.Ordinal))
|
||||
{
|
||||
|
||||
@@ -12,6 +12,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 ApparelReplaced = "apparel-replaced";
|
||||
public const string ApparelChanged = "apparel-changed";
|
||||
public const string LessonNoTeacher = "lesson-no-teacher";
|
||||
@@ -67,6 +68,14 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "LessonNoTeacher"), name);
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.TalkEnded, 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);
|
||||
}
|
||||
|
||||
return Type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +324,23 @@ internal static class PresenceSystem
|
||||
{
|
||||
var now = school.Clock.Time;
|
||||
var walks = school.Walks!;
|
||||
var slot = SchoolDay.At(school.Catalog!, now, school.SchoolWeekDays);
|
||||
var weekday = SchoolDay.WeekdayIndex(now);
|
||||
var lessons = Duty.LessonsToday(person, ClassOf(school, person), school.Timetable, weekday);
|
||||
var bound = Duty.IsOtherStaff(person)
|
||||
? slot.Kind != DaySlotKind.Outside
|
||||
: slot.Kind == DaySlotKind.Lesson && lessons.Any(lesson => lesson.Period == slot.Index);
|
||||
if (activity.IsActive && TalkActions.IsTalk(activity.ActionId))
|
||||
{
|
||||
if (!bound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TalkCircleSystem.Interrupt(school, person.Id);
|
||||
activity = PersonActivity.Idle;
|
||||
}
|
||||
|
||||
if (!presence.IsOnCampus)
|
||||
{
|
||||
intent = Intent.None;
|
||||
@@ -369,16 +386,11 @@ internal static class PresenceSystem
|
||||
return null;
|
||||
}
|
||||
|
||||
var slot = SchoolDay.At(school.Catalog!, now, school.SchoolWeekDays);
|
||||
var weekday = SchoolDay.WeekdayIndex(now);
|
||||
var lessons = Duty.LessonsToday(person, ClassOf(school, person), school.Timetable, weekday);
|
||||
var bound = Duty.IsOtherStaff(person)
|
||||
? slot.Kind != DaySlotKind.Outside
|
||||
: slot.Kind == DaySlotKind.Lesson && lessons.Any(lesson => lesson.Period == slot.Index);
|
||||
var frame = school.Catalog!.DayFrame;
|
||||
var lunchOpen = frame is not null
|
||||
&& SchoolDay.IsLunchWindow(frame, slot, ClassOf(school, person)?.Year);
|
||||
var apparel = ApparelPresence.Build(school, person, ClassOf(school, person));
|
||||
var talk = BuildTalkContext(school, person, bound, lunchOpen, slot);
|
||||
var state = new ActorState(
|
||||
presence.NodeId,
|
||||
presence.DestinationId,
|
||||
@@ -392,7 +404,8 @@ internal static class PresenceSystem
|
||||
needs.Values,
|
||||
intent,
|
||||
lunchOpen,
|
||||
apparel);
|
||||
apparel,
|
||||
talk);
|
||||
var decision = DecisionPlanner.Decide(
|
||||
school.Catalog!,
|
||||
school.Map!,
|
||||
@@ -401,17 +414,19 @@ internal static class PresenceSystem
|
||||
(node, thing) => occupied.GetValueOrDefault((node, thing)));
|
||||
|
||||
var changing = activity.IsActive && ActivitySystem.IsChangeClothes(activity.ActionId);
|
||||
var inTalk = activity.IsActive && TalkActions.IsTalk(activity.ActionId);
|
||||
|
||||
if (decision.WalkTo is not null
|
||||
&& !decision.WalkTo.Equals(presence.NodeId, StringComparison.Ordinal)
|
||||
&& activity.IsActive
|
||||
&& !changing)
|
||||
&& !changing
|
||||
&& !inTalk)
|
||||
{
|
||||
activity = PersonActivity.Idle;
|
||||
}
|
||||
|
||||
intent = decision.Intent;
|
||||
if (decision.WalkTo is not null && !changing)
|
||||
if (decision.WalkTo is not null && !changing && !inTalk)
|
||||
{
|
||||
presence = PresenceStepper.StartWalk(presence, walks, decision.WalkTo, headingHome: false);
|
||||
}
|
||||
@@ -523,6 +538,40 @@ internal static class PresenceSystem
|
||||
return new Intent(kind, row.GoalId, row.GoalWeight, row.GoalAction);
|
||||
}
|
||||
|
||||
private static TalkPlannerContext BuildTalkContext(
|
||||
School school,
|
||||
Person person,
|
||||
bool boundToLesson,
|
||||
bool lunchOpen,
|
||||
DaySlot slot)
|
||||
{
|
||||
var opinions = person.Opinions as IReadOnlyDictionary<string, int> ?? new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var nodes = SnapshotPersonNodes(school);
|
||||
var friendPull = !boundToLesson && (slot.Kind == DaySlotKind.Break || lunchOpen);
|
||||
return new TalkPlannerContext(
|
||||
person.Id,
|
||||
opinions,
|
||||
nodes,
|
||||
person.ClassId,
|
||||
TalkCircles.HasPhone(person.Items),
|
||||
friendPull,
|
||||
school.Catalog?.BehaviorRules);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> SnapshotPersonNodes(School school)
|
||||
{
|
||||
var nodes = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var world = school.World;
|
||||
world.Query(in People, (ref PersonIdentity identity, ref Presence presence) =>
|
||||
{
|
||||
if (presence.IsOnCampus && presence.NodeId is not null)
|
||||
{
|
||||
nodes[identity.Id] = presence.NodeId;
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static SchoolClass? ClassOf(School school, Person person)
|
||||
{
|
||||
if (person.ClassId is null)
|
||||
|
||||
@@ -132,6 +132,10 @@ public sealed class School : IDisposable
|
||||
|
||||
internal Dictionary<string, string?> LoggedActivity { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
internal Dictionary<string, ActiveTalkCircle> TalkCirclesById { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
internal Dictionary<string, string> TalkCircleByPerson { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
internal void ResetDayLog()
|
||||
{
|
||||
_dayLog.Clear();
|
||||
@@ -356,6 +360,11 @@ public sealed class School : IDisposable
|
||||
PresenceSystem.Enqueue(this, id);
|
||||
}
|
||||
|
||||
foreach (var id in TalkCircleSystem.Apply(this, gameMinutes))
|
||||
{
|
||||
PresenceSystem.Enqueue(this, id);
|
||||
}
|
||||
|
||||
PersonDayLog.Sync(this);
|
||||
|
||||
if (Catalog is not null)
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
internal sealed class ActiveTalkCircle
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required string ActionId { get; init; }
|
||||
|
||||
public required string TopicId { get; init; }
|
||||
|
||||
public required string NodeId { get; init; }
|
||||
|
||||
public required List<string> Members { get; init; }
|
||||
|
||||
public float RemainingMinutes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Forms 2–4 person talk circles, applies outcomes, writes topic log lines.</summary>
|
||||
internal static class TalkCircleSystem
|
||||
{
|
||||
private static readonly QueryDescription People =
|
||||
new QueryDescription().WithAll<PersonIdentity, PersonRoles, PersonTraits, PersonSkills, PersonNeeds, Presence, PersonActivity>();
|
||||
|
||||
public static bool IsInCircle(School school, string personId) =>
|
||||
school.TalkCircleByPerson.ContainsKey(personId);
|
||||
|
||||
/// <summary>Ends an active circle when duty overrides break talk (bell rang).</summary>
|
||||
public static void Interrupt(School school, string personId)
|
||||
{
|
||||
if (!school.TalkCircleByPerson.TryGetValue(personId, out var circleId)
|
||||
|| !school.TalkCirclesById.TryGetValue(circleId, out var circle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Finish(school, circle);
|
||||
}
|
||||
|
||||
public static bool TryStart(School school, string personId, string actionId)
|
||||
{
|
||||
if (school.Catalog is null || school.Map is null || school.Roster is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TalkActions.IsTalk(actionId) || !school.Catalog.Actions.TryGetValue(actionId, out var action) || action.Abstract)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (school.TalkCircleByPerson.ContainsKey(personId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var person = school.Roster.People.FirstOrDefault(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
if (person is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? nodeId = null;
|
||||
var busy = false;
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref Presence presence, ref PersonActivity activity) =>
|
||||
{
|
||||
if (!identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nodeId = presence.NodeId;
|
||||
busy = activity.IsActive;
|
||||
});
|
||||
|
||||
if (nodeId is null || busy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var location = school.Map.NodeDef(nodeId);
|
||||
if (location is null || !location.Equals(action.Room, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.DefName.Equals(TalkActions.PhoneChat, StringComparison.Ordinal))
|
||||
{
|
||||
return TryStartPhone(school, person, nodeId, action);
|
||||
}
|
||||
|
||||
var existing = FindOpenCircle(school, nodeId, actionId);
|
||||
if (existing is not null)
|
||||
{
|
||||
return JoinCircle(school, existing, personId, action);
|
||||
}
|
||||
|
||||
return TryStartGroup(school, person, nodeId, action);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> Apply(School school, double gameMinutes)
|
||||
{
|
||||
if (gameMinutes <= 0 || school.Catalog is null || school.Roster is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var minutes = (float)gameMinutes;
|
||||
var completed = new List<string>();
|
||||
foreach (var circle in school.TalkCirclesById.Values.ToArray())
|
||||
{
|
||||
circle.RemainingMinutes -= minutes;
|
||||
SyncActivity(school, circle);
|
||||
if (circle.RemainingMinutes > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Finish(school, circle);
|
||||
completed.AddRange(circle.Members);
|
||||
}
|
||||
|
||||
completed.Sort(StringComparer.Ordinal);
|
||||
return completed;
|
||||
}
|
||||
|
||||
private static bool TryStartPhone(School school, Person initiator, string nodeId, ActionDef action)
|
||||
{
|
||||
if (!TalkCircles.HasPhone(initiator.Items))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidates = GatherCandidates(school, initiator.Id, nodeId, requirePhone: true);
|
||||
var ranked = TalkCircles.RankInvitees(initiator, candidates, school.Catalog!.BehaviorRules);
|
||||
if (ranked.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var partnerId = ranked[0];
|
||||
var partner = school.Roster!.People.First(row => row.Id.Equals(partnerId, StringComparison.Ordinal));
|
||||
if (!TalkCircles.HasPhone(partner.Items))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var topicId = TalkCircles.PickTopic(
|
||||
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;
|
||||
|
||||
var circle = new ActiveTalkCircle
|
||||
{
|
||||
Id = $"phone-{initiator.Id}-{partnerId}",
|
||||
ActionId = action.DefName,
|
||||
TopicId = topicId,
|
||||
NodeId = nodeId,
|
||||
Members = [initiator.Id, partnerId],
|
||||
RemainingMinutes = action.Minutes,
|
||||
};
|
||||
Register(school, circle, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryStartGroup(School school, Person initiator, string nodeId, ActionDef action)
|
||||
{
|
||||
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 ranked = TalkCircles.RankInvitees(initiator, candidates, rules);
|
||||
var members = new List<string> { initiator.Id };
|
||||
foreach (var id in ranked)
|
||||
{
|
||||
if (members.Count >= max)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
members.Add(id);
|
||||
}
|
||||
|
||||
var min = rules?.TalkCircleMin ?? 2;
|
||||
if (members.Count < min)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var topicId = TalkCircles.PickTopic(
|
||||
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;
|
||||
|
||||
var circle = new ActiveTalkCircle
|
||||
{
|
||||
Id = $"talk-{initiator.Id}-{nodeId}-{school.Clock.Time.Ticks}",
|
||||
ActionId = action.DefName,
|
||||
TopicId = topicId,
|
||||
NodeId = nodeId,
|
||||
Members = members,
|
||||
RemainingMinutes = action.Minutes,
|
||||
};
|
||||
Register(school, circle, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool JoinCircle(School school, ActiveTalkCircle circle, string personId, ActionDef action)
|
||||
{
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var person = school.Roster!.People.First(row => row.Id.Equals(personId, StringComparison.Ordinal));
|
||||
var max = TalkCircles.MaxSize(action.DefName, rules, person.Traits, school.Catalog);
|
||||
if (circle.Members.Count >= max || circle.Members.Contains(personId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
circle.Members.Add(personId);
|
||||
circle.Members.Sort(StringComparer.Ordinal);
|
||||
school.TalkCircleByPerson[personId] = circle.Id;
|
||||
SetActivity(school, personId, action.DefName, circle.TopicId, circle.RemainingMinutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ActiveTalkCircle? FindOpenCircle(School school, string nodeId, string actionId)
|
||||
{
|
||||
foreach (var circle in school.TalkCirclesById.Values)
|
||||
{
|
||||
if (circle.NodeId.Equals(nodeId, StringComparison.Ordinal)
|
||||
&& circle.ActionId.Equals(actionId, StringComparison.Ordinal)
|
||||
&& circle.RemainingMinutes > 0)
|
||||
{
|
||||
var rules = school.Catalog!.BehaviorRules;
|
||||
var max = rules?.TalkCircleMax ?? 4;
|
||||
if (circle.Members.Count < max)
|
||||
{
|
||||
return circle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<TalkCircles.Candidate> GatherCandidates(
|
||||
School school,
|
||||
string exceptId,
|
||||
string nodeId,
|
||||
bool requirePhone)
|
||||
{
|
||||
var roster = school.Roster!.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var list = new List<TalkCircles.Candidate>();
|
||||
school.World.Query(
|
||||
in People,
|
||||
(ref PersonIdentity identity, ref PersonRoles roles, ref Presence presence, ref PersonActivity activity) =>
|
||||
{
|
||||
if (identity.Id.Equals(exceptId, StringComparison.Ordinal)
|
||||
|| presence.NodeId is null
|
||||
|| !presence.NodeId.Equals(nodeId, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!roster.TryGetValue(identity.Id, out var person))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (requirePhone && !TalkCircles.HasPhone(person.Items))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
list.Add(new TalkCircles.Candidate(
|
||||
identity.Id,
|
||||
roles.ClassId,
|
||||
!activity.IsActive && !school.TalkCircleByPerson.ContainsKey(identity.Id),
|
||||
school.TalkCircleByPerson.ContainsKey(identity.Id),
|
||||
TalkCircles.HasPhone(person.Items)));
|
||||
});
|
||||
|
||||
return list.Where(candidate => candidate.IsIdle).ToArray();
|
||||
}
|
||||
|
||||
private static void Register(School school, ActiveTalkCircle circle, ActionDef action)
|
||||
{
|
||||
school.TalkCirclesById[circle.Id] = circle;
|
||||
foreach (var member in circle.Members)
|
||||
{
|
||||
school.TalkCircleByPerson[member] = circle.Id;
|
||||
SetActivity(school, member, action.DefName, circle.TopicId, circle.RemainingMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetActivity(School school, string personId, string actionId, string topicId, float remaining)
|
||||
{
|
||||
school.World.Query(
|
||||
in People,
|
||||
(ref PersonIdentity identity, ref PersonActivity activity) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
activity = new PersonActivity(actionId, topicId, remaining);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void SyncActivity(School school, ActiveTalkCircle circle)
|
||||
{
|
||||
foreach (var member in circle.Members)
|
||||
{
|
||||
SetActivity(school, member, circle.ActionId, circle.TopicId, Math.Max(0f, circle.RemainingMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
private static void Finish(School school, ActiveTalkCircle circle)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
var rules = catalog.BehaviorRules;
|
||||
var roster = school.Roster!;
|
||||
var people = circle.Members
|
||||
.Select(id => roster.People.First(person => person.Id.Equals(id, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
if (!catalog.Topics.TryGetValue(circle.TopicId, out var topic))
|
||||
{
|
||||
topic = catalog.Topics.Values.First(def => !def.Abstract);
|
||||
}
|
||||
|
||||
if (!catalog.Actions.TryGetValue(circle.ActionId, out var action))
|
||||
{
|
||||
action = catalog.Actions[TalkActions.Chat];
|
||||
}
|
||||
|
||||
var hours = action.Minutes / 60f;
|
||||
var sharedLanguage = TalkCircles.SharedLanguage(catalog, people, rules) is not null;
|
||||
var language = TalkCircles.SharedLanguage(catalog, people, rules);
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(action.Need)
|
||||
&& catalog.Needs.TryGetValue(action.Need, out var need))
|
||||
{
|
||||
var current = NeedOf(school, person.Id, action.Need);
|
||||
if (float.IsNaN(current))
|
||||
{
|
||||
current = need.Min;
|
||||
}
|
||||
|
||||
var next = ActionStepper.ApplyNeedGain(current, action, need);
|
||||
MutateNeed(school, person.Id, action.Need, next);
|
||||
}
|
||||
|
||||
if (catalog.Skills.TryGetValue("Communication", out var communicationSkill))
|
||||
{
|
||||
var level = SkillOf(school, person.Id, "Communication");
|
||||
var next = TalkCircles.CommunicationGain(level, communicationSkill, hours, rules);
|
||||
MutateSkill(school, person.Id, "Communication", next);
|
||||
}
|
||||
|
||||
if (language is not null
|
||||
&& catalog.Skills.TryGetValue(language, out var languageSkill)
|
||||
&& SkillOf(school, person.Id, language) < languageSkill.Range.Max)
|
||||
{
|
||||
var level = SkillOf(school, person.Id, language);
|
||||
var next = TalkCircles.LanguageGain(level, languageSkill, hours, rules);
|
||||
MutateSkill(school, person.Id, language, next);
|
||||
}
|
||||
|
||||
foreach (var other in people)
|
||||
{
|
||||
if (other.Id.Equals(person.Id, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var communication = SkillOf(school, person.Id, "Communication");
|
||||
var appearance = AppearanceOf(school, other);
|
||||
var delta = TalkCircles.OpinionDelta(
|
||||
person,
|
||||
other,
|
||||
topic,
|
||||
communication,
|
||||
sharedLanguage,
|
||||
circle.ActionId,
|
||||
appearance,
|
||||
rules,
|
||||
catalog);
|
||||
if (delta == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var current = OpinionStore.Get(person, other.Id) ?? 0;
|
||||
OpinionStore.Set(person, other.Id, current + delta);
|
||||
}
|
||||
|
||||
school.AppendDayLog(new PersonLogEvent(
|
||||
person.Id,
|
||||
school.Clock.Time,
|
||||
PersonLogTypes.TalkEnded,
|
||||
circle.TopicId));
|
||||
}
|
||||
|
||||
foreach (var member in circle.Members)
|
||||
{
|
||||
school.TalkCircleByPerson.Remove(member);
|
||||
ClearActivity(school, member);
|
||||
}
|
||||
|
||||
school.TalkCirclesById.Remove(circle.Id);
|
||||
}
|
||||
|
||||
private static ApparelIssue AppearanceOf(School school, Person person)
|
||||
{
|
||||
var schoolClass = person.ClassId is { } classId
|
||||
? school.Roster!.Classes.FirstOrDefault(row => row.Id.Equals(classId, StringComparison.Ordinal))
|
||||
: null;
|
||||
var mode = ApparelPresence.ModeAt(school, person, schoolClass);
|
||||
return ApparelDresser.CurrentIssues(school, person, mode);
|
||||
}
|
||||
|
||||
private static float NeedOf(School school, string personId, string need)
|
||||
{
|
||||
var value = float.NaN;
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref PersonNeeds needs) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
value = needs.Values.GetValueOrDefault(need, float.NaN);
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
private static float SkillOf(School school, string personId, string skill)
|
||||
{
|
||||
var value = 0f;
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref PersonSkills skills) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
value = skills.Values.GetValueOrDefault(skill);
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void MutateSkill(School school, string personId, string skill, float value)
|
||||
{
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref PersonSkills skills) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
skills.Values[skill] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void MutateNeed(School school, string personId, string need, float value)
|
||||
{
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref PersonNeeds needs) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
needs.Values[need] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ClearActivity(School school, string personId)
|
||||
{
|
||||
school.World.Query(in People, (ref PersonIdentity identity, ref PersonActivity activity) =>
|
||||
{
|
||||
if (identity.Id.Equals(personId, StringComparison.Ordinal))
|
||||
{
|
||||
activity = PersonActivity.Idle;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user