6 Commits

Author SHA1 Message Date
shahdlala66 fd8ac63dc3 fix: Merge branch 'feat/NCS-69' of git.janis-eccarius.de:NowChess/NowChess-Frontend into feat/NCS-69 2026-05-12 22:33:01 +02:00
shahdlala66 c4575fd219 fix: Merge branch 'feat/NCS-69' of git.janis-eccarius.de:NowChess/NowChess-Frontend into feat/NCS-69 2026-05-12 22:30:36 +02:00
TeamCity 3c1f5c76e0 ci: bump version to v0.1.0 2026-05-12 16:33:23 +00:00
shosho996 36d72fd6cd fix: build issues 2026-05-12 17:33:03 +02:00
shosho996 bd7ec581e3 feat: NCS-75 Frontend Deployment Dockerfile (#4)
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #4
Co-authored-by: Shahd Lala <shosho996@blackhole.local>
Co-committed-by: Shahd Lala <shosho996@blackhole.local>
2026-05-12 11:21:04 +02:00
shosho996 ff75c8ce2f feat: NCS-63 User account implementation (#2)
User Profile info, no game before login/register, menu bar

---------

Co-authored-by: Lala, Shahd <Shahd.Lala@sybit.de>
Co-authored-by: shahdlala66 <shahd.lala66@gmail.com>
Reviewed-on: #2
2026-05-06 10:51:30 +02:00
5 changed files with 25 additions and 675 deletions
@@ -82,148 +82,3 @@
.ms-auto {
margin-left: auto;
}
.notification-container {
position: relative;
}
.notification-badge {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
position: relative;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
&.has-notifications {
animation: pulse 2s infinite;
}
.badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #ff6b6b;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
}
@keyframes pulse {
0% {
filter: drop-shadow(0 0 3px rgba(0, 213, 255, 0.3));
}
50% {
filter: drop-shadow(0 0 8px rgba(0, 213, 255, 0.6));
}
100% {
filter: drop-shadow(0 0 3px rgba(0, 213, 255, 0.3));
}
}
.notification-menu {
position: absolute;
top: 100%;
right: 0;
background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%);
border: 1px solid rgba(0, 213, 255, 0.3);
border-radius: 8px;
min-width: 250px;
max-height: 300px;
overflow-y: auto;
z-index: 1001;
box-shadow: 0 0 20px rgba(0, 213, 255, 0.3);
margin-top: 10px;
.notification-menu-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid rgba(0, 213, 255, 0.2);
color: #00d5ff;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
.close-btn {
background: none;
border: none;
color: #00d5ff;
font-size: 20px;
cursor: pointer;
padding: 0;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
}
.notification-list {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
}
.notification-item {
padding: 10px 12px;
background-color: rgba(0, 213, 255, 0.05);
border: 1px solid rgba(0, 213, 255, 0.1);
border-radius: 4px;
font-size: 12px;
color: #b0b0d0;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(0, 213, 255, 0.1);
border-color: rgba(0, 213, 255, 0.3);
}
}
.notification-menu-footer {
padding: 8px 8px 0;
border-top: 1px solid rgba(0, 213, 255, 0.2);
}
.view-all-btn {
width: 100%;
padding: 8px 12px;
background-color: rgba(0, 213, 255, 0.1);
border: 1px solid rgba(0, 213, 255, 0.3);
border-radius: 4px;
color: #00d5ff;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: rgba(0, 213, 255, 0.2);
border-color: #00d5ff;
}
}
}
@@ -3,37 +3,6 @@
<span class="navbar-brand">NowChess</span>
<div class="ms-auto">
<div class="d-flex align-items-center gap-2">
<!-- Challenge Notification Badge -->
<div class="notification-container">
<button type="button" class="notification-badge" (click)="toggleNotificationMenu()"
[class.has-notifications]="incomingChallenges.length > 0" title="View challenges">
🔔
<span *ngIf="incomingChallenges.length > 0" class="badge">
{{ incomingChallenges.length }}
</span>
</button>
<!-- Notification Menu Dropdown -->
@if (showNotificationMenu && incomingChallenges.length > 0) {
<div class="notification-menu" (click)="$event.stopPropagation()">
<div class="notification-menu-header">
Challenges
<button type="button" class="close-btn" (click)="closeNotificationMenu()">×</button>
</div>
<div class="notification-list">
<div *ngFor="let challenge of incomingChallenges" class="notification-item">
{{ challenge.challenger.name }}
</div>
</div>
<div class="notification-menu-footer">
<button type="button" class="view-all-btn" (click)="goToChallenges()">
View All Challenges →
</button>
</div>
</div>
}
</div>
<button type="button" class="app-btn" (click)="toggleTheme()">
{{ isDarkMode ? 'Light mode' : 'Dark mode' }}
</button>
@@ -42,9 +11,6 @@
<button type="button" class="me-btn" (click)="goToProfile()">
👤 {{ user.username }}
</button>
<button type="button" class="app-btn" (click)="goToChallenges()">
Challenges
</button>
<button type="button" class="app-btn" (click)="logout()">Logout</button>
</div>
} @else {
@@ -60,12 +26,6 @@
</div>
</nav>
<!-- Challenge Notification Popup -->
@if (displayedChallenge) {
<app-challenge-notification [challenge]="displayedChallenge" (accept)="onChallengeAccepted($event)"
(decline)="onChallengeDeclined($event)" (close)="onNotificationClose()" />
}
@if (showLoginDialog) {
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
}
@@ -8,14 +8,11 @@ import { CurrentUser } from '../../models/auth.models';
import { LoginDialogComponent } from '../login-dialog/login-dialog.component';
import { RegisterDialogComponent } from '../register-dialog/register-dialog.component';
import { ThemeService } from '../../services/theme.service';
import { ChallengeEventService } from '../../services/challenge-event.service';
import { ChallengeNotificationComponent } from '../challenge-notification/challenge-notification.component';
import { Challenge } from '../../models/challenge.models';
@Component({
selector: 'app-toolbar',
standalone: true,
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent, ChallengeNotificationComponent],
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.css'
})
@@ -24,16 +21,12 @@ export class ToolbarComponent implements OnInit {
private readonly authService = inject(AuthService);
private readonly authDialogService = inject(AuthDialogService);
private readonly themeService = inject(ThemeService);
private readonly challengeEventService = inject(ChallengeEventService);
private readonly router = inject(Router);
currentUser: CurrentUser | null = null;
showLoginDialog = false;
showRegisterDialog = false;
isDarkMode = false;
incomingChallenges: Challenge[] = [];
showNotificationMenu = false;
displayedChallenge: Challenge | null = null;
ngOnInit(): void {
this.authService.currentUser$
@@ -54,22 +47,6 @@ export class ToolbarComponent implements OnInit {
.subscribe((isDarkMode) => {
this.isDarkMode = isDarkMode;
});
this.challengeEventService.getIncomingChallenges$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((challenges) => {
this.incomingChallenges = challenges;
// Show the most recent challenge as notification
if (challenges.length > 0) {
this.displayedChallenge = challenges[0];
}
});
this.challengeEventService.getChallengeReceived$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((challenge) => {
this.displayedChallenge = challenge;
});
}
openLoginDialog(): void {
@@ -107,34 +84,4 @@ export class ToolbarComponent implements OnInit {
onRegisterSuccess(): void {
this.closeRegisterDialog();
}
toggleNotificationMenu(): void {
this.showNotificationMenu = !this.showNotificationMenu;
}
closeNotificationMenu(): void {
this.showNotificationMenu = false;
}
onChallengeAccepted(challenge: Challenge): void {
this.challengeEventService.onChallengeAccepted(challenge);
this.displayedChallenge = null;
this.closeNotificationMenu();
// Navigate to the game (once game creation is handled by backend)
}
onChallengeDeclined(challenge: Challenge): void {
this.challengeEventService.removeChallenge(challenge.id);
this.displayedChallenge = null;
this.closeNotificationMenu();
}
onNotificationClose(): void {
this.displayedChallenge = null;
}
goToChallenges(): void {
this.closeNotificationMenu();
void this.router.navigate(['/challenges']);
}
}
+9 -66
View File
@@ -46,6 +46,13 @@
<div class="w" *ngFor="let win of windows['wA3']" [ngStyle]="win.style"></div>
</div>
</div>
<!-- Draggable Meat Emoji -->
@if (showMeatEmoji) {
<div class="meat-emoji" [style.left.px]="meatX" [style.top.px]="meatY" (mousedown)="onMeatMouseDown($event)">
🍖
</div>
}
</div>
<div class="bwrap" style="left:21%;width:15%;">
@@ -94,8 +101,8 @@
<div class="bb-tag">WELCOME</div>
<div class="bb-title" style="font-size:clamp(16px,1.8vw,26px);">WELCOME TO<br />NOWCHESS</div>
<div class="bb-subtitle">Play your next move from the skyline.</div>
<button type="button" class="app-btn" (click)="openChallengeDialog()" [disabled]="creating">
{{ creating ? 'CREATING...' : 'CREATE GAME →' }}
<button type="button" class="app-btn" (click)="startOneVsOne()" [disabled]="creating">
{{ creating ? 'CREATING...' : 'START NOW →' }}
</button>
</div>
</div>
@@ -138,14 +145,6 @@
</div>
</div>
<!-- Speech Bubble -->
@if (showSpeechBubble) {
<div class="speech-bubble-container" (click)="onSpeechBubbleClick()">
<div class="speech-bubble">
<div class="bubble-text">{{ bubbleMessage }}</div>
<div class="bubble-tail"></div>
</div>
<!-- Speech Bubble -->
@if (showSpeechBubble) {
<div class="speech-bubble-container" (click)="onSpeechBubbleClick()">
@@ -156,39 +155,6 @@
</div>
}
<!-- Zoomed Window View -->
@if (isZoomedIn) {
<div class="zoom-overlay" (click)="onZoomedViewClick()" (mousemove)="onMouseMove($event)" (mouseup)="onMouseUp()"
(mouseleave)="onMouseUp()">
<div class="zoom-window-wrapper" (click)="$event.stopPropagation()">
<div class="zoom-window-frame">
<div class="zoom-player-2">
<img src="/assets/arabian-chess/player-two.gif" alt="Player 2" class="player-2-gif"
(click)="$event.stopPropagation()" />
@if (showSecondSpeechBubble) {
<div class="second-speech-bubble">
<div class="bubble-text">Feed me! 🍖</div>
<div class="bubble-tail"></div>
</div>
}
@if (showHappyBubble) {
<div class="happy-speech-bubble">
<div class="bubble-text">Happy meow! 😸</div>
<div class="bubble-tail"></div>
</div>
}
</div>
</div>
<!-- Draggable Meat Emoji -->
@if (showMeatEmoji) {
<div class="meat-emoji" [style.left.px]="meatX" [style.top.px]="meatY" (mousedown)="onMeatMouseDown($event)">
🍖
</div>
}
</div>
}
<!-- Zoomed Window View -->
@if (isZoomedIn) {
<div class="zoom-overlay" (click)="onZoomedViewClick()" (mousemove)="onMouseMove($event)" (mouseup)="onMouseUp()"
@@ -227,20 +193,6 @@
<div class="ground"></div>
</div>
@if (showDifficultyDialog) {
<div class="dialog-overlay" (click)="closeDifficultyDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-title">SELECT DIFFICULTY</div>
<div class="dialog-actions">
<button type="button" class="app-btn" (click)="startVsBot('easy')" [disabled]="creating">EASY</button>
<button type="button" class="app-btn" (click)="startVsBot('medium')" [disabled]="creating">MEDIUM</button>
<button type="button" class="app-btn" (click)="startVsBot('hard')" [disabled]="creating">HARD</button>
}
<div class="haze"></div>
<div class="ground"></div>
</div>
@if (showDifficultyDialog) {
<div class="dialog-overlay" (click)="closeDifficultyDialog()">
<div class="dialog-card" (click)="$event.stopPropagation()">
@@ -259,7 +211,6 @@
<div class="dialog-card" (click)="$event.stopPropagation()">
<div class="dialog-title">MORE OPTIONS</div>
<div class="dialog-actions">
<button type="button" class="app-btn" (click)="openChallengeDialog()">START GAME</button>
<button type="button" class="app-btn" (click)="openImportDialog()">IMPORT GAME</button>
</div>
</div>
@@ -312,14 +263,6 @@
</div>
}
@if (showChallengeDialog) {
<app-challenge-create-dialog (closeChallengeDialog)="closeChallengeDialog()"></app-challenge-create-dialog>
}
@if (errorMessage) {
<p class="error-banner">{{ errorMessage }}</p>
}
</div>
@if (errorMessage) {
<p class="error-banner">{{ errorMessage }}</p>
}
+15 -370
View File
@@ -1,18 +1,13 @@
import { CommonModule } from '@angular/common';
import { Component, DestroyRef, OnDestroy, OnInit, inject } from '@angular/core';
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 { 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 { 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';
@@ -35,20 +30,28 @@ interface WindowCell {
style: Record<string, string>;
}
interface Star {
style: Record<string, string>;
}
interface BackgroundBuilding {
style: Record<string, string>;
}
interface WindowCell {
state: 'off' | 'on';
color?: string;
glowColor?: string;
style: Record<string, string>;
}
@Component({
selector: 'app-welcome',
standalone: true,
imports: [CommonModule, FormsModule, ChallengeCreateDialogComponent],
templateUrl: './welcome.component.html',
styleUrls: ['./welcome.component.css']
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);
export class WelcomeComponent implements OnInit, OnDestroy {
private readonly destroyRef = inject(DestroyRef);
private readonly authService = inject(AuthService);
@@ -58,15 +61,8 @@ export class WelcomeComponent implements OnInit, OnDestroy {
creating = false;
joiningGame = false;
importing = false;
showChallengeDialog = false;
errorMessage = '';
showDifficultyDialog = false;
showOptionsDialog = false;
showJoinDialog = false;
showImportDialog = false;
showDifficultyDialog = false;
showOptionsDialog = false;
showJoinDialog = false;
@@ -108,43 +104,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
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'];
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<string, WindowCell[]> = {};
private flickerIntervalId: ReturnType<typeof setInterval> | undefined;
private speechBubbleTimeoutId: ReturnType<typeof setTimeout> | undefined;
private zoomTimeoutId: ReturnType<typeof setTimeout> | 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'];
@@ -211,20 +170,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.errorMessage = '';
}
openChallengeDialog(): void {
if (!this.requireAuth(() => this.showChallengeDialog = true)) {
return;
}
this.closeAllDialogs();
this.showChallengeDialog = true;
}
closeChallengeDialog(): void {
this.showChallengeDialog = false;
this.errorMessage = '';
}
openOptionsDialog(): void {
this.closeAllDialogs();
this.showOptionsDialog = true;
@@ -407,136 +352,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
action();
}
private performStartOneVsOne(): void {
if (!this.requireAuth(() => this.performStartOneVsOne())) {
return;
}
this.performStartOneVsOne();
}
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 performStartOneVsOne(): void {
if (this.creating) {
return;
@@ -553,9 +368,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
},
error: (error) => {
this.errorMessage = getErrorMessage(error, 'Unable to create a game.');
@@ -563,7 +375,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
});
}
private performStartVsBot(difficulty: Difficulty): void {
private performStartVsBot(difficulty: Difficulty): void {
if (this.creating) {
return;
@@ -572,7 +383,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.errorMessage = '';
this.creating = true;
this.showDifficultyDialog = false;
this.showDifficultyDialog = false;
this.gameApi
.createGameVsBot(difficulty)
@@ -582,9 +392,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
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.');
@@ -592,9 +399,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
});
}
private performSubmitJoinGame(): void {
const gameId = this.gameIdInput.trim();
if (this.joiningGame || !gameId) {
private performSubmitJoinGame(): void {
const gameId = this.gameIdInput.trim();
if (this.joiningGame || !gameId) {
@@ -605,7 +409,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.joiningGame = true;
this.gameApi
.getGame(gameId)
.getGame(gameId)
.pipe(finalize(() => (this.joiningGame = false)))
.subscribe({
@@ -614,10 +417,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
void this.router.navigate(['/game', game.gameId], {
state: { theme: this.isSunsetMode ? 'light' : 'dark' }
});
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.');
@@ -656,7 +455,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.showOptionsDialog = false;
this.showJoinDialog = false;
this.showImportDialog = false;
this.showChallengeDialog = false;
this.errorMessage = '';
}
@@ -774,159 +572,6 @@ export class WelcomeComponent implements OnInit, OnDestroy {
private stopWindowFlicker(): void {
if (this.flickerIntervalId === undefined) {
return;
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 = {};
}
}
clearInterval(this.flickerIntervalId);
this.flickerIntervalId = undefined;