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:
@@ -261,6 +261,11 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException($"TraitDef '{trait.DefName}' skill modifier references unknown SkillDef '{modifier.Skill}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (trait.WhisperCatchMultiplier < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"TraitDef '{trait.DefName}' whisperCatchMultiplier cannot be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateNeed(NeedDef need)
|
||||
@@ -574,6 +579,39 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' talk circle size must be at least 2 with max ≥ min.");
|
||||
}
|
||||
|
||||
if (behavior.LessonWhisperSkillFactor is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonWhisperSkillFactor must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.WhisperCatchChance is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' whisperCatchChance must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.WhisperCatchMax is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' whisperCatchMax must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.WhisperStartChance is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' whisperStartChance must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.TalkPedagogyOpinionScale < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' talkPedagogyOpinionScale cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.TeacherAfterLessonWeight < 0f
|
||||
|| behavior.TeacherQuestionWeight < 0f
|
||||
|| behavior.TeacherPraiseWeight < 0f
|
||||
|| behavior.TeacherReprimandWeight < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' teacher-talk weights cannot be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTopic(TopicDef topic, DefCatalog catalog)
|
||||
@@ -611,10 +649,7 @@ internal static class PeopleDefValidator
|
||||
private static void RequireTalkTopics(DefCatalog catalog)
|
||||
{
|
||||
var hasTalkAction = catalog.Actions.Values.Any(action =>
|
||||
!action.Abstract
|
||||
&& (action.DefName.Equals(TalkActions.Chat, StringComparison.Ordinal)
|
||||
|| action.DefName.Equals(TalkActions.StaffChat, StringComparison.Ordinal)
|
||||
|| action.DefName.Equals(TalkActions.PhoneChat, StringComparison.Ordinal)));
|
||||
!action.Abstract && TalkActions.IsTalk(action.DefName));
|
||||
if (!hasTalkAction)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -210,6 +210,11 @@ public sealed class TraitDef : Def
|
||||
|
||||
/// <summary>Pull toward topics carrying these tags when this person picks the subject.</summary>
|
||||
public IReadOnlyList<TopicTagWeight> TalkTagWeights { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies the chance a whisper is caught. Quiet below 1, outgoing above. Missing is 1.
|
||||
/// </summary>
|
||||
public float WhisperCatchMultiplier { get; init; } = 1f;
|
||||
}
|
||||
|
||||
/// <summary>Conversation subject. Tags gate appropriateness; language is optional skill help.</summary>
|
||||
@@ -415,6 +420,37 @@ public sealed class BehaviorDef : Def
|
||||
/// <summary>Opinion at or below — enemy; never invited, lunch nodes avoided.</summary>
|
||||
public int TalkEnemyThreshold { get; init; } = -40;
|
||||
|
||||
/// <summary>
|
||||
/// Lesson skill multiplier while the pupil is in a whisper circle. Missing keeps a cut, not
|
||||
/// a skipped lesson — whispering is costly, not a free skip.
|
||||
/// </summary>
|
||||
public float LessonWhisperSkillFactor { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Base chance the teacher notices one whisper circle. Traits scale it; never 1.</summary>
|
||||
public float WhisperCatchChance { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Ceiling after traits. A pack cannot make every whisper a sure catch.</summary>
|
||||
public float WhisperCatchMax { get; init; } = 0.85f;
|
||||
|
||||
/// <summary>
|
||||
/// How far pedagogy 0…100 bends a pupil's opinion of the teacher. 50 is identity; above it
|
||||
/// praise grows and reprimand softens, below it the reverse.
|
||||
/// </summary>
|
||||
public float TalkPedagogyOpinionScale { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>Leisure weight for a teacher taking pupils after the bell. Below lesson, above chat.</summary>
|
||||
public float TeacherAfterLessonWeight { get; init; } = 4f;
|
||||
|
||||
/// <summary>Relative weights when the teacher picks question / praise / reprimand.</summary>
|
||||
public float TeacherQuestionWeight { get; init; } = 2f;
|
||||
|
||||
public float TeacherPraiseWeight { get; init; } = 2f;
|
||||
|
||||
public float TeacherReprimandWeight { get; init; } = 1f;
|
||||
|
||||
/// <summary>Per-decision chance a pupil in class tries to start a whisper, before initiative.</summary>
|
||||
public float WhisperStartChance { get; init; } = 0.12f;
|
||||
|
||||
public static IReadOnlyList<OpinionBand> DefaultOpinionBands { get; } =
|
||||
[
|
||||
new() { Min = 70, Id = "OpinionCloseFriend" },
|
||||
|
||||
@@ -6,12 +6,22 @@ public static class TalkActions
|
||||
public const string Chat = "Chat";
|
||||
public const string StaffChat = "StaffChat";
|
||||
public const string PhoneChat = "PhoneChat";
|
||||
public const string Whisper = "Whisper";
|
||||
public const string TeacherTalk = "TeacherTalk";
|
||||
|
||||
public static bool IsTalk(string? actionId) =>
|
||||
actionId is not null
|
||||
&& (actionId.Equals(Chat, StringComparison.Ordinal)
|
||||
|| actionId.Equals(StaffChat, StringComparison.Ordinal)
|
||||
|| actionId.Equals(PhoneChat, StringComparison.Ordinal));
|
||||
|| actionId.Equals(PhoneChat, StringComparison.Ordinal)
|
||||
|| actionId.Equals(Whisper, StringComparison.Ordinal)
|
||||
|| actionId.Equals(TeacherTalk, StringComparison.Ordinal));
|
||||
|
||||
public static bool IsWhisper(string? actionId) =>
|
||||
actionId is not null && actionId.Equals(Whisper, StringComparison.Ordinal);
|
||||
|
||||
public static bool IsTeacherTalk(string? actionId) =>
|
||||
actionId is not null && actionId.Equals(TeacherTalk, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Vanilla topic tags from the social slice design doc.</summary>
|
||||
@@ -26,3 +36,11 @@ public static class TopicTags
|
||||
public const string Rude = "rude";
|
||||
public const string Appearance = "appearance";
|
||||
}
|
||||
|
||||
/// <summary>Teacher-after-lesson and reprimand topics in core. Names match TopicDef ids.</summary>
|
||||
public static class TeacherTopics
|
||||
{
|
||||
public const string Question = "TopicQuestion";
|
||||
public const string Praise = "TopicPraise";
|
||||
public const string Discipline = "TopicDiscipline";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user