Update wire protocol to version 5, introducing pupil slots and item counts in map snapshots. Enhance UI components to display pupil slots and item counts in game screens and map editors. Revise localization strings for improved user guidance. Update tests to validate new functionalities and ensure robustness in handling room and item data.
ci / server (push) Failing after 3m37s
ci / client (push) Successful in 28s

This commit is contained in:
Leonid Pershin
2026-08-18 17:21:16 +03:00
parent f000b128f6
commit 38afbcad36
26 changed files with 349 additions and 64 deletions
@@ -20,6 +20,7 @@ describe('t', () => {
it('interpolates placeholders', () => {
setLocale('en');
expect(t('schoolCount', { current: 2, max: 6 })).toBe('Schools: 2 of 6.');
expect(t('pupilSlots', { count: 16 })).toBe('Pupil places: 16');
});
});
+4
View File
@@ -62,6 +62,7 @@ const ru = {
addRoom: 'Добавить комнату',
removeRoom: 'Удалить комнату',
slotEmpty: '— пусто —',
slotCount: 'Количество',
backToMenu: '← В главное меню',
pause: 'Пауза',
@@ -76,6 +77,7 @@ const ru = {
locationActivities: 'Сейчас',
locationPositions: 'Должности',
itemsEmpty: 'Пусто.',
pupilSlots: 'Ученических мест: {count}',
charactersEmpty: 'Никого нет.',
activitiesEmpty: 'Ничего не происходит.',
positionsEmpty: 'Нет должностей.',
@@ -145,6 +147,7 @@ const en: Messages = {
addRoom: 'Add room',
removeRoom: 'Remove room',
slotEmpty: '— empty —',
slotCount: 'Count',
backToMenu: '← Main menu',
pause: 'Pause',
@@ -159,6 +162,7 @@ const en: Messages = {
locationActivities: 'Now',
locationPositions: 'Positions',
itemsEmpty: 'Empty.',
pupilSlots: 'Pupil places: {count}',
charactersEmpty: 'Nobody here.',
activitiesEmpty: 'Nothing is happening.',
positionsEmpty: 'No positions.',
+3 -1
View File
@@ -71,11 +71,13 @@ export interface ModInfo {
export interface DefInfo {
readonly defName: string;
readonly label: string;
readonly pupilSlots?: number;
}
export interface RoomSlotInfo {
readonly key: string;
readonly thing: string;
readonly count: number;
}
export interface RoomInfo {
@@ -95,7 +97,7 @@ export interface MapLayout {
building: string;
floor: string;
label?: string;
slots?: { key: string; thing: string }[];
slots?: { key: string; thing: string; count?: number }[];
}[];
links: { a: string; b: string }[];
}
+61 -2
View File
@@ -129,7 +129,7 @@ describe('decodeServerMessage', () => {
const id = encoder.encode('yard');
const parent = encoder.encode('');
const name = encoder.encode('Двор');
const buffer = new ArrayBuffer(7 + 1 + 2 + id.length + 2 + parent.length + 2 + name.length + 1 + 1);
const buffer = new ArrayBuffer(7 + 1 + 2 + id.length + 2 + parent.length + 2 + name.length + 2 + 1 + 1);
const view = new DataView(buffer);
view.setUint8(0, MessageType.ServerMapSnapshot);
view.setInt32(1, 7, true);
@@ -147,6 +147,8 @@ describe('decodeServerMessage', () => {
offset += 2;
new Uint8Array(buffer).set(name, offset);
offset += name.length;
view.setUint16(offset, 0, true);
offset += 2;
view.setUint8(offset, 0);
offset += 1;
view.setUint8(offset, 0);
@@ -155,7 +157,64 @@ describe('decodeServerMessage', () => {
type: 'map-snapshot',
schoolId: 7,
nodes: [
{ kind: 0, id: 'yard', parentId: '', name: 'Двор', items: [], positions: [] },
{ kind: 0, id: 'yard', parentId: '', name: 'Двор', pupilSlots: 0, items: [], positions: [] },
],
});
});
it('reads stacked items and pupil slots at the documented offsets', () => {
const encoder = new TextEncoder();
const id = encoder.encode('classroom-1a');
const parent = encoder.encode('floor-1');
const name = encoder.encode('Класс 1A');
const itemName = encoder.encode('Парта');
const buffer = new ArrayBuffer(
7 + 1 + 2 + id.length + 2 + parent.length + 2 + name.length + 2 + 1 + 2 + itemName.length + 1 + 1,
);
const view = new DataView(buffer);
view.setUint8(0, MessageType.ServerMapSnapshot);
view.setInt32(1, 1, true);
view.setUint16(5, 1, true);
let offset = 7;
view.setUint8(offset, 3);
offset += 1;
view.setUint16(offset, id.length, true);
offset += 2;
new Uint8Array(buffer).set(id, offset);
offset += id.length;
view.setUint16(offset, parent.length, true);
offset += 2;
new Uint8Array(buffer).set(parent, offset);
offset += parent.length;
view.setUint16(offset, name.length, true);
offset += 2;
new Uint8Array(buffer).set(name, offset);
offset += name.length;
view.setUint16(offset, 16, true);
offset += 2;
view.setUint8(offset, 1);
offset += 1;
view.setUint16(offset, itemName.length, true);
offset += 2;
new Uint8Array(buffer).set(itemName, offset);
offset += itemName.length;
view.setUint8(offset, 16);
offset += 1;
view.setUint8(offset, 0);
expect(decodeServerMessage(buffer)).toEqual({
type: 'map-snapshot',
schoolId: 1,
nodes: [
{
kind: 3,
id: 'classroom-1a',
parentId: 'floor-1',
name: 'Класс 1A',
pupilSlots: 16,
items: [{ name: 'Парта', count: 16 }],
positions: [],
},
],
});
});
+24 -5
View File
@@ -5,7 +5,7 @@
* changed together and documented in `docs/protocol.md`. All numbers are little-endian.
*/
export const PROTOCOL_VERSION = 4;
export const PROTOCOL_VERSION = 5;
export const MessageType = {
ClientHello: 0x01,
@@ -69,12 +69,18 @@ export const MapNodeKind = {
Room: 3,
} as const;
export interface MapSnapshotItem {
readonly name: string;
readonly count: number;
}
export interface MapSnapshotNode {
readonly kind: number;
readonly id: string;
readonly parentId: string;
readonly name: string;
readonly items: readonly string[];
readonly pupilSlots: number;
readonly items: readonly MapSnapshotItem[];
readonly positions: readonly string[];
}
@@ -236,13 +242,18 @@ function decodeMapSnapshot(view: DataView): MapSnapshotMessage {
offset = parentId.next;
const name = readString(view, offset);
offset = name.next;
ensure(view, offset + 2);
const pupilSlots = view.getUint16(offset, true);
offset += 2;
const itemCount = readU8(view, offset);
offset += 1;
const items: string[] = [];
const items: MapSnapshotItem[] = [];
for (let item = 0; item < itemCount; item++) {
const value = readString(view, offset);
items.push(value.text);
offset = value.next;
const count = readU8(view, offset);
offset += 1;
items.push({ name: value.text, count });
}
const positionCount = readU8(view, offset);
@@ -254,7 +265,15 @@ function decodeMapSnapshot(view: DataView): MapSnapshotMessage {
offset = value.next;
}
nodes.push({ kind, id: id.text, parentId: parentId.text, name: name.text, items, positions });
nodes.push({
kind,
id: id.text,
parentId: parentId.text,
name: name.text,
pupilSlots,
items,
positions,
});
}
return { type: 'map-snapshot', schoolId, nodes };
+11
View File
@@ -317,6 +317,12 @@ body {
font-size: 13px;
}
.panel__meta {
margin: 6px 0 0;
color: var(--text-muted);
font-size: 13px;
}
.tree {
margin: 0;
padding: 0;
@@ -542,6 +548,11 @@ body {
flex: 1;
}
.field__row > .input--count {
flex: 0 0 4.5rem;
width: 4.5rem;
}
.field .hint {
margin: 0;
font-size: 13px;
+11 -3
View File
@@ -1,4 +1,4 @@
import { CLOCK_SPEEDS, type ClockMessage, type MapSnapshotNode } from '../net/protocol.ts';
import { CLOCK_SPEEDS, type ClockMessage, type MapSnapshotItem, type MapSnapshotNode } from '../net/protocol.ts';
import { formatGameDate, formatGameTimeOfDay, formatGameWeekday } from '../format/gameTime.ts';
import { t } from '../i18n/strings.ts';
import type { School } from '../net/api.ts';
@@ -36,6 +36,7 @@ export class GameScreen {
private readonly itemsHeading = el('h3', { class: 'panel__section-title' });
private readonly itemsEmpty = el('p', { class: 'panel__empty' });
private readonly itemsList = el('ul', { class: 'panel__list' });
private readonly pupilSlotsLine = el('p', { class: 'panel__meta' });
private readonly charactersHeading = el('h3', { class: 'panel__section-title' });
private readonly charactersEmpty = el('p', { class: 'panel__empty' });
private readonly activitiesHeading = el('h3', { class: 'panel__section-title' });
@@ -101,7 +102,7 @@ export class GameScreen {
'div',
{ class: 'panel__body' },
this.locationName,
el('div', { class: 'panel__section' }, this.itemsHeading, this.itemsEmpty, this.itemsList),
el('div', { class: 'panel__section' }, this.itemsHeading, this.itemsEmpty, this.itemsList, this.pupilSlotsLine),
el('div', { class: 'panel__section' }, this.charactersHeading, this.charactersEmpty),
el('div', { class: 'panel__section' }, this.activitiesHeading, this.activitiesEmpty),
el('div', { class: 'panel__section' }, this.positionsHeading, this.positionsEmpty, this.positionsList),
@@ -210,7 +211,10 @@ export class GameScreen {
const node = this.nodes.find((candidate) => candidate.id === this.selectedId);
this.locationName.textContent = node?.name ?? '';
paintList(this.itemsList, this.itemsEmpty, node?.items ?? []);
paintList(this.itemsList, this.itemsEmpty, (node?.items ?? []).map(formatItem));
const pupilSlots = node?.pupilSlots ?? 0;
this.pupilSlotsLine.hidden = pupilSlots <= 0;
this.pupilSlotsLine.textContent = pupilSlots > 0 ? t('pupilSlots', { count: pupilSlots }) : '';
paintList(this.positionsList, this.positionsEmpty, node?.positions ?? []);
}
@@ -236,6 +240,10 @@ function childrenOf(nodes: readonly MapSnapshotNode[], parentId: string): MapSna
return nodes.filter((node) => node.parentId === parentId);
}
function formatItem(item: MapSnapshotItem): string {
return item.count === 1 ? item.name : `${item.name} ×${item.count}`;
}
function paintList(list: HTMLUListElement, empty: HTMLParagraphElement, values: readonly string[]): void {
clear(list);
empty.hidden = values.length > 0;
+20 -5
View File
@@ -116,15 +116,29 @@ export function mapEditor(options: MapEditorOptions): {
const fill = (room.slots ?? []).find((candidate) => candidate.key === slot.key);
select.value = fill?.thing ?? '';
select.addEventListener('change', () => {
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 !== '') {
slots.push({ key: slot.key, thing: 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(
@@ -132,6 +146,7 @@ export function mapEditor(options: MapEditorOptions): {
{ class: 'field__row' },
el('span', { class: 'field__label', text: slot.key }),
select,
countInput,
),
);
}
@@ -288,8 +303,8 @@ function hasLink(map: MapLayout, a: string, b: string): boolean {
);
}
function defaultSlots(def: RoomInfo): { key: string; thing: string }[] {
return def.slots.map((slot) => ({ key: slot.key, thing: slot.thing }));
function defaultSlots(def: RoomInfo): { key: string; thing: string; count: number }[] {
return def.slots.map((slot) => ({ key: slot.key, thing: slot.thing, count: slot.count }));
}
function nextId(prefix: string, used: Set<string>): string {
+10
View File
@@ -352,6 +352,11 @@ public sealed class CatalogLoader
{
foreach (var thing in catalog.Things.Values)
{
if (thing.PupilSlots < 0 || thing.PupilSlots > byte.MaxValue)
{
throw new ContentLoadException($"ThingDef '{thing.DefName}' pupilSlots must be 0{byte.MaxValue}.");
}
foreach (var action in thing.Actions)
{
if (!catalog.Actions.ContainsKey(action))
@@ -369,6 +374,11 @@ public sealed class CatalogLoader
{
throw new ContentLoadException($"RoomDef '{room.DefName}' slot '{slot.Key}' references unknown ThingDef '{slot.Thing}'.");
}
if (slot.Count < 0 || slot.Count > byte.MaxValue)
{
throw new ContentLoadException($"RoomDef '{room.DefName}' slot '{slot.Key}' count must be 0{byte.MaxValue}.");
}
}
foreach (var position in room.Positions)
+6
View File
@@ -27,6 +27,12 @@ public sealed class ActionDef : Def;
public sealed class ThingDef : Def
{
public IReadOnlyList<string> Actions { get; init; } = [];
/// <summary>
/// How many pupils this thing hosts for lessons. Zero for teacher's furniture — a chair
/// must not inflate class size just because someone sits in it.
/// </summary>
public int PupilSlots { get; init; }
}
public sealed class PositionDef : Def;
+6
View File
@@ -63,6 +63,12 @@ public sealed class SlotFill
public required string Key { get; init; }
public required string Thing { get; init; }
/// <summary>
/// How many of <see cref="Thing"/> occupy this slot. Missing or non-positive JSON is 1, so
/// older maps without a count still round-trip.
/// </summary>
public int Count { get; init; } = 1;
}
public sealed class MapLink
+5
View File
@@ -140,6 +140,11 @@ public static class MapValidator
{
throw new MapValidationException($"Room '{room.Id}' slot '{fill.Key}' uses unknown or abstract ThingDef '{fill.Thing}'.");
}
if (fill.Count > byte.MaxValue)
{
throw new MapValidationException($"Room '{room.Id}' slot '{fill.Key}' count {fill.Count} is above {byte.MaxValue}.");
}
}
}
+38 -8
View File
@@ -21,11 +21,17 @@ public sealed class MapViewNode
public required string Name { get; init; }
public IReadOnlyList<string> Items { get; init; } = [];
public IReadOnlyList<MapViewItem> Items { get; init; } = [];
/// <summary>Sum of <c>ThingDef.PupilSlots × fill count</c> for this node. Zero off rooms.</summary>
public int PupilSlots { get; init; }
public IReadOnlyList<string> Positions { get; init; } = [];
}
/// <summary>One stacked thing in a room. The client formats <c>Парта ×16</c>; this is the data.</summary>
public sealed record MapViewItem(string Name, int Count);
/// <summary>
/// Builds the location tree the client draws. Labels come from the frozen catalog in the
/// requested locale; people and in-place activities are not part of this view.
@@ -47,6 +53,7 @@ public static class MapView
parentId: string.Empty,
LabelOf(catalog, locale, DefKind.Territory, territory.Def),
items: [],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Territory, territory.Def)),
};
@@ -58,6 +65,7 @@ public static class MapView
territory.Id,
LabelOf(catalog, locale, DefKind.Building, building.Def),
[],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Building, building.Def)));
}
@@ -69,23 +77,20 @@ public static class MapView
floor.Building,
FloorName(catalog, locale, floor),
[],
pupilSlots: 0,
PositionsOf(catalog, locale, DefKind.Floor, floor.Def)));
}
foreach (var room in map.Rooms)
{
var items = new List<string>(room.Slots.Count);
foreach (var fill in room.Slots)
{
items.Add(LabelOf(catalog, locale, DefKind.Thing, fill.Thing));
}
var (items, pupilSlots) = RoomContents(catalog, locale, room);
nodes.Add(Node(
MapNodeKind.Room,
room.Id,
room.Floor,
RoomName(catalog, locale, room),
items,
pupilSlots,
PositionsOf(catalog, locale, DefKind.Room, room.Def)));
}
@@ -97,7 +102,8 @@ public static class MapView
string id,
string parentId,
string name,
IReadOnlyList<string> items,
IReadOnlyList<MapViewItem> items,
int pupilSlots,
IReadOnlyList<string> positions) =>
new()
{
@@ -106,9 +112,33 @@ public static class MapView
ParentId = parentId,
Name = name,
Items = items,
PupilSlots = pupilSlots,
Positions = positions,
};
private static (IReadOnlyList<MapViewItem> Items, int PupilSlots) RoomContents(
DefCatalog catalog,
string locale,
RoomNode room)
{
var items = new List<MapViewItem>(room.Slots.Count);
var pupilSlots = 0L;
foreach (var fill in room.Slots)
{
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;
}
}
return (items, (int)Math.Clamp(pupilSlots, 0, ushort.MaxValue));
}
private static int SlotQuantity(int count) =>
count < 1 ? 1 : Math.Min(count, byte.MaxValue);
/// <summary>
/// A floor's <see cref="FloorNode.Label"/> is its designation, not a replacement name — the
/// vanilla map says "1". On its own that is a stray digit in the client's tree, so it follows
+7 -1
View File
@@ -44,15 +44,21 @@ public readonly record struct ServerSchoolGoneMessage(int SchoolId);
/// <summary>
/// Tree node in a map snapshot. Kind is <c>0</c> territory, <c>1</c> building, <c>2</c> floor, <c>3</c> room.
/// <paramref name="ParentId"/> is empty for the yard.
/// <paramref name="PupilSlots"/> is how many pupils can take a lesson here — summed from things
/// on the server, not by the client.
/// </summary>
public sealed record MapSnapshotNode(
byte Kind,
string Id,
string ParentId,
string Name,
IReadOnlyList<string> Items,
ushort PupilSlots,
IReadOnlyList<MapSnapshotItem> Items,
IReadOnlyList<string> Positions);
/// <summary>One stacked thing in a room. <paramref name="Count"/> is 1255.</summary>
public sealed record MapSnapshotItem(string Name, byte Count);
/// <summary>
/// One school's map, labelled in the Hello locale. Sent once when that school is opened, not every tick.
/// People and in-place activities are omitted — the client keeps those sections empty.
+11 -5
View File
@@ -116,11 +116,12 @@ public static class ProtocolCodec
{
size += sizeof(byte);
size += StringSize(node.Id) + StringSize(node.ParentId) + StringSize(node.Name);
size += sizeof(ushort);
size += sizeof(byte);
foreach (var item in node.Items)
{
size += StringSize(item);
size += StringSize(item.Name) + sizeof(byte);
}
size += sizeof(byte);
@@ -156,10 +157,12 @@ public static class ProtocolCodec
writer.WriteString(node.Id);
writer.WriteString(node.ParentId);
writer.WriteString(node.Name);
writer.WriteUInt16(node.PupilSlots);
writer.WriteByte((byte)node.Items.Count);
foreach (var item in node.Items)
{
writer.WriteString(item);
writer.WriteString(item.Name);
writer.WriteByte(item.Count);
}
writer.WriteByte((byte)node.Positions.Count);
@@ -263,11 +266,14 @@ public static class ProtocolCodec
var id = reader.ReadString();
var parentId = reader.ReadString();
var name = reader.ReadString();
var pupilSlots = reader.ReadUInt16();
var itemCount = reader.ReadByte();
var items = new string[itemCount];
var items = new MapSnapshotItem[itemCount];
for (var item = 0; item < itemCount; item++)
{
items[item] = reader.ReadString();
var itemName = reader.ReadString();
var count = reader.ReadByte();
items[item] = new MapSnapshotItem(itemName, count);
}
var positionCount = reader.ReadByte();
@@ -277,7 +283,7 @@ public static class ProtocolCodec
positions[position] = reader.ReadString();
}
nodes[i] = new MapSnapshotNode(kind, id, parentId, name, items, positions);
nodes[i] = new MapSnapshotNode(kind, id, parentId, name, pupilSlots, items, positions);
}
return new ServerMapSnapshotMessage(schoolId, nodes);
+1 -1
View File
@@ -4,7 +4,7 @@ namespace HSchool.Protocol;
public static class ProtocolConstants
{
/// <summary>Bumped on every breaking change to the binary layout.</summary>
public const byte Version = 4;
public const byte Version = 5;
/// <summary>Upper bound for a single WebSocket frame accepted by the server.</summary>
public const int MaxMessageSize = 8 * 1024;
+11 -4
View File
@@ -94,7 +94,7 @@ internal sealed record CatalogResponse(
Placeable(catalog.Buildings.Values, catalog, locale),
Placeable(catalog.Floors.Values, catalog, locale),
PlaceableRooms(catalog, locale),
Placeable(catalog.Things.Values, catalog, locale),
PlaceableThings(catalog, locale),
map);
private static IReadOnlyList<DefInfoResponse> Placeable<T>(IEnumerable<T> defs, DefCatalog catalog, string locale)
@@ -105,6 +105,13 @@ internal sealed record CatalogResponse(
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def)))
.ToArray();
private static IReadOnlyList<DefInfoResponse> PlaceableThings(DefCatalog catalog, string locale) =>
catalog.Things.Values
.Where(def => !def.Abstract)
.OrderBy(def => def.DefName, StringComparer.Ordinal)
.Select(def => new DefInfoResponse(def.DefName, catalog.Label(locale, def), def.PupilSlots))
.ToArray();
private static IReadOnlyList<RoomInfoResponse> PlaceableRooms(DefCatalog catalog, string locale) =>
catalog.Rooms.Values
.Where(def => !def.Abstract)
@@ -112,12 +119,12 @@ internal sealed record CatalogResponse(
.Select(def => new RoomInfoResponse(
def.DefName,
catalog.Label(locale, def),
def.Slots.Select(slot => new RoomSlotInfo(slot.Key, slot.Thing)).ToArray(),
def.Slots.Select(slot => new RoomSlotInfo(slot.Key, slot.Thing, slot.Count)).ToArray(),
def.Positions.ToArray()))
.ToArray();
}
internal sealed record DefInfoResponse(string DefName, string Label);
internal sealed record DefInfoResponse(string DefName, string Label, int PupilSlots = 0);
internal sealed record RoomInfoResponse(
string DefName,
@@ -125,4 +132,4 @@ internal sealed record RoomInfoResponse(
IReadOnlyList<RoomSlotInfo> Slots,
IReadOnlyList<string> Positions);
internal sealed record RoomSlotInfo(string Key, string Thing);
internal sealed record RoomSlotInfo(string Key, string Thing, int Count);
+8 -1
View File
@@ -449,12 +449,19 @@ internal sealed class SchoolWorker
for (var i = 0; i < view.Count; i++)
{
var node = view[i];
var items = new MapSnapshotItem[node.Items.Count];
for (var item = 0; item < node.Items.Count; item++)
{
items[item] = new MapSnapshotItem(node.Items[item].Name, (byte)node.Items[item].Count);
}
nodes[i] = new MapSnapshotNode(
(byte)node.Kind,
node.Id,
node.ParentId,
node.Name,
node.Items,
(ushort)node.PupilSlots,
items,
node.Positions);
}
@@ -4,6 +4,7 @@
"slots": [
{ "key": "board", "thing": "Blackboard" },
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
],
"positions": ["Teacher"],
@@ -22,6 +23,7 @@
"defName": "ComputerLab",
"slots": [
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "computers", "thing": "Computer", "count": 12 },
],
"positions": ["Teacher"],
@@ -1,10 +1,10 @@
[
{ "defName": "Blackboard" },
{ "defName": "StudentDesk", "parent": "Desk", "actions": ["Sit"] },
{ "defName": "StudentDesk", "parent": "Desk", "actions": ["Sit"], "pupilSlots": 1 },
{ "defName": "Bookshelf" },
{ "defName": "DiningTable" },
{ "defName": "Bench", "actions": ["Sit"] },
{ "defName": "Computer" },
{ "defName": "Computer", "pupilSlots": 1 },
{ "defName": "MedicalCouch" },
{ "defName": "Locker" },
]
+16 -11
View File
@@ -23,7 +23,7 @@
"slots": [
{ "key": "directorChair", "thing": "DirectorsChair" },
{ "key": "desk", "thing": "Desk" },
{ "key": "guestChair", "thing": "Chair" },
{ "key": "guestChair", "thing": "Chair", "count": 2 },
],
},
{
@@ -43,7 +43,7 @@
"floor": "floor-1",
"slots": [
{ "key": "table", "thing": "DiningTable" },
{ "key": "chairs", "thing": "Chair" },
{ "key": "chairs", "thing": "Chair", "count": 6 },
],
},
{
@@ -55,7 +55,8 @@
"slots": [
{ "key": "board", "thing": "Blackboard" },
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "studentDesks", "thing": "StudentDesk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
],
},
{
@@ -67,7 +68,8 @@
"slots": [
{ "key": "board", "thing": "Blackboard" },
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "studentDesks", "thing": "StudentDesk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
],
},
{
@@ -77,7 +79,7 @@
"floor": "floor-1",
"slots": [
{ "key": "counter", "thing": "DiningTable" },
{ "key": "seats", "thing": "Chair" },
{ "key": "seats", "thing": "Chair", "count": 8 },
],
},
{ "id": "restroom-1", "def": "Restroom", "building": "main", "floor": "floor-1", "label": "1" },
@@ -102,7 +104,8 @@
"slots": [
{ "key": "board", "thing": "Blackboard" },
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "studentDesks", "thing": "StudentDesk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
],
},
{
@@ -114,7 +117,8 @@
"slots": [
{ "key": "board", "thing": "Blackboard" },
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "studentDesks", "thing": "StudentDesk" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "studentDesks", "thing": "StudentDesk", "count": 16 },
],
},
{
@@ -124,7 +128,7 @@
"floor": "floor-2",
"slots": [
{ "key": "desk", "thing": "Desk" },
{ "key": "shelves", "thing": "Bookshelf" },
{ "key": "shelves", "thing": "Bookshelf", "count": 4 },
],
},
{
@@ -134,7 +138,8 @@
"floor": "floor-2",
"slots": [
{ "key": "teacherDesk", "thing": "Desk" },
{ "key": "computers", "thing": "Computer" },
{ "key": "teacherChair", "thing": "Chair" },
{ "key": "computers", "thing": "Computer", "count": 12 },
],
},
{ "id": "restroom-2", "def": "Restroom", "building": "main", "floor": "floor-2", "label": "2" },
@@ -144,7 +149,7 @@
"building": "gym",
"floor": "gym-floor",
"slots": [
{ "key": "benches", "thing": "Bench" },
{ "key": "benches", "thing": "Bench", "count": 4 },
],
},
{
@@ -153,7 +158,7 @@
"building": "gym",
"floor": "gym-floor",
"slots": [
{ "key": "lockers", "thing": "Locker" },
{ "key": "lockers", "thing": "Locker", "count": 12 },
],
},
],