diff --git a/docs/roadmap.md b/docs/roadmap.md index d7412f7..69bbc76 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -14,7 +14,7 @@ Build a small complete tactical RPG loop: - Vite, TypeScript, and Phaser project foundation - Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio -- Cinematic prologue pages with high-resolution story backgrounds and scenario data +- Cinematic prologue pages with high-resolution story backgrounds, character portraits, and scenario data - First battle scene with a 12x12 map, terrain colors, unit placement, and movement range preview - Flow verification script from title to first battle diff --git a/src/assets/images/portraits/guan-yu.png b/src/assets/images/portraits/guan-yu.png new file mode 100644 index 0000000..5fec9cb Binary files /dev/null and b/src/assets/images/portraits/guan-yu.png differ diff --git a/src/assets/images/portraits/liu-bei.png b/src/assets/images/portraits/liu-bei.png new file mode 100644 index 0000000..2e07bb4 Binary files /dev/null and b/src/assets/images/portraits/liu-bei.png differ diff --git a/src/assets/images/portraits/zhang-fei.png b/src/assets/images/portraits/zhang-fei.png new file mode 100644 index 0000000..475baaa Binary files /dev/null and b/src/assets/images/portraits/zhang-fei.png differ diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index 0f0fa98..e41002c 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -5,6 +5,9 @@ import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png'; import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png'; import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png'; import storySortieUrl from '../../assets/images/story/05-first-sortie.png'; +import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png'; +import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png'; +import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png'; import { palette } from '../ui/palette'; export class BootScene extends Phaser.Scene { @@ -19,6 +22,9 @@ export class BootScene extends Phaser.Scene { this.load.image('story-three-heroes', storyThreeHeroesUrl); this.load.image('story-militia', storyMilitiaUrl); this.load.image('story-sortie', storySortieUrl); + this.load.image('portrait-liu-bei', liuBeiPortraitUrl); + this.load.image('portrait-guan-yu', guanYuPortraitUrl); + this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl); } create() { @@ -27,29 +33,11 @@ export class BootScene extends Phaser.Scene { } private createGeneratedTextures() { - this.createPortrait('portrait-liu-bei', 0x445f92, 0xd8b15f); - this.createPortrait('portrait-guan-yu', 0x1f5f43, 0xd8b15f); - this.createPortrait('portrait-zhang-fei', 0x8a4f3d, 0xe8dfca); this.createPetalTexture(); this.createUnitTexture('unit-ally', palette.blue, 0xe7edf7); this.createUnitTexture('unit-enemy', palette.red, 0xffebe7); } - private createPortrait(key: string, primary: number, accent: number) { - const graphics = this.make.graphics({ x: 0, y: 0 }); - graphics.fillStyle(0x141820, 1); - graphics.fillRoundedRect(0, 0, 128, 128, 8); - graphics.fillStyle(primary, 1); - graphics.fillCircle(64, 46, 26); - graphics.fillRoundedRect(31, 71, 66, 42, 12); - graphics.fillStyle(accent, 1); - graphics.fillTriangle(38, 29, 64, 8, 90, 29); - graphics.lineStyle(4, 0x0b0d12, 0.45); - graphics.strokeRoundedRect(2, 2, 124, 124, 8); - graphics.generateTexture(key, 128, 128); - graphics.destroy(); - } - private createPetalTexture() { const graphics = this.make.graphics({ x: 0, y: 0 }); graphics.fillStyle(0xf0a6a4, 1); diff --git a/src/game/scenes/StoryScene.ts b/src/game/scenes/StoryScene.ts index 678875f..c18bdc2 100644 --- a/src/game/scenes/StoryScene.ts +++ b/src/game/scenes/StoryScene.ts @@ -1,11 +1,20 @@ import Phaser from 'phaser'; -import { prologuePages, type StoryPage } from '../data/scenario'; +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' +}; + export class StoryScene extends Phaser.Scene { private pageIndex = 0; 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; @@ -43,7 +52,7 @@ export class StoryScene extends Phaser.Scene { 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, 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); @@ -56,7 +65,15 @@ export class StoryScene extends Phaser.Scene { shadow: { offsetX: 0, offsetY: 2, color: '#05070a', blur: 6, fill: true } }); - this.nameText = this.add.text(panelX + 24, panelY + 28, '', { + 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', @@ -64,11 +81,11 @@ export class StoryScene extends Phaser.Scene { padding: { top: 4, bottom: 4 } }); - this.bodyText = this.add.text(panelX + 24, panelY + 78, '', { + this.bodyText = this.add.text(panelX + 198, panelY + 78, '', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: '25px', color: '#e8dfca', - wordWrap: { width: panelW - 56, useAdvancedWrap: true }, + wordWrap: { width: panelW - 236, useAdvancedWrap: true }, lineSpacing: 10, padding: { top: 4, bottom: 6 }, shadow: { offsetX: 0, offsetY: 2, color: '#05070a', blur: 5, fill: true } @@ -99,9 +116,25 @@ export class StoryScene extends Phaser.Scene { 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); + + 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} / ${prologuePages.length} SPACE / ENTER`);