Add city gameplay, snapshot migrations, administration and Compose operations

This commit is contained in:
mrleo1nid committed 2026-10-30 22:17:17 +03:00
1 parent 3b1fcd996a
commit 85df37c26e
24 files changed
+1042 -55

No files matched your search

+17
View File
@@ -7,6 +7,7 @@ import type { World } from './types';
import { TileLoader } from '../client/tiles';
import type { Bounds } from './city/format';
import { RoomConnection } from '../client/connection';
import { showAdmin } from '../client/admin';
import { create, fromBinary, toBinary } from '@bufbuild/protobuf';
import { CityJobSchema, CreateCitySchema } from '../client/generated/city_pb';
@@ -325,6 +326,14 @@ async function showTiledCity(url: string, connection?: RoomConnection) {
}
const vote = document.createElement('span'); vote.className = 'room-vote'; vote.setAttribute('role','status'); controls.appendChild(vote);
tiledViewer.root.querySelector('.viewer-top')!.appendChild(controls);
const gameplay = document.createElement('div'); gameplay.className = 'row';
const state = document.createElement('span'); state.className = 'room-gameplay'; state.setAttribute('role', 'status'); gameplay.appendChild(state);
for (const [action, label] of [['supplies', 'Запасы · 2¤/жит.'], ['meal', 'Обед · 1¤/жит.'], ['rest', 'Отдых · 3¤/жит.']]) {
const button = document.createElement('button'); button.className = 'btn'; button.dataset.action = action; button.textContent = label;
button.title = 'Общая казна. Одна команда на игровой час; исполнение в следующую игровую минуту.';
button.onclick = () => connection.intervene(action); gameplay.appendChild(button);
}
tiledViewer.root.querySelector('.viewer-top')!.appendChild(gameplay);
}
(window as any).viewer = viewer;
hideProgress();
@@ -361,6 +370,13 @@ async function showLobby() {
status: text => { status.textContent = text; viewer?.setLoadStatus(text); },
clock: c => { const clock = app.querySelector('.room-clock'); if (clock) clock.textContent = `День ${Math.floor(c.gameSeconds / 86400) + 1} · ${String(Math.floor(c.gameSeconds/3600)%24).padStart(2,'0')}:${String(Math.floor(c.gameSeconds/60)%60).padStart(2,'0')} · ×${c.speed}`; },
vote: v => { const vote = app.querySelector('.room-vote'); if (vote) vote.textContent = v.active ? `Голосование за ×${v.speed}: ${v.yes}/${v.players}, ${v.remainingSeconds} с` : `${v.players} игроков`; },
gameplay: g => {
const state = app.querySelector('.room-gameplay');
const names: Record<string, string> = { harvest: 'Урожай', illness: 'Болезнь', rain: 'Дождь', supplies: 'Запасы пополнены', meal: 'Общий обед', rest: 'Отдых' };
const event = g.events.at(-1);
if (state) state.textContent = `Казна ${g.treasury}¤ · Еда ${g.food} · Голод ${g.needs?.hunger ?? 0}% · Усталость ${g.needs?.fatigue ?? 0}% · Настроение ${g.needs?.morale ?? 0}%${g.pending.length ? ' · Команда в очереди' : ''}${event ? ` · ${names[event.kind] ?? event.kind}` : ''}`;
for (const button of app.querySelectorAll<HTMLButtonElement>('[data-action]')) button.disabled = g.pending.length > 0 || g.minute < g.nextInterventionMinute;
},
});
roomConnection = connection;
};
@@ -394,6 +410,7 @@ async function showLobby() {
(async () => {
const params = new URLSearchParams(location.search);
if (params.has('admin')) return showAdmin(app);
if (params.has('play')) return showLobby();
if (params.has('city')) return showTiledCity(params.get('city')!);
if (params.has('last') && (await hasWorld())) {