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
+25
View File
@@ -266,6 +266,16 @@ internal static class PeopleDefValidator
{
throw new ContentLoadException($"TraitDef '{trait.DefName}' whisperCatchMultiplier cannot be negative.");
}
if (trait.ConflictChance < 0f)
{
throw new ContentLoadException($"TraitDef '{trait.DefName}' conflictChance cannot be negative.");
}
if (trait.ApologyChance < 0f)
{
throw new ContentLoadException($"TraitDef '{trait.DefName}' apologyChance cannot be negative.");
}
}
private static void ValidateNeed(NeedDef need)
@@ -612,6 +622,21 @@ internal static class PeopleDefValidator
{
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' teacher-talk weights cannot be negative.");
}
if (behavior.QuarrelChance is < 0f or > 1f)
{
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' quarrelChance must be 01.");
}
if (behavior.FightChance is < 0f or > 1f)
{
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' fightChance must be 01.");
}
if (behavior.ApologyRestoreFraction is < 0f or > 1f)
{
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' apologyRestoreFraction must be 01.");
}
}
private static void ValidateTopic(TopicDef topic, DefCatalog catalog)
+32
View File
@@ -215,6 +215,20 @@ public sealed class TraitDef : Def
/// Multiplies the chance a whisper is caught. Quiet below 1, outgoing above. Missing is 1.
/// </summary>
public float WhisperCatchMultiplier { get; init; } = 1f;
/// <summary>
/// Multiplies quarrel and fight chance. Hot-tempered and bully above 1. Missing is 1.
/// </summary>
public float ConflictChance { get; init; } = 1f;
/// <summary>
/// This person keeps one victim id until they leave or opinion hits the floor.
/// A field, not a <c>defName == "Bully"</c> branch.
/// </summary>
public bool RemembersVictim { get; init; }
/// <summary>Multiplies the chance to start an apology. Bully below 1. Missing is 1.</summary>
public float ApologyChance { get; init; } = 1f;
}
/// <summary>Conversation subject. Tags gate appropriateness; language is optional skill help.</summary>
@@ -451,6 +465,24 @@ public sealed class BehaviorDef : Def
/// <summary>Per-decision chance a pupil in class tries to start a whisper, before initiative.</summary>
public float WhisperStartChance { get; init; } = 0.12f;
/// <summary>Rivals always roll this before traits. 1 means every enemy pair can start a quarrel.</summary>
public float QuarrelChance { get; init; } = 1f;
/// <summary>Base chance a yard or gym clash becomes a fight. Traits scale it; kept rare.</summary>
public float FightChance { get; init; } = 0.1f;
/// <summary>Opinion shift per other participant when a quarrel ends. Stronger than rude talk (3).</summary>
public int QuarrelOpinionShift { get; init; } = -8;
/// <summary>Opinion shift when a fight ends. No health need is touched.</summary>
public int FightOpinionShift { get; init; } = -16;
/// <summary>Fraction of lost opinion an apology returns. Never climbs past the pre-quarrel value.</summary>
public float ApologyRestoreFraction { get; init; } = 0.5f;
/// <summary>Opinion of the victim at or above this — a third person may join the quarrel.</summary>
public int DefendOpinionMin { get; init; } = 40;
public static IReadOnlyList<OpinionBand> DefaultOpinionBands { get; } =
[
new() { Min = 70, Id = "OpinionCloseFriend" },
+23 -1
View File
@@ -8,6 +8,11 @@ public static class TalkActions
public const string PhoneChat = "PhoneChat";
public const string Whisper = "Whisper";
public const string TeacherTalk = "TeacherTalk";
public const string Quarrel = "Quarrel";
public const string QuarrelYard = "QuarrelYard";
public const string Fight = "Fight";
public const string FightGym = "FightGym";
public const string Apologize = "Apologize";
public static bool IsTalk(string? actionId) =>
actionId is not null
@@ -15,7 +20,24 @@ public static class TalkActions
|| actionId.Equals(StaffChat, StringComparison.Ordinal)
|| actionId.Equals(PhoneChat, StringComparison.Ordinal)
|| actionId.Equals(Whisper, StringComparison.Ordinal)
|| actionId.Equals(TeacherTalk, StringComparison.Ordinal));
|| actionId.Equals(TeacherTalk, StringComparison.Ordinal)
|| IsConflict(actionId));
public static bool IsQuarrel(string? actionId) =>
actionId is not null
&& (actionId.Equals(Quarrel, StringComparison.Ordinal)
|| actionId.Equals(QuarrelYard, StringComparison.Ordinal));
public static bool IsFight(string? actionId) =>
actionId is not null
&& (actionId.Equals(Fight, StringComparison.Ordinal)
|| actionId.Equals(FightGym, StringComparison.Ordinal));
public static bool IsApologize(string? actionId) =>
actionId is not null && actionId.Equals(Apologize, StringComparison.Ordinal);
public static bool IsConflict(string? actionId) =>
IsQuarrel(actionId) || IsFight(actionId) || IsApologize(actionId);
public static bool IsWhisper(string? actionId) =>
actionId is not null && actionId.Equals(Whisper, StringComparison.Ordinal);