100 Days After — complete playable survival strategy game
- Seeded procedural runs (survivors, locations, weather, events) - 100-day campaign with 5 phases, scripted story beats, Day-100 finale - 8 endings incl. hidden 'First Light Again' - Data-driven events (people/world/story packs) with callback chains - Relationships, memories, grief, karma/hope systems - Tactical encounters (fight/hide/run/negotiate) with honest odds - 8 upgradeable camp buildings, 13 location templates - Canvas parallax scenes, procedural SVG portraits, weather FX - Synthesized adaptive audio (WebAudio, no assets) - Save slots + autosave (localStorage), shareable seeds - Tests: headless multi-seed simulator + browser E2E (playwright)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/* Browser smoke test: boots the game, starts a run, plays a few days
|
||||
* through the real UI, and fails on any console error or page crash.
|
||||
*
|
||||
* node scripts/smoke.mjs [url]
|
||||
*/
|
||||
import { chromium } from 'playwright-core';
|
||||
|
||||
const url = process.argv[2] ?? 'http://127.0.0.1:5199';
|
||||
const exe = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell';
|
||||
|
||||
const browser = await chromium.launch({ executablePath: exe, args: ['--no-sandbox','--disable-gpu','--disable-dev-shm-usage'] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
page.setDefaultTimeout(9000);
|
||||
|
||||
const errors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push('console: ' + msg.text());
|
||||
});
|
||||
page.on('pageerror', err => errors.push('pageerror: ' + err.message));
|
||||
|
||||
const step = async (name, fn) => {
|
||||
try { await fn(); console.log('ok ', name); }
|
||||
catch (e) { console.log('FAIL', name, '-', e.message.split('\n')[0]); errors.push(name + ': ' + e.message); }
|
||||
};
|
||||
|
||||
await step('load page', async () => {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('.title-screen', { timeout: 10000 });
|
||||
});
|
||||
|
||||
await step('new run dialog', async () => {
|
||||
await page.click('text=NEW RUN');
|
||||
await page.waitForSelector('.seed-input');
|
||||
});
|
||||
|
||||
await step('start run', async () => {
|
||||
await page.fill('.seed-input', '777001');
|
||||
await page.click('text=BEGIN DAY 1');
|
||||
await page.waitForSelector('.game-layout', { timeout: 10000 });
|
||||
});
|
||||
|
||||
await step('HUD renders', async () => {
|
||||
await page.waitForFunction(() => document.querySelector('#hud-day')?.textContent?.includes('DAY'));
|
||||
const resCount = await page.locator('.res-chip').count();
|
||||
if (resCount !== 7) throw new Error('expected 7 resource chips, got ' + resCount);
|
||||
});
|
||||
|
||||
await step('survivor list renders', async () => {
|
||||
const n = await page.locator('.surv-card:not(.dead)').count();
|
||||
if (n < 3 || n > 4) throw new Error('unexpected survivor count ' + n);
|
||||
await page.locator('.surv-card:not(.dead)').first().click();
|
||||
await page.waitForSelector('.sd-body');
|
||||
await page.click('.modal-x');
|
||||
});
|
||||
|
||||
await step('map tab renders', async () => {
|
||||
await page.click('[data-tab="map"]');
|
||||
const entries = await page.locator('.map-entry').count();
|
||||
if (entries < 2) throw new Error('map too empty');
|
||||
// hover preview shouldn't crash
|
||||
await page.locator('.map-entry').nth(1).hover();
|
||||
await page.click('[data-tab="log"]');
|
||||
});
|
||||
|
||||
await step('scavenge action flow', async () => {
|
||||
await page.click('.action:has-text("SCAVENGE")');
|
||||
await page.waitForSelector('.site-grid .site-card');
|
||||
await page.locator('.site-card').first().click();
|
||||
const rows = page.locator('.party-check .check-row');
|
||||
await rows.first().locator('input').check();
|
||||
if (await rows.count() > 1) await rows.nth(1).locator('input').check();
|
||||
await page.click('text=HEAD OUT');
|
||||
await page.waitForSelector('.report, .enc-modal', { timeout: 8000 });
|
||||
// resolve possible encounter
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const enc = page.locator('.enc-modal .btn.choice');
|
||||
if (await enc.count()) { await enc.first().click(); await page.waitForTimeout(300); continue; }
|
||||
const cont = page.locator('.modal-card .btn:has-text("CONTINUE")');
|
||||
if (await cont.count()) { await cont.first().click(); break; }
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(400);
|
||||
});
|
||||
|
||||
// play several days via END DAY until an event modal appears and handle choices
|
||||
for (let d = 0; d < 8; d++) {
|
||||
await step(`day cycle ${d + 1}`, async () => {
|
||||
// close any open report/event by clicking CONTINUE / first choice repeatedly
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const choice = page.locator('.ev-choice:not(.locked)');
|
||||
const cont = page.locator('.modal-card .btn:has-text("CONTINUE")');
|
||||
const encBtns = page.locator('.enc-modal .btn.choice');
|
||||
if (await encBtns.count()) { await encBtns.first().click(); await page.waitForTimeout(250); continue; }
|
||||
if (await choice.count()) { await choice.first().click(); await page.waitForTimeout(250); continue; }
|
||||
if (await cont.count()) { await cont.first().click(); await page.waitForTimeout(250); continue; }
|
||||
break;
|
||||
}
|
||||
const endBtn = page.locator('#btn-endday');
|
||||
if (await endBtn.isEnabled()) await endBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const cont = page.locator('.modal-card .btn:has-text("CONTINUE")');
|
||||
const encBtns = page.locator('.enc-modal .btn.choice');
|
||||
const evChoice = page.locator('.ev-choice:not(.locked)');
|
||||
if (await encBtns.count()) { await encBtns.first().click(); await page.waitForTimeout(250); continue; }
|
||||
if (await cont.count()) { await cont.first().click(); await page.waitForTimeout(350); continue; }
|
||||
if (await evChoice.count()) { await evChoice.first().click(); await page.waitForTimeout(250); continue; }
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await step('save to slot', async () => {
|
||||
await page.click('#btn-menu');
|
||||
await page.click('text=SAVE SLOT 1');
|
||||
await page.waitForTimeout(300);
|
||||
});
|
||||
|
||||
await step('reload + continue via load modal', async () => {
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('.title-screen');
|
||||
await page.click('text=LOAD RUN');
|
||||
await page.waitForSelector('.save-row');
|
||||
await page.locator('.save-row .btn:has-text("LOAD")').first().click();
|
||||
await page.waitForSelector('.game-layout', { timeout: 8000 });
|
||||
});
|
||||
|
||||
console.log('\n---- RESULT ----');
|
||||
if (errors.length) {
|
||||
console.log(errors.length + ' ERRORS:');
|
||||
for (const e of errors.slice(0, 20)) console.log(' -', e);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log('SMOKE TEST PASSED — no console/page errors across boot, play, save, load.');
|
||||
}
|
||||
await browser.close();
|
||||
Reference in New Issue
Block a user