import { CommonModule } from '@angular/common'; import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { finalize } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { getErrorMessage } from '../../core/http/error-message.util'; import { CurrentUser } from '../../models/auth.models'; import { AuthDialogService } from '../../services/auth-dialog.service'; import { AuthService } from '../../services/auth.service'; import { GameApiService } from '../../services/game-api.service'; import { ThemeService } from '../../services/theme.service'; import { ChallengeCreateDialogComponent } from '../../components/challenge-create-dialog/challenge-create-dialog.component'; type Difficulty = 'easy' | 'medium' | 'hard'; type ImportMode = 'fen' | 'pgn'; interface Star { style: Record; } interface BackgroundBuilding { style: Record; } interface WindowCell { state: 'off' | 'on'; color?: string; glowColor?: string; style: Record; } interface Star { style: Record; } interface BackgroundBuilding { style: Record; } interface WindowCell { state: 'off' | 'on'; color?: string; glowColor?: string; style: Record; } @Component({ selector: 'app-welcome', standalone: true, imports: [CommonModule, FormsModule, ChallengeCreateDialogComponent], templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.css'] }) export class WelcomeComponent implements OnInit, OnDestroy { private readonly destroyRef = inject(DestroyRef); private readonly authService = inject(AuthService); private readonly authDialogService = inject(AuthDialogService); private readonly themeService = inject(ThemeService); creating = false; joiningGame = false; importing = false; errorMessage = ''; showDifficultyDialog = false; showOptionsDialog = false; showJoinDialog = false; showImportDialog = false; showChallengeDialog = false; gameIdInput = ''; importMode: ImportMode = 'fen'; importText = ''; isSunsetMode = false; modeBadge = 'NIGHT MODE'; currentUser: CurrentUser | null = null; private authDialogState: 'login' | 'register' | null = null; private pendingAction: (() => void) | null = null; // Speech bubble and zoom features showSpeechBubble = false; isZoomedIn = false; showSecondSpeechBubble = false; showHappyBubble = false; showMeatEmoji = false; bubbleMessage = 'meow'; // Meat emoji drag state meatX = 0; meatY = 0; isDraggingMeat = false; meatDragOffsetX = 0; meatDragOffsetY = 0; stars: Star[] = []; bgBuildings: BackgroundBuilding[] = []; windows: Record = {}; private flickerIntervalId: ReturnType | undefined; private speechBubbleTimeoutId: ReturnType | undefined; private zoomTimeoutId: ReturnType | undefined; private coolColors = ['#7de8ff', '#00d5ff', '#5bc0de', '#31b0d5', '#4fc3f7', '#29b6f6']; private coolGlowColors = ['#00d5ff', '#00d5ff', '#31b0d5', '#31b0d5', '#03a9f4', '#0288d1']; private warmColors = ['#ffe88a', '#ffcc30', '#f0ad4e', '#ec971f', '#ffb74d', '#ffa726']; private warmGlowColors = ['#ffcc30', '#ffcc30', '#ec971f', '#ec971f', '#ff9800', '#fb8c00']; constructor( private readonly router: Router, private readonly gameApi: GameApiService ) { } ngOnInit(): void { this.themeService.darkMode$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((isDarkMode) => { this.isSunsetMode = !isDarkMode; this.modeBadge = this.isSunsetMode ? 'SUNSET MODE' : 'NIGHT MODE'; }); this.authService.currentUser$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.currentUser = user; this.maybeRunPendingAction(); }); this.authDialogService.dialogState$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { this.authDialogState = state; this.maybeRunPendingAction(); }); this.generateStars(220); this.generateBackgroundBuildings(); this.generateWindowsForAllBuildings(); this.startWindowFlicker(); // Show speech bubble after 5 seconds this.speechBubbleTimeoutId = setTimeout(() => { this.showSpeechBubble = true; }, 5000); } ngOnDestroy(): void { this.stopWindowFlicker(); if (this.speechBubbleTimeoutId) { clearTimeout(this.speechBubbleTimeoutId); } if (this.zoomTimeoutId) { clearTimeout(this.zoomTimeoutId); } } openDifficultyDialog(): void { if (!this.requireAuth(() => this.showDifficultyDialog = true)) { return; } this.closeAllDialogs(); this.showDifficultyDialog = true; } closeDifficultyDialog(): void { this.showDifficultyDialog = false; this.errorMessage = ''; } openOptionsDialog(): void { this.closeAllDialogs(); this.showOptionsDialog = true; } closeOptionsDialog(): void { this.showOptionsDialog = false; this.errorMessage = ''; } openJoinDialog(): void { if (!this.requireAuth(() => this.showJoinDialog = true)) { return; } this.closeAllDialogs(); this.showJoinDialog = true; } closeJoinDialog(): void { if (this.joiningGame) { return; } this.showJoinDialog = false; this.gameIdInput = ''; this.errorMessage = ''; } openImportDialog(): void { if (!this.requireAuth(() => this.showImportDialog = true)) { return; } this.closeAllDialogs(); this.showImportDialog = true; } closeImportDialog(): void { if (this.importing) { return; } this.showImportDialog = false; this.importText = ''; this.importMode = 'fen'; this.errorMessage = ''; } setImportMode(mode: ImportMode): void { this.importMode = mode; this.errorMessage = ''; } startOneVsOne(): void { if (!this.requireAuth(() => this.openChallengeDialog())) { return; } this.openChallengeDialog(); } openChallengeDialog(): void { this.closeAllDialogs(); this.showChallengeDialog = true; } closeChallengeDialog(): void { this.showChallengeDialog = false; this.errorMessage = ''; } startVsBot(difficulty: Difficulty): void { if (!this.requireAuth(() => this.performStartVsBot(difficulty))) { return; } this.performStartVsBot(difficulty); } submitJoinGame(): void { if (!this.requireAuth(() => this.performSubmitJoinGame())) { return; } this.performSubmitJoinGame(); } submitImportGame(): void { if (!this.requireAuth(() => this.performSubmitImportGame())) { return; } this.performSubmitImportGame(); } onSpeechBubbleClick(): void { this.showSpeechBubble = false; this.isZoomedIn = true; this.bubbleMessage = 'meow'; this.showMeatEmoji = true; this.showHappyBubble = false; this.showSecondSpeechBubble = true; // Reset meat position this.meatX = window.innerWidth / 2 - 100; this.meatY = window.innerHeight / 2 + 150; } onZoomedViewClick(): void { this.isZoomedIn = false; this.showSecondSpeechBubble = false; this.showHappyBubble = false; this.showMeatEmoji = false; this.bubbleMessage = 'meow'; if (this.zoomTimeoutId) { clearTimeout(this.zoomTimeoutId); } } onMeatMouseDown(event: MouseEvent): void { this.isDraggingMeat = true; const rect = (event.target as HTMLElement).getBoundingClientRect(); this.meatDragOffsetX = event.clientX - rect.left; this.meatDragOffsetY = event.clientY - rect.top; } onMouseMove(event: MouseEvent): void { if (!this.isDraggingMeat) { return; } this.meatX = event.clientX - this.meatDragOffsetX; this.meatY = event.clientY - this.meatDragOffsetY; const gifElement = document.querySelector('.player-2-gif') as HTMLElement; if (!gifElement) { return; } const gifRect = gifElement.getBoundingClientRect(); const gifCenterX = gifRect.left + gifRect.width / 2; const gifCenterY = gifRect.top + gifRect.height / 2; const meatElement = document.querySelector('.meat-emoji') as HTMLElement; if (!meatElement) { return; } const meatRect = meatElement.getBoundingClientRect(); const meatCenterX = meatRect.left + meatRect.width / 2; const meatCenterY = meatRect.top + meatRect.height / 2; const distance = Math.sqrt( Math.pow(meatCenterX - gifCenterX, 2) + Math.pow(meatCenterY - gifCenterY, 2) ); if (distance < 50) { this.onMeatFed(); } } onMouseUp(): void { this.isDraggingMeat = false; } onMeatFed(): void { this.showMeatEmoji = false; this.showSecondSpeechBubble = false; this.showHappyBubble = true; this.isDraggingMeat = false; } private requireAuth(action: () => void): boolean { if (this.authService.isLoggedIn()) { return true; } this.pendingAction = action; this.authDialogService.openLogin(); return false; } private maybeRunPendingAction(): void { if (!this.currentUser || this.authDialogState !== null || !this.pendingAction) { return; } const action = this.pendingAction; this.pendingAction = null; action(); } private performStartVsBot(difficulty: Difficulty): void { if (this.creating) { return; } this.errorMessage = ''; this.creating = true; this.showDifficultyDialog = false; this.gameApi .createGameVsBot(difficulty) .pipe(finalize(() => (this.creating = false))) .subscribe({ next: (game) => { void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to create a game against bot.'); } }); } private performSubmitJoinGame(): void { const gameId = this.gameIdInput.trim(); if (this.joiningGame || !gameId) { return; } this.errorMessage = ''; this.joiningGame = true; this.gameApi .getGame(gameId) .pipe(finalize(() => (this.joiningGame = false))) .subscribe({ next: (game) => { this.closeJoinDialog(); void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); }, error: (error) => { this.errorMessage = getErrorMessage(error, 'Unable to find or join the game.'); } }); } private performSubmitImportGame(): void { const trimmedImport = this.importText.trim(); if (this.importing || !trimmedImport) { return; } this.errorMessage = ''; this.importing = true; const importRequest = this.importMode === 'fen' ? this.gameApi.importFen(trimmedImport) : this.gameApi.importPgn(trimmedImport); importRequest.pipe(finalize(() => (this.importing = false))).subscribe({ next: (game) => { this.closeImportDialog(); void this.router.navigate(['/game', game.gameId], { state: { theme: this.isSunsetMode ? 'light' : 'dark' } }); }, error: (error) => { const defaultMessage = this.importMode === 'fen' ? 'Unable to import FEN.' : 'Unable to import PGN.'; this.errorMessage = getErrorMessage(error, defaultMessage); } }); } private closeAllDialogs(): void { this.showDifficultyDialog = false; this.showOptionsDialog = false; this.showJoinDialog = false; this.showImportDialog = false; this.errorMessage = ''; } private generateStars(count: number): void { this.stars = Array.from({ length: count }, () => { const size = Math.random() * 2 + 0.5; return { style: { width: `${size}px`, height: `${size}px`, left: `${Math.random() * 100}%`, top: `${Math.random() * 62}%`, '--d': `${(Math.random() * 3 + 1.5).toFixed(1)}s`, '--dl': `${-(Math.random() * 6).toFixed(1)}s` } }; }); } private generateBackgroundBuildings(): void { const specs = [ { l: '0%', w: '7%', h: '30vh' }, { l: '3%', w: '4%', h: '18vh' }, // New building { l: '7%', w: '5%', h: '22vh' }, { l: '11%', w: '8%', h: '28vh' }, { l: '15%', w: '6%', h: '20vh' }, { l: '18.5%', w: '4%', h: '18vh' }, { l: '22.5%', w: '6%', h: '26vh' }, { l: '28%', w: '5%', h: '25vh' }, { l: '32%', w: '4%', h: '15vh' }, { l: '35.5%', w: '4.5%', h: '20vh' }, { l: '42%', w: '5%', h: '28vh' }, { l: '47%', w: '5%', h: '22vh' }, // New building { l: '50%', w: '7%', h: '30vh' }, { l: '55%', w: '6%', h: '27vh' }, { l: '60.5%', w: '5%', h: '24vh' }, { l: '64.5%', w: '3.5%', h: '17vh' }, { l: '70%', w: '6%', h: '23vh' }, { l: '75%', w: '4%', h: '19vh' }, { l: '80.5%', w: '4%', h: '21vh' }, { l: '85.5%', w: '9%', h: '32vh' }, { l: '88%', w: '5%', h: '20vh' }, { l: '91%', w: '3%', h: '16vh' }, // New building { l: '94%', w: '6%', h: '27vh' } ]; this.bgBuildings = specs.map((spec) => ({ style: { left: spec.l, width: spec.w, height: spec.h } })); } private generateWindowsForAllBuildings(): void { this.windows = { wA1: this.generateWindows(3, 4, 0.6), wA2: this.generateWindows(4, 5, 0.55), wA3: this.generateWindows(5, 18, 0.5), wB1: this.generateWindows(4, 3, 0.6), wB2: this.generateWindows(5, 20, 0.55), wC1: this.generateWindows(5, 3, 0.7), wC2: this.generateWindows(6, 5, 0.65), wC3: this.generateWindows(7, 24, 0.6), wD1: this.generateWindows(6, 3, 0.6), wD2: this.generateWindows(6, 20, 0.5), wE1: this.generateWindows(3, 16, 0.45) }; } private generateWindows(cols: number, rows: number, litRate: number): WindowCell[] { const total = cols * rows; return Array.from({ length: total }, () => this.createWindowCell(litRate)); } private createWindowCell(litRate: number): WindowCell { const random = Math.random(); let state: WindowCell['state'] = 'off'; let color: string | undefined; let glowColor: string | undefined; if (random < litRate * 0.58) { // Cool color state = 'on'; const coolIndex = Math.floor(Math.random() * this.coolColors.length); color = this.coolColors[coolIndex]; glowColor = this.coolGlowColors[coolIndex]; } else if (random < litRate) { // Warm color state = 'on'; const warmIndex = Math.floor(Math.random() * this.warmColors.length); color = this.warmColors[warmIndex]; glowColor = this.warmGlowColors[warmIndex]; } if (state === 'off') { return { state, style: {} }; } const baseDuration = (color && this.coolColors.includes(color)) ? 3 : 4; return { state, color, glowColor, style: { 'background-color': color || '', 'box-shadow': glowColor ? `0 0 6px ${glowColor}, 0 0 16px ${glowColor}35` : '', '--wd': `${(Math.random() * 4 + baseDuration).toFixed(1)}s`, '--wdl': `${-(Math.random() * 8).toFixed(1)}s` } }; } private startWindowFlicker(): void { this.flickerIntervalId = setInterval(() => { this.randomFlicker(); }, 2800); } private stopWindowFlicker(): void { if (this.flickerIntervalId === undefined) { return; } clearInterval(this.flickerIntervalId); this.flickerIntervalId = undefined; } private randomFlicker(): void { const allWindows = Object.values(this.windows).flat(); if (allWindows.length === 0) { return; } const pickCount = Math.floor(Math.random() * 6) + 1; for (let i = 0; i < pickCount; i += 1) { const target = allWindows[Math.floor(Math.random() * allWindows.length)]; if (!target) { continue; } if (target.state === 'off') { target.state = 'on'; const isCool = Math.random() < 0.5; const colors = isCool ? this.coolColors : this.warmColors; const glowColors = isCool ? this.coolGlowColors : this.warmGlowColors; const index = Math.floor(Math.random() * colors.length); target.color = colors[index]; target.glowColor = glowColors[index]; target.style = { 'background-color': target.color || '', 'box-shadow': target.glowColor ? `0 0 6px ${target.glowColor}, 0 0 16px ${target.glowColor}35` : '', '--wd': `${(Math.random() * 4 + (isCool ? 3 : 4)).toFixed(1)}s`, '--wdl': `${-(Math.random() * 8).toFixed(1)}s` }; } else { target.state = 'off'; target.color = undefined; target.glowColor = undefined; target.style = {}; } } } }