257 lines
8.0 KiB
TypeScript
257 lines
8.0 KiB
TypeScript
import Phaser from 'phaser';
|
|
import { soundDirector } from '../audio/SoundDirector';
|
|
import { prologuePages, type PortraitKey, type StoryPage } from '../data/scenario';
|
|
import { palette } from '../ui/palette';
|
|
|
|
const portraitKeys: Record<PortraitKey, string> = {
|
|
liuBei: 'portrait-liu-bei',
|
|
guanYu: 'portrait-guan-yu',
|
|
zhangFei: 'portrait-zhang-fei'
|
|
};
|
|
|
|
type AlphaObject = Phaser.GameObjects.Image | Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text;
|
|
|
|
type StorySceneData = {
|
|
pages?: StoryPage[];
|
|
nextScene?: string;
|
|
nextSceneData?: Record<string, unknown>;
|
|
};
|
|
|
|
export class StoryScene extends Phaser.Scene {
|
|
private pageIndex = 0;
|
|
private pages: StoryPage[] = prologuePages;
|
|
private nextScene = 'BattleScene';
|
|
private nextSceneData?: Record<string, unknown>;
|
|
private transitioning = false;
|
|
private background?: Phaser.GameObjects.Image;
|
|
private chapterText?: Phaser.GameObjects.Text;
|
|
private portrait?: Phaser.GameObjects.Image;
|
|
private portraitFrame?: Phaser.GameObjects.Rectangle;
|
|
private portraitDivider?: Phaser.GameObjects.Rectangle;
|
|
private nameText?: Phaser.GameObjects.Text;
|
|
private bodyText?: Phaser.GameObjects.Text;
|
|
private progressText?: Phaser.GameObjects.Text;
|
|
|
|
constructor() {
|
|
super('StoryScene');
|
|
}
|
|
|
|
init(data?: StorySceneData) {
|
|
this.pages = data?.pages?.length ? data.pages : prologuePages;
|
|
this.nextScene = data?.nextScene ?? 'BattleScene';
|
|
this.nextSceneData = data?.nextSceneData;
|
|
this.pageIndex = 0;
|
|
this.transitioning = false;
|
|
}
|
|
|
|
create() {
|
|
const { width, height } = this.scale;
|
|
this.add.rectangle(0, 0, width, height, 0x0b0d12).setOrigin(0);
|
|
this.background = this.add.image(width / 2, height / 2, this.pages[0].background);
|
|
this.drawSceneShade(width, height);
|
|
this.drawDialogPanel(width, height);
|
|
this.showPage(0, true);
|
|
|
|
this.input.on('pointerdown', () => this.advance());
|
|
this.input.keyboard?.on('keydown-SPACE', () => this.advance());
|
|
this.input.keyboard?.on('keydown-ENTER', () => this.advance());
|
|
}
|
|
|
|
private drawSceneShade(width: number, height: number) {
|
|
const shade = this.add.graphics();
|
|
shade.fillStyle(0x040608, 0.22);
|
|
shade.fillRect(0, 0, width, height);
|
|
shade.fillStyle(0x040608, 0.5);
|
|
shade.fillRect(0, height - 270, width, 270);
|
|
shade.fillStyle(0x040608, 0.26);
|
|
shade.fillRect(0, 0, width, 96);
|
|
}
|
|
|
|
private drawDialogPanel(width: number, height: number) {
|
|
const panelY = height - 238;
|
|
const panelX = 72;
|
|
const panelW = width - 144;
|
|
const panelH = 172;
|
|
|
|
this.add.rectangle(panelX, panelY, panelW, panelH, palette.panelDark, 0.88).setOrigin(0);
|
|
this.add.rectangle(panelX, panelY, panelW, 3, palette.gold, 0.9).setOrigin(0);
|
|
this.add.rectangle(panelX, panelY + panelH, panelW, 1, palette.gold, 0.35).setOrigin(0);
|
|
|
|
this.chapterText = this.add.text(panelX, panelY - 42, '', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
|
|
fontSize: '22px',
|
|
color: '#d8b15f',
|
|
fontStyle: '700',
|
|
padding: { top: 4, bottom: 4 },
|
|
shadow: { offsetX: 0, offsetY: 2, color: '#05070a', blur: 6, fill: true }
|
|
});
|
|
|
|
this.portraitFrame = this.add.rectangle(panelX + 86, panelY + 86, 136, 136, 0x090d12, 0.88);
|
|
this.portraitFrame.setStrokeStyle(2, palette.gold, 0.58);
|
|
|
|
this.portrait = this.add.image(panelX + 86, panelY + 86, 'portrait-liu-bei');
|
|
this.portrait.setDisplaySize(128, 128);
|
|
|
|
this.portraitDivider = this.add.rectangle(panelX + 166, panelY + 28, 1, panelH - 56, palette.gold, 0.22).setOrigin(0);
|
|
|
|
this.nameText = this.add.text(panelX + 198, panelY + 28, '', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
|
|
fontSize: '24px',
|
|
color: '#f1e3c2',
|
|
fontStyle: '700',
|
|
padding: { top: 4, bottom: 4 }
|
|
});
|
|
|
|
this.bodyText = this.add.text(panelX + 198, panelY + 78, '', {
|
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
|
|
fontSize: '25px',
|
|
color: '#e8dfca',
|
|
wordWrap: { width: panelW - 236, useAdvancedWrap: true },
|
|
lineSpacing: 10,
|
|
padding: { top: 4, bottom: 6 },
|
|
shadow: { offsetX: 0, offsetY: 2, color: '#05070a', blur: 5, fill: true }
|
|
});
|
|
|
|
this.progressText = this.add.text(width - 220, panelY + panelH + 20, '', {
|
|
fontFamily: '"Segoe UI", sans-serif',
|
|
fontSize: '15px',
|
|
color: '#9aa3ad'
|
|
});
|
|
}
|
|
|
|
private showPage(index: number, immediate = false) {
|
|
const page = this.pages[index];
|
|
|
|
if (immediate) {
|
|
this.applyPage(page);
|
|
return;
|
|
}
|
|
|
|
this.transitioning = true;
|
|
this.tweens.killTweensOf([this.background, ...this.dialogueObjects()]);
|
|
this.tweens.add({
|
|
targets: this.dialogueObjects(),
|
|
alpha: 0.18,
|
|
duration: 90,
|
|
ease: 'Sine.easeIn'
|
|
});
|
|
this.tweens.add({
|
|
targets: this.background,
|
|
alpha: 0.68,
|
|
duration: 120,
|
|
ease: 'Sine.easeIn',
|
|
onComplete: () => {
|
|
this.applyPage(page);
|
|
this.background?.setAlpha(0.68);
|
|
this.setDialogueAlpha(0.18);
|
|
this.tweens.add({
|
|
targets: this.dialogueObjects(),
|
|
alpha: 1,
|
|
duration: 170,
|
|
ease: 'Sine.easeOut'
|
|
});
|
|
this.tweens.add({
|
|
targets: this.background,
|
|
alpha: 1,
|
|
duration: 220,
|
|
ease: 'Sine.easeOut',
|
|
onComplete: () => {
|
|
this.transitioning = false;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private applyPage(page: StoryPage) {
|
|
soundDirector.playMusic(page.bgm);
|
|
this.applyBackground(page.background);
|
|
this.chapterText?.setText(page.chapter);
|
|
|
|
if (page.portrait) {
|
|
this.portraitFrame?.setVisible(true);
|
|
this.portrait?.setVisible(true);
|
|
this.portraitDivider?.setVisible(true);
|
|
this.portrait?.setTexture(portraitKeys[page.portrait]);
|
|
this.portrait?.setDisplaySize(128, 128);
|
|
this.nameText?.setPosition(270, this.scale.height - 210);
|
|
this.bodyText?.setPosition(270, this.scale.height - 160);
|
|
this.bodyText?.setWordWrapWidth(this.scale.width - 374, true);
|
|
} else {
|
|
this.portraitFrame?.setVisible(false);
|
|
this.portrait?.setVisible(false);
|
|
this.portraitDivider?.setVisible(false);
|
|
this.nameText?.setPosition(96, this.scale.height - 210);
|
|
this.bodyText?.setPosition(96, this.scale.height - 160);
|
|
this.bodyText?.setWordWrapWidth(this.scale.width - 200, true);
|
|
}
|
|
|
|
this.nameText?.setText(page.speaker ?? '나레이션');
|
|
this.bodyText?.setText(page.text);
|
|
this.progressText?.setText(`${this.pageIndex + 1} / ${this.pages.length} SPACE / ENTER`);
|
|
}
|
|
|
|
private applyBackground(textureKey: string) {
|
|
if (!this.background) {
|
|
return;
|
|
}
|
|
|
|
const { width, height } = this.scale;
|
|
const source = this.textures.get(textureKey).getSourceImage();
|
|
const imageWidth = source.width;
|
|
const imageHeight = source.height;
|
|
const coverScale = Math.max(width / imageWidth, height / imageHeight);
|
|
|
|
this.tweens.killTweensOf(this.background);
|
|
this.background.setTexture(textureKey);
|
|
this.background.setAlpha(1);
|
|
this.background.setScale(coverScale * 1.07);
|
|
this.background.setPosition(width / 2 - 16, height / 2);
|
|
|
|
this.tweens.add({
|
|
targets: this.background,
|
|
x: width / 2 + 16,
|
|
y: height / 2 - 8,
|
|
scale: coverScale * 1.11,
|
|
duration: 15000,
|
|
ease: 'Sine.easeInOut',
|
|
yoyo: true,
|
|
repeat: -1
|
|
});
|
|
}
|
|
|
|
private advance() {
|
|
if (this.transitioning) {
|
|
return;
|
|
}
|
|
|
|
if (this.pageIndex >= this.pages.length - 1) {
|
|
this.scene.start(this.nextScene, this.nextSceneData);
|
|
return;
|
|
}
|
|
|
|
this.pageIndex += 1;
|
|
this.showPage(this.pageIndex);
|
|
}
|
|
|
|
private dialogueObjects() {
|
|
const objects: Array<AlphaObject | undefined> = [
|
|
this.chapterText,
|
|
this.portrait,
|
|
this.portraitFrame,
|
|
this.portraitDivider,
|
|
this.nameText,
|
|
this.bodyText,
|
|
this.progressText
|
|
];
|
|
|
|
return objects.filter((object): object is AlphaObject => object !== undefined);
|
|
}
|
|
|
|
private setDialogueAlpha(alpha: number) {
|
|
this.dialogueObjects().forEach((object) => {
|
|
object.setAlpha(alpha);
|
|
});
|
|
}
|
|
}
|