Merge branch 'main' into phase/32-weather-warmth
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # src/HSchool.Simulation/School.cs
This commit is contained in:
@@ -303,6 +303,8 @@ export interface WornItem {
|
||||
readonly color: string | null;
|
||||
readonly colorLabel: string | null;
|
||||
readonly layers: readonly DefLabel[];
|
||||
readonly condition: number;
|
||||
readonly conditionLabel: string;
|
||||
}
|
||||
|
||||
export interface CarriedItem {
|
||||
|
||||
@@ -676,6 +676,34 @@ body {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.people__garments {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
.people__garment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.people__wear {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__wear-label {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.people__wear .people__need-track {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.people__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -41,6 +41,8 @@ function card(overrides: Partial<PersonCard> = {}): PersonCard {
|
||||
color: 'White',
|
||||
colorLabel: 'Белый',
|
||||
layers: [{ defName: 'Top', label: 'Верх' }],
|
||||
condition: 1,
|
||||
conditionLabel: 'целая',
|
||||
},
|
||||
],
|
||||
carried: [
|
||||
@@ -74,4 +76,58 @@ describe('renderPersonCard', () => {
|
||||
expect(root.textContent).toContain('Учебник · Математика');
|
||||
expect(root.querySelector('.people__tabs')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the server condition caption, not a band inferred from the number', () => {
|
||||
setLocale('ru');
|
||||
const root = document.createElement('div');
|
||||
renderPersonCard(
|
||||
root,
|
||||
card({
|
||||
worn: [
|
||||
{
|
||||
defName: 'Shirt',
|
||||
label: 'Рубашка',
|
||||
color: 'White',
|
||||
colorLabel: 'Белый',
|
||||
layers: [{ defName: 'Top', label: 'Верх' }],
|
||||
condition: 0.1,
|
||||
conditionLabel: 'целая',
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(root.textContent).toContain('целая');
|
||||
expect(root.textContent).not.toContain('висит лохмотьями');
|
||||
expect(root.querySelector('.people__wear-label')?.textContent).toBe('целая');
|
||||
expect((root.querySelector('.people__need-fill') as HTMLElement | null)?.style.width).toBe('10%');
|
||||
});
|
||||
|
||||
it('changes the caption when the payload label crosses a threshold', () => {
|
||||
setLocale('ru');
|
||||
const root = document.createElement('div');
|
||||
renderPersonCard(root, card(), () => {});
|
||||
expect(root.querySelector('.people__wear-label')?.textContent).toBe('целая');
|
||||
|
||||
root.replaceChildren();
|
||||
renderPersonCard(
|
||||
root,
|
||||
card({
|
||||
worn: [
|
||||
{
|
||||
defName: 'Shirt',
|
||||
label: 'Рубашка',
|
||||
color: 'White',
|
||||
colorLabel: 'Белый',
|
||||
layers: [{ defName: 'Top', label: 'Верх' }],
|
||||
condition: 0.1,
|
||||
conditionLabel: 'висит лохмотьями',
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => {},
|
||||
);
|
||||
expect(root.querySelector('.people__wear-label')?.textContent).toBe('висит лохмотьями');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,22 +119,36 @@ function appendApparel(parent: HTMLElement, rows: PersonCard['worn']): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = el('dl', { class: 'people__pairs' });
|
||||
const list = el('div', { class: 'people__garments' });
|
||||
for (const row of rows) {
|
||||
const layers = row.layers.map((layer) => layer.label).join(', ');
|
||||
const color = row.colorLabel ?? row.color;
|
||||
const value = color !== null && color.length > 0 ? `${row.label} · ${color}` : row.label;
|
||||
grid.append(
|
||||
const share = Math.max(0, Math.min(1, row.condition));
|
||||
const fill = el('span', { class: 'people__need-fill' });
|
||||
fill.style.width = `${Math.round(share * 100)}%`;
|
||||
fill.classList.toggle('people__need-fill--low', share < 0.25);
|
||||
list.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__pair' },
|
||||
el('dt', { text: layers.length > 0 ? layers : row.label }),
|
||||
el('dd', { text: value }),
|
||||
{ class: 'people__garment' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__pair' },
|
||||
el('dt', { text: layers.length > 0 ? layers : row.label }),
|
||||
el('dd', { text: value }),
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__wear' },
|
||||
el('span', { class: 'people__wear-label', text: row.conditionLabel }),
|
||||
el('span', { class: 'people__need-track' }, fill),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
parent.append(section(t('peopleApparel')), grid);
|
||||
parent.append(section(t('peopleApparel')), list);
|
||||
}
|
||||
|
||||
function appendCarry(parent: HTMLElement, card: PersonCard): void {
|
||||
|
||||
@@ -531,6 +531,18 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' optionalApparelChance must be 0–1.");
|
||||
}
|
||||
|
||||
if (behavior.ApparelWearPerHour < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' apparelWearPerHour cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.ApparelReplaceBelow is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' apparelReplaceBelow must be 0–1.");
|
||||
}
|
||||
|
||||
ValidateConditionBands(behavior);
|
||||
|
||||
// A pack may invert this on purpose — lunch then pulls the class out of the lesson.
|
||||
// The warning is the catch; refusing to load would make the number unmoddable.
|
||||
if (behavior.LunchWeight > behavior.DutyLessonWeight)
|
||||
@@ -540,6 +552,47 @@ internal static class PeopleDefValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateConditionBands(BehaviorDef behavior)
|
||||
{
|
||||
var bands = behavior.ApparelConditionBands;
|
||||
if (bands.Count == 0)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' apparelConditionBands cannot be empty.");
|
||||
}
|
||||
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
var sawZero = false;
|
||||
foreach (var band in bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' apparel condition band is missing an id.");
|
||||
}
|
||||
|
||||
if (band.Min is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' apparel condition '{band.Id}' min must be 0–1.");
|
||||
}
|
||||
|
||||
if (!ids.Add(band.Id))
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' repeats apparel condition '{band.Id}'.");
|
||||
}
|
||||
|
||||
if (band.Min == 0f)
|
||||
{
|
||||
sawZero = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawZero)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"BehaviorDef '{behavior.DefName}' apparelConditionBands must include a min of 0 so rags have a caption.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateHoliday(HolidayDef holiday)
|
||||
{
|
||||
if (holiday.Abstract)
|
||||
|
||||
@@ -268,6 +268,40 @@ public sealed class BehaviorDef : Def
|
||||
|
||||
/// <summary>Chance each optional layer (sweater, coat, hat, accessory) is worn at generation.</summary>
|
||||
public float OptionalApparelChance { get; init; } = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// Condition lost per game hour while the thing is worn on campus. Bag, locker and home do
|
||||
/// not wear. The number lives here so a pack can make clothes last a term or a week.
|
||||
/// </summary>
|
||||
public float ApparelWearPerHour { get; init; } = 0.01f;
|
||||
|
||||
/// <summary>
|
||||
/// Worn apparel below this stays home: morning replacement issues a fresh instance.
|
||||
/// At the threshold they still go out, even when the caption already says torn.
|
||||
/// </summary>
|
||||
public float ApparelReplaceBelow { get; init; } = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// Caption bands for a 0–1 condition bar. Highest <see cref="ApparelConditionBand.Min"/>
|
||||
/// the value still meets wins. Empty falls back to <see cref="DefaultConditionBands"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ApparelConditionBand> ApparelConditionBands { get; init; } = DefaultConditionBands;
|
||||
|
||||
public static IReadOnlyList<ApparelConditionBand> DefaultConditionBands { get; } =
|
||||
[
|
||||
new() { Min = 0.75f, Id = "ApparelConditionIntact" },
|
||||
new() { Min = 0.4f, Id = "ApparelConditionWorn" },
|
||||
new() { Min = 0.15f, Id = "ApparelConditionTorn" },
|
||||
new() { Min = 0f, Id = "ApparelConditionRags" },
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>One caption on the condition bar. <see cref="Id"/> is a locale key, not a Def.</summary>
|
||||
public sealed class ApparelConditionBand
|
||||
{
|
||||
public float Min { get; init; }
|
||||
|
||||
public required string Id { get; init; }
|
||||
}
|
||||
|
||||
public enum BodyAttributeKind
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class ItemLocations
|
||||
|
||||
/// <summary>
|
||||
/// One thing on a person. <see cref="Subject"/> is set only on a textbook instance — the def is
|
||||
/// shared. Condition is 1 at birth; wear is a later phase.
|
||||
/// shared. Condition is 0–1; it falls while worn on campus and resets on the morning issue.
|
||||
/// </summary>
|
||||
public sealed record InventoryItem(
|
||||
string Def,
|
||||
|
||||
@@ -72,7 +72,9 @@ internal sealed record WornItemResponse(
|
||||
string Label,
|
||||
string? Color,
|
||||
string? ColorLabel,
|
||||
IReadOnlyList<DefLabelResponse> Layers);
|
||||
IReadOnlyList<DefLabelResponse> Layers,
|
||||
float Condition,
|
||||
string ConditionLabel);
|
||||
|
||||
internal sealed record CarriedItemResponse(
|
||||
string DefName,
|
||||
|
||||
@@ -288,7 +288,11 @@ internal static class PersonCardReader
|
||||
ThingLabel(catalog, locale, item.Def),
|
||||
item.Color,
|
||||
ColorLabel(catalog, locale, item.Color),
|
||||
layers));
|
||||
layers,
|
||||
item.Condition,
|
||||
catalog is null
|
||||
? ApparelCondition.BandId(null, item.Condition)
|
||||
: ApparelCondition.Label(catalog, locale, item.Condition)));
|
||||
}
|
||||
|
||||
return rows;
|
||||
|
||||
@@ -28,4 +28,14 @@
|
||||
"carryMassPerHauling": 0.08,
|
||||
// Sweater, coat, hat, accessory — each rolled separately at generation.
|
||||
"optionalApparelChance": 0.4,
|
||||
// Worn on campus only. 0.01 per hour → a school week (~40 h) drops a fifth of the bar.
|
||||
"apparelWearPerHour": 0.01,
|
||||
// Below this they do not leave home; morning issues a fresh instance of the same def.
|
||||
"apparelReplaceBelow": 0.15,
|
||||
"apparelConditionBands": [
|
||||
{ "min": 0.75, "id": "ApparelConditionIntact" },
|
||||
{ "min": 0.4, "id": "ApparelConditionWorn" },
|
||||
{ "min": 0.15, "id": "ApparelConditionTorn" },
|
||||
{ "min": 0, "id": "ApparelConditionRags" },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -162,5 +162,10 @@
|
||||
"WinterBreak": "Winter break",
|
||||
"SpringBreak": "Spring break",
|
||||
"SummerBreak": "Summer break",
|
||||
"ApparelConditionIntact": "intact",
|
||||
"ApparelConditionWorn": "worn",
|
||||
"ApparelConditionTorn": "torn",
|
||||
"ApparelConditionRags": "in rags",
|
||||
"ApparelReplaced": "got a new {0}",
|
||||
"core": "Core",
|
||||
}
|
||||
|
||||
@@ -162,5 +162,10 @@
|
||||
"WinterBreak": "Зимние каникулы",
|
||||
"SpringBreak": "Весенние каникулы",
|
||||
"SummerBreak": "Летние каникулы",
|
||||
"ApparelConditionIntact": "целая",
|
||||
"ApparelConditionWorn": "поношенная",
|
||||
"ApparelConditionTorn": "порванная",
|
||||
"ApparelConditionRags": "висит лохмотьями",
|
||||
"ApparelReplaced": "получил новую {0}",
|
||||
"core": "Базовая игра",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
using System.Globalization;
|
||||
using Arch.Core;
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation;
|
||||
|
||||
/// <summary>Log row types phase 36 will page. Captions are ready even while the card has no tab.</summary>
|
||||
public static class PersonLogTypes
|
||||
{
|
||||
public const string ApparelReplaced = "apparel-replaced";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One thing that happened to a person today. Stored on the worker, not in <c>people.json</c>.
|
||||
/// The day boundary is six in the morning — the same hour skip lands on.
|
||||
/// </summary>
|
||||
public sealed record PersonLogEvent(string PersonId, DateTime Time, string Type, string? ThingDef)
|
||||
{
|
||||
public string Caption(DefCatalog catalog, string locale)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
if (!Type.Equals(PersonLogTypes.ApparelReplaced, StringComparison.Ordinal) || ThingDef is null)
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
|
||||
var name = catalog.Things.TryGetValue(ThingDef, out var def)
|
||||
? catalog.Label(locale, def)
|
||||
: ThingDef;
|
||||
return string.Format(CultureInfo.InvariantCulture, catalog.Text(locale, "ApparelReplaced"), name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Condition captions live in the catalog. The client draws what it is told.</summary>
|
||||
public static class ApparelCondition
|
||||
{
|
||||
public static string BandId(BehaviorDef? rules, float condition)
|
||||
{
|
||||
var bands = rules?.ApparelConditionBands is { Count: > 0 } listed
|
||||
? listed
|
||||
: BehaviorDef.DefaultConditionBands;
|
||||
ApparelConditionBand? best = null;
|
||||
foreach (var band in bands)
|
||||
{
|
||||
if (condition >= band.Min && (best is null || band.Min > best.Min))
|
||||
{
|
||||
best = band;
|
||||
}
|
||||
}
|
||||
|
||||
return best?.Id ?? "ApparelConditionRags";
|
||||
}
|
||||
|
||||
public static string Label(DefCatalog catalog, string locale, float condition) =>
|
||||
catalog.Text(locale, BandId(catalog.BehaviorRules, condition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worn apparel loses condition on campus from game time, then morning issues a fresh copy
|
||||
/// of anything too ragged to leave home in. Bag and locker are left alone.
|
||||
/// </summary>
|
||||
internal static class ApparelWear
|
||||
{
|
||||
private static readonly QueryDescription Identities =
|
||||
new QueryDescription().WithAll<PersonIdentity, Presence>();
|
||||
|
||||
public static bool Apply(School school, double gameMinutes, DateTime before)
|
||||
{
|
||||
var bandCrossed = Wear(school, gameMinutes);
|
||||
if (!CrossedDayStart(before, school.Clock.Time))
|
||||
{
|
||||
return bandCrossed;
|
||||
}
|
||||
|
||||
school.ResetDayLog();
|
||||
if (school.Catalog is null || !SchoolDay.IsWorkday(school.Catalog, school.Clock.Time, school.SchoolWeekDays))
|
||||
{
|
||||
return bandCrossed;
|
||||
}
|
||||
|
||||
return bandCrossed | ReplaceRags(school);
|
||||
}
|
||||
|
||||
internal static bool CrossedDayStart(DateTime before, DateTime after)
|
||||
{
|
||||
if (after <= before)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cursor = DateTime.SpecifyKind(before.Date, DateTimeKind.Utc).Add(SchoolDay.DayStart.ToTimeSpan());
|
||||
if (before >= cursor)
|
||||
{
|
||||
cursor = cursor.AddDays(1);
|
||||
}
|
||||
|
||||
return after >= cursor;
|
||||
}
|
||||
|
||||
private static bool Wear(School school, double gameMinutes)
|
||||
{
|
||||
if (gameMinutes <= 0 || school.Roster is null || school.Catalog?.BehaviorRules is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rate = school.Catalog.BehaviorRules.ApparelWearPerHour;
|
||||
if (rate <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var onCampus = OnCampusIds(school);
|
||||
if (onCampus.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var drop = (float)(rate * (gameMinutes / 60d));
|
||||
var crossed = false;
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
if (!onCampus.Contains(person.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
crossed |= WearPerson(school.Catalog, person, drop);
|
||||
}
|
||||
|
||||
return crossed;
|
||||
}
|
||||
|
||||
private static bool WearPerson(DefCatalog catalog, Person person, float drop)
|
||||
{
|
||||
var crossed = false;
|
||||
for (var i = 0; i < person.Items.Count; i++)
|
||||
{
|
||||
var item = person.Items[i];
|
||||
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)
|
||||
|| !IsApparel(catalog, item.Def))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = Math.Clamp(item.Condition - drop, 0f, 1f);
|
||||
if (next == item.Condition)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ApparelCondition.BandId(catalog.BehaviorRules, item.Condition)
|
||||
!= ApparelCondition.BandId(catalog.BehaviorRules, next))
|
||||
{
|
||||
crossed = true;
|
||||
}
|
||||
|
||||
SetItem(person, i, item with { Condition = next });
|
||||
}
|
||||
|
||||
return crossed;
|
||||
}
|
||||
|
||||
private static bool ReplaceRags(School school)
|
||||
{
|
||||
if (school.Roster is null || school.Catalog?.BehaviorRules is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var onCampus = OnCampusIds(school);
|
||||
var threshold = school.Catalog.BehaviorRules.ApparelReplaceBelow;
|
||||
var replaced = false;
|
||||
foreach (var person in school.Roster.People)
|
||||
{
|
||||
if (onCampus.Contains(person.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
replaced |= ReplacePerson(school, person, threshold);
|
||||
}
|
||||
|
||||
return replaced;
|
||||
}
|
||||
|
||||
private static bool ReplacePerson(School school, Person person, float threshold)
|
||||
{
|
||||
var catalog = school.Catalog!;
|
||||
var replaced = false;
|
||||
for (var i = 0; i < person.Items.Count; i++)
|
||||
{
|
||||
var item = person.Items[i];
|
||||
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal)
|
||||
|| item.Condition >= threshold
|
||||
|| !IsApparel(catalog, item.Def))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var color = NearestColor(catalog, item.Def, item.Color);
|
||||
SetItem(person, i, item with { Condition = 1f, Color = color });
|
||||
school.AppendDayLog(new PersonLogEvent(
|
||||
person.Id,
|
||||
school.Clock.Time,
|
||||
PersonLogTypes.ApparelReplaced,
|
||||
item.Def));
|
||||
replaced = true;
|
||||
}
|
||||
|
||||
return replaced;
|
||||
}
|
||||
|
||||
private static HashSet<string> OnCampusIds(School school)
|
||||
{
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
var world = school.World;
|
||||
world.Query(in Identities, (ref PersonIdentity identity, ref Presence presence) =>
|
||||
{
|
||||
if (presence.IsOnCampus)
|
||||
{
|
||||
ids.Add(identity.Id);
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static bool IsApparel(DefCatalog catalog, string defName) =>
|
||||
catalog.Things.TryGetValue(defName, out var def) && def.Layers.Count > 0;
|
||||
|
||||
private static string? NearestColor(DefCatalog catalog, string defName, string? current)
|
||||
{
|
||||
if (!catalog.Things.TryGetValue(defName, out var def) || def.Colors.Count == 0)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current is not null
|
||||
&& def.Colors.Contains(current, StringComparer.Ordinal))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
return def.Colors[0];
|
||||
}
|
||||
|
||||
private static void SetItem(Person person, int index, InventoryItem item)
|
||||
{
|
||||
if (person.Items is IList<InventoryItem> list && !list.IsReadOnly)
|
||||
{
|
||||
list[index] = item;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Cannot mutate items for {person.Id}.");
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public sealed class School : IDisposable
|
||||
public const int MaxNameLength = 40;
|
||||
|
||||
private bool _disposed;
|
||||
private readonly List<PersonLogEvent> _dayLog = [];
|
||||
|
||||
internal School(int id, string name, DateTime startDate, DefCatalog? catalog, MapLayout? map)
|
||||
{
|
||||
@@ -114,6 +115,15 @@ public sealed class School : IDisposable
|
||||
|
||||
public int PendingDecisionCount => DecisionQueue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Today's history for phase 36. Cleared at six in the morning. Not written to disk.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PersonLogEvent> DayLog => _dayLog;
|
||||
|
||||
internal void ResetDayLog() => _dayLog.Clear();
|
||||
|
||||
internal void AppendDayLog(PersonLogEvent row) => _dayLog.Add(row);
|
||||
|
||||
public void QueueDecision(string personId)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -212,6 +222,7 @@ public sealed class School : IDisposable
|
||||
PlanDay = null;
|
||||
LastDecisionSlot = null;
|
||||
NeedDecay.Apply(World, Catalog, (next.Value - before).TotalMinutes);
|
||||
peopleChanged |= ApparelWear.Apply(this, gameMinutes: 0, before);
|
||||
SyncWeather(force: true);
|
||||
return new SkipEmptyResult(SkipEmptyError.None, next.Value, peopleChanged);
|
||||
}
|
||||
@@ -251,8 +262,8 @@ public sealed class School : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Runs one fixed step of the school: calendar, yearly intake, applicant refresh, presence, actions, then need decay.</summary>
|
||||
/// <returns><see langword="true"/> when the roster or the applicant pool changed this step.</returns>
|
||||
/// <summary>Runs one fixed step of the school: calendar, yearly intake, applicant refresh, presence, actions, need decay, then apparel wear.</summary>
|
||||
/// <returns><see langword="true"/> when the roster, applicant pool or wardrobe changed this step.</returns>
|
||||
public bool Tick(double deltaTime, double gameMinutesPerRealSecond)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
@@ -285,6 +296,7 @@ public sealed class School : IDisposable
|
||||
PresenceSystem.EnqueueNewlyUrgent(this, below);
|
||||
PresenceSystem.DrainDecisions(this);
|
||||
LessonLearningSystem.Apply(this, gameMinutes);
|
||||
peopleChanged |= ApparelWear.Apply(this, gameMinutes, before);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user