Honey Village: six model builds + hub frontend, Dockerized for Dokploy

This commit is contained in:
2026-07-15 10:50:12 -04:00
commit 67253bded5
523 changed files with 58879 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
// 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();
}
}