add shel
This commit is contained in:
@@ -9,8 +9,8 @@ import {
|
||||
toDateAndTimeInputs,
|
||||
} from './gameTime.ts';
|
||||
|
||||
// The default start of a new school: 3 April 2012, 06:00 — a Tuesday.
|
||||
const START = new Date(Date.UTC(2012, 3, 3, 6, 0, 0));
|
||||
// The default start of a new school: 31 March 2012, 06:00 — a Saturday.
|
||||
const START = new Date(Date.UTC(2012, 2, 31, 6, 0, 0));
|
||||
const initial = getLocale();
|
||||
|
||||
beforeEach(() => setLocale('ru'));
|
||||
@@ -22,25 +22,25 @@ describe('game time formatting', () => {
|
||||
});
|
||||
|
||||
it('shows the weekday of the game date', () => {
|
||||
expect(formatGameWeekday(START)).toBe('вторник');
|
||||
expect(formatGameWeekday(START)).toBe('суббота');
|
||||
});
|
||||
|
||||
it('shows the full date', () => {
|
||||
expect(formatGameDate(START)).toContain('2012');
|
||||
expect(formatGameDate(START)).toContain('апреля');
|
||||
expect(formatGameDate(START)).toContain('марта');
|
||||
});
|
||||
|
||||
it('shows a compact date and time for the school cards', () => {
|
||||
expect(formatGameDateTime(START)).toContain('03.04.2012');
|
||||
expect(formatGameDateTime(START)).toContain('31.03.2012');
|
||||
expect(formatGameDateTime(START)).toContain('06:00');
|
||||
});
|
||||
|
||||
it('formats the same instant in English when the locale is en', () => {
|
||||
setLocale('en');
|
||||
|
||||
expect(formatGameWeekday(START)).toBe('Tuesday');
|
||||
expect(formatGameDate(START)).toContain('April');
|
||||
expect(formatGameDateTime(START)).toContain('03/04/2012');
|
||||
expect(formatGameWeekday(START)).toBe('Saturday');
|
||||
expect(formatGameDate(START)).toContain('March');
|
||||
expect(formatGameDateTime(START)).toContain('31/03/2012');
|
||||
expect(formatGameTimeOfDay(START)).toBe('06:00');
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('game time formatting', () => {
|
||||
|
||||
describe('date inputs', () => {
|
||||
it('splits an instant into the date and time input values', () => {
|
||||
expect(toDateAndTimeInputs(START)).toEqual({ date: '2012-04-03', time: '06:00' });
|
||||
expect(toDateAndTimeInputs(START)).toEqual({ date: '2012-03-31', time: '06:00' });
|
||||
});
|
||||
|
||||
it('rebuilds the same instant from those values', () => {
|
||||
|
||||
@@ -65,6 +65,7 @@ const ru = {
|
||||
removeRoom: 'Удалить комнату',
|
||||
slotEmpty: '— пусто —',
|
||||
slotCount: 'Количество',
|
||||
editorSeats: 'Мест',
|
||||
|
||||
backToMenu: '← В главное меню',
|
||||
pause: 'Пауза',
|
||||
@@ -193,6 +194,7 @@ const en: Messages = {
|
||||
removeRoom: 'Remove room',
|
||||
slotEmpty: '— empty —',
|
||||
slotCount: 'Count',
|
||||
editorSeats: 'Seats',
|
||||
|
||||
backToMenu: '← Main menu',
|
||||
pause: 'Pause',
|
||||
|
||||
@@ -85,10 +85,27 @@ export interface RoomSlotInfo {
|
||||
export interface RoomInfo {
|
||||
readonly defName: string;
|
||||
readonly label: string;
|
||||
readonly homeroom: boolean;
|
||||
readonly seatThing?: string | null;
|
||||
readonly defaultSeats: number;
|
||||
readonly slots: readonly RoomSlotInfo[];
|
||||
readonly positions: readonly string[];
|
||||
}
|
||||
|
||||
export interface SubjectSkillShare {
|
||||
readonly skill: string;
|
||||
readonly share: number;
|
||||
}
|
||||
|
||||
export interface SubjectInfo {
|
||||
readonly defName: string;
|
||||
readonly label: string;
|
||||
readonly gradeMin: number;
|
||||
readonly gradeMax: number;
|
||||
readonly hoursPerWeek: number;
|
||||
readonly skills: readonly SubjectSkillShare[];
|
||||
}
|
||||
|
||||
export interface MapLayout {
|
||||
territory: { id: string; def: string };
|
||||
buildings: { id: string; def: string }[];
|
||||
@@ -99,6 +116,7 @@ export interface MapLayout {
|
||||
building: string;
|
||||
floor: string;
|
||||
label?: string;
|
||||
seats?: number;
|
||||
slots?: { key: string; thing: string; count?: number }[];
|
||||
}[];
|
||||
links: { a: string; b: string }[];
|
||||
@@ -112,6 +130,7 @@ export interface CatalogResponse {
|
||||
readonly things: readonly DefInfo[];
|
||||
readonly defaultMap: MapLayout;
|
||||
readonly nameSets: readonly DefInfo[];
|
||||
readonly subjects: readonly SubjectInfo[];
|
||||
}
|
||||
|
||||
export async function fetchMods(): Promise<readonly ModInfo[]> {
|
||||
|
||||
@@ -532,8 +532,9 @@ body {
|
||||
}
|
||||
|
||||
.people__card-name {
|
||||
margin: 0 0 4px;
|
||||
font-size: 16px;
|
||||
margin: 0 0 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.people__card-meta {
|
||||
@@ -542,12 +543,122 @@ body {
|
||||
}
|
||||
|
||||
.people__section {
|
||||
margin-top: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.people__stats {
|
||||
.people__section-title {
|
||||
margin: 0 0 3px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/*
|
||||
* Name on the left, value on the right, as many columns as the panel is wide. As a bulleted list
|
||||
* fourteen skills ran the card past the fold with three quarters of the width empty.
|
||||
*/
|
||||
.people__pairs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 0 16px;
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__pair {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 1px 0;
|
||||
border-bottom: 1px solid rgba(42, 50, 66, 0.5);
|
||||
}
|
||||
|
||||
.people__pair dt {
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.people__pair dd {
|
||||
margin: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.people__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.people__tag {
|
||||
padding: 1px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__needs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||||
gap: 6px 16px;
|
||||
}
|
||||
|
||||
.people__need {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__need-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.people__need-value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.people__need-track {
|
||||
grid-column: 1 / -1;
|
||||
height: 4px;
|
||||
margin-top: 2px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.people__need-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* An empty need is the one worth spotting without reading the number. */
|
||||
.people__need-fill--low {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.people__rel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.people__rel-title {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.people__rel-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 10px;
|
||||
}
|
||||
|
||||
.people__link {
|
||||
|
||||
@@ -103,52 +103,78 @@ export function mapEditor(options: MapEditorOptions): {
|
||||
}
|
||||
|
||||
const rows: HTMLElement[] = [];
|
||||
for (const slot of roomDef.slots) {
|
||||
const select = el('select', { class: 'input' });
|
||||
const empty = el('option', { text: t('slotEmpty') });
|
||||
empty.value = '';
|
||||
select.append(empty);
|
||||
for (const thing of catalog.things) {
|
||||
const option = el('option', { text: thing.label });
|
||||
option.value = thing.defName;
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
const fill = (room.slots ?? []).find((candidate) => candidate.key === slot.key);
|
||||
select.value = fill?.thing ?? '';
|
||||
|
||||
const countInput = el('input', { class: 'input input--count', type: 'number' });
|
||||
countInput.min = '1';
|
||||
countInput.max = '255';
|
||||
countInput.step = '1';
|
||||
countInput.value = String(fill?.count ?? slot.count ?? 1);
|
||||
countInput.title = t('slotCount');
|
||||
|
||||
const writeSlot = (): void => {
|
||||
const slots = [...(room.slots ?? [])].filter((candidate) => candidate.key !== slot.key);
|
||||
if (select.value !== '') {
|
||||
const parsed = Number.parseInt(countInput.value, 10);
|
||||
const count = Number.isFinite(parsed) ? Math.min(255, Math.max(1, parsed)) : (slot.count ?? 1);
|
||||
countInput.value = String(count);
|
||||
slots.push({ key: slot.key, thing: select.value, count });
|
||||
}
|
||||
|
||||
room.slots = slots;
|
||||
if (roomDef.homeroom) {
|
||||
const seatsInput = el('input', { class: 'input input--count', type: 'number' });
|
||||
seatsInput.min = '1';
|
||||
seatsInput.max = '255';
|
||||
seatsInput.step = '1';
|
||||
seatsInput.value = String(room.seats ?? roomDef.defaultSeats ?? 16);
|
||||
seatsInput.title = t('editorSeats');
|
||||
seatsInput.addEventListener('change', () => {
|
||||
const parsed = Number.parseInt(seatsInput.value, 10);
|
||||
const seats = Number.isFinite(parsed) ? Math.min(255, Math.max(1, parsed)) : (roomDef.defaultSeats || 16);
|
||||
seatsInput.value = String(seats);
|
||||
room.seats = seats;
|
||||
room.slots = [];
|
||||
emit();
|
||||
};
|
||||
|
||||
select.addEventListener('change', writeSlot);
|
||||
countInput.addEventListener('change', writeSlot);
|
||||
});
|
||||
|
||||
rows.push(
|
||||
el(
|
||||
'label',
|
||||
{ class: 'field__row' },
|
||||
el('span', { class: 'field__label', text: slot.key }),
|
||||
select,
|
||||
countInput,
|
||||
el('span', { class: 'field__label', text: t('editorSeats') }),
|
||||
seatsInput,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
for (const slot of roomDef.slots) {
|
||||
const select = el('select', { class: 'input' });
|
||||
const empty = el('option', { text: t('slotEmpty') });
|
||||
empty.value = '';
|
||||
select.append(empty);
|
||||
for (const thing of catalog.things) {
|
||||
const option = el('option', { text: thing.label });
|
||||
option.value = thing.defName;
|
||||
select.append(option);
|
||||
}
|
||||
|
||||
const fill = (room.slots ?? []).find((candidate) => candidate.key === slot.key);
|
||||
select.value = fill?.thing ?? '';
|
||||
|
||||
const countInput = el('input', { class: 'input input--count', type: 'number' });
|
||||
countInput.min = '1';
|
||||
countInput.max = '255';
|
||||
countInput.step = '1';
|
||||
countInput.value = String(fill?.count ?? slot.count ?? 1);
|
||||
countInput.title = t('slotCount');
|
||||
|
||||
const writeSlot = (): void => {
|
||||
const slots = [...(room.slots ?? [])].filter((candidate) => candidate.key !== slot.key);
|
||||
if (select.value !== '') {
|
||||
const parsed = Number.parseInt(countInput.value, 10);
|
||||
const count = Number.isFinite(parsed) ? Math.min(255, Math.max(1, parsed)) : (slot.count ?? 1);
|
||||
countInput.value = String(count);
|
||||
slots.push({ key: slot.key, thing: select.value, count });
|
||||
}
|
||||
|
||||
room.slots = slots;
|
||||
emit();
|
||||
};
|
||||
|
||||
select.addEventListener('change', writeSlot);
|
||||
countInput.addEventListener('change', writeSlot);
|
||||
|
||||
rows.push(
|
||||
el(
|
||||
'label',
|
||||
{ class: 'field__row' },
|
||||
el('span', { class: 'field__label', text: slot.key }),
|
||||
select,
|
||||
countInput,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return section(
|
||||
@@ -251,7 +277,9 @@ export function mapEditor(options: MapEditorOptions): {
|
||||
def: def.defName,
|
||||
building: building.id,
|
||||
floor: floor.id,
|
||||
slots: defaultSlots(def),
|
||||
...(def.homeroom
|
||||
? { seats: def.defaultSeats || 16 }
|
||||
: { slots: defaultSlots(def) }),
|
||||
});
|
||||
if (!hasLink(map, map.territory.id, id)) {
|
||||
map.links.push({ a: map.territory.id, b: id });
|
||||
|
||||
@@ -361,16 +361,12 @@ export class PeoplePanel {
|
||||
el('h3', { class: 'people__card-name', text: card.fullName }),
|
||||
el('p', { class: 'people__card-meta', text: cardMeta(card) }),
|
||||
);
|
||||
appendStats(this.card, t('peopleBody'), card.body.map((row) => `${row.label}: ${row.value}`));
|
||||
appendStats(this.card, t('peopleSkills'), card.skills.map((row) => `${row.label}: ${row.value}`));
|
||||
appendStats(this.card, t('peopleTraits'), card.traits.map((row) => row.label));
|
||||
appendStats(
|
||||
this.card,
|
||||
t('peopleNeeds'),
|
||||
card.needs.map((row) => `${row.label}: ${Math.round(row.value * 100)}%`),
|
||||
);
|
||||
appendPairs(this.card, t('peopleBody'), card.body);
|
||||
appendPairs(this.card, t('peopleSkills'), card.skills);
|
||||
appendTags(this.card, t('peopleTraits'), card.traits.map((row) => row.label));
|
||||
appendNeeds(this.card, t('peopleNeeds'), card.needs);
|
||||
|
||||
const family = el('div', { class: 'people__section' }, el('h4', { class: 'panel__section-title', text: t('peopleFamily') }));
|
||||
const family = section(t('peopleFamily'));
|
||||
appendRelatives(family, t('peopleParents'), card.family.parents, (id) => void this.openCard(id));
|
||||
appendRelatives(family, t('peopleChildren'), card.family.children, (id) => void this.openCard(id));
|
||||
appendRelatives(family, t('peopleSiblings'), card.family.siblings, (id) => void this.openCard(id));
|
||||
@@ -445,17 +441,79 @@ function cardMeta(card: PersonCard): string {
|
||||
return bits.join(' · ');
|
||||
}
|
||||
|
||||
function appendStats(parent: HTMLElement, title: string, values: readonly string[]): void {
|
||||
function section(title: string): HTMLElement {
|
||||
return el('div', { class: 'people__section' }, el('h4', { class: 'people__section-title', text: title }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Name and value in two columns, several columns per row. Fourteen skills as a bulleted list ran
|
||||
* the card past the fold while three quarters of its width sat empty.
|
||||
*/
|
||||
function appendPairs(
|
||||
parent: HTMLElement,
|
||||
title: string,
|
||||
rows: readonly { readonly label: string; readonly value: string }[],
|
||||
): void {
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = el('dl', { class: 'people__pairs' });
|
||||
for (const row of rows) {
|
||||
grid.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__pair' },
|
||||
el('dt', { text: row.label }),
|
||||
el('dd', { text: row.value }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
parent.append(section(title), grid);
|
||||
}
|
||||
|
||||
function appendTags(parent: HTMLElement, title: string, values: readonly string[]): void {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = el('ul', { class: 'people__stats' });
|
||||
const tags = el('div', { class: 'people__tags' });
|
||||
for (const value of values) {
|
||||
list.append(el('li', { text: value }));
|
||||
tags.append(el('span', { class: 'people__tag', text: value }));
|
||||
}
|
||||
|
||||
parent.append(el('div', { class: 'people__section' }, el('h4', { class: 'panel__section-title', text: title }), list));
|
||||
parent.append(section(title), tags);
|
||||
}
|
||||
|
||||
function appendNeeds(
|
||||
parent: HTMLElement,
|
||||
title: string,
|
||||
rows: readonly { readonly label: string; readonly value: number }[],
|
||||
): void {
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = el('div', { class: 'people__needs' });
|
||||
for (const row of rows) {
|
||||
const share = Math.max(0, Math.min(1, row.value));
|
||||
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);
|
||||
|
||||
grid.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__need' },
|
||||
el('span', { class: 'people__need-label', text: row.label }),
|
||||
el('span', { class: 'people__need-value', text: `${Math.round(share * 100)}%` }),
|
||||
el('span', { class: 'people__need-track' }, fill),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
parent.append(section(title), grid);
|
||||
}
|
||||
|
||||
function appendRelatives(
|
||||
@@ -468,10 +526,9 @@ function appendRelatives(
|
||||
return;
|
||||
}
|
||||
|
||||
const list = el('ul', { class: 'people__stats' });
|
||||
const list = el('span', { class: 'people__rel-list' });
|
||||
for (const relative of relatives) {
|
||||
const item = el('li');
|
||||
item.append(
|
||||
list.append(
|
||||
el('button', {
|
||||
class: 'people__link',
|
||||
type: 'button',
|
||||
@@ -479,8 +536,14 @@ function appendRelatives(
|
||||
onClick: () => open(relative.id),
|
||||
}),
|
||||
);
|
||||
list.append(item);
|
||||
}
|
||||
|
||||
parent.append(el('h4', { class: 'panel__section-title', text: title }), list);
|
||||
parent.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'people__rel' },
|
||||
el('span', { class: 'people__rel-title', text: title }),
|
||||
list,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -307,6 +307,7 @@ public sealed class CatalogLoader
|
||||
var bodyAttributes = new Dictionary<string, BodyAttributeDef>(StringComparer.Ordinal);
|
||||
var needs = new Dictionary<string, NeedDef>(StringComparer.Ordinal);
|
||||
var nameSets = new Dictionary<string, NameSetDef>(StringComparer.Ordinal);
|
||||
var subjects = new Dictionary<string, SubjectDef>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var (key, json) in resolved)
|
||||
{
|
||||
@@ -351,6 +352,9 @@ public sealed class CatalogLoader
|
||||
case DefKind.NameSet:
|
||||
nameSets[key.Name] = Jsonc.Deserialize<NameSetDef>(json);
|
||||
break;
|
||||
case DefKind.Subject:
|
||||
subjects[key.Name] = Jsonc.Deserialize<SubjectDef>(json);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +373,7 @@ public sealed class CatalogLoader
|
||||
bodyAttributes,
|
||||
needs,
|
||||
nameSets,
|
||||
subjects,
|
||||
ru,
|
||||
en);
|
||||
}
|
||||
@@ -421,6 +426,33 @@ public sealed class CatalogLoader
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' references unknown WorkDef '{work}'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (room.Homeroom)
|
||||
{
|
||||
if (room.Slots.Count > 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' is a homeroom and cannot declare named slots.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(room.SeatThing) || !catalog.Things.TryGetValue(room.SeatThing, out var seat) || seat.Abstract)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing is missing or unknown.");
|
||||
}
|
||||
|
||||
if (seat.PupilSlots <= 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' seatThing '{room.SeatThing}' must have pupilSlots.");
|
||||
}
|
||||
|
||||
if (room.DefaultSeats < 1 || room.DefaultSeats > byte.MaxValue)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' defaultSeats must be 1–{byte.MaxValue}.");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(room.SeatThing) || room.DefaultSeats != 0)
|
||||
{
|
||||
throw new ContentLoadException($"RoomDef '{room.DefName}' is not a homeroom and cannot set seatThing or defaultSeats.");
|
||||
}
|
||||
}
|
||||
|
||||
PeopleDefValidator.Validate(catalog);
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class DefCatalog
|
||||
IReadOnlyDictionary<string, BodyAttributeDef> bodyAttributes,
|
||||
IReadOnlyDictionary<string, NeedDef> needs,
|
||||
IReadOnlyDictionary<string, NameSetDef> nameSets,
|
||||
IReadOnlyDictionary<string, SubjectDef> subjects,
|
||||
IReadOnlyDictionary<string, string> ru,
|
||||
IReadOnlyDictionary<string, string> en)
|
||||
{
|
||||
@@ -38,6 +39,7 @@ public sealed class DefCatalog
|
||||
BodyAttributes = bodyAttributes;
|
||||
Needs = needs;
|
||||
NameSets = nameSets;
|
||||
Subjects = subjects;
|
||||
_ru = ru;
|
||||
_en = en;
|
||||
AnyNeedDecays = needs.Values.Any(need => !need.Abstract && need.DecayPerHour > 0f);
|
||||
@@ -78,6 +80,8 @@ public sealed class DefCatalog
|
||||
|
||||
public IReadOnlyDictionary<string, NameSetDef> NameSets { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, SubjectDef> Subjects { get; }
|
||||
|
||||
private readonly IReadOnlyDictionary<string, string> _ru;
|
||||
private readonly IReadOnlyDictionary<string, string> _en;
|
||||
|
||||
@@ -98,6 +102,7 @@ public sealed class DefCatalog
|
||||
DefKind.BodyAttribute => BodyAttributes.GetValueOrDefault(defName),
|
||||
DefKind.Need => Needs.GetValueOrDefault(defName),
|
||||
DefKind.NameSet => NameSets.GetValueOrDefault(defName),
|
||||
DefKind.Subject => Subjects.GetValueOrDefault(defName),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -143,6 +148,13 @@ public sealed class DefCatalog
|
||||
/// </summary>
|
||||
public string Text(string locale, string key) => TryText(locale, key, out var text) ? text : key;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this locale actually carries the key. <see cref="Text"/> falls back to the key
|
||||
/// itself, which reads as a label in English and as debug output in Russian — so a test that
|
||||
/// wants to prove nothing is unlabelled has to ask this instead of comparing strings.
|
||||
/// </summary>
|
||||
public bool HasText(string locale, string key) => TryText(locale, key, out _);
|
||||
|
||||
private bool TryText(string locale, string key, out string text)
|
||||
{
|
||||
var table = locale.Equals("en", StringComparison.OrdinalIgnoreCase) ? _en : _ru;
|
||||
@@ -164,6 +176,7 @@ public sealed class DefCatalog
|
||||
BodyAttributeDef => DefKind.BodyAttribute,
|
||||
NeedDef => DefKind.Need,
|
||||
NameSetDef => DefKind.NameSet,
|
||||
SubjectDef => DefKind.Subject,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(def)),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public enum DefKind
|
||||
BodyAttribute,
|
||||
Need,
|
||||
NameSet,
|
||||
Subject,
|
||||
}
|
||||
|
||||
/// <summary>Shared JSONC fields. Kind comes from the folder under <c>defs/</c>, not from the file.</summary>
|
||||
@@ -64,8 +65,15 @@ public sealed class RoomDef : Def
|
||||
/// <summary>
|
||||
/// When true, a map room of this def with pupil slots becomes a roster class.
|
||||
/// Labs keep <see cref="ThingDef.PupilSlots"/> for lessons without forming a homeroom.
|
||||
/// Homerooms are a seat count of <see cref="SeatThing"/>, not a table of named slots.
|
||||
/// </summary>
|
||||
public bool Homeroom { get; init; }
|
||||
|
||||
/// <summary>Thing whose <see cref="ThingDef.PupilSlots"/> × map seats is class capacity.</summary>
|
||||
public string? SeatThing { get; init; }
|
||||
|
||||
/// <summary>Editor default when placing a new homeroom. Vanilla classrooms are 16.</summary>
|
||||
public int DefaultSeats { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BuildingDef : Def;
|
||||
|
||||
@@ -55,6 +55,12 @@ public sealed class RoomNode
|
||||
/// <summary>Optional designation such as a classroom number. The def label still supplies the noun.</summary>
|
||||
public string? Label { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Homeroom seat count. Capacity is <c>SeatThing.PupilSlots × Seats</c>. Named
|
||||
/// <see cref="Slots"/> stay on rooms whose furnishing actually varies.
|
||||
/// </summary>
|
||||
public int Seats { get; init; }
|
||||
|
||||
public IReadOnlyList<SlotFill> Slots { get; init; } = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class MapValidator
|
||||
throw new MapValidationException($"Room '{room.Id}' is on floor '{room.Floor}', which belongs to another building.");
|
||||
}
|
||||
|
||||
ValidateSlotFills(room, catalog);
|
||||
ValidateRoomContents(room, catalog);
|
||||
rooms[room.Id] = room;
|
||||
}
|
||||
|
||||
@@ -121,13 +121,33 @@ public static class MapValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSlotFills(RoomNode room, DefCatalog catalog)
|
||||
private static void ValidateRoomContents(RoomNode room, DefCatalog catalog)
|
||||
{
|
||||
if (!catalog.Rooms.TryGetValue(room.Def, out var roomDef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomDef.Homeroom)
|
||||
{
|
||||
if (room.Slots.Count > 0)
|
||||
{
|
||||
throw new MapValidationException($"Homeroom '{room.Id}' cannot fill named slots.");
|
||||
}
|
||||
|
||||
if (room.Seats < 1 || room.Seats > byte.MaxValue)
|
||||
{
|
||||
throw new MapValidationException($"Homeroom '{room.Id}' seats must be 1–{byte.MaxValue}.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.Seats != 0)
|
||||
{
|
||||
throw new MapValidationException($"Room '{room.Id}' is not a homeroom and cannot set seats.");
|
||||
}
|
||||
|
||||
var keys = roomDef.Slots.Select(slot => slot.Key).ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
|
||||
@@ -121,23 +121,24 @@ public static class MapView
|
||||
string locale,
|
||||
RoomNode room)
|
||||
{
|
||||
var items = new List<MapViewItem>(room.Slots.Count);
|
||||
var pupilSlots = 0L;
|
||||
foreach (var fill in room.Slots)
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom && !string.IsNullOrWhiteSpace(def.SeatThing))
|
||||
{
|
||||
var count = SlotQuantity(fill.Count);
|
||||
items.Add(new MapViewItem(LabelOf(catalog, locale, DefKind.Thing, fill.Thing), count));
|
||||
if (catalog.Things.TryGetValue(fill.Thing, out var thing) && thing.PupilSlots > 0)
|
||||
{
|
||||
pupilSlots += (long)thing.PupilSlots * count;
|
||||
}
|
||||
var seats = RoomOccupancy.Quantity(room.Seats > 0 ? room.Seats : def.DefaultSeats);
|
||||
return (
|
||||
[new MapViewItem(LabelOf(catalog, locale, DefKind.Thing, def.SeatThing), seats)],
|
||||
RoomOccupancy.PupilSlots(catalog, room));
|
||||
}
|
||||
|
||||
return (items, (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue));
|
||||
}
|
||||
var items = new List<MapViewItem>(room.Slots.Count);
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
items.Add(new MapViewItem(
|
||||
LabelOf(catalog, locale, DefKind.Thing, fill.Thing),
|
||||
RoomOccupancy.Quantity(fill.Count)));
|
||||
}
|
||||
|
||||
private static int SlotQuantity(int count) =>
|
||||
count < 1 ? 1 : Math.Min(count, byte.MaxValue);
|
||||
return (items, RoomOccupancy.PupilSlots(catalog, room));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A floor's <see cref="FloorNode.Label"/> is its designation, not a replacement name — the
|
||||
|
||||
@@ -101,6 +101,9 @@ internal static class PackPaths
|
||||
case "namesets":
|
||||
kind = DefKind.NameSet;
|
||||
return true;
|
||||
case "subjects":
|
||||
kind = DefKind.Subject;
|
||||
return true;
|
||||
default:
|
||||
kind = default;
|
||||
return false;
|
||||
|
||||
@@ -29,6 +29,11 @@ internal static class PeopleDefValidator
|
||||
ValidateNameSet(names);
|
||||
}
|
||||
|
||||
foreach (var subject in catalog.Subjects.Values)
|
||||
{
|
||||
ValidateSubject(subject, catalog);
|
||||
}
|
||||
|
||||
RequireBuildInputs(catalog);
|
||||
}
|
||||
|
||||
@@ -218,6 +223,43 @@ internal static class PeopleDefValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSubject(SubjectDef subject, DefCatalog catalog)
|
||||
{
|
||||
if (subject.HoursPerWeek < 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' hoursPerWeek cannot be negative.");
|
||||
}
|
||||
|
||||
if (subject.Grades.Min < 1 || subject.Grades.Max > 11 || subject.Grades.Min > subject.Grades.Max)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' grades must be 1–11 with min ≤ max.");
|
||||
}
|
||||
|
||||
if (subject.Skills.Count == 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' needs at least one skill.");
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var share in subject.Skills)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(share.Skill) || !seen.Add(share.Skill))
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' has a missing or duplicate skill.");
|
||||
}
|
||||
|
||||
if (!catalog.Skills.TryGetValue(share.Skill, out var skill) || skill.Abstract)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' references unknown SkillDef '{share.Skill}'.");
|
||||
}
|
||||
|
||||
if (share.Share < 0)
|
||||
{
|
||||
throw new ContentLoadException($"SubjectDef '{subject.DefName}' skill '{share.Skill}' share cannot be negative.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateNameSet(NameSetDef names)
|
||||
{
|
||||
if (!NameGrammar.IsKnownPatronymic(names.PatronymicRule))
|
||||
|
||||
@@ -122,6 +122,22 @@ public sealed class SkillDef : Def
|
||||
public IReadOnlyList<BodySkillLimit> BodyLimits { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class SubjectSkillShare
|
||||
{
|
||||
public required string Skill { get; init; }
|
||||
|
||||
public float Share { get; init; }
|
||||
}
|
||||
|
||||
public sealed class SubjectDef : Def
|
||||
{
|
||||
public IntRange Grades { get; init; } = new() { Min = 1, Max = 11 };
|
||||
|
||||
public int HoursPerWeek { get; init; }
|
||||
|
||||
public IReadOnlyList<SubjectSkillShare> Skills { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class TraitSkillModifier
|
||||
{
|
||||
public required string Skill { get; init; }
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace HSchool.Content;
|
||||
|
||||
/// <summary>
|
||||
/// How many pupils a map room can host. Homerooms are a seat count of one thing; other rooms
|
||||
/// still sum <c>ThingDef.PupilSlots × fill count</c> across named slots.
|
||||
/// </summary>
|
||||
public static class RoomOccupancy
|
||||
{
|
||||
public static int Quantity(int count) =>
|
||||
count < 1 ? 1 : Math.Min(count, byte.MaxValue);
|
||||
|
||||
public static int PupilSlots(DefCatalog catalog, RoomNode room)
|
||||
{
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def) && def.Homeroom)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(def.SeatThing)
|
||||
|| !catalog.Things.TryGetValue(def.SeatThing, out var seat)
|
||||
|| seat.PupilSlots <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var seats = room.Seats > 0 ? Quantity(room.Seats) : Quantity(def.DefaultSeats);
|
||||
return (int)Math.Clamp((long)seat.PupilSlots * seats, 0, ushort.MaxValue);
|
||||
}
|
||||
|
||||
var pupilSlots = 0L;
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
if (catalog.Things.TryGetValue(fill.Thing, out var thing) && thing.PupilSlots > 0)
|
||||
{
|
||||
pupilSlots += (long)thing.PupilSlots * Quantity(fill.Count);
|
||||
}
|
||||
}
|
||||
|
||||
return (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public static class RosterGenerator
|
||||
/// Matches <c>SimulationOptions.DefaultStartDate</c> so tests without a clock still sit in
|
||||
/// the default school year. Callers with a live clock must pass <paramref name="asOf"/>.
|
||||
/// </summary>
|
||||
public static readonly DateTime DefaultAsOf = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
public static readonly DateTime DefaultAsOf = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public static Roster Generate(
|
||||
DefCatalog catalog,
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class SchoolDemand
|
||||
|
||||
foreach (var room in map.Rooms)
|
||||
{
|
||||
var slots = PupilSlotsOf(catalog, room);
|
||||
var slots = RoomOccupancy.PupilSlots(catalog, room);
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def))
|
||||
{
|
||||
foreach (var position in def.Positions)
|
||||
@@ -67,19 +67,4 @@ public sealed class SchoolDemand
|
||||
|
||||
return new SchoolDemand(classes, seats, staff);
|
||||
}
|
||||
|
||||
private static int PupilSlotsOf(DefCatalog catalog, RoomNode room)
|
||||
{
|
||||
var pupilSlots = 0L;
|
||||
foreach (var fill in room.Slots)
|
||||
{
|
||||
var count = fill.Count < 1 ? 1 : Math.Min(fill.Count, byte.MaxValue);
|
||||
if (catalog.Things.TryGetValue(fill.Thing, out var thing) && thing.PupilSlots > 0)
|
||||
{
|
||||
pupilSlots += (long)thing.PupilSlots * count;
|
||||
}
|
||||
}
|
||||
|
||||
return (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ internal sealed record CatalogResponse(
|
||||
IReadOnlyList<RoomInfoResponse> Rooms,
|
||||
IReadOnlyList<DefInfoResponse> Things,
|
||||
IReadOnlyList<DefInfoResponse> NameSets,
|
||||
IReadOnlyList<SubjectInfoResponse> Subjects,
|
||||
MapLayout DefaultMap)
|
||||
{
|
||||
public static CatalogResponse From(DefCatalog catalog, MapLayout map, string locale) =>
|
||||
@@ -97,6 +98,7 @@ internal sealed record CatalogResponse(
|
||||
PlaceableRooms(catalog, locale),
|
||||
PlaceableThings(catalog, locale),
|
||||
Placeable(catalog.NameSets.Values, catalog, locale),
|
||||
PlaceableSubjects(catalog, locale),
|
||||
map);
|
||||
|
||||
private static IReadOnlyList<DefInfoResponse> Placeable<T>(IEnumerable<T> defs, DefCatalog catalog, string locale)
|
||||
@@ -121,9 +123,25 @@ internal sealed record CatalogResponse(
|
||||
.Select(def => new RoomInfoResponse(
|
||||
def.DefName,
|
||||
catalog.Label(locale, def),
|
||||
def.Homeroom,
|
||||
def.SeatThing,
|
||||
def.DefaultSeats,
|
||||
def.Slots.Select(slot => new RoomSlotInfo(slot.Key, slot.Thing, slot.Count)).ToArray(),
|
||||
def.Positions.ToArray()))
|
||||
.ToArray();
|
||||
|
||||
private static IReadOnlyList<SubjectInfoResponse> PlaceableSubjects(DefCatalog catalog, string locale) =>
|
||||
catalog.Subjects.Values
|
||||
.Where(def => !def.Abstract)
|
||||
.OrderBy(def => def.DefName, StringComparer.Ordinal)
|
||||
.Select(def => new SubjectInfoResponse(
|
||||
def.DefName,
|
||||
catalog.Label(locale, def),
|
||||
def.Grades.Min,
|
||||
def.Grades.Max,
|
||||
def.HoursPerWeek,
|
||||
def.Skills.Select(share => new SubjectSkillInfo(share.Skill, share.Share)).ToArray()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
|
||||
@@ -131,7 +149,20 @@ internal sealed record DefInfoResponse(string DefName, string Label, int PupilSl
|
||||
internal sealed record RoomInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
bool Homeroom,
|
||||
string? SeatThing,
|
||||
int DefaultSeats,
|
||||
IReadOnlyList<RoomSlotInfo> Slots,
|
||||
IReadOnlyList<string> Positions);
|
||||
|
||||
internal sealed record RoomSlotInfo(string Key, string Thing, int Count);
|
||||
|
||||
internal sealed record SubjectSkillInfo(string Skill, float Share);
|
||||
|
||||
internal sealed record SubjectInfoResponse(
|
||||
string DefName,
|
||||
string Label,
|
||||
int GradeMin,
|
||||
int GradeMax,
|
||||
int HoursPerWeek,
|
||||
IReadOnlyList<SubjectSkillInfo> Skills);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"TickRate": 20,
|
||||
"MaxSchools": 6,
|
||||
"GameMinutesPerRealSecond": 5,
|
||||
"DefaultStartDate": "2012-04-03T06:00:00",
|
||||
"DefaultStartDate": "2012-03-31T06:00:00",
|
||||
"SavesDirectory": "saves",
|
||||
"ModsDirectory": "mods",
|
||||
"SaveIntervalSeconds": 30
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
{ "defName": "Teacher" },
|
||||
{ "defName": "Librarian" },
|
||||
{ "defName": "Nurse" },
|
||||
{ "defName": "PETeacher" },
|
||||
{ "defName": "CafeteriaCook" },
|
||||
]
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
[
|
||||
{
|
||||
"defName": "Classroom",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"positions": ["Teacher"],
|
||||
"works": ["TeachLesson"],
|
||||
"homeroom": true,
|
||||
"seatThing": "StudentDesk",
|
||||
"defaultSeats": 16,
|
||||
"works": ["TeachLesson"],
|
||||
},
|
||||
{
|
||||
"defName": "Library",
|
||||
@@ -27,7 +22,6 @@
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "computers", "thing": "Computer", "count": 12 },
|
||||
],
|
||||
"positions": ["Teacher"],
|
||||
"works": ["TeachLesson"],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
"slots": [
|
||||
{ "key": "benches", "thing": "Bench", "count": 4 },
|
||||
],
|
||||
"positions": ["PETeacher"],
|
||||
"works": ["PELesson"],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
[
|
||||
{
|
||||
"defName": "PrimarySchool",
|
||||
"grades": { "min": 1, "max": 4 },
|
||||
"hoursPerWeek": 20,
|
||||
"skills": [
|
||||
{ "skill": "Mathematics", "share": 0.3 },
|
||||
{ "skill": "RussianLanguage", "share": 0.3 },
|
||||
{ "skill": "Literature", "share": 0.2 },
|
||||
{ "skill": "Biology", "share": 0.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
"defName": "Mathematics",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 5,
|
||||
"skills": [{ "skill": "Mathematics", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "RussianLanguage",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 4,
|
||||
"skills": [{ "skill": "RussianLanguage", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Literature",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 3,
|
||||
"skills": [{ "skill": "Literature", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "ForeignLanguage",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 3,
|
||||
"skills": [{ "skill": "ForeignLanguage", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "History",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 2,
|
||||
"skills": [{ "skill": "History", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Geography",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 2,
|
||||
"skills": [{ "skill": "Geography", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Biology",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 2,
|
||||
"skills": [{ "skill": "Biology", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Informatics",
|
||||
"grades": { "min": 5, "max": 11 },
|
||||
"hoursPerWeek": 1,
|
||||
"skills": [{ "skill": "Informatics", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Physics",
|
||||
"grades": { "min": 7, "max": 11 },
|
||||
"hoursPerWeek": 2,
|
||||
"skills": [{ "skill": "Physics", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "Chemistry",
|
||||
"grades": { "min": 8, "max": 11 },
|
||||
"hoursPerWeek": 2,
|
||||
"skills": [{ "skill": "Chemistry", "share": 1 }],
|
||||
},
|
||||
{
|
||||
"defName": "PhysicalEducation",
|
||||
"grades": { "min": 1, "max": 11 },
|
||||
"hoursPerWeek": 3,
|
||||
"skills": [{ "skill": "PhysicalEducation", "share": 1 }],
|
||||
},
|
||||
]
|
||||
@@ -16,8 +16,8 @@
|
||||
"Teacher": "Teacher",
|
||||
"Librarian": "Librarian",
|
||||
"Nurse": "Nurse",
|
||||
"PETeacher": "PE teacher",
|
||||
"CafeteriaCook": "Cook",
|
||||
"PrimarySchool": "Primary",
|
||||
"PrincipalOfficeWork": "Principal's work",
|
||||
"TeachLesson": "Teach a lesson",
|
||||
"WalkSchool": "Walk the school",
|
||||
@@ -87,6 +87,7 @@
|
||||
"Toilet": "Toilet",
|
||||
"Social": "Social",
|
||||
"Slavic": "Slavic",
|
||||
"Build": "Build",
|
||||
"Skinny": "Skinny",
|
||||
"Average": "Average",
|
||||
"Athletic": "Athletic",
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"Teacher": "Учитель",
|
||||
"Librarian": "Библиотекарь",
|
||||
"Nurse": "Медсестра",
|
||||
"PETeacher": "Учитель физкультуры",
|
||||
"CafeteriaCook": "Повар",
|
||||
"PrimarySchool": "Начальные классы",
|
||||
"PrincipalOfficeWork": "Работа директора",
|
||||
"TeachLesson": "Урок",
|
||||
"WalkSchool": "Обход школы",
|
||||
@@ -87,6 +87,7 @@
|
||||
"Toilet": "Туалет",
|
||||
"Social": "Общение",
|
||||
"Slavic": "Славянский",
|
||||
"Build": "Телосложение",
|
||||
"Skinny": "Худощавое",
|
||||
"Average": "Обычное",
|
||||
"Athletic": "Атлетическое",
|
||||
|
||||
@@ -52,12 +52,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "101",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-102",
|
||||
@@ -65,12 +60,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "102",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-103",
|
||||
@@ -78,12 +68,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "103",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-104",
|
||||
@@ -91,12 +76,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-1",
|
||||
"label": "104",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "cafeteria",
|
||||
@@ -127,12 +107,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "201",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-202",
|
||||
@@ -140,12 +115,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "202",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-203",
|
||||
@@ -153,12 +123,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "203",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-204",
|
||||
@@ -166,12 +131,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "204",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-205",
|
||||
@@ -179,12 +139,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "205",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-206",
|
||||
@@ -192,12 +147,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "206",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "classroom-207",
|
||||
@@ -205,12 +155,7 @@
|
||||
"building": "main",
|
||||
"floor": "floor-2",
|
||||
"label": "207",
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "teacherDesk", "thing": "Desk" },
|
||||
{ "key": "teacherChair", "thing": "Chair" },
|
||||
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
|
||||
],
|
||||
"seats": 16,
|
||||
},
|
||||
{
|
||||
"id": "library",
|
||||
|
||||
@@ -5,7 +5,7 @@ public sealed class SimulationOptions
|
||||
{
|
||||
public const string SectionName = "Simulation";
|
||||
|
||||
private DateTime _defaultStartDate = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
private DateTime _defaultStartDate = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>Fixed simulation steps per second.</summary>
|
||||
public int TickRate { get; set; } = 20;
|
||||
|
||||
Reference in New Issue
Block a user