Files
ModelTest20260714/Kimi-K3/src/scenes/EndingScene.ts
T

221 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// EndingSceneE1-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<void> {
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<void> {
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<void> {
// 骤停:黑场
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<void> {
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<void> {
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();
}
}