Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// 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<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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user