Add cinematic prologue story
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user