import Phaser from 'phaser'; import { soundDirector } from '../audio/SoundDirector'; import { palette } from '../ui/palette'; export class TitleScene extends Phaser.Scene { private focusedButton?: Phaser.GameObjects.Text; private settingsPanel?: Phaser.GameObjects.Container; constructor() { super('TitleScene'); } create() { const { width, height } = this.scale; this.drawBackground(width, height); this.drawAtmosphere(width, height); this.drawTitleMark(width, height); this.drawMenu(width, height); soundDirector.playMusic('title-theme'); const unlockAudio = () => { soundDirector.start(); soundDirector.resume(); soundDirector.playMusic('title-theme'); }; this.input.once('pointerdown', unlockAudio); this.input.keyboard?.once('keydown', unlockAudio); this.input.keyboard?.once('keydown-ENTER', () => this.startGame()); } private drawBackground(width: number, height: number) { const background = this.add.image(width / 2, height / 2, 'title-taoyuan'); const texture = this.textures.get('title-taoyuan').getSourceImage(); const coverScale = Math.max(width / texture.width, height / texture.height); background.setScale(coverScale * 1.08); background.setPosition(width / 2 - 18, height / 2 + 4); this.tweens.add({ targets: background, x: width / 2 + 18, y: height / 2 - 6, scale: coverScale * 1.12, duration: 18000, ease: 'Sine.easeInOut', yoyo: true, repeat: -1 }); this.add.rectangle(0, 0, width, height, 0x080a0e, 0.2).setOrigin(0); this.add.image(0, 0, this.createShadeTexture(width, height)).setOrigin(0); } private createShadeTexture(width: number, height: number) { const key = 'title-shade'; if (this.textures.exists(key)) { this.textures.remove(key); } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d'); if (!context) { return key; } const rightShade = context.createLinearGradient(width * 0.42, 0, width, 0); rightShade.addColorStop(0, 'rgba(8, 10, 14, 0)'); rightShade.addColorStop(0.42, 'rgba(8, 10, 14, 0.2)'); rightShade.addColorStop(1, 'rgba(8, 10, 14, 0.66)'); context.fillStyle = rightShade; context.fillRect(0, 0, width, height); const bottomShade = context.createLinearGradient(0, height * 0.55, 0, height); bottomShade.addColorStop(0, 'rgba(8, 10, 14, 0)'); bottomShade.addColorStop(1, 'rgba(8, 10, 14, 0.56)'); context.fillStyle = bottomShade; context.fillRect(0, 0, width, height); this.textures.addCanvas(key, canvas); return key; } private drawAtmosphere(width: number, height: number) { const mist = this.add.ellipse(width / 2, height * 0.78, width * 1.25, 190, 0xd8b15f, 0.04); this.tweens.add({ targets: mist, alpha: 0.11, x: width / 2 + 22, duration: 8000, ease: 'Sine.easeInOut', yoyo: true, repeat: -1 }); for (let index = 0; index < 34; index += 1) { const petal = this.add.image( Phaser.Math.Between(0, width), Phaser.Math.Between(-80, height), 'petal' ); const scale = Phaser.Math.FloatBetween(0.35, 0.9); petal.setScale(scale); petal.setAlpha(Phaser.Math.FloatBetween(0.22, 0.62)); petal.setRotation(Phaser.Math.FloatBetween(-1.2, 1.2)); this.tweens.add({ targets: petal, x: petal.x + Phaser.Math.Between(80, 220), y: height + Phaser.Math.Between(40, 180), rotation: petal.rotation + Phaser.Math.FloatBetween(2.4, 5.6), duration: Phaser.Math.Between(12000, 24000), delay: Phaser.Math.Between(0, 6000), ease: 'Sine.easeInOut', repeat: -1, onRepeat: () => { petal.setPosition(Phaser.Math.Between(-120, width), Phaser.Math.Between(-180, -40)); } }); } } private drawTitleMark(width: number, height: number) { const compact = width < 720; const titleX = compact ? width / 2 : 92; const title = this.add.text(titleX, height - (compact ? 190 : 204), '삼국지', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: compact ? '50px' : '64px', color: '#f1e3c2', fontStyle: '700', padding: { top: 8, bottom: 8 }, shadow: { offsetX: 0, offsetY: 4, color: '#080a0e', blur: 10, fill: true } }); title.setOrigin(compact ? 0.5 : 0, 0); const subtitle = this.add.text(compact ? width / 2 : 96, height - (compact ? 112 : 118), '세 형제의 맹세', { fontFamily: '"Malgun Gothic", "Noto Sans KR", serif', fontSize: compact ? '26px' : '34px', color: '#d8b15f', fontStyle: '700', padding: { top: 10, bottom: 8 }, shadow: { offsetX: 0, offsetY: 2, color: '#080a0e', blur: 6, fill: true } }); subtitle.setOrigin(compact ? 0.5 : 0, 0); const lineX = compact ? width / 2 - 110 : 98; const lineY = height - (compact ? 76 : 70); this.add.rectangle(lineX, lineY, compact ? 220 : 310, 2, palette.gold, 0.68).setOrigin(0, 0.5); } private drawMenu(width: number, height: number) { const compact = width < 720; const menuX = compact ? width / 2 : width - 318; const menuY = height * (compact ? 0.36 : 0.43); const plate = this.add.rectangle(menuX, menuY, 250, 252, 0x111821, 0.66); plate.setStrokeStyle(1, palette.gold, 0.45); this.add.rectangle(menuX, menuY - 126, 186, 2, palette.gold, 0.7); this.add.rectangle(menuX, menuY + 126, 186, 2, palette.gold, 0.36); this.createMenuButton(menuX, menuY - 70, '새 게임', true, () => this.startGame()); this.createMenuButton(menuX, menuY, '이어하기', false, () => undefined); this.createMenuButton(menuX, menuY + 70, '설정', true, () => this.openSettingsPanel(menuX, menuY)); } private createMenuButton( x: number, y: number, label: string, enabled: boolean, onSelect: () => void ) { const text = this.add.text(x, y, label, { fontSize: '30px', color: enabled ? '#f1e3c2' : '#7d8189', fontStyle: '700', fixedWidth: 190, align: 'center', padding: { top: 10, bottom: 10 } }); text.setOrigin(0.5); text.setShadow(0, 3, '#080a0e', 8, true, true); if (!enabled) { return text; } text.setInteractive({ useHandCursor: true }); text.on('pointerover', () => { this.focusButton(text); soundDirector.playHover(); }); text.on('pointerout', () => this.unfocusButton(text)); text.on('pointerdown', () => { soundDirector.start(); soundDirector.resume(); onSelect(); }); return text; } private focusButton(text: Phaser.GameObjects.Text) { this.focusedButton = text; text.setColor('#111821'); text.setBackgroundColor('#d8b15f'); } private unfocusButton(text: Phaser.GameObjects.Text) { if (this.focusedButton === text) { this.focusedButton = undefined; } text.setColor('#f1e3c2'); text.setBackgroundColor('rgba(0,0,0,0)'); } private openSettingsPanel(x: number, y: number) { if (this.settingsPanel) { this.closeSettingsPanel(); return; } soundDirector.playSelect(); const panel = this.add.container(x, y + 238); const backdrop = this.add.rectangle(0, 0, 286, 188, 0x0e151d, 0.9); backdrop.setStrokeStyle(1, palette.gold, 0.52); const title = this.add.text(0, -66, '설정', { fontSize: '24px', color: '#f1e3c2', fontStyle: '700', fixedWidth: 210, align: 'center' }); title.setOrigin(0.5); const soundText = this.createSettingsButton(0, -10, this.soundLabel()); soundText.on('pointerdown', () => { soundDirector.setMuted(!soundDirector.isMuted()); soundText.setText(this.soundLabel()); soundDirector.playSelect(); }); const close = this.createSettingsButton(0, 48, '닫기'); close.on('pointerdown', () => { soundDirector.playSelect(); this.closeSettingsPanel(); }); panel.add([backdrop, title, soundText, close]); panel.setAlpha(0); this.settingsPanel = panel; this.tweens.add({ targets: panel, alpha: 1, y: y + 228, duration: 160, ease: 'Sine.easeOut' }); } private createSettingsButton(x: number, y: number, label: string) { const text = this.add.text(x, y, label, { fontSize: '20px', color: '#d8b15f', fixedWidth: 210, align: 'center', padding: { top: 8, bottom: 8 } }); text.setOrigin(0.5); text.setInteractive({ useHandCursor: true }); text.on('pointerover', () => text.setColor('#f1e3c2')); text.on('pointerout', () => text.setColor('#d8b15f')); return text; } private soundLabel() { return `음향: ${soundDirector.isMuted() ? '꺼짐' : '켜짐'}`; } private closeSettingsPanel() { this.settingsPanel?.destroy(); this.settingsPanel = undefined; } private startGame() { soundDirector.start(); soundDirector.resume(); soundDirector.playSelect(); this.scene.start('StoryScene'); } }