feat: economy and polishing

This commit is contained in:
2026-09-05 19:12:28 +05:00
parent ca830b00b9
commit c56ec113cc
18 changed files with 868 additions and 42 deletions
+80 -1
View File
@@ -21,7 +21,7 @@ const COOLDOWN = 1400;
function connect(onMsg) {
const ws = new WebSocket(WS);
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 5 })));
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 6 })));
ws.on('message', (d) => onMsg(JSON.parse(String(d))));
return ws;
}
@@ -40,6 +40,11 @@ async function state() {
return res.json();
}
async function inventory(name) {
const res = await fetch(`${BASE}/api/inventory?name=${encodeURIComponent(name)}`);
return res.json();
}
function waitForDelta(pred, timeoutMs = 9000) {
return new Promise((resolve, reject) => {
const ws = connect((m) => {
@@ -73,6 +78,80 @@ function firstSnapshot() {
const phase = process.argv[2] ?? 'phase1';
if (phase === 'm5') {
const snap = await firstSnapshot();
if (!snap.campfire) throw new Error('no campfire state');
console.log('welcome v6 ok');
// два зрителя
await dev('/dev/command', { name: 'Тестер', text: '!рубить' });
await sleep(300);
await dev('/dev/command', { name: 'Тестер2', text: '!рубить' });
await sleep(COOLDOWN);
// подарок: у Тестера один топор → однозначно
await dev('/dev/give', { name: 'Тестер', item: 'log', qty: 5 });
await dev('/dev/command', { name: 'Тестер', text: '!подарить Тестер2 топор' });
const d1 = await waitForDelta((m) => m.events?.some((e) => e.k === 'gift'), 5000);
const gift = d1.events.find((e) => e.k === 'gift');
console.log(`gift: ${gift.name}${gift.toName}: ${gift.qty}× ${gift.item}`);
if (gift.item !== 'axe_rusty' || gift.toName !== 'Тестер2') throw new Error('bad gift');
const inv1 = await inventory('Тестер2');
if ((inv1.items?.axe_rusty ?? 0) !== 2) throw new Error('gift not delivered: ' + JSON.stringify(inv1.items));
// неоднозначность: оба топора в инвентаре → «уточни»
await dev('/dev/give', { name: 'Тестер', item: 'axe_rusty', qty: 1 });
await dev('/dev/give', { name: 'Тестер', item: 'axe_iron', qty: 1 });
await sleep(COOLDOWN);
await dev('/dev/command', { name: 'Тестер', text: '!подарить Тестер2 топор' });
const d2 = await waitForDelta((m) => m.events?.some((e) => e.k === 'blocked' && /уточни/.test(e.reason)), 5000);
console.log('ambiguous:', d2.events.find((e) => e.k === 'blocked').reason);
// склонение + количество: «брёвна 2»
await sleep(COOLDOWN);
await dev('/dev/command', { name: 'Тестер', text: '!подарить Тестер2 брёвна 2' });
const d3 = await waitForDelta((m) => m.events?.some((e) => e.k === 'gift' && e.item === 'log'), 5000);
console.log('gift2: логи ×' + d3.events.find((e) => e.k === 'gift').qty);
const inv2 = await inventory('Тестер');
if ((inv2.items?.log ?? 0) !== 3) throw new Error('log qty wrong: ' + JSON.stringify(inv2.items));
// спотлайт: камера едет к аватару (прямоугольник 620×430 применяется со следующего тика)
await dev('/dev/spotlight', { name: 'Тестер' });
const d4 = await waitForDelta((m) => m.camera?.w === 620, 5000);
const cam = d4.camera;
const st = await state();
const av = st.avatars.find((a) => a.name === 'Тестер');
const inFrame = cam && av && av.x >= cam.x && av.x <= cam.x + cam.w && av.y >= cam.y && av.y <= cam.y + cam.h;
console.log(`spotlight: камера ${cam.w}×${cam.h}, аватар (${av.x},${av.y}), в кадре: ${inFrame ? 'да' : 'НЕТ'}`);
if (!inFrame) throw new Error('camera did not follow avatar');
// дашборд: state, сброс костра, html
const admin = await fetch(`${BASE}/api/admin/state`).then((r) => r.json());
console.log(`admin: канал=${admin.channel}, зрителей=${admin.viewers}, спотлайт-очередь=${admin.spotlightQueue}`);
const reset = await fetch(`${BASE}/api/admin/campfire/reset`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ keepLedger: true }),
});
const resetBody = await reset.json();
if (resetBody.campfire?.progress !== 0) throw new Error('campfire reset failed');
const dash = await fetch(`${BASE}/dashboard/`);
console.log('dashboard:', dash.status, dash.headers.get('content-type'));
if (dash.status !== 200) throw new Error('dashboard not served');
// сброс зрителя: аватар уходит из мира (removed)
await fetch(`${BASE}/api/admin/viewer/reset`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Тестер2' }),
});
const d5 = await waitForDelta((m) => m.removed?.includes(`dev:Тестер2`), 5000);
console.log('viewer reset: removed from world ok');
console.log('M5 smoke: OK');
process.exit(0);
}
if (phase === 'phase1') {
const snap = await firstSnapshot();
console.log('welcome v5 | campfire:', JSON.stringify(snap.campfire));