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:
2026-08-23 06:58:47 +00:00
commit 486201655b
36 changed files with 9989 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import { chromium } from 'playwright-core';
// build a Day-100-ready save with node + tsx first
const { execSync } = await import('node:child_process');
const stateJson = execSync(`npx tsx -e "
import { createNewGame } from './src/engine/state';
const s = createNewGame(2024);
s.day = 99; s.ap = 3;
s.flags.evacPrep = 1; s.flags.heardTruth = 1; s.flags.militaryRep = 3;
s.res.food = 40; s.res.water = 40; s.res.medicine = 6;
console.log(JSON.stringify({ v: s.version, ts: Date.now(), state: s }));
"`, { encoding: 'utf8', cwd: '/root/100days' }).trim();
const browser = await chromium.launch({ executablePath: '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell', args:['--no-sandbox','--disable-gpu','--disable-dev-shm-usage'] });
const ctx = await browser.newContext({ viewport:{width:1440,height:900} });
await ctx.addInitScript((save) => {
localStorage.setItem('d100_save_auto', save);
}, stateJson);
const page = await ctx.newPage();
page.setDefaultTimeout(9000);
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('http://127.0.0.1:5199', { waitUntil:'domcontentloaded' });
await page.waitForSelector('.title-screen');
await page.click('text=CONTINUE');
await page.waitForSelector('.game-layout');
console.log('loaded day:', await page.evaluate(()=>document.querySelector('#hud-day')?.textContent));
// end days 99 -> 100 -> finale
for (let i = 0; i < 10; i++) {
// drain modals
for (let k = 0; k < 12; k++) {
const r = await page.evaluate(() => {
const ov = document.querySelectorAll('.modal-root .overlay');
const last = ov[ov.length - 1];
if (!last) return { none: true };
const btns = [...last.querySelectorAll('button')].filter(b => !b.disabled && b.offsetParent !== null);
if (/DAY 100/i.test(last.querySelector('.modal-head h2')?.textContent ?? '')) {
const muster = btns.find(b => /convoy|muster/i.test(b.textContent));
const any = muster ?? btns[0];
any.click();
return { finale: true, clicked: any.textContent.slice(0, 30) };
}
const t = btns.find(b => /CONTINUE|STEEL/i.test(b.textContent))
|| btns.find(b => b.classList.contains('ev-choice') && !b.classList.contains('locked'))
|| btns.find(b => b.classList.contains('choice'));
if (!t) return { stuck: true };
t.click();
return { clicked: true };
});
if (r.none) break;
if (r.finale) { console.log('FINALE choice:', r.clicked); continue; }
if (r.stuck) { console.log('STUCK on modal'); break; }
await page.waitForTimeout(300);
}
const ended = await page.evaluate(() => !!document.querySelector('.ending-modal'));
if (ended) break;
const btn = page.locator('#btn-endday');
if (!(await btn.isEnabled())) break;
await btn.click();
await page.waitForTimeout(500);
}
await page.waitForTimeout(800);
const ending = await page.evaluate(() => {
const el = document.querySelector('.ending-title');
return el ? el.textContent : null;
});
await page.screenshot({ path: 'shots/ending.png' });
console.log('ENDING SHOWN:', ending);
console.log(errors.length ? 'PAGE ERRORS: ' + errors.join(' | ') : 'no page errors');
await browser.close();
if (!ending || errors.length) process.exit(1);
console.log('FINALE TEST PASSED');