Merge branch 'main' into phase/32-weather-warmth
This commit is contained in:
@@ -26,6 +26,7 @@ describe('t', () => {
|
||||
.toBe('Not enough money: 8 000 of 10 000 is committed, 2 000 free, 12 000 needed.');
|
||||
expect(t('staffUncoveredShort', { label: 'Начальные классы', min: 1, max: 4, n: 3 }))
|
||||
.toBe('Начальные классы (1–4) — 3 short');
|
||||
expect(t('peopleCarryMass', { held: '1.2', cap: '14' })).toBe('1.2 / 14 kg');
|
||||
expect(t('mapOccupancy', { name: 'Кабинет 204', activity: 'Математика · 5Б' }))
|
||||
.toBe('Кабинет 204 (Математика · 5Б)');
|
||||
expect(t('weatherPrecip', { temp: '\u22125', precip: t('precipSnow') })).toBe('\u22125 °C, snow');
|
||||
|
||||
@@ -128,6 +128,9 @@ const ru = {
|
||||
peopleSkills: 'Навыки',
|
||||
peopleTraits: 'Черты',
|
||||
peopleNeeds: 'Нужды',
|
||||
peopleApparel: 'Одежда',
|
||||
peopleCarry: 'Ноша',
|
||||
peopleCarryMass: '{held} / {cap} кг',
|
||||
peopleFamily: 'Семья',
|
||||
peopleParents: 'Родители',
|
||||
peopleChildren: 'Дети',
|
||||
@@ -349,6 +352,9 @@ const en: Messages = {
|
||||
peopleSkills: 'Skills',
|
||||
peopleTraits: 'Traits',
|
||||
peopleNeeds: 'Needs',
|
||||
peopleApparel: 'Clothes',
|
||||
peopleCarry: 'Carried',
|
||||
peopleCarryMass: '{held} / {cap} kg',
|
||||
peopleFamily: 'Family',
|
||||
peopleParents: 'Parents',
|
||||
peopleChildren: 'Children',
|
||||
|
||||
@@ -291,6 +291,28 @@ export interface PersonCard {
|
||||
readonly siblings: readonly PersonRel[];
|
||||
readonly partners: readonly PersonRel[];
|
||||
};
|
||||
readonly worn: readonly WornItem[];
|
||||
readonly carried: readonly CarriedItem[];
|
||||
readonly carryMass: number;
|
||||
readonly carryCapacity: number;
|
||||
}
|
||||
|
||||
export interface WornItem {
|
||||
readonly defName: string;
|
||||
readonly label: string;
|
||||
readonly color: string | null;
|
||||
readonly colorLabel: string | null;
|
||||
readonly layers: readonly DefLabel[];
|
||||
}
|
||||
|
||||
export interface CarriedItem {
|
||||
readonly defName: string;
|
||||
readonly label: string;
|
||||
readonly color: string | null;
|
||||
readonly colorLabel: string | null;
|
||||
readonly subject: string | null;
|
||||
readonly subjectLabel: string | null;
|
||||
readonly mass: number;
|
||||
}
|
||||
|
||||
export async function fetchPeople(schoolId: number, query: PeopleQuery, lang: string): Promise<PeoplePage> {
|
||||
|
||||
@@ -650,6 +650,11 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__carry-mass {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__pair {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -55,6 +55,10 @@ function personCard(): PersonCard {
|
||||
activity: null,
|
||||
activityLabel: null,
|
||||
family: { parents: [], children: [], siblings: [], partners: [] },
|
||||
worn: [],
|
||||
carried: [],
|
||||
carryMass: 0,
|
||||
carryCapacity: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { PersonCard } from '../net/api.ts';
|
||||
import { getLocale, setLocale } from '../i18n/locale.ts';
|
||||
import { t } from '../i18n/strings.ts';
|
||||
import { renderPersonCard } from './personCard.ts';
|
||||
|
||||
const initial = getLocale();
|
||||
|
||||
afterEach(() => setLocale(initial));
|
||||
|
||||
function card(overrides: Partial<PersonCard> = {}): PersonCard {
|
||||
return {
|
||||
id: 'f0.c0',
|
||||
fullName: 'Иванова Мария',
|
||||
surname: 'Иванова',
|
||||
given: 'Мария',
|
||||
patronymic: '',
|
||||
female: true,
|
||||
age: 12,
|
||||
birthDate: '2000-03-14',
|
||||
roles: ['student'],
|
||||
classYear: 5,
|
||||
classLetter: 'А',
|
||||
classId: 'class-1',
|
||||
position: null,
|
||||
positionLabel: null,
|
||||
body: [],
|
||||
skills: [],
|
||||
traits: [],
|
||||
needs: [],
|
||||
activity: null,
|
||||
activityLabel: null,
|
||||
family: { parents: [], children: [], siblings: [], partners: [] },
|
||||
worn: [
|
||||
{
|
||||
defName: 'Shirt',
|
||||
label: 'Рубашка',
|
||||
color: 'White',
|
||||
colorLabel: 'Белый',
|
||||
layers: [{ defName: 'Top', label: 'Верх' }],
|
||||
},
|
||||
],
|
||||
carried: [
|
||||
{
|
||||
defName: 'Textbook',
|
||||
label: 'Учебник',
|
||||
color: null,
|
||||
colorLabel: null,
|
||||
subject: 'Mathematics',
|
||||
subjectLabel: 'Математика',
|
||||
mass: 0.4,
|
||||
},
|
||||
],
|
||||
carryMass: 1.2,
|
||||
carryCapacity: 14,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('renderPersonCard', () => {
|
||||
it('shows worn layers with colour and carried mass in the same scroll', () => {
|
||||
setLocale('ru');
|
||||
const root = document.createElement('div');
|
||||
renderPersonCard(root, card(), () => {});
|
||||
|
||||
expect(root.textContent).toContain(t('peopleApparel'));
|
||||
expect(root.textContent).toContain('Верх');
|
||||
expect(root.textContent).toContain('Рубашка · Белый');
|
||||
expect(root.textContent).toContain(t('peopleCarry'));
|
||||
expect(root.textContent).toContain(t('peopleCarryMass', { held: '1.2', cap: '14' }));
|
||||
expect(root.textContent).toContain('Учебник · Математика');
|
||||
expect(root.querySelector('.people__tabs')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,8 @@ export function renderPersonCard(
|
||||
appendPairs(parent, t('peopleSkills'), card.skills);
|
||||
appendTags(parent, t('peopleTraits'), card.traits.map((row) => row.label));
|
||||
appendNeeds(parent, t('peopleNeeds'), card.needs);
|
||||
appendApparel(parent, card.worn);
|
||||
appendCarry(parent, card);
|
||||
|
||||
const family = section(t('peopleFamily'));
|
||||
appendRelatives(family, t('peopleParents'), card.family.parents, onRelative);
|
||||
@@ -112,6 +114,58 @@ function appendPairs(
|
||||
parent.append(section(title), grid);
|
||||
}
|
||||
|
||||
function appendApparel(parent: HTMLElement, rows: PersonCard['worn']): void {
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = el('dl', { class: 'people__pairs' });
|
||||
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(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__pair' },
|
||||
el('dt', { text: layers.length > 0 ? layers : row.label }),
|
||||
el('dd', { text: value }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
parent.append(section(t('peopleApparel')), grid);
|
||||
}
|
||||
|
||||
function appendCarry(parent: HTMLElement, card: PersonCard): void {
|
||||
const mass = el('p', {
|
||||
class: 'people__carry-mass',
|
||||
text: t('peopleCarryMass', { held: formatMass(card.carryMass), cap: formatMass(card.carryCapacity) }),
|
||||
});
|
||||
const grid = el('dl', { class: 'people__pairs' });
|
||||
for (const row of card.carried) {
|
||||
const extra = row.subjectLabel ?? row.colorLabel;
|
||||
const value = extra !== null && extra.length > 0 ? `${row.label} · ${extra}` : row.label;
|
||||
grid.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__pair' },
|
||||
el('dt', { text: value }),
|
||||
el('dd', { text: formatMass(row.mass) }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
parent.append(section(t('peopleCarry')), mass);
|
||||
if (card.carried.length > 0) {
|
||||
parent.append(grid);
|
||||
}
|
||||
}
|
||||
|
||||
function formatMass(value: number): string {
|
||||
return String(Math.round(value * 100) / 100);
|
||||
}
|
||||
|
||||
function appendTags(parent: HTMLElement, title: string, values: readonly string[]): void {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -609,6 +609,11 @@ public sealed class CatalogLoader
|
||||
throw new ContentLoadException($"ThingDef '{thing.DefName}' has unknown skirtLength '{thing.SkirtLength}'.");
|
||||
}
|
||||
|
||||
if (thing.CarryChance is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"ThingDef '{thing.DefName}' carryChance must be 0–1.");
|
||||
}
|
||||
|
||||
var layers = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var layer in thing.Layers)
|
||||
{
|
||||
|
||||
@@ -104,6 +104,12 @@ public sealed class ThingDef : Def
|
||||
|
||||
/// <summary>PE kit. Worn for that lesson; not a second wardrobe of everyday clothes.</summary>
|
||||
public bool Pe { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Chance the dress generator issues this portable non-apparel item. 0 — not rolled (textbooks
|
||||
/// are granted per subject instead). Apparel ignores this and fills layers.
|
||||
/// </summary>
|
||||
public float CarryChance { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PositionDef : Def;
|
||||
|
||||
@@ -518,6 +518,19 @@ internal static class PeopleDefValidator
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' goal weights cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.CarryMassBase < 0f
|
||||
|| behavior.CarryMassPerStrength < 0f
|
||||
|| behavior.CarryMassPerEndurance < 0f
|
||||
|| behavior.CarryMassPerHauling < 0f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' carry mass cannot be negative.");
|
||||
}
|
||||
|
||||
if (behavior.OptionalApparelChance is < 0f or > 1f)
|
||||
{
|
||||
throw new ContentLoadException($"BehaviorDef '{behavior.DefName}' optionalApparelChance must be 0–1.");
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -256,6 +256,18 @@ public sealed class BehaviorDef : Def
|
||||
/// pulls anybody out of a lesson.
|
||||
/// </summary>
|
||||
public float LunchWeight { get; init; } = 6f;
|
||||
|
||||
/// <summary>Kilograms a person can carry at strength/endurance/hauling 0, before the per-skill terms.</summary>
|
||||
public float CarryMassBase { get; init; } = 5f;
|
||||
|
||||
public float CarryMassPerStrength { get; init; } = 0.08f;
|
||||
|
||||
public float CarryMassPerEndurance { get; init; } = 0.04f;
|
||||
|
||||
public float CarryMassPerHauling { get; init; } = 0.08f;
|
||||
|
||||
/// <summary>Chance each optional layer (sweater, coat, hat, accessory) is worn at generation.</summary>
|
||||
public float OptionalApparelChance { get; init; } = 0.4f;
|
||||
}
|
||||
|
||||
public enum BodyAttributeKind
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>
|
||||
/// How much a person may carry in the bag. The curve lives on <see cref="BehaviorDef"/>;
|
||||
/// these fallbacks match vanilla so a pack without the new fields still dresses people.
|
||||
/// Worn mass does not count in this slice, and overload does not slow walking.
|
||||
/// </summary>
|
||||
public static class CarryMass
|
||||
{
|
||||
public const string Strength = "Strength";
|
||||
public const string Endurance = "Endurance";
|
||||
public const string Hauling = "Hauling";
|
||||
|
||||
public const float DefaultBase = 5f;
|
||||
public const float DefaultPerStrength = 0.08f;
|
||||
public const float DefaultPerEndurance = 0.04f;
|
||||
public const float DefaultPerHauling = 0.08f;
|
||||
|
||||
public static float Capacity(DefCatalog catalog, IReadOnlyDictionary<string, int> skills) =>
|
||||
Capacity(catalog, Skill(skills, Strength), Skill(skills, Endurance), Skill(skills, Hauling));
|
||||
|
||||
public static float Capacity(DefCatalog catalog, IReadOnlyDictionary<string, float> skills) =>
|
||||
Capacity(
|
||||
catalog,
|
||||
Skill(skills, Strength),
|
||||
Skill(skills, Endurance),
|
||||
Skill(skills, Hauling));
|
||||
|
||||
public static float Capacity(DefCatalog catalog, float strength, float endurance, float hauling)
|
||||
{
|
||||
var rules = catalog.BehaviorRules;
|
||||
var value = (rules?.CarryMassBase ?? DefaultBase)
|
||||
+ (strength * (rules?.CarryMassPerStrength ?? DefaultPerStrength))
|
||||
+ (endurance * (rules?.CarryMassPerEndurance ?? DefaultPerEndurance))
|
||||
+ (hauling * (rules?.CarryMassPerHauling ?? DefaultPerHauling));
|
||||
return MathF.Round(MathF.Max(value, 0f), 2);
|
||||
}
|
||||
|
||||
public static float Held(DefCatalog catalog, IEnumerable<InventoryItem> items)
|
||||
{
|
||||
var sum = 0f;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (catalog.Things.TryGetValue(item.Def, out var def))
|
||||
{
|
||||
sum += def.Mass;
|
||||
}
|
||||
}
|
||||
|
||||
return MathF.Round(sum, 2);
|
||||
}
|
||||
|
||||
private static float Skill(IReadOnlyDictionary<string, int> skills, string name) =>
|
||||
skills.TryGetValue(name, out var value) ? value : 0f;
|
||||
|
||||
private static float Skill(IReadOnlyDictionary<string, float> skills, string name) =>
|
||||
skills.TryGetValue(name, out var value) ? value : 0f;
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>
|
||||
/// Fills a person's wardrobe from a stream that does not touch looks or skills. Same person id
|
||||
/// and school seed always yield the same items, colours and places.
|
||||
/// </summary>
|
||||
public static class DressGenerator
|
||||
{
|
||||
public static Person Dress(DefCatalog catalog, Person person, int schoolSeed, int? pupilYear)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
|
||||
var age = person.AgeOn(RosterGenerator.DefaultAsOf);
|
||||
return Dress(catalog, person, schoolSeed, pupilYear, age);
|
||||
}
|
||||
|
||||
public static Person Dress(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int schoolSeed,
|
||||
int? pupilYear,
|
||||
int age)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
|
||||
var rng = new Random(Seed.Mix(schoolSeed, person.Id, dayNumber: 0, Seed.ApparelSalt));
|
||||
var items = new List<InventoryItem>();
|
||||
var occupied = new HashSet<string>(StringComparer.Ordinal);
|
||||
var chance = catalog.BehaviorRules?.OptionalApparelChance ?? 0.4f;
|
||||
|
||||
foreach (var layer in RequiredLayers)
|
||||
{
|
||||
TryWear(catalog, person, age, rng, layer, occupied, items);
|
||||
}
|
||||
|
||||
foreach (var layer in OptionalLayers)
|
||||
{
|
||||
if (rng.NextDouble() < chance)
|
||||
{
|
||||
TryWear(catalog, person, age, rng, layer, occupied, items);
|
||||
}
|
||||
}
|
||||
|
||||
if (!occupied.Contains(ApparelLayers.Outer))
|
||||
{
|
||||
TryStashOuter(catalog, person, age, rng, items);
|
||||
}
|
||||
|
||||
if (person.IsStudent && HasPhysicalEducation(catalog, pupilYear))
|
||||
{
|
||||
GrantPeKit(catalog, person, age, rng, items);
|
||||
}
|
||||
|
||||
var capacity = CarryMass.Capacity(catalog, person.Skills);
|
||||
var held = 0f;
|
||||
GrantChanceCarry(catalog, person, age, rng, items, ref held, capacity);
|
||||
if (person.IsStudent && pupilYear is { } year)
|
||||
{
|
||||
GrantTextbooks(catalog, year, rng, items, ref held, capacity);
|
||||
}
|
||||
|
||||
return person with { Items = items };
|
||||
}
|
||||
|
||||
public static Person EnsureDressed(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int schoolSeed,
|
||||
int? pupilYear,
|
||||
int age)
|
||||
{
|
||||
if (person.Items.Count > 0)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
return Dress(catalog, person, schoolSeed, pupilYear, age);
|
||||
}
|
||||
|
||||
public static Roster EnsureRoster(DefCatalog catalog, Roster roster, int schoolSeed, DateTime asOf)
|
||||
{
|
||||
var years = roster.Classes.ToDictionary(
|
||||
schoolClass => schoolClass.Id,
|
||||
schoolClass => schoolClass.Year,
|
||||
StringComparer.Ordinal);
|
||||
var people = new Person[roster.People.Count];
|
||||
for (var i = 0; i < roster.People.Count; i++)
|
||||
{
|
||||
var person = roster.People[i];
|
||||
int? year = person.ClassId is { } classId && years.TryGetValue(classId, out var value)
|
||||
? value
|
||||
: null;
|
||||
people[i] = EnsureDressed(catalog, person, schoolSeed, year, person.AgeOn(asOf));
|
||||
}
|
||||
|
||||
return new Roster(people, roster.Families, roster.Classes);
|
||||
}
|
||||
|
||||
public static ApplicantPool EnsurePool(
|
||||
DefCatalog catalog,
|
||||
ApplicantPool pool,
|
||||
Roster roster,
|
||||
int schoolSeed,
|
||||
DateTime asOf)
|
||||
{
|
||||
var rosterPeople = roster.People.ToDictionary(person => person.Id, StringComparer.Ordinal);
|
||||
var applicants = new Applicant[pool.Applicants.Count];
|
||||
for (var i = 0; i < pool.Applicants.Count; i++)
|
||||
{
|
||||
var applicant = pool.Applicants[i];
|
||||
if (rosterPeople.TryGetValue(applicant.Person.Id, out var member))
|
||||
{
|
||||
applicants[i] = applicant with { Person = member };
|
||||
continue;
|
||||
}
|
||||
|
||||
applicants[i] = applicant with
|
||||
{
|
||||
Person = EnsureDressed(
|
||||
catalog,
|
||||
applicant.Person,
|
||||
schoolSeed,
|
||||
pupilYear: null,
|
||||
applicant.Person.AgeOn(asOf)),
|
||||
};
|
||||
}
|
||||
|
||||
return pool with { Applicants = applicants };
|
||||
}
|
||||
|
||||
public static bool NeedsDressing(Roster roster, ApplicantPool? pool)
|
||||
{
|
||||
foreach (var person in roster.People)
|
||||
{
|
||||
if (person.Items.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pool is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var applicant in pool.Applicants)
|
||||
{
|
||||
if (applicant.Person.Items.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds textbooks for subjects the pupil just became old enough for. Existing clothes stay.
|
||||
/// </summary>
|
||||
public static Person EnsureYearTextbooks(DefCatalog catalog, Person person, int year)
|
||||
{
|
||||
if (!person.IsStudent)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
var items = person.Items.ToList();
|
||||
var held = CarryMass.Held(catalog, items);
|
||||
var capacity = CarryMass.Capacity(catalog, person.Skills);
|
||||
var rng = new Random(0);
|
||||
GrantTextbooks(catalog, year, rng, items, ref held, capacity);
|
||||
return person with { Items = items };
|
||||
}
|
||||
|
||||
private static readonly string[] RequiredLayers =
|
||||
[ApparelLayers.Underwear, ApparelLayers.Socks, ApparelLayers.Bottom, ApparelLayers.Top, ApparelLayers.Shoes];
|
||||
|
||||
private static readonly string[] OptionalLayers =
|
||||
[ApparelLayers.OverTop, ApparelLayers.Outer, ApparelLayers.Head, ApparelLayers.Accessory];
|
||||
|
||||
private static void TryWear(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int age,
|
||||
Random rng,
|
||||
string layer,
|
||||
HashSet<string> occupied,
|
||||
List<InventoryItem> items)
|
||||
{
|
||||
if (occupied.Contains(layer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var candidates = Apparel(catalog, person.Female, age, everyday: true)
|
||||
.Where(def => def.Layers.Contains(layer, StringComparer.Ordinal)
|
||||
&& def.Layers.All(candidate => !occupied.Contains(candidate)))
|
||||
.ToList();
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pick = candidates[rng.Next(candidates.Count)];
|
||||
var color = PickColor(catalog, pick, rng, worn: true);
|
||||
items.Add(new InventoryItem(pick.DefName, color, Condition: 1, ItemLocations.Worn));
|
||||
foreach (var taken in pick.Layers)
|
||||
{
|
||||
occupied.Add(taken);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryStashOuter(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int age,
|
||||
Random rng,
|
||||
List<InventoryItem> items)
|
||||
{
|
||||
var candidates = Apparel(catalog, person.Female, age, everyday: true)
|
||||
.Where(def => def.Layers.Contains(ApparelLayers.Outer, StringComparer.Ordinal))
|
||||
.ToList();
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pick = candidates[rng.Next(candidates.Count)];
|
||||
var color = PickColor(catalog, pick, rng, worn: false);
|
||||
items.Add(new InventoryItem(pick.DefName, color, Condition: 1, ItemLocations.Home));
|
||||
}
|
||||
|
||||
private static void GrantPeKit(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int age,
|
||||
Random rng,
|
||||
List<InventoryItem> items)
|
||||
{
|
||||
foreach (var def in Apparel(catalog, person.Female, age, everyday: false).Where(candidate => candidate.Pe))
|
||||
{
|
||||
var color = PickColor(catalog, def, rng, worn: false);
|
||||
items.Add(new InventoryItem(def.DefName, color, Condition: 1, ItemLocations.Home));
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantChanceCarry(
|
||||
DefCatalog catalog,
|
||||
Person person,
|
||||
int age,
|
||||
Random rng,
|
||||
List<InventoryItem> items,
|
||||
ref float held,
|
||||
float capacity)
|
||||
{
|
||||
foreach (var def in catalog.Things.Values
|
||||
.Where(thing => !thing.Abstract
|
||||
&& thing.Portable
|
||||
&& thing.Layers.Count == 0
|
||||
&& thing.CarryChance > 0)
|
||||
.OrderBy(thing => thing.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (!AgeFits(def, age) || !SexFits(def, person.Female))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rng.NextDouble() >= def.CarryChance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var color = PickColor(catalog, def, rng, worn: false);
|
||||
Place(catalog, items, def, color, subject: null, ref held, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantTextbooks(
|
||||
DefCatalog catalog,
|
||||
int year,
|
||||
Random rng,
|
||||
List<InventoryItem> items,
|
||||
ref float held,
|
||||
float capacity)
|
||||
{
|
||||
var textbook = catalog.Things.Values
|
||||
.Where(thing => !thing.Abstract
|
||||
&& thing.Portable
|
||||
&& thing.Layers.Count == 0
|
||||
&& thing.CarryChance <= 0)
|
||||
.OrderBy(thing => thing.DefName, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
if (textbook is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var have = items
|
||||
.Where(item => item.Def.Equals(textbook.DefName, StringComparison.Ordinal) && item.Subject is not null)
|
||||
.Select(item => item.Subject!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var subject in catalog.Subjects.Values
|
||||
.Where(def => !def.Abstract && year >= def.Grades.Min && year <= def.Grades.Max)
|
||||
.OrderBy(def => def.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (!have.Add(subject.DefName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var color = PickColor(catalog, textbook, rng, worn: false);
|
||||
Place(catalog, items, textbook, color, subject.DefName, ref held, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Place(
|
||||
DefCatalog catalog,
|
||||
List<InventoryItem> items,
|
||||
ThingDef def,
|
||||
string? color,
|
||||
string? subject,
|
||||
ref float held,
|
||||
float capacity)
|
||||
{
|
||||
var location = held + def.Mass <= capacity ? ItemLocations.Bag : ItemLocations.Home;
|
||||
items.Add(new InventoryItem(def.DefName, color, Condition: 1, location, subject));
|
||||
if (location.Equals(ItemLocations.Bag, StringComparison.Ordinal))
|
||||
{
|
||||
held = MathF.Round(held + def.Mass, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ThingDef> Apparel(DefCatalog catalog, bool female, int age, bool everyday)
|
||||
{
|
||||
foreach (var def in catalog.Things.Values.OrderBy(thing => thing.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
if (def.Abstract || def.Layers.Count == 0 || !AgeFits(def, age) || !SexFits(def, female))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (everyday)
|
||||
{
|
||||
if (def.Pe)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (def.SkirtLength is not null
|
||||
&& def.SkirtLength.Equals(SkirtLengths.Short, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
yield return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? PickColor(DefCatalog catalog, ThingDef def, Random rng, bool worn)
|
||||
{
|
||||
if (def.Colors.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pool = def.Colors.ToList();
|
||||
if (worn)
|
||||
{
|
||||
var quiet = def.Colors
|
||||
.Where(id => catalog.Colors.TryGetValue(id, out var color)
|
||||
&& !color.Tags.Contains(ColorTags.Bright, StringComparer.Ordinal))
|
||||
.ToList();
|
||||
if (quiet.Count > 0)
|
||||
{
|
||||
pool = quiet;
|
||||
}
|
||||
}
|
||||
|
||||
pool.Sort(StringComparer.Ordinal);
|
||||
return pool[rng.Next(pool.Count)];
|
||||
}
|
||||
|
||||
private static bool AgeFits(ThingDef def, int age) =>
|
||||
def.Age is not { } range || (age >= range.Min && age <= range.Max);
|
||||
|
||||
private static bool SexFits(ThingDef def, bool female)
|
||||
{
|
||||
if (def.Sex is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var want = female ? "female" : "male";
|
||||
return def.Sex.Equals(want, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool HasPhysicalEducation(DefCatalog catalog, int? pupilYear)
|
||||
{
|
||||
if (pupilYear is not { } year)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return catalog.Subjects.Values.Any(subject =>
|
||||
!subject.Abstract
|
||||
&& subject.DefName.Equals("PhysicalEducation", StringComparison.Ordinal)
|
||||
&& year >= subject.Grades.Min
|
||||
&& year <= subject.Grades.Max);
|
||||
}
|
||||
}
|
||||
@@ -78,40 +78,50 @@ internal static class FamilyFactory
|
||||
{
|
||||
parentIds.Add(fatherId);
|
||||
members.Add(
|
||||
RollAdult(
|
||||
DressGenerator.Dress(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
fatherId,
|
||||
familyId,
|
||||
female: false,
|
||||
fatherBirth,
|
||||
asOf,
|
||||
surname,
|
||||
fatherGiven,
|
||||
NameGrammar.Patronymic(fatherPatronymicSource.Form, female: false, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage));
|
||||
RollAdult(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
fatherId,
|
||||
familyId,
|
||||
female: false,
|
||||
fatherBirth,
|
||||
asOf,
|
||||
surname,
|
||||
fatherGiven,
|
||||
NameGrammar.Patronymic(fatherPatronymicSource.Form, female: false, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage),
|
||||
schoolSeed,
|
||||
pupilYear: null,
|
||||
SchoolYears.AgeYears(fatherBirth, asOf)));
|
||||
}
|
||||
|
||||
if (hasMother)
|
||||
{
|
||||
parentIds.Add(motherId);
|
||||
members.Add(
|
||||
RollAdult(
|
||||
DressGenerator.Dress(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
motherId,
|
||||
familyId,
|
||||
female: true,
|
||||
motherBirth,
|
||||
asOf,
|
||||
surname,
|
||||
motherGiven,
|
||||
NameGrammar.Patronymic(motherPatronymicSource.Form, female: true, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage));
|
||||
RollAdult(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
motherId,
|
||||
familyId,
|
||||
female: true,
|
||||
motherBirth,
|
||||
asOf,
|
||||
surname,
|
||||
motherGiven,
|
||||
NameGrammar.Patronymic(motherPatronymicSource.Form, female: true, names.PatronymicRule),
|
||||
isParent: childDrafts.Length > 0,
|
||||
nativeLanguage),
|
||||
schoolSeed,
|
||||
pupilYear: null,
|
||||
SchoolYears.AgeYears(motherBirth, asOf)));
|
||||
}
|
||||
|
||||
var childIds = new List<string>(childDrafts.Length);
|
||||
@@ -121,17 +131,22 @@ internal static class FamilyFactory
|
||||
var id = $"{familyId}.c{i}";
|
||||
childIds.Add(id);
|
||||
members.Add(
|
||||
RollChild(
|
||||
DressGenerator.Dress(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
id,
|
||||
familyId,
|
||||
draft,
|
||||
asOf,
|
||||
surname,
|
||||
fatherGiven.Form,
|
||||
nativeLanguage));
|
||||
RollChild(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
id,
|
||||
familyId,
|
||||
draft,
|
||||
asOf,
|
||||
surname,
|
||||
fatherGiven.Form,
|
||||
nativeLanguage),
|
||||
schoolSeed,
|
||||
draft.Seat.Year,
|
||||
SchoolYears.AgeYears(draft.Birth, asOf)));
|
||||
}
|
||||
|
||||
var family = new Family(familyId, parentIds, childIds, childIds.Count, fatherGiven.Form, surname.Male);
|
||||
@@ -151,6 +166,7 @@ internal static class FamilyFactory
|
||||
DateTime yearStart,
|
||||
DateTime asOf,
|
||||
int childIndex,
|
||||
int schoolSeed,
|
||||
string? nativeLanguage = null)
|
||||
{
|
||||
var fatherGiven = FatherGivenOf(family, members);
|
||||
@@ -184,23 +200,28 @@ internal static class FamilyFactory
|
||||
surnameCases,
|
||||
PatronymicTable(patronymic, female));
|
||||
|
||||
return new Person
|
||||
{
|
||||
Id = $"{family.Id}.c{childIndex}",
|
||||
FamilyId = family.Id,
|
||||
Female = female,
|
||||
BirthDate = DateTime.SpecifyKind(birth, DateTimeKind.Utc),
|
||||
Name = name,
|
||||
IsStudent = true,
|
||||
IsStaff = false,
|
||||
IsParent = false,
|
||||
ClassId = seat.ClassId,
|
||||
Numbers = numbers,
|
||||
Choices = choices,
|
||||
Skills = skills,
|
||||
Traits = traits,
|
||||
Needs = needs,
|
||||
};
|
||||
return DressGenerator.Dress(
|
||||
catalog,
|
||||
new Person
|
||||
{
|
||||
Id = $"{family.Id}.c{childIndex}",
|
||||
FamilyId = family.Id,
|
||||
Female = female,
|
||||
BirthDate = DateTime.SpecifyKind(birth, DateTimeKind.Utc),
|
||||
Name = name,
|
||||
IsStudent = true,
|
||||
IsStaff = false,
|
||||
IsParent = false,
|
||||
ClassId = seat.ClassId,
|
||||
Numbers = numbers,
|
||||
Choices = choices,
|
||||
Skills = skills,
|
||||
Traits = traits,
|
||||
Needs = needs,
|
||||
},
|
||||
schoolSeed,
|
||||
seat.Year,
|
||||
age);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -226,20 +247,25 @@ internal static class FamilyFactory
|
||||
var birth = asOf.AddYears(-(24 + rng.Next(38))).AddDays(-rng.Next(365));
|
||||
|
||||
var id = $"{familyId}.p0";
|
||||
var person = RollAdult(
|
||||
var person = DressGenerator.Dress(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
id,
|
||||
familyId,
|
||||
female,
|
||||
birth,
|
||||
asOf,
|
||||
surname,
|
||||
given,
|
||||
NameGrammar.Patronymic(patronymicSource.Form, female, names.PatronymicRule),
|
||||
isParent: false,
|
||||
nativeLanguage);
|
||||
RollAdult(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
id,
|
||||
familyId,
|
||||
female,
|
||||
birth,
|
||||
asOf,
|
||||
surname,
|
||||
given,
|
||||
NameGrammar.Patronymic(patronymicSource.Form, female, names.PatronymicRule),
|
||||
isParent: false,
|
||||
nativeLanguage),
|
||||
schoolSeed,
|
||||
pupilYear: null,
|
||||
SchoolYears.AgeYears(birth, asOf));
|
||||
|
||||
return (new Family(familyId, [id], [], NextChild: 0, FatherGiven: string.Empty, Surname: surname.Male), person);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>Where an instance lives. One place at a time — worn, bag, locker or home.</summary>
|
||||
public static class ItemLocations
|
||||
{
|
||||
public const string Worn = "worn";
|
||||
public const string Bag = "bag";
|
||||
public const string Locker = "locker";
|
||||
public const string Home = "home";
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public sealed record InventoryItem(
|
||||
string Def,
|
||||
string? Color,
|
||||
float Condition,
|
||||
string Location,
|
||||
string? Subject = null);
|
||||
@@ -46,6 +46,9 @@ public sealed record Person
|
||||
/// <summary>Hourly rate frozen at hire. Null until the person is staff.</summary>
|
||||
public float? HourlyWageAsk { get; init; }
|
||||
|
||||
/// <summary>Worn, bag, locker and home. Empty on rosters written before clothes existed.</summary>
|
||||
public IReadOnlyList<InventoryItem> Items { get; init; } = [];
|
||||
|
||||
public int AgeOn(DateTime asOf) => SchoolYears.AgeYears(BirthDate, asOf);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public static class Seed
|
||||
public const int SkillGrantSalt = 8;
|
||||
public const int NativeLanguageSalt = 9;
|
||||
public const int ClimatePresetSalt = 10;
|
||||
public const int ApparelSalt = 11;
|
||||
|
||||
/// <summary>A stream that belongs to the school rather than to one family.</summary>
|
||||
public static int ForSchool(int schoolSeed, int salt) => Mix(schoolSeed, familyIndex: -1, salt);
|
||||
|
||||
@@ -175,19 +175,22 @@ public static class YearlyIntake
|
||||
}
|
||||
|
||||
var rng = new Random(Seed.Mix(schoolSeed, person.Id, when.Year, Seed.SkillGrantSalt));
|
||||
people[id] = person with
|
||||
{
|
||||
Skills = PersonSampler.EnsurePupilYear(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
person.AgeOn(when),
|
||||
person.Choices,
|
||||
person.Traits,
|
||||
person.Skills,
|
||||
year,
|
||||
nativeLanguage),
|
||||
};
|
||||
people[id] = DressGenerator.EnsureYearTextbooks(
|
||||
catalog,
|
||||
person with
|
||||
{
|
||||
Skills = PersonSampler.EnsurePupilYear(
|
||||
catalog,
|
||||
names,
|
||||
rng,
|
||||
person.AgeOn(when),
|
||||
person.Choices,
|
||||
person.Traits,
|
||||
person.Skills,
|
||||
year,
|
||||
nativeLanguage),
|
||||
},
|
||||
year);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +239,7 @@ public static class YearlyIntake
|
||||
|
||||
var childIndex = family.NextChildIndex;
|
||||
var rng = new Random(Seed.Mix(schoolSeed, index, Seed.IntakeSalt + yearStart.Year * 10 + childIndex));
|
||||
var child = FamilyFactory.AddChild(catalog, names, rng, family, members, seats[cursor], yearStart, asOf, childIndex, nativeLanguage);
|
||||
var child = FamilyFactory.AddChild(catalog, names, rng, family, members, seats[cursor], yearStart, asOf, childIndex, schoolSeed, nativeLanguage);
|
||||
people[child.Id] = child;
|
||||
var familyAt = families.FindIndex(candidate => candidate.Id.Equals(family.Id, StringComparison.Ordinal));
|
||||
families[familyAt] = family with
|
||||
|
||||
@@ -61,7 +61,27 @@ internal sealed record PersonCardResponse(
|
||||
IReadOnlyList<NeedStatResponse> Needs,
|
||||
string? Activity,
|
||||
string? ActivityLabel,
|
||||
PersonFamilyResponse Family);
|
||||
PersonFamilyResponse Family,
|
||||
IReadOnlyList<WornItemResponse> Worn,
|
||||
IReadOnlyList<CarriedItemResponse> Carried,
|
||||
float CarryMass,
|
||||
float CarryCapacity);
|
||||
|
||||
internal sealed record WornItemResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
string? Color,
|
||||
string? ColorLabel,
|
||||
IReadOnlyList<DefLabelResponse> Layers);
|
||||
|
||||
internal sealed record CarriedItemResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
string? Color,
|
||||
string? ColorLabel,
|
||||
string? Subject,
|
||||
string? SubjectLabel,
|
||||
float Mass);
|
||||
|
||||
internal sealed record LabeledStatResponse(string Id, string Label, string Value);
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ internal static class PersonCardReader
|
||||
Needs(needs, catalog, locale),
|
||||
activityId,
|
||||
activityLabel,
|
||||
Family(roster, person));
|
||||
Family(roster, person),
|
||||
Worn(person, catalog, locale),
|
||||
Carried(person, catalog, locale),
|
||||
catalog is null ? 0f : CarryMass.Held(catalog, person.Items),
|
||||
Capacity(person, skills, catalog));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, float>? LiveNeeds(World world, string personId)
|
||||
@@ -260,6 +264,99 @@ internal static class PersonCardReader
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<WornItemResponse> Worn(Person person, DefCatalog? catalog, string locale)
|
||||
{
|
||||
var rows = new List<WornItemResponse>();
|
||||
foreach (var item in person.Items)
|
||||
{
|
||||
if (!item.Location.Equals(ItemLocations.Worn, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> layerIds = [];
|
||||
if (catalog is not null && catalog.Things.TryGetValue(item.Def, out var def))
|
||||
{
|
||||
layerIds = def.Layers;
|
||||
}
|
||||
|
||||
var layers = layerIds
|
||||
.Select(layer => new DefLabelResponse(layer, catalog?.Text(locale, layer) ?? layer))
|
||||
.ToArray();
|
||||
rows.Add(new WornItemResponse(
|
||||
item.Def,
|
||||
ThingLabel(catalog, locale, item.Def),
|
||||
item.Color,
|
||||
ColorLabel(catalog, locale, item.Color),
|
||||
layers));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CarriedItemResponse> Carried(Person person, DefCatalog? catalog, string locale)
|
||||
{
|
||||
var rows = new List<CarriedItemResponse>();
|
||||
foreach (var item in person.Items)
|
||||
{
|
||||
if (!item.Location.Equals(ItemLocations.Bag, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mass = catalog is not null && catalog.Things.TryGetValue(item.Def, out var def) ? def.Mass : 0f;
|
||||
string? subjectLabel = null;
|
||||
if (item.Subject is not null)
|
||||
{
|
||||
subjectLabel = catalog is not null && catalog.Subjects.TryGetValue(item.Subject, out var subject)
|
||||
? catalog.Label(locale, subject)
|
||||
: item.Subject;
|
||||
}
|
||||
|
||||
rows.Add(new CarriedItemResponse(
|
||||
item.Def,
|
||||
ThingLabel(catalog, locale, item.Def),
|
||||
item.Color,
|
||||
ColorLabel(catalog, locale, item.Color),
|
||||
item.Subject,
|
||||
subjectLabel,
|
||||
mass));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static string ThingLabel(DefCatalog? catalog, string locale, string defName)
|
||||
{
|
||||
if (catalog is not null && catalog.Things.TryGetValue(defName, out var def))
|
||||
{
|
||||
return catalog.Label(locale, def);
|
||||
}
|
||||
|
||||
return defName;
|
||||
}
|
||||
|
||||
private static string? ColorLabel(DefCatalog? catalog, string locale, string? color) =>
|
||||
color is null ? null : catalog?.Text(locale, color) ?? color;
|
||||
|
||||
private static float Capacity(
|
||||
Person person,
|
||||
IReadOnlyDictionary<string, float>? live,
|
||||
DefCatalog? catalog)
|
||||
{
|
||||
if (catalog is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (live is not null)
|
||||
{
|
||||
return CarryMass.Capacity(catalog, live);
|
||||
}
|
||||
|
||||
return CarryMass.Capacity(catalog, person.Skills);
|
||||
}
|
||||
|
||||
private static PersonFamilyResponse Family(Roster roster, Person person)
|
||||
{
|
||||
var family = roster.Families.FirstOrDefault(candidate => candidate.Id.Equals(person.FamilyId, StringComparison.Ordinal));
|
||||
|
||||
@@ -924,6 +924,13 @@ internal sealed class SchoolWorker
|
||||
}
|
||||
}
|
||||
|
||||
if (DressGenerator.NeedsDressing(roster, applicants))
|
||||
{
|
||||
roster = DressGenerator.EnsureRoster(catalog, roster, seed, school.Clock.Time);
|
||||
applicants = DressGenerator.EnsurePool(catalog, applicants, roster, seed, school.Clock.Time);
|
||||
generated = true;
|
||||
}
|
||||
|
||||
if (!RosterFit.Matches(roster, demand))
|
||||
{
|
||||
throw new SchoolContentUnavailableException(
|
||||
|
||||
@@ -21,4 +21,11 @@
|
||||
// beats walking on to the next room, below the lesson so it never pulls
|
||||
// anybody out of class. Raise it past dutyLessonWeight and they will.
|
||||
"lunchWeight": 6,
|
||||
// Bag limit: base kg + per point of Strength, Endurance and Hauling.
|
||||
"carryMassBase": 5,
|
||||
"carryMassPerStrength": 0.08,
|
||||
"carryMassPerEndurance": 0.04,
|
||||
"carryMassPerHauling": 0.08,
|
||||
// Sweater, coat, hat, accessory — each rolled separately at generation.
|
||||
"optionalApparelChance": 0.4,
|
||||
}
|
||||
|
||||
@@ -30,4 +30,10 @@
|
||||
{ "attribute": "Build", "value": "Athletic", "min": 40 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Hauling",
|
||||
"always": true,
|
||||
"range": { "min": 0, "max": 100 },
|
||||
"distribution": { "mean": 50, "stdDev": 12 },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{ "defName": "SchoolBag", "portable": true, "mass": 0.8 },
|
||||
{ "defName": "SchoolBag", "portable": true, "mass": 0.8, "age": { "min": 6, "max": 18 }, "carryChance": 1 },
|
||||
{ "defName": "Textbook", "portable": true, "mass": 0.4 },
|
||||
{ "defName": "Phone", "portable": true, "mass": 0.15, "colors": ["Black", "White", "Gray"] },
|
||||
{ "defName": "Bottle", "portable": true, "mass": 0.3, "colors": ["Blue", "Green", "White"] },
|
||||
{ "defName": "Phone", "portable": true, "mass": 0.15, "colors": ["Black", "White", "Gray"], "age": { "min": 11 }, "carryChance": 0.45 },
|
||||
{ "defName": "Bottle", "portable": true, "mass": 0.3, "colors": ["Blue", "Green", "White"], "age": { "min": 6 }, "carryChance": 0.7 },
|
||||
]
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"Agility": "Agility",
|
||||
"Strength": "Strength",
|
||||
"Endurance": "Endurance",
|
||||
"Hauling": "Hauling",
|
||||
"Diligent": "Diligent",
|
||||
"AbsentMinded": "Absent-minded",
|
||||
"Bully": "Bully",
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
"Agility": "Ловкость",
|
||||
"Strength": "Сила",
|
||||
"Endurance": "Выносливость",
|
||||
"Hauling": "Переноска",
|
||||
"Diligent": "Усидчивый",
|
||||
"AbsentMinded": "Рассеянный",
|
||||
"Bully": "Задира",
|
||||
|
||||
Reference in New Issue
Block a user