diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..97ce0c0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,20 @@
+## 0.0.0 (2026-05-12)
+
+### Features
+
+* added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48))
+* added web view 1v1 ([1828fa3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1828fa3275ddb8ce6bb90a9f6a970ec428ebce3a))
+* NCS-63 User account implementation ([#2](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/2)) ([ff75c8c](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8))
+* NCS-75 Frontend Deployment Dockerfile ([#4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/4)) ([bd7ec58](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd7ec581e38b5d8e775741bf16e19b4dc67b979e))
+
+### Bug Fixes
+
+* build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91))
+* cleaner components seperation ([8b090e4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8b090e4d96c64c0adb253e3aefad7930108ccfb9))
+* gitignore ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d))
+* GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02))
+* npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04))
+* removed .vs ([2833ead](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2833ead7be3b47ee5c188d2d21b7326cb3cb3f26))
+* removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04))
+* size of pieces and removed text view of the game state ([c60d00f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c60d00f9d25247504845654615065fbccd7fe448))
+* structure ([3e8c7c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/3e8c7c4057e55aeec7cee8c24f6751ff24912c93))
diff --git a/Dockerfile b/Dockerfile
index 1f05e67..0aafc7a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,25 @@
-FROM node:20-alpine AS build
+FROM --platform=$BUILDPLATFORM node:lts-alpine3.23 AS builder
WORKDIR /app
COPY package*.json ./
-RUN npm ci --silent
+RUN npm install
COPY . .
+RUN npm run build
-RUN npm run build -- --configuration production
+FROM --platform=$TARGETPLATFORM nginx:stable-alpine AS production
-FROM nginx:stable-alpine
+RUN apk add --no-cache gettext
+
+RUN rm -rf /usr/share/nginx/html/*
+COPY --from=builder /app/dist/nowchess-frontend/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
-COPY --from=build /app/dist/nowchess-frontend/browser /usr/share/nginx/html
+COPY public/env.template.js /usr/share/nginx/html/env.template.js
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+RUN chmod +x /docker-entrypoint.sh
EXPOSE 80
-CMD ["nginx", "-g", "daemon off;"]
+ENTRYPOINT ["/docker-entrypoint.sh"]
diff --git a/angular.json b/angular.json
index 3542eee..53f40a6 100644
--- a/angular.json
+++ b/angular.json
@@ -47,17 +47,25 @@
"budgets": [
{
"type": "initial",
- "maximumWarning": "500kB",
- "maximumError": "4MB"
+ "maximumWarning": "1MB",
+ "maximumError": "5MB"
},
{
"type": "anyComponentStyle",
- "maximumWarning": "1.5MB",
- "maximumError": "2MB"
+ "maximumWarning": "1MB",
+ "maximumError": "5MB"
}
],
"outputHashing": "all"
},
+ "staging": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.staging.ts"
+ }
+ ]
+ },
"development": {
"fileReplacements": [
{
@@ -83,6 +91,9 @@
"production": {
"buildTarget": "nowchess-frontend:build:production"
},
+ "staging": {
+ "buildTarget": "nowchess-frontend:build:staging"
+ },
"development": {
"buildTarget": "nowchess-frontend:build:development"
}
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 0000000..dab211f
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -e
+
+# Replace placeholders in env.template.js with environment variables and write env.js
+TEMPLATE_PATH="/usr/share/nginx/html/env.template.js"
+TARGET_PATH="/usr/share/nginx/html/env.js"
+
+if [ -f "$TEMPLATE_PATH" ]; then
+ echo "Rendering runtime config from $TEMPLATE_PATH"
+ echo "Using environment variables:"
+ printenv
+ echo "----"
+ envsubst < "$TEMPLATE_PATH" > "$TARGET_PATH"
+else
+ echo "No runtime template found at $TEMPLATE_PATH, skipping"
+fi
+
+exec nginx -g 'daemon off;'
diff --git a/nginx.conf b/nginx.conf
index 4a983a1..ff4e939 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,16 +1,22 @@
server {
- listen 80;
- server_name _;
- root /usr/share/nginx/html;
- index index.html;
+ listen 80;
+ server_name localhost;
- location / {
- try_files $uri $uri/ /index.html;
- }
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
- location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ {
- try_files $uri =404;
- expires 1y;
- add_header Cache-Control "public";
- }
+ location /env.js {
+ root /usr/share/nginx/html;
+ default_type application/javascript;
+ expires -1;
+ add_header 'Cache-Control' 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
}
diff --git a/package.json b/package.json
index 0882715..37a84ae 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
- "build": "ng build",
+ "build": "ng build --configuration production",
+ "build:staging": "ng build --configuration staging",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
diff --git a/public/env.template.js b/public/env.template.js
new file mode 100644
index 0000000..72477ac
--- /dev/null
+++ b/public/env.template.js
@@ -0,0 +1,4 @@
+window.__RUNTIME_CONFIG__ = {
+ API_URL: "${API_URL}",
+ WEBSOCKET_URL: "${WEBSOCKET_URL}"
+};
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 59dd95e..9fb0ff3 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -2,12 +2,10 @@ import { Routes } from '@angular/router';
import { GameComponent } from './pages/game/game.component';
import { WelcomeComponent } from './pages/welcome/welcome.component';
import { ProfileComponent } from './pages/profile/profile.component';
-import { ChallengesComponent } from './pages/challenges/challenges.component';
export const routes: Routes = [
{ path: '', component: WelcomeComponent },
{ path: 'profile', component: ProfileComponent },
- { path: 'challenges', component: ChallengesComponent },
{ path: 'game/:gameId', component: GameComponent },
{ path: '**', redirectTo: '' }
];
diff --git a/src/app/app.ts b/src/app/app.ts
index 5211464..4206e4c 100644
--- a/src/app/app.ts
+++ b/src/app/app.ts
@@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
import { ThemeService } from './services/theme.service';
-import { ChallengeWebSocketService } from './services/challenge-websocket.service';
@Component({
selector: 'app-root',
@@ -11,13 +10,9 @@ import { ChallengeWebSocketService } from './services/challenge-websocket.servic
styleUrl: './app.css'
})
export class App implements OnInit {
- constructor(
- private readonly themeService: ThemeService,
- private readonly challengeWs: ChallengeWebSocketService
- ) { }
+ constructor(private readonly themeService: ThemeService) { }
ngOnInit(): void {
this.themeService.initTheme();
- this.challengeWs.connect();
}
}
diff --git a/src/app/core/config.loader.ts b/src/app/core/config.loader.ts
new file mode 100644
index 0000000..2d10fd5
--- /dev/null
+++ b/src/app/core/config.loader.ts
@@ -0,0 +1,11 @@
+/**
+ * Load runtime configuration from window.__RUNTIME_CONFIG__
+ * This is injected by docker-entrypoint.sh at container startup
+ */
+export function loadRuntimeConfig() {
+ const config = (window as any).__RUNTIME_CONFIG__ || {};
+ return {
+ apiUrl: config.API_URL || '',
+ wsUrl: config.WEBSOCKET_URL || 'ws://localhost:8080'
+ };
+}
diff --git a/src/app/pages/welcome/welcome.component.html b/src/app/pages/welcome/welcome.component.html
index 98509a7..487018a 100644
--- a/src/app/pages/welcome/welcome.component.html
+++ b/src/app/pages/welcome/welcome.component.html
@@ -138,6 +138,14 @@
+
+ @if (showSpeechBubble) {
+
+
+
{{ bubbleMessage }}
+
+
+
@if (showSpeechBubble) {
@@ -148,6 +156,39 @@
}
+
+ @if (isZoomedIn) {
+
+
+
+
+

+ @if (showSecondSpeechBubble) {
+
+ }
+ @if (showHappyBubble) {
+
+ }
+
+
+
+
+ @if (showMeatEmoji) {
+
+ 🍖
+
+ }
+
+ }
+
@if (isZoomedIn) {
+ @if (showDifficultyDialog) {
+
+
+
SELECT DIFFICULTY
+
+
+
+
+ }
+
+
+
+
+
@if (showDifficultyDialog) {
@@ -261,6 +316,10 @@
}
+ @if (errorMessage) {
+
{{ errorMessage }}
+ }
+
@if (errorMessage) {
{{ errorMessage }}
}
diff --git a/src/app/pages/welcome/welcome.component.ts b/src/app/pages/welcome/welcome.component.ts
index 31f8d27..bac297f 100644
--- a/src/app/pages/welcome/welcome.component.ts
+++ b/src/app/pages/welcome/welcome.component.ts
@@ -1,13 +1,18 @@
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';
@@ -36,7 +41,14 @@ interface WindowCell {
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);
@@ -49,6 +61,12 @@ export class WelcomeComponent implements OnInit, OnDestroy {
showChallengeDialog = false;
errorMessage = '';
+ showDifficultyDialog = false;
+ showOptionsDialog = false;
+ showJoinDialog = false;
+ showImportDialog = false;
+
+
showDifficultyDialog = false;
showOptionsDialog = false;
showJoinDialog = false;
@@ -90,6 +108,43 @@ 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
= {};
+
+ 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'];
@@ -352,6 +407,136 @@ 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;
@@ -368,6 +553,9 @@ 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.');
@@ -375,6 +563,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
});
}
+ private performStartVsBot(difficulty: Difficulty): void {
private performStartVsBot(difficulty: Difficulty): void {
if (this.creating) {
return;
@@ -383,6 +572,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.errorMessage = '';
this.creating = true;
this.showDifficultyDialog = false;
+ this.showDifficultyDialog = false;
this.gameApi
.createGameVsBot(difficulty)
@@ -392,6 +582,9 @@ 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.');
@@ -399,6 +592,9 @@ 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) {
@@ -409,6 +605,7 @@ export class WelcomeComponent implements OnInit, OnDestroy {
this.joiningGame = true;
this.gameApi
+ .getGame(gameId)
.getGame(gameId)
.pipe(finalize(() => (this.joiningGame = false)))
.subscribe({
@@ -417,6 +614,10 @@ 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.');
@@ -573,6 +774,159 @@ 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;
diff --git a/src/environments/environment.staging.ts b/src/environments/environment.staging.ts
new file mode 100644
index 0000000..a2f73b6
--- /dev/null
+++ b/src/environments/environment.staging.ts
@@ -0,0 +1,11 @@
+import { loadRuntimeConfig } from '../app/core/config.loader';
+
+const runtimeConfig = loadRuntimeConfig();
+
+export const environment = {
+ production: true,
+ apiBaseUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de',
+ accountServiceUrl: runtimeConfig.apiUrl || 'https://st.nowchess.janis-eccarius.de',
+ wsBaseUrl: runtimeConfig.wsUrl || 'wss://st.nowchess.janis-eccarius.de',
+ apiPath: '/api/board/game'
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index 8c38733..2478e38 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -1,7 +1,11 @@
+import { loadRuntimeConfig } from '../app/core/config.loader';
+
+const runtimeConfig = loadRuntimeConfig();
+
export const environment = {
- production: true,
- apiBaseUrl: '',
- accountServiceUrl: '',
- wsBaseUrl: 'ws://localhost:8080',
+ production: false,
+ apiBaseUrl: runtimeConfig.apiUrl || '',
+ accountServiceUrl: runtimeConfig.apiUrl || '',
+ wsBaseUrl: runtimeConfig.wsUrl,
apiPath: '/api/board/game'
};
diff --git a/src/index.html b/src/index.html
index 5f83c38..1d5f35e 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,6 +6,7 @@
+
diff --git a/versions.env b/versions.env
new file mode 100644
index 0000000..0e288f2
--- /dev/null
+++ b/versions.env
@@ -0,0 +1,3 @@
+MAJOR=0
+MINOR=1
+PATCH=0