Halve lesson gain when the bag has no textbook for the subject.
Locker and home do not count. A pack without the field keeps vanilla 0.5. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Ai;
|
||||
|
||||
/// <summary>
|
||||
/// How much a lesson adds to one skill this step. Hungry or cold learns worse; trait offsets and
|
||||
/// the teacher's subject skill scale the rate. Missing warmth is treated as 1.
|
||||
/// the teacher's subject skill scale the rate. Missing warmth is treated as 1. A missing textbook
|
||||
/// uses <see cref="BehaviorDef.LessonNoTextbookFactor"/>.
|
||||
/// </summary>
|
||||
public static class LessonLearning
|
||||
{
|
||||
@@ -45,6 +47,28 @@ public static class LessonLearning
|
||||
return count == 0 ? 0f : total / count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a bag instance carries this lesson's <see cref="InventoryItem.Subject"/>.
|
||||
/// Locker and home do not count — the pupil is already in class.
|
||||
/// </summary>
|
||||
public static bool HasTextbookInBag(IReadOnlyList<InventoryItem> items, string subject)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subject);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal)
|
||||
&& item.Subject is not null
|
||||
&& item.Subject.Equals(subject, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static float Gain(
|
||||
float current,
|
||||
SkillDef skill,
|
||||
@@ -54,7 +78,8 @@ public static class LessonLearning
|
||||
float hunger,
|
||||
int traitOffset,
|
||||
float teacherSkill,
|
||||
float warmth = 1f)
|
||||
float warmth = 1f,
|
||||
float textbookFactor = 1f)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(skill);
|
||||
var delta = share
|
||||
@@ -63,7 +88,8 @@ public static class LessonLearning
|
||||
* NeedFactor(hunger)
|
||||
* NeedFactor(warmth)
|
||||
* TraitFactor(traitOffset)
|
||||
* TeacherFactor(teacherSkill);
|
||||
* TeacherFactor(teacherSkill)
|
||||
* textbookFactor;
|
||||
return Math.Clamp(current + delta, skill.Range.Min, skill.Range.Max);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,11 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonSkillPerHour cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.LessonNoTextbookFactor is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' lessonNoTextbookFactor must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.SwitchMargin < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' switchMargin cannot be negative.");
|
||||
|
||||
@@ -274,6 +274,12 @@ public sealed class BehaviorDef : Def
|
||||
/// <summary>Skill points a lesson adds per game hour, before traits and need state.</summary>
|
||||
public float LessonSkillPerHour { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier when the bag has no textbook for this lesson. Locker and home do not count.
|
||||
/// A pack without the field keeps vanilla half-gain so the catalog still loads.
|
||||
/// </summary>
|
||||
public float LessonNoTextbookFactor { get; init; } = 0.5f;
|
||||
|
||||
/// <summary>Inclusive range of extra commute minutes rolled per person per day.</summary>
|
||||
public int CommuteSlackMin { get; init; }
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"needThreshold": 0.35,
|
||||
// Skill points a lesson adds per game hour, before traits and need state.
|
||||
"lessonSkillPerHour": 0.05,
|
||||
// No textbook for this lesson in the bag (locker and home do not count).
|
||||
"lessonNoTextbookFactor": 0.5,
|
||||
// Inclusive extra commute minutes. 0–6 matches the previous hardcoded roll so
|
||||
// the same seed still arrives at the same minute.
|
||||
"commuteSlackMin": 0,
|
||||
|
||||
@@ -206,5 +206,6 @@
|
||||
"ApparelChanged": "changed clothes: {0}",
|
||||
"LessonNoTeacher": "lesson without a teacher: {0}",
|
||||
"LessonCold": "too cold in class: {0}",
|
||||
"LessonNoTextbook": "no textbook: {0}",
|
||||
"core": "Core",
|
||||
}
|
||||
|
||||
@@ -206,5 +206,6 @@
|
||||
"ApparelChanged": "переоделся: {0}",
|
||||
"LessonNoTeacher": "урок без учителя: {0}",
|
||||
"LessonCold": "замёрз на уроке: {0}",
|
||||
"LessonNoTextbook": "нет учебника: {0}",
|
||||
"core": "Базовая игра",
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>
|
||||
/// Grows skills for people who are actually in the lesson: at the room, not walking, not off
|
||||
/// doing something else, and with the assigned teacher standing there. The formula lives in
|
||||
/// <see cref="HSchool.Ai.LessonLearning"/>.
|
||||
/// doing something else, and with the assigned teacher standing there. A pupil without today's
|
||||
/// textbook in the bag learns at <see cref="BehaviorDef.LessonNoTextbookFactor"/>. The formula
|
||||
/// lives in <see cref="HSchool.Ai.LessonLearning"/>.
|
||||
/// </summary>
|
||||
internal static class LessonLearningSystem
|
||||
{
|
||||
@@ -90,6 +91,13 @@ internal static class LessonLearningSystem
|
||||
school.TryLogLessonOnce(personId, PersonLogTypes.LessonCold, lesson.Subject);
|
||||
}
|
||||
|
||||
var textbookFactor = 1f;
|
||||
if (person.IsStudent && !LessonLearning.HasTextbookInBag(person.Items, lesson.Subject))
|
||||
{
|
||||
textbookFactor = rules.LessonNoTextbookFactor;
|
||||
school.TryLogLessonOnce(personId, PersonLogTypes.LessonNoTextbook, lesson.Subject);
|
||||
}
|
||||
|
||||
IReadOnlyDictionary<string, float> taught = teacherSkills.TryGetValue(lesson.TeacherId, out var found)
|
||||
? found
|
||||
: new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
@@ -115,7 +123,8 @@ internal static class LessonLearningSystem
|
||||
hunger,
|
||||
TraitOffset(catalog, traits, share.Skill),
|
||||
teacherSkill,
|
||||
warmth);
|
||||
warmth,
|
||||
textbookFactor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public static class PersonLogTypes
|
||||
public const string ApparelChanged = "apparel-changed";
|
||||
public const string LessonNoTeacher = "lesson-no-teacher";
|
||||
public const string LessonCold = "lesson-cold";
|
||||
public const string LessonNoTextbook = "lesson-no-textbook";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,14 +63,17 @@ public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type,
|
||||
}
|
||||
|
||||
if (Type.Equals(PersonLogTypes.LessonNoTeacher, StringComparison.Ordinal)
|
||||
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal))
|
||||
|| Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
||||
|| Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal))
|
||||
{
|
||||
var name = catalog.Subjects.TryGetValue(ThingDef, out var subject)
|
||||
? catalog.Label(locale, subject)
|
||||
: ThingDef;
|
||||
var key = Type.Equals(PersonLogTypes.LessonCold, StringComparison.Ordinal)
|
||||
? "LessonCold"
|
||||
: "LessonNoTeacher";
|
||||
: Type.Equals(PersonLogTypes.LessonNoTextbook, StringComparison.Ordinal)
|
||||
? "LessonNoTextbook"
|
||||
: "LessonNoTeacher";
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, key), name);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user