Add recess quarrels, rare yard and gym fights, and apologies.

Bullies remember a victim by a trait field, a defender can join, staff break up a fight with a reprimand, and skip empty time clears leftover clashes so they cannot last forever.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 11:31:06 +03:00
co-authored by Cursor
parent f45347b335
commit 395107c959
20 changed files with 1504 additions and 23 deletions
+112 -3
View File
@@ -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 || TalkActions.IsWhisper(activity.ActionId))
if (!bound || TalkActions.IsWhisper(activity.ActionId) || TalkActions.IsFight(activity.ActionId))
{
return null;
}
@@ -434,7 +434,25 @@ internal static class PresenceSystem
if (decision.StartAction is not null && !activity.IsActive)
{
return decision.StartAction;
var start = decision.StartAction;
var location = state.NodeId is null ? null : school.Map?.NodeDef(state.NodeId);
if (TalkActions.IsQuarrel(start)
&& location is not null
&& Conflict.CanFightHere(location)
&& Conflict.RollFight(
person.Traits,
school.Catalog!,
school.Catalog.BehaviorRules,
Seed.Mix(
school.PeopleSeed,
person.Id,
DateOnly.FromDateTime(school.Clock.Time).DayNumber,
Seed.ConflictSalt + 2)))
{
start = Conflict.FightActionFor(location);
}
return start;
}
if (!activity.IsActive
@@ -444,6 +462,20 @@ internal static class PresenceSystem
return TalkActions.Whisper;
}
if (!activity.IsActive
&& decision.WalkTo is null
&& ShouldStartFightOnPe(school, person, state))
{
return TalkActions.FightGym;
}
if (!activity.IsActive
&& decision.WalkTo is null
&& ShouldStartApology(school, person, state))
{
return TalkActions.Apologize;
}
return null;
}
@@ -476,6 +508,82 @@ internal static class PresenceSystem
return TalkCircles.WhisperCaught(roll, chance, 0.5f, 1f);
}
private static bool ShouldStartFightOnPe(School school, Person person, ActorState state)
{
if (!state.BoundToLesson
|| !person.IsStudent
|| state.IsWalking
|| state.NodeId is null
|| school.Catalog is null)
{
return false;
}
var location = school.Map?.NodeDef(state.NodeId);
if (location is null || !location.Equals("GymHall", StringComparison.Ordinal))
{
return false;
}
if (!Conflict.HasRival(state.Talk))
{
return false;
}
return Conflict.RollFight(
person.Traits,
school.Catalog,
school.Catalog.BehaviorRules,
Seed.Mix(
school.PeopleSeed,
person.Id,
DateOnly.FromDateTime(school.Clock.Time).DayNumber,
Seed.ConflictSalt + 3 + (school.Clock.Time.Minute / 2)));
}
private static bool ShouldStartApology(School school, Person person, ActorState state)
{
if (!person.IsStudent || state.IsWalking || state.NodeId is null || school.Catalog is null)
{
return false;
}
var hasDebtHere = false;
foreach (var debt in school.ApologyDebts.Values)
{
if (!debt.FromId.Equals(person.Id, StringComparison.Ordinal))
{
continue;
}
if (!state.Talk.PersonNodes.TryGetValue(debt.ToId, out var node)
|| !node.Equals(state.NodeId, StringComparison.Ordinal))
{
continue;
}
hasDebtHere = true;
break;
}
if (!hasDebtHere)
{
return false;
}
var chance = Math.Clamp(
Conflict.ApologyChance(person.Traits, school.Catalog),
0f,
1f);
return Conflict.RollStarts(
chance,
Seed.Mix(
school.PeopleSeed,
person.Id,
DateOnly.FromDateTime(school.Clock.Time).DayNumber,
Seed.ConflictSalt + 4));
}
private static Dictionary<(string Node, string Thing), int> SnapshotOccupied(School school, string exceptId)
{
var occupied = new Dictionary<(string Node, string Thing), int>();
@@ -597,7 +705,8 @@ internal static class PresenceSystem
person.ClassId,
TalkCircles.HasPhone(person.Items),
friendPull,
school.Catalog?.BehaviorRules);
school.Catalog?.BehaviorRules,
person.BullyVictimId);
}
private static Dictionary<string, string> SnapshotPersonNodes(School school)