Add cinematic prologue story

This commit is contained in:
2026-06-21 23:36:21 +09:00
parent 34611bc633
commit 591141c492
11 changed files with 217 additions and 71 deletions

View File

@@ -1,18 +1,14 @@
import Phaser from 'phaser';
import { prologueLines, type StoryLine } from '../data/scenario';
import { prologuePages, type StoryPage } from '../data/scenario';
import { palette } from '../ui/palette';
const portraitKeys: Record<StoryLine['portrait'], string> = {
lord: 'portrait-lord',
strategist: 'portrait-strategist',
captain: 'portrait-captain'
};
export class StoryScene extends Phaser.Scene {
private lineIndex = 0;
private portrait?: Phaser.GameObjects.Image;
private pageIndex = 0;
private background?: Phaser.GameObjects.Image;
private chapterText?: Phaser.GameObjects.Text;
private nameText?: Phaser.GameObjects.Text;
private bodyText?: Phaser.GameObjects.Text;
private progressText?: Phaser.GameObjects.Text;
constructor() {
super('StoryScene');
@@ -20,66 +16,137 @@ export class StoryScene extends Phaser.Scene {
create() {
const { width, height } = this.scale;
this.add.rectangle(0, 0, width, height, 0x151922).setOrigin(0);
this.drawCampBackground(width, height);
this.add.rectangle(0, 0, width, height, 0x0b0d12).setOrigin(0);
this.background = this.add.image(width / 2, height / 2, prologuePages[0].background);
this.drawSceneShade(width, height);
this.drawDialogPanel(width, height);
this.showLine(0);
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 drawCampBackground(width: number, height: number) {
const graphics = this.add.graphics();
graphics.fillStyle(0x26303a, 1);
graphics.fillRect(0, height * 0.54, width, height * 0.46);
graphics.fillStyle(0x5f6d5a, 1);
graphics.fillTriangle(0, height * 0.54, 240, 240, 470, height * 0.54);
graphics.fillStyle(0x384858, 1);
graphics.fillTriangle(300, height * 0.54, 570, 160, 870, height * 0.54);
graphics.fillStyle(0x8a4f3d, 1);
graphics.fillRect(935, 264, 98, 200);
graphics.fillStyle(0xd8b15f, 1);
graphics.fillTriangle(1033, 264, 1180, 304, 1033, 344);
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 - 214;
this.add.rectangle(64, panelY, width - 128, 158, palette.panel, 0.95).setOrigin(0);
this.add.rectangle(64, panelY, width - 128, 4, palette.gold, 0.9).setOrigin(0);
this.portrait = this.add.image(150, panelY + 79, 'portrait-lord');
this.nameText = this.add.text(238, panelY + 28, '', {
fontSize: '24px',
const panelY = height - 238;
const panelX = 72;
const panelW = width - 144;
const panelH = 172;
this.add.rectangle(panelX, panelY, panelW, panelH, palette.panelDark, 0.86).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'
fontStyle: '700',
padding: { top: 4, bottom: 4 },
shadow: { offsetX: 0, offsetY: 2, color: '#05070a', blur: 6, fill: true }
});
this.bodyText = this.add.text(238, panelY + 68, '', {
this.nameText = this.add.text(panelX + 24, panelY + 28, '', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
fontSize: '24px',
color: '#e8dfca',
wordWrap: { width: width - 370, useAdvancedWrap: true },
lineSpacing: 8
color: '#f1e3c2',
fontStyle: '700',
padding: { top: 4, bottom: 4 }
});
this.add.text(width - 190, panelY + 122, 'SPACE / ENTER', {
fontSize: '16px',
this.bodyText = this.add.text(panelX + 24, panelY + 78, '', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", serif',
fontSize: '25px',
color: '#e8dfca',
wordWrap: { width: panelW - 56, 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 showLine(index: number) {
const line = prologueLines[index];
this.portrait?.setTexture(portraitKeys[line.portrait]);
this.nameText?.setText(line.speaker);
this.bodyText?.setText(line.text);
private showPage(index: number, immediate = false) {
const page = prologuePages[index];
if (immediate) {
this.applyPage(page);
return;
}
this.cameras.main.fadeOut(120, 0, 0, 0);
this.time.delayedCall(130, () => {
this.applyPage(page);
this.cameras.main.fadeIn(180, 0, 0, 0);
});
}
private applyPage(page: StoryPage) {
this.applyBackground(page.background);
this.chapterText?.setText(page.chapter);
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} / ${prologuePages.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.lineIndex >= prologueLines.length - 1) {
if (this.cameras.main.fadeEffect?.isRunning) {
return;
}
if (this.pageIndex >= prologuePages.length - 1) {
this.scene.start('BattleScene');
return;
}
this.lineIndex += 1;
this.showLine(this.lineIndex);
this.pageIndex += 1;
this.showPage(this.pageIndex);
}
}