// EndingScene:E1-E4 四结局演出 + 病历 + 「回到标题」按钮(PRD §9)。 import Phaser from 'phaser'; import { director } from '../core/CorruptionDirector'; import { audio } from '../audio/AudioEngine'; import { GAME_W, GAME_H, NAMED_NPCS, NPC_DEFS, type EndingId } from '../types'; import { ENDING_TITLES, E1_SUBTITLES, E2_SUBTITLE, E4_SUBTITLE, E3_RECORD, } from '../data/endings'; export class EndingScene extends Phaser.Scene { private ending: EndingId = 'E1'; private cm: Phaser.FX.ColorMatrix | null = null; constructor() { super('EndingScene'); } create(data: { ending?: EndingId }): void { this.ending = data?.ending ?? 'E1'; if (this.scene.isActive('UIScene')) this.scene.stop('UIScene'); this.cameras.main.clearFX(); this.cm = this.cameras.main.postFX.addColorMatrix(); this.cameras.main.fadeIn(400, 0, 0, 0); switch (this.ending) { case 'E1': void this.playE1(); break; case 'E2': void this.playE2(); break; case 'E3': void this.playE3(); break; case 'E4': void this.playE4(); break; } } private wait(ms: number): Promise { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); } private subtitle(text: string, y = GAME_H - 24): Phaser.GameObjects.Text { return this.add.text(GAME_W / 2, y, text, { fontFamily: 'monospace', fontSize: '9px', color: '#ffffff', stroke: '#000000', strokeThickness: 2, }).setOrigin(0.5); } private endScreen(): void { this.add.rectangle(GAME_W / 2, GAME_H - 9, GAME_W, 14, 0x000000, 0.7); this.add.text(GAME_W / 2 - 40, GAME_H - 9, ENDING_TITLES[this.ending], { fontFamily: 'monospace', fontSize: '8px', color: '#cccccc', }).setOrigin(0.5); const btn = this.add.text(GAME_W / 2 + 56, GAME_H - 9, '【回到标题】', { fontFamily: 'monospace', fontSize: '8px', color: '#F5A623', }).setOrigin(0.5).setInteractive({ useHandCursor: true }); btn.on('pointerdown', () => { // 重开即从 Day 1 全新开始,无继承 this.cameras.main.fadeOut(300, 0, 0, 0); this.cameras.main.once('camerafadeoutcomplete', () => { this.scene.start('TitleScene'); }); }); } /** E1 糖衣:全村微笑合影定格,饱和度缓慢推到 1.5 */ private async playE1(): Promise { this.cameras.main.setBackgroundColor('#8FD3FF'); this.add.rectangle(GAME_W / 2, GAME_H - 40, GAME_W, 80, 0x7dc95e); this.add.rectangle(GAME_W / 2, 30, 200, 20, 0xf7b7cb).setStrokeStyle(2, 0xf5a623); this.add.text(GAME_W / 2, 30, '第 413 届丰收庆典', { fontFamily: 'monospace', fontSize: '10px', color: '#fff', }).setOrigin(0.5); // 全村微笑合影(7 组 NPC + 主角) const all: string[] = [...NAMED_NPCS.map((id) => NPC_DEFS[id].spriteKey), 'player']; all.forEach((key, i) => { const x = 40 + i * 34; this.add.sprite(x, GAME_H - 60, key, 0).setOrigin(0.5, 1).setScale(1.5); }); // 主角头像永远同一帧微笑(支柱 2) this.add.image(GAME_W / 2, GAME_H - 100, 'portrait_player_smile').setScale(2); // 饱和度缓慢推到 1.5 let sat = 1.0; this.tweens.addCounter({ from: 1.0, to: 1.5, duration: 5000, onUpdate: (tw) => { sat = tw.getValue() ?? 1; this.cm?.reset(); this.cm?.saturate(sat); }, }); await this.wait(3500); this.subtitle(E1_SUBTITLES[0]); await this.wait(2500); this.subtitle(E1_SUBTITLES[1], GAME_H - 40); await this.wait(2000); this.endScreen(); } /** E2 回收:画面骤停,负片 2 秒,心电滴一响;回到 Day 1 清晨的床上 */ private async playE2(): Promise { // 骤停:黑场 this.cameras.main.setBackgroundColor('#000000'); await this.wait(600); // 负片 2 秒 + 心电滴声一响 this.cm?.negative(); audio.ecg(); await this.wait(2000); this.cm?.reset(); // 回到 Day 1 清晨的床上(小屋地图做背景),日历翻回同一天 this.cameras.main.setBackgroundColor('#1a1a3a'); const map = this.make.tilemap({ key: 'map_house' }); const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy'; const tiles = map.addTilesetImage(tsName, 'tileset'); if (tiles) { map.createLayer('ground', tiles, 80, 30); map.createLayer('obstacles', tiles, 80, 30); } this.add.sprite(120, 96, 'player', 0).setOrigin(0.5, 1); // 日历(tiles16 帧 64) this.add.image(150, 60, 'tiles16', 64); this.tweens.add({ targets: this.add.image(150, 60, 'tiles16', 64).setAlpha(0.6), alpha: 0, duration: 1500, yoyo: true, repeat: 1, }); await this.wait(1800); this.subtitle(E2_SUBTITLE); await this.wait(2200); this.endScreen(); } /** E3 苏醒:病历特写 → 最后一帧微笑消失 + 窗外灰麻雀 */ private async playE3(): Promise { this.cameras.main.setBackgroundColor('#f5f5f5'); this.cameras.main.fadeIn(1000, 255, 255, 255); await this.wait(1200); // 病房背景:白床 + 监护仪 this.add.image(60, 100, 'tiles16', 87).setOrigin(0).setScale(2); this.add.image(92, 100, 'tiles16', 88).setOrigin(0).setScale(2); this.add.image(60, 132, 'tiles16', 89).setOrigin(0).setScale(2); this.add.image(92, 132, 'tiles16', 90).setOrigin(0).setScale(2); this.add.image(140, 96, 'tiles16', 92).setOrigin(0).setScale(2); // 病历特写 const card = this.add.rectangle(GAME_W / 2, GAME_H / 2, 240, 120, 0xffffff) .setStrokeStyle(2, 0xbdbdbd); void card; const lines = [ E3_RECORD.header, '─────────────────', `${E3_RECORD.labels.name}:${director.playerName}`, `${E3_RECORD.labels.no}:${E3_RECORD.no}`, `${E3_RECORD.labels.duration}:${director.playTimeFormatted}`, ]; this.add.text(GAME_W / 2, GAME_H / 2, lines.join('\n'), { fontFamily: 'monospace', fontSize: '10px', color: '#333333', align: 'center', lineSpacing: 6, }).setOrigin(0.5); await this.wait(5000); // 最后一帧:微笑消失,变成一张疲惫而真实的脸(支柱 2 全流程唯一一次变化) this.cameras.main.fadeOut(800, 0, 0, 0); await this.wait(850); this.children.removeAll(); this.cameras.main.setBackgroundColor('#dadada'); this.cameras.main.fadeIn(600, 0, 0, 0); // 窗 this.add.rectangle(GAME_W / 2, 80, 120, 80, 0xf5f5f5).setStrokeStyle(2, 0x9e9e9e); // 窗外,一只灰色的、真的麻雀 const sparrow = this.add.graphics(); sparrow.fillStyle(0x777777, 1); sparrow.fillCircle(GAME_W / 2 + 30, 96, 3); // 身 sparrow.fillCircle(GAME_W / 2 + 33, 93, 2); // 头 sparrow.fillStyle(0x555555, 1); sparrow.fillTriangle(GAME_W / 2 + 27, 96, GAME_W / 2 + 22, 94, GAME_W / 2 + 27, 99); // 尾 sparrow.fillStyle(0x333333, 1); sparrow.fillCircle(GAME_W / 2 + 34, 92, 0.5); // 眼 // 真实的脸 this.add.image(GAME_W / 2, GAME_H - 60, 'portrait_player_real').setScale(2); this.subtitle('(疗程结束。欢迎回来。)', GAME_H - 24); await this.wait(3000); this.endScreen(); } /** E4 静默:空广场,彩带在没有风的空气里飘 */ private async playE4(): Promise { this.cameras.main.setBackgroundColor('#cfcfcf'); // 空广场(plaza 地图做背景,无 NPC) const map = this.make.tilemap({ key: 'map_plaza' }); const tsName = map.tilesets.length > 0 ? map.tilesets[0].name : 'candy'; const tiles = map.addTilesetImage(tsName, 'tileset'); if (tiles) { map.createLayer('ground', tiles, 0, -60); map.createLayer('obstacles', tiles, 0, -60); } this.cm?.saturate(0.4); // 彩带在没有风的空气里飘 const ribbons: Phaser.GameObjects.Image[] = []; for (let i = 0; i < 6; i++) { const r = this.add.image(40 + i * 50, 30 + (i % 3) * 20, 'tiles16', [29, 30, 31][i % 3]); ribbons.push(r); this.tweens.add({ targets: r, y: r.y + 6, duration: 2200 + i * 300, yoyo: true, repeat: -1, ease: 'Sine.easeInOut', }); } await this.wait(3500); this.subtitle(E4_SUBTITLE); await this.wait(2500); // 黑屏 this.cameras.main.fadeOut(1500, 0, 0, 0); await this.wait(1600); this.endScreen(); } }