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 = { 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; }; export class StoryScene extends Phaser.Scene { private pageIndex = 0; private pages: StoryPage[] = prologuePages; private nextScene = 'BattleScene'; private nextSceneData?: Record; 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 = [ 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); }); } }