import { expect, test } from '@playwright/test'; import { readFileSync, readdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { ANNOUNCEMENTS, C9_LINES, CLUES, CORRUPTIONS, MAP_KEYS, TASKS } from '../src/game/data'; test('fixed PRD tables remain exact', () => { expect(TASKS).toHaveLength(15); expect(Object.keys(CLUES)).toHaveLength(9); expect(CORRUPTIONS).toEqual([ ['幸福', '辛福'], ['开心', '开欣'], ['美好', '美妤'], ['大家', '大伽'], ['微笑', '微效'], ['明天', '名天'], ['甜', '恬'], ['阳光', '央光'], ['朋友', '棚友'], ['永远', '泳远'], ]); expect(ANNOUNCEMENTS).toEqual({ 1: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', 2: '早上好呀!新的一天,新的幸福。祝你开心!', 3: '早上好!今天要把幸福装得满满的哦。要开心!', 4: '早上好呀!今天的幸福任务贴在板上啦。祝你开心!', 5: '早上好。今天是自由日,你可以自由地选择幸福。要开心哦。', 6: '明天就是庆典了。大家都会到场,大家都会微笑。你必须开心。', 7: '庆典开始了。来吧。', }); expect(CLUES).toEqual({ C1: '每一页都是同一个日期', C2: '手写小字:「按疗程配比。甜度:最大。」', C3: '所有邮戳是同一天', C4: '署名闪过乱码「PT-0▓」', C5: '同一商品重复排列,标签全部空白', C6: '「第 412 届丰收庆典」上覆盖着更早的「第 411 届」「第 410 届」', C7: '井底回声:极轻的监护仪滴声(靠近并交互 5 秒)', C8: '芙医生桌上的表格抬头被手肘挡住,只露出「…谷情绪疗…」', C9: '三段递进:「湖水一直很平静」→「面包一直是那个味道」→「孩子,今天是第几个今天?」', }); expect(C9_LINES).toEqual({ 2: '湖水一直很平静', 4: '面包一直是那个味道', 6: '孩子,今天是第几个今天?' }); expect(TASKS.slice(7, 10).map(({ title, reward }) => ({ title, reward }))).toEqual(TASKS.slice(0, 3).map(({ title, reward }) => ({ title, reward }))); }); test('Aseprite sheets and Tiled exports have the required dimensions and layers', () => { const pngSize = (path: string) => { const png = readFileSync(path); return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) }; }; expect(pngSize(resolve('public/assets/tileset.png'))).toEqual({ width: 256, height: 64 }); expect(pngSize(resolve('public/assets/characters.png'))).toEqual({ width: 256, height: 96 }); expect(pngSize(resolve('public/assets/portraits.png'))).toEqual({ width: 432, height: 48 }); expect(readdirSync('public/assets/source').filter((file) => file.endsWith('.aseprite'))).toHaveLength(3); const expectedSizes: Record = { HouseScene: [12, 8], PlazaScene: [30, 20], BakeryScene: [15, 10], ShopScene: [15, 10], ClinicScene: [12, 8], LakeScene: [24, 16], FieldScene: [20, 14], WellScene: [14, 10], MistScene: [20, 8], WhiteScene: [30, 8], }; const maps = new Map>(); MAP_KEYS.forEach((key) => { const map = JSON.parse(readFileSync(resolve(`public/maps/${key}.json`), 'utf8')) as { width: number; height: number; layers: Array<{ name: string; type: string; objects?: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }>; tilesets: Array<{ tilecount: number }>; }; expect([map.width, map.height]).toEqual(expectedSizes[key]); expect(map.layers.map((layer) => layer.name)).toEqual(['ground', 'obstacles', 'objects']); expect(map.tilesets[0].tilecount).toBe(64); maps.set(key, map as unknown as Record); }); for (const key of MAP_KEYS) { const map = maps.get(key) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> }; const doors = map.layers.find((layer) => layer.name === 'objects')?.objects.filter((object) => object.type === 'door') ?? []; doors.forEach((door) => { const target = door.properties?.find((property) => property.name === 'target')?.value; const targetMap = maps.get(String(target)) as unknown as { layers: Array<{ name: string; objects: Array<{ type: string; properties?: Array<{ name: string; value: string }> }> }> }; const reverse = targetMap.layers.find((layer) => layer.name === 'objects')?.objects.some((object) => object.type === 'door' && object.properties?.some((property) => property.name === 'target' && property.value === key)); expect(reverse, `${key} -> ${target} needs a reverse door`).toBe(true); }); } }); test('director enforces rewards, clues, sad behavior, stages, overflow, and endings', async ({ page }) => { await page.goto('/'); await page.locator('#player-name').press('Enter'); await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true); const result = await page.evaluate(({ tasks, corruptionPairs, clueIds }) => { const d = window.__HONEY_GAME__.registry.get('director') as any; const rewards = tasks.map((task: { id: string; day: number; reward: number }) => { d.reset('Test'); d.setDay(task.day); d.acceptDay(); d.setHappiness(0); d.completeTask(task.id); return [task.id, d.snapshot.happiness]; }); d.reset('Test'); const clueDeltas: number[] = []; clueIds.forEach((id) => { const before = d.snapshot.awareness; d.discoverClue(id); clueDeltas.push(d.snapshot.awareness - before); }); d.reset('Test'); d.setHappiness(50); ['refuse', 'negative', 'idle', 'cling', 'rain'].forEach((key) => d.sadBehavior(key)); const sad = { happiness: d.snapshot.happiness, awareness: d.snapshot.awareness }; const stages = [10, 40, 70, 90, 100].map((value) => { d.setHappiness(value); return d.stage; }); const corruptions = corruptionPairs.map(([clean, broken]: [string, string]) => { d.setHappiness(100); return [broken, d.corruptText(`${clean}。`, '咪咪')]; }); d.reset('Test'); d.setHappiness(100); d.adjustHappiness(-5); d.adjustHappiness(-5); d.adjustHappiness(-5); const overflow = { happiness: d.snapshot.happiness, overflow: d.snapshot.overflow }; d.reset('Test'); d.setHappiness(30); const small = [d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.smallInteraction('water'), d.snapshot.happiness]; const endings: string[] = []; d.reset('Test'); d.setHappiness(90); endings.push(d.ending()); d.setAwareness(30); endings.push(d.ending()); d.reset('Test'); clueIds.slice(0, 6).forEach((id) => d.discoverClue(id)); d.setAwareness(70); endings.push(d.ending()); d.reset('Test'); d.setHappiness(10); d.setDay(7); d.snapshot.cheerEscapes = 3; endings.push(d.ending()); d.reset('Test'); d.setHappiness(90); d.setAwareness(70); endings.push(d.ending() ?? 'none'); d.reset('Test'); d.setHappiness(10); d.snapshot.lowSince = performance.now() - 121_000; d.tick(); const forcedCheer = d.snapshot.cheerActive; d.reset('Test'); d.snapshot.dayStartedAt = performance.now() - 241_000; d.acceptDay(); ['T1', 'T2', 'T3'].forEach((id) => d.completeTask(id)); const duskReached = d.snapshot.sunset; d.advanceDay(); const nextDay = { day: d.snapshot.day, boardRead: d.snapshot.boardRead, sunset: d.snapshot.sunset }; return { rewards, clueDeltas, clueCount: d.snapshot.clues.size, sad, stages, corruptions, overflow, small, endings, forcedCheer, duskReached, nextDay }; }, { tasks: TASKS.map(({ id, day, reward }) => ({ id, day, reward })), corruptionPairs: CORRUPTIONS, clueIds: Object.keys(CLUES) }); expect(result.rewards).toEqual(TASKS.map(({ id, reward }) => [id, reward])); expect(result.clueDeltas).toEqual(Array(9).fill(8)); expect(result.sad).toEqual({ happiness: 25, awareness: 15 }); expect(result.stages).toEqual(['S0', 'S1', 'S2', 'S3', 'S4']); result.corruptions.forEach(([broken, rendered]) => expect(rendered).toContain(broken)); expect(result.overflow).toEqual({ happiness: 100, overflow: 0 }); expect(result.small).toEqual([true, true, true, false, 33]); expect(result.endings).toEqual(['E1', 'E2', 'E3', 'E4', 'none']); expect(result.forcedCheer).toBe(true); expect(result.duskReached).toBe(true); expect(result.nextDay).toEqual({ day: 2, boardRead: false, sunset: false }); }); test('Day 6 smile timer completes at 60 seconds and scripted rain records sadness', async ({ page }) => { await page.goto('/'); await page.locator('#player-name').press('Enter'); await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('HouseScene'))).toBe(true); await page.evaluate(() => (window.__HONEY_GAME__.registry.get('director') as { emit: (event: string, key: string) => void }).emit('debug-teleport', 'PlazaScene')); await expect.poll(() => page.evaluate(() => window.__HONEY_GAME__.scene.isActive('PlazaScene'))).toBe(true); const result = await page.evaluate(() => { const director = window.__HONEY_GAME__.registry.get('director') as any; const scene = window.__HONEY_GAME__.scene.getScene('PlazaScene') as any; director.reset('Test'); director.setDay(6); director.acceptDay(); director.setHappiness(30); director.snapshot.dayStartedAt = performance.now() - 241_000; director.completeTask('T13'); director.setT14(true); scene.t14Start = performance.now() - 60_100; scene.updateT14(); const smile = { completed: director.snapshot.completed.has('T14'), active: director.snapshot.t14Active, sunset: director.snapshot.sunset, happiness: director.snapshot.happiness }; scene.rainSince = performance.now() - 10_100; scene.updateRain(16); return { smile, rain: { awareness: director.snapshot.awareness, happiness: director.snapshot.happiness } }; }); expect(result.smile).toEqual({ completed: true, active: false, sunset: true, happiness: 55 }); expect(result.rain).toEqual({ awareness: 3, happiness: 50 }); });