diff --git a/src/game/scenes/StoryScene.ts b/src/game/scenes/StoryScene.ts index be9ccc0..df3999e 100644 --- a/src/game/scenes/StoryScene.ts +++ b/src/game/scenes/StoryScene.ts @@ -37,6 +37,18 @@ const storyUnitFrameRows: Record = { north: 2, west: 3 }; +const cutsceneUiColors = { + veil: 0x020304, + ink: 0x070809, + silk: 0x17100d, + silkDeep: 0x0d0a08, + woodDark: 0x160d07, + woodMid: 0x2d1b0f, + woodLight: 0x5f3d20, + parchment: 0x715638, + parchmentLight: 0x947345, + parchmentDark: 0x2d1a0e +} as const; type CutsceneStageBounds = { x: number; @@ -208,9 +220,23 @@ export class StoryScene extends Phaser.Scene { const panelW = width - 144; const panelH = 172; - this.add.rectangle(panelX, panelY, panelW, panelH, palette.panelDark, 0.9).setOrigin(0).setDepth(dialogDepth); - this.add.rectangle(panelX, panelY, panelW, 3, palette.gold, 0.9).setOrigin(0).setDepth(dialogDepth + 1); - this.add.rectangle(panelX, panelY + panelH, panelW, 1, palette.gold, 0.35).setOrigin(0).setDepth(dialogDepth + 1); + const panel = this.add.graphics(); + panel.setDepth(dialogDepth); + panel.fillStyle(cutsceneUiColors.veil, 0.52); + panel.fillRoundedRect(panelX - 10, panelY + 10, panelW + 20, panelH + 18, 10); + panel.fillStyle(cutsceneUiColors.woodDark, 0.94); + panel.fillRoundedRect(panelX, panelY, panelW, panelH, 8); + panel.fillStyle(cutsceneUiColors.silk, 0.82); + panel.fillRoundedRect(panelX + 10, panelY + 10, panelW - 20, panelH - 20, 6); + panel.lineStyle(2, palette.gold, 0.42); + panel.strokeRoundedRect(panelX, panelY, panelW, panelH, 8); + panel.lineStyle(1, 0xf0d08a, 0.22); + panel.strokeRoundedRect(panelX + 10, panelY + 10, panelW - 20, panelH - 20, 6); + panel.lineStyle(1, cutsceneUiColors.woodLight, 0.18); + for (let index = 0; index < 5; index += 1) { + const y = panelY + 24 + index * 28; + panel.lineBetween(panelX + 188, y, panelX + panelW - 28, y + 4); + } this.chapterText = this.add.text(panelX, panelY - 42, '', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', @@ -222,15 +248,15 @@ export class StoryScene extends Phaser.Scene { }); this.chapterText.setDepth(dialogDepth + 2); - this.portraitFrame = this.add.rectangle(panelX + 86, panelY + 86, 136, 136, 0x090d12, 0.88); - this.portraitFrame.setStrokeStyle(2, palette.gold, 0.58); + this.portraitFrame = this.add.rectangle(panelX + 86, panelY + 86, 136, 136, cutsceneUiColors.ink, 0.92); + this.portraitFrame.setStrokeStyle(2, palette.gold, 0.48); this.portraitFrame.setDepth(dialogDepth + 2); this.portrait = this.add.image(panelX + 86, panelY + 86, this.pagePortraitTextureKey(this.pages[0]) ?? 'story-fallback'); this.portrait.setDisplaySize(128, 128); this.portrait.setDepth(dialogDepth + 3); - this.portraitDivider = this.add.rectangle(panelX + 166, panelY + 28, 1, panelH - 56, palette.gold, 0.22).setOrigin(0); + this.portraitDivider = this.add.rectangle(panelX + 166, panelY + 28, 1, panelH - 56, palette.gold, 0.16).setOrigin(0); this.portraitDivider.setDepth(dialogDepth + 2); this.nameText = this.add.text(panelX + 198, panelY + 28, '', { @@ -420,17 +446,45 @@ export class StoryScene extends Phaser.Scene { private renderCutsceneStage(layer: Phaser.GameObjects.Container, cutscene: StoryCutscene) { const { x: stageX, y: stageY, width: stageW, height: stageH } = this.cutsceneStageBounds(); - const fill = cutscene.kind === 'victory' ? 0x121912 : cutscene.kind === 'operation' ? 0x111720 : 0x171510; - const stroke = cutscene.kind === 'victory' ? palette.green : palette.gold; - const shade = this.add.rectangle(stageX - 10, stageY - 10, stageW + 20, stageH + 20, 0x030405, 0.72).setOrigin(0); - const bg = this.add.rectangle(stageX, stageY, stageW, stageH, fill, 0.97).setOrigin(0); - bg.setStrokeStyle(2, stroke, 0.62); - const inner = this.add.rectangle(stageX + 12, stageY + 12, stageW - 24, stageH - 24, 0x21170f, 0.44).setOrigin(0); - inner.setStrokeStyle(1, 0xf0d08a, 0.18); - const topLine = this.add.rectangle(stageX, stageY, stageW, 4, stroke, 0.8).setOrigin(0); - const titlePlate = this.add.rectangle(stageX + 22, stageY + 18, 360, 48, 0x090b0f, 0.82).setOrigin(0); - titlePlate.setStrokeStyle(1, stroke, 0.46); - const title = this.add.text(stageX + 24, stageY + 18, cutscene.title, { + const accent = cutscene.kind === 'victory' ? palette.green : cutscene.kind === 'operation' ? palette.blue : palette.gold; + const stage = this.add.graphics(); + stage.fillStyle(cutsceneUiColors.veil, 0.58); + stage.fillRoundedRect(stageX - 18, stageY - 14, stageW + 36, stageH + 28, 12); + stage.fillStyle(cutsceneUiColors.woodDark, 0.98); + stage.fillRoundedRect(stageX, stageY, stageW, stageH, 10); + stage.fillStyle(cutsceneUiColors.woodMid, 0.86); + stage.fillRoundedRect(stageX + 13, stageY + 13, stageW - 26, stageH - 26, 7); + stage.fillStyle(cutsceneUiColors.silkDeep, 0.62); + stage.fillRoundedRect(stageX + 24, stageY + 74, stageW - 48, stageH - 108, 7); + stage.lineStyle(2, accent, 0.42); + stage.strokeRoundedRect(stageX, stageY, stageW, stageH, 10); + stage.lineStyle(1, 0xf0d08a, 0.18); + stage.strokeRoundedRect(stageX + 13, stageY + 13, stageW - 26, stageH - 26, 7); + stage.lineStyle(1, cutsceneUiColors.woodLight, 0.2); + for (let index = 0; index < 9; index += 1) { + const y = stageY + 30 + index * 31; + stage.lineBetween(stageX + 28, y, stageX + stageW - 28, y + (index % 2 === 0 ? 5 : -4)); + } + + const foldingScreen = this.add.graphics(); + foldingScreen.lineStyle(1, 0xd0b16e, 0.1); + foldingScreen.fillStyle(0x342216, 0.22); + for (let index = 0; index < 5; index += 1) { + const panelX = stageX + 26 + index * 86; + foldingScreen.fillRoundedRect(panelX, stageY + 92, 66, stageH - 134, 4); + foldingScreen.strokeRoundedRect(panelX, stageY + 92, 66, stageH - 134, 4); + } + + const tableShadow = this.add.ellipse(stageX + stageW * 0.62, stageY + stageH - 34, stageW * 0.6, 38, 0x030405, 0.34); + const titlePlate = this.add.graphics(); + titlePlate.fillStyle(cutsceneUiColors.ink, 0.8); + titlePlate.fillRoundedRect(stageX + 22, stageY + 18, 366, 48, 7); + titlePlate.lineStyle(1, accent, 0.36); + titlePlate.strokeRoundedRect(stageX + 22, stageY + 18, 366, 48, 7); + titlePlate.lineStyle(1, 0xf0d08a, 0.14); + titlePlate.lineBetween(stageX + 34, stageY + 60, stageX + 374, stageY + 60); + + const title = this.add.text(stageX + 34, stageY + 18, cutscene.title, { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: '26px', color: '#f2e3bf', @@ -438,21 +492,12 @@ export class StoryScene extends Phaser.Scene { stroke: '#05070a', strokeThickness: 4 }); - const subtitle = this.add.text(stageX + 26, stageY + 55, cutscene.subtitle ?? '', { + const subtitle = this.add.text(stageX + 36, stageY + 53, cutscene.subtitle ?? '', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', - color: '#c8d2dd' + color: '#cdbb94' }); - const foldingScreen = this.add.graphics(); - foldingScreen.lineStyle(1, 0xd0b16e, 0.14); - foldingScreen.fillStyle(0x302215, 0.34); - for (let index = 0; index < 6; index += 1) { - const panelX = stageX + 22 + index * 88; - foldingScreen.fillRect(panelX, stageY + 84, 72, stageH - 118); - foldingScreen.strokeRect(panelX, stageY + 84, 72, stageH - 118); - } - const tableShadow = this.add.ellipse(stageX + stageW * 0.62, stageY + stageH - 34, stageW * 0.58, 40, 0x030405, 0.4); - layer.add([shade, bg, inner, foldingScreen, tableShadow, topLine, titlePlate, title, subtitle]); + layer.add([stage, foldingScreen, tableShadow, titlePlate, title, subtitle]); } private renderTacticalBoard(layer: Phaser.GameObjects.Container, cutscene: StoryCutscene) { @@ -462,14 +507,35 @@ export class StoryScene extends Phaser.Scene { const boardW = Math.round(stage.width * 0.58); const boardH = stage.height - 118; const board: CutsceneBoardBounds = { x: boardX, y: boardY, width: boardW, height: boardH }; - const parchment = this.add.rectangle(boardX, boardY, boardW, boardH, 0xc2a66b, 0.96).setOrigin(0); - parchment.setStrokeStyle(3, 0x5b3a1e, 0.84); - const inner = this.add.rectangle(boardX + 12, boardY + 12, boardW - 24, boardH - 24, 0xd4bd82, 0.34).setOrigin(0); - inner.setStrokeStyle(1, 0x5b3a1e, 0.32); - layer.add([parchment, inner]); + const parchment = this.add.graphics(); + parchment.fillStyle(cutsceneUiColors.veil, 0.28); + parchment.fillRoundedRect(boardX + 10, boardY + 10, boardW, boardH, 8); + parchment.fillStyle(cutsceneUiColors.parchmentDark, 0.96); + parchment.fillRoundedRect(boardX, boardY, boardW, boardH, 8); + parchment.fillStyle(cutsceneUiColors.parchment, 0.98); + parchment.fillRoundedRect(boardX + 5, boardY + 5, boardW - 10, boardH - 10, 6); + parchment.fillStyle(cutsceneUiColors.parchmentLight, 0.12); + parchment.fillRoundedRect(boardX + 18, boardY + 18, boardW - 36, boardH - 36, 5); + parchment.lineStyle(3, 0x3a2110, 0.78); + parchment.strokeRoundedRect(boardX, boardY, boardW, boardH, 8); + parchment.lineStyle(1, 0xf0d08a, 0.22); + parchment.strokeRoundedRect(boardX + 10, boardY + 10, boardW - 20, boardH - 20, 5); + parchment.lineStyle(1, 0x5b3a1e, 0.18); + for (let index = 0; index < 18; index += 1) { + const y = boardY + 28 + index * 10; + const xOffset = index % 3 === 0 ? 18 : index % 3 === 1 ? 34 : 26; + parchment.lineBetween(boardX + xOffset, y, boardX + boardW - 24, y + (index % 2 === 0 ? 2 : -2)); + } + for (let index = 0; index < 28; index += 1) { + const x = boardX + 30 + ((index * 47) % Math.max(60, boardW - 58)); + const y = boardY + 34 + ((index * 29) % Math.max(60, boardH - 62)); + parchment.fillStyle(index % 2 === 0 ? 0x6a4726 : 0xd2b172, 0.12); + parchment.fillCircle(x, y, index % 3 === 0 ? 1.5 : 1); + } + layer.add(parchment); const map = this.add.graphics(); - map.lineStyle(1, 0x5b3a1e, 0.18); + map.lineStyle(1, 0x3e2614, 0.14); for (let index = 1; index < 7; index += 1) { const x = boardX + (boardW / 7) * index; map.lineBetween(x, boardY + 16, x, boardY + boardH - 16); @@ -478,19 +544,19 @@ export class StoryScene extends Phaser.Scene { const y = boardY + (boardH / 4) * index; map.lineBetween(boardX + 16, y, boardX + boardW - 16, y); } - map.fillStyle(0x3e6f4e, 0.28); + map.fillStyle(0x37563b, 0.24); map.fillEllipse(boardX + boardW * 0.36, boardY + boardH * 0.58, boardW * 0.38, boardH * 0.52); - map.fillStyle(0x405e7d, 0.22); + map.fillStyle(0x3d5161, 0.18); map.fillEllipse(boardX + boardW * 0.72, boardY + boardH * 0.36, boardW * 0.26, boardH * 0.3); - map.lineStyle(5, 0x7b5630, 0.42); + map.lineStyle(6, 0x6b4826, 0.3); map.beginPath(); map.moveTo(boardX + boardW * 0.16, boardY + boardH * 0.74); map.lineTo(boardX + boardW * 0.38, boardY + boardH * 0.54); map.lineTo(boardX + boardW * 0.66, boardY + boardH * 0.36); map.lineTo(boardX + boardW * 0.83, boardY + boardH * 0.24); map.strokePath(); - map.lineStyle(2, 0x2b2114, 0.42); - map.strokeRect(boardX + 24, boardY + 24, boardW - 48, boardH - 48); + map.lineStyle(2, 0x2b2114, 0.32); + map.strokeRoundedRect(boardX + 24, boardY + 24, boardW - 48, boardH - 48, 4); layer.add(map); this.renderBoardLegend(layer, cutscene, board); @@ -502,9 +568,12 @@ export class StoryScene extends Phaser.Scene { private renderBoardLegend(layer: Phaser.GameObjects.Container, cutscene: StoryCutscene, board: CutsceneBoardBounds) { const label = cutscene.kind === 'victory' ? 'RESULT MAP' : cutscene.kind === 'operation' ? 'TACTICAL MAP' : 'SORTIE BOARD'; - const bg = this.add.rectangle(board.x + 18, board.y + 16, 132, 26, 0x16100b, 0.74).setOrigin(0); - bg.setStrokeStyle(1, 0xe3c271, 0.36); - const text = this.add.text(board.x + 32, board.y + 22, label, { + const bg = this.add.graphics(); + bg.fillStyle(cutsceneUiColors.silkDeep, 0.72); + bg.fillRoundedRect(board.x + 18, board.y + 16, 128, 24, 5); + bg.lineStyle(1, 0xe3c271, 0.26); + bg.strokeRoundedRect(board.x + 18, board.y + 16, 128, 24, 5); + const text = this.add.text(board.x + 32, board.y + 21, label, { fontFamily: '"Segoe UI", sans-serif', fontSize: '12px', color: '#f2e3bf', @@ -553,16 +622,29 @@ export class StoryScene extends Phaser.Scene { return; } - const bg = this.add.rectangle(x, y, width, height, 0x080b0e, 0.9).setOrigin(0); - bg.setStrokeStyle(1, profile.color, 0.56); - const accent = this.add.rectangle(x, y, 5, height, profile.color, 0.9).setOrigin(0); - layer.add([bg, accent]); + const card = this.add.graphics(); + card.fillStyle(cutsceneUiColors.veil, 0.34); + card.fillRoundedRect(x + 5, y + 6, width, height, 7); + card.fillStyle(cutsceneUiColors.woodDark, 0.94); + card.fillRoundedRect(x, y, width, height, 7); + card.fillStyle(cutsceneUiColors.silk, 0.78); + card.fillRoundedRect(x + 7, y + 7, width - 14, height - 14, 5); + card.fillStyle(profile.color, 0.72); + card.fillRoundedRect(x + 8, y + 9, 4, height - 18, 3); + card.lineStyle(1, profile.color, 0.42); + card.strokeRoundedRect(x, y, width, height, 7); + card.lineStyle(1, 0xf0d08a, 0.12); + card.lineBetween(x + 20, y + height - 9, x + width - 20, y + height - 9); + layer.add(card); const portraitTexture = this.actorPortraitTextureKey(actor, index); const portraitX = x + 38; const portraitY = y + height / 2; - const portraitFrame = this.add.rectangle(portraitX, portraitY, height - 12, height - 12, 0x0f1419, 0.94); - portraitFrame.setStrokeStyle(1, profile.color, 0.54); + const portraitFrame = this.add.graphics(); + portraitFrame.fillStyle(cutsceneUiColors.ink, 0.9); + portraitFrame.fillRoundedRect(portraitX - (height - 12) / 2, portraitY - (height - 12) / 2, height - 12, height - 12, 5); + portraitFrame.lineStyle(1, profile.color, 0.44); + portraitFrame.strokeRoundedRect(portraitX - (height - 12) / 2, portraitY - (height - 12) / 2, height - 12, height - 12, 5); layer.add(portraitFrame); if (portraitTexture) { @@ -581,18 +663,23 @@ export class StoryScene extends Phaser.Scene { color: '#f5e6c8', fontStyle: '700' }); - const role = this.add.text(x + height + 10, y + 34, profile.roleLabel, { + const roleBg = this.add.graphics(); + roleBg.fillStyle(cutsceneUiColors.parchmentDark, 0.5); + roleBg.fillRoundedRect(x + height + 8, y + 34, Math.min(108, width - height - 22), 20, 4); + roleBg.lineStyle(1, 0xf0d08a, 0.16); + roleBg.strokeRoundedRect(x + height + 8, y + 34, Math.min(108, width - height - 22), 20, 4); + const role = this.add.text(x + height + 18, y + 37, profile.roleLabel, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '12px', - color: '#cdbb94' + color: '#ddc99f' }); - layer.add([name, role]); + layer.add([roleBg, name, role]); if (actor.line && height >= 66) { const line = this.add.text(x + 138, y + 50, actor.line, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '11px', - color: '#e8dfca', + color: '#d8cbb2', wordWrap: { width: width - 152, useAdvancedWrap: true } }); layer.add(line); @@ -613,23 +700,26 @@ export class StoryScene extends Phaser.Scene { const y = board.y + board.height * Phaser.Math.Clamp(normalizedY, 0.18, 0.82); const sideColor = actor.unitId === 'rebel-leader' ? 0xd46a4c : profile.color; const token = this.add.container(x, y); - const base = this.add.rectangle(0, 0, 46, 34, 0x15100b, 0.86); - base.setStrokeStyle(2, sideColor, 0.76); - token.add(base); + const shadow = this.add.ellipse(2, 13, 42, 14, 0x05070a, 0.3); + const base = this.add.ellipse(0, 5, 34, 24, cutsceneUiColors.silkDeep, 0.86); + base.setStrokeStyle(2, sideColor, 0.62); + const pin = this.add.circle(0, 5, 4, sideColor, 0.92); + pin.setStrokeStyle(1, 0x05070a, 0.7); + token.add([shadow, base, pin]); if (this.textures.exists(profile.unitTextureKey)) { const sprite = this.add.sprite(0, -8, profile.unitTextureKey, this.storyUnitFrameIndex(actor.direction ?? 'south')); - sprite.setDisplaySize(42, 42); + sprite.setDisplaySize(36, 36); token.add(sprite); } else { - const fallback = this.add.rectangle(0, -8, 28, 28, sideColor, 0.9); + const fallback = this.add.rectangle(0, -8, 22, 24, sideColor, 0.9); fallback.setStrokeStyle(1, 0x05070a, 0.82); token.add(fallback); } - const label = this.add.text(0, 11, actor.label ?? profile.name, { + const label = this.add.text(0, 14, actor.label ?? profile.name, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '10px', + fontSize: '9px', color: '#f2e3bf', stroke: '#05070a', strokeThickness: 2 @@ -646,8 +736,13 @@ export class StoryScene extends Phaser.Scene { return; } - const bg = this.add.rectangle(x, y, width, height, 0x0b1017, 0.88).setOrigin(0); - bg.setStrokeStyle(1, palette.gold, 0.46); + const bg = this.add.graphics(); + bg.fillStyle(cutsceneUiColors.veil, 0.28); + bg.fillRoundedRect(x + 4, y + 5, width, height, 7); + bg.fillStyle(cutsceneUiColors.silkDeep, 0.78); + bg.fillRoundedRect(x, y, width, height, 7); + bg.lineStyle(1, palette.gold, 0.3); + bg.strokeRoundedRect(x, y, width, height, 7); const title = this.add.text(x + 18, y + 16, cutscene.briefing?.title ?? '전황', { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '18px', @@ -657,11 +752,11 @@ export class StoryScene extends Phaser.Scene { layer.add([bg, title]); briefingLines.slice(0, 4).forEach((line, index) => { - const bullet = this.add.circle(x + 24, y + 54 + index * 30, 4, palette.gold, 0.92); + const bullet = this.add.circle(x + 24, y + 54 + index * 30, 3, palette.gold, 0.72); const text = this.add.text(x + 38, y + 43 + index * 30, line, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '14px', - color: '#e8dfca', + color: '#d8cbb2', wordWrap: { width: width - 58, useAdvancedWrap: true } }); layer.add([bullet, text]); @@ -711,17 +806,17 @@ export class StoryScene extends Phaser.Scene { const color = marker.side === 'ally' ? palette.blue : marker.side === 'enemy' ? 0xd46a4c : palette.gold; const x = boardX + boardW * marker.x; const y = boardY + boardH * marker.y; - const halo = this.add.circle(x, y, 16, color, 0.18); - const dot = this.add.circle(x, y, 7, color, 0.96); - dot.setStrokeStyle(2, 0x05070a, 0.85); - const labelX = marker.side === 'objective' ? x - 8 : x + 14; - const labelY = marker.side === 'objective' ? y - 30 : y - 11; + const halo = this.add.circle(x, y, 12, color, 0.12); + const dot = this.add.circle(x, y, 5, color, 0.9); + dot.setStrokeStyle(1, 0x05070a, 0.82); + const labelX = marker.side === 'objective' ? x - 8 : x + 11; + const labelY = marker.side === 'objective' ? y - 27 : y - 10; const label = this.add.text(labelX, labelY, marker.label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', - fontSize: '12px', - color: '#f2e3bf', + fontSize: '11px', + color: '#ead9b8', stroke: '#05070a', - strokeThickness: 3 + strokeThickness: 2 }); if (marker.side === 'objective') { label.setOrigin(0.5, 0); @@ -803,8 +898,15 @@ export class StoryScene extends Phaser.Scene { rewards.slice(0, maxItems).forEach((reward, index) => { const toneColor = reward.tone === 'next' ? palette.blue : reward.tone === 'bond' ? palette.green : palette.gold; const x = startX + index * (itemW + gap); - const bg = this.add.rectangle(x, y, itemW, 34, 0x0b1017, 0.9).setOrigin(0); - bg.setStrokeStyle(1, toneColor, 0.58); + const bg = this.add.graphics(); + bg.fillStyle(cutsceneUiColors.veil, 0.24); + bg.fillRoundedRect(x + 3, y + 4, itemW, 34, 6); + bg.fillStyle(cutsceneUiColors.silkDeep, 0.78); + bg.fillRoundedRect(x, y, itemW, 34, 6); + bg.fillStyle(toneColor, 0.14); + bg.fillRoundedRect(x + 5, y + 5, itemW - 10, 24, 4); + bg.lineStyle(1, toneColor, 0.34); + bg.strokeRoundedRect(x, y, itemW, 34, 6); const text = this.add.text(x + 14, y + 8, reward.label, { fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif', fontSize: '13px',