Skip to content
This repository was archived by the owner on Aug 20, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ events {
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Ensure ETag headers are generated so clients can revalidate quickly
etag on;

# Gzip compression for text-based / compressible assets
gzip on; # Enable gzip
gzip_comp_level 6; # Balance (1=fastest, 9=slowest). 6 is a good trade-off.
gzip_min_length 1024; # Only compress responses >= 1KB
gzip_vary on; # Add Vary: Accept-Encoding for proxies/CDNs
gzip_proxied any; # Allow compression for all proxied requests
gzip_disable "msie6"; # Disable for very old browsers
# Types to compress (avoid already-compressed formats like images, woff2)
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/atom+xml
application/vnd.ms-fontobject
font/ttf
font/otf
image/svg+xml;

sendfile on;
keepalive_timeout 65;
Expand All @@ -13,17 +38,29 @@ http {
listen 80;
server_name localhost;

# SPA / root handler
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}

# Handle static assets
# HTML should never be aggressively cached so new deployments are seen immediately
location ~* \.html$ {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
}

# Handle static assets (non-hashed filenames) with a short cache + revalidation.
# We avoid 'immutable' and long max-age because filenames do not contain content hashes.
# Using a modest max-age allows brief client-side caching while ETag enables fresh checks.
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|ttf|woff|woff2)$ {
root /usr/share/nginx/html;
expires 1d;
add_header Cache-Control "public, immutable";
# Provide a single authoritative Cache-Control header (avoid duplicate via 'expires').
# max-age=300 (5 minutes) balances freshness and some caching; adjust as needed.
add_header Cache-Control "public, max-age=600, must-revalidate" always;
}

# Error pages
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "core-debug-visualizer",
"scripts": {
"dev": "tsc --watch & npm run serve",
"serve": "tsc && http-server src/public -p 8080 -H \"Cache-Control: no-cache\" -o html/index.html",
"serve": "tsc && http-server src/public -p 8080 -H \"Cache-Control: no-cache\" -o index.html",
"build": "tsc"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/public/assets/ui-svgs/fullscreen-close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/public/assets/ui-svgs/fullscreen-open.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions src/public/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,29 @@ body {
transform-origin: top right;
}

#fullscreen-toggle-button.floating-control {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 1000;
background: #fff;
border: 1px solid #d0d0d0;
border-radius: 10px;
padding: 10px 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
cursor: pointer;
line-height: 1;
}
#fullscreen-toggle-button.floating-control:active,
#fullscreen-toggle-button.floating-control.active {
transform: translateY(1px);
}
#fullscreen-toggle-button #fullscreen-icon {
width: 18px;
height: 18px;
display: block;
}

/* Winner Display */

#win-display-box {
Expand Down
10 changes: 8 additions & 2 deletions src/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/fireworks-js@2.x/dist/index.umd.js"></script>
<script type="module" src="/js/renderer/fireworksRenderer.js"></script>
<script type="module" src="/js/main.js"></script>
<script type="module" src="/js/renderer/layoutRenderer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fireworks-js@2.x/dist/index.umd.js"></script>

<div class="container">
<div class="top-bar">
Expand Down Expand Up @@ -50,20 +49,27 @@
</div>
<div id="tooltip"></div>

<!-- Win Display -->
<div id="win-display-box" class="win-display">
<h1>🎖️🎉 Winner: <span id="winnername"></span> 🎈🏁</h1>
<h3 id="winreason"></h3>
</div>
<div id="background-darkener" class="win-display"></div>
<div class="fireworks win-display"></div>

<!-- Corner Buttons -->

<a href="https://github.com/42core-team" target="_blank">
<button id="corner-bottom-left" class="corner-button"></button>
</a>

<a href="https://coregame.de/" target="_blank">
<button id="corner-top-right" class="corner-button"></button>
</a>

<button id="fullscreen-toggle-button" class="floating-control" title="Toggle Fullscreen" aria-label="Toggle Fullscreen" aria-pressed="false">
<img id="fullscreen-icon" src="/assets/ui-svgs/fullscreen-open.svg" alt="Enter Fullscreen" />
</button>
</div>
</body>
</html>
1 change: 0 additions & 1 deletion src/public/misc/replay_latest.json

This file was deleted.

1 change: 1 addition & 0 deletions src/public/replays/replay_latest.json

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion src/ts/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const svgCanvas = document.getElementById('svg-canvas') as HTMLElement;

window.addEventListener('DOMContentLoaded', async () => {
let replayFilePath = '../misc/replay_latest.json';
let replayFilePath = '/replays/replay_latest.json';
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('replay')) {
replayFilePath = urlParams.get('replay') || replayFilePath;
Expand All @@ -13,4 +15,12 @@ window.addEventListener('DOMContentLoaded', async () => {

const { setupRenderer } = await import('./renderer/renderer.js');
await setupRenderer();

function updateSvgSize() {
const svgHeight = window.innerHeight - svgCanvas.getBoundingClientRect().top;
document.documentElement.style.setProperty('--svg-canvas-scale', `${svgHeight - 24}px`);
}
window.addEventListener('resize', updateSvgSize);
window.addEventListener('load', updateSvgSize);
updateSvgSize();
});
50 changes: 50 additions & 0 deletions src/ts/renderer/animationUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
type timingCurvePoints = { realTime: number; animationProgress: number }[];

abstract class TimingCurve {
protected abstract curve: timingCurvePoints;
public getValue(t: number): number {
const pts = this.curve;
if (pts.length === 0) return t;
if (t <= pts[0].realTime) return pts[0].animationProgress;
const last = pts[pts.length - 1];
if (t >= last.realTime) return last.animationProgress;
for (let i = 0; i < pts.length - 1; i++) {
const a = pts[i],
b = pts[i + 1];
if (t >= a.realTime && t <= b.realTime) {
const u = b.realTime === a.realTime ? 1 : (t - a.realTime) / (b.realTime - a.realTime);
return a.animationProgress + u * (b.animationProgress - a.animationProgress);
}
}
return last.animationProgress;
}
}

export class LinearTimingCurve extends TimingCurve {
protected curve: timingCurvePoints = [
{ realTime: 0, animationProgress: 0 },
{ realTime: 1, animationProgress: 1 },
];
}

export class EaseInOutTimingCurve extends TimingCurve {
protected curve: timingCurvePoints = [
{ realTime: 0, animationProgress: 0 },
{ realTime: 0.25, animationProgress: 0.125 },
{ realTime: 0.5, animationProgress: 0.5 },
{ realTime: 0.75, animationProgress: 0.875 },
{ realTime: 1, animationProgress: 1 },
];
public getValue(t: number): number {
if (t <= 0) return 0;
if (t >= 1) return 1;
return 0.5 - 0.5 * Math.cos(Math.PI * t);
}
}

export class MidTickIncreaseTimingCurve extends TimingCurve {
protected curve: timingCurvePoints = [
{ realTime: 0.4, animationProgress: 0 },
{ realTime: 0.6, animationProgress: 1 },
];
}
89 changes: 49 additions & 40 deletions src/ts/renderer/fireworksRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,106 @@
declare const Fireworks: any;

const container = document.querySelector('.fireworks');
const fireworks = new Fireworks.default(container, {
autoresize: true,
opacity: 0.6,
acceleration: 1.02,
friction: 0.98,
gravity: 0,
particles: 40,
traceLength: 2,
traceSpeed: 6,
explosion: 10,
intensity: 60,
flickering: 30,
rocketsPoint: { min: 25, max: 75 },
delay: { min: 1000000000, max: 1000000000 },
brightness: { min: 50, max: 100 },
decay: { min: 0.015, max: 0.025 },
});
let fireworks: any | null = null;
let renderFireworks = false;
let fireworkStrength = 1;
let fireworkDelay = 2000;
let isActive = false;

// Helpers
function isWindowMinimized(): boolean {
return window.outerWidth === 0 && window.outerHeight === 0;
}

function shouldRun(): boolean {
const visible = document.visibilityState === 'visible' && !document.hidden;
return visible && !isWindowMinimized() && renderFireworks;
}

function ensureFireworks(): void {
if (fireworks) return;
const globalFW = (window as any).Fireworks;
const Ctor = globalFW?.default ?? globalFW;
const container = document.querySelector('.fireworks') as HTMLElement | null;
if (!Ctor || !container) return;
fireworks = new Ctor(container, {
autoresize: true,
opacity: 0.6,
acceleration: 1.02,
friction: 0.98,
gravity: 0,
particles: 40,
traceLength: 2,
traceSpeed: 6,
explosion: 10,
intensity: 60,
flickering: 30,
rocketsPoint: { min: 25, max: 75 },
delay: { min: 1000000000, max: 1000000000 },
brightness: { min: 50, max: 100 },
decay: { min: 0.015, max: 0.025 },
});
}

function applyActivity(): void {
const nextActive = shouldRun();

if (nextActive && !isActive) {
isActive = true;
fireworks.start?.();
ensureFireworks();
fireworks?.start?.();
} else if (!nextActive && isActive) {
isActive = false;
fireworks.clear();
fireworks.stop?.();
fireworks?.clear?.();
fireworks?.stop?.();
}
}

// hooks
document.addEventListener('visibilitychange', applyActivity);
window.addEventListener('focus', applyActivity);
window.addEventListener('blur', applyActivity);
window.addEventListener('resize', applyActivity);
window.addEventListener('pageshow', applyActivity);
window.addEventListener('load', applyActivity);
window.addEventListener('pagehide', () => {
fireworks.clear();
fireworks.stop?.();
fireworks?.clear?.();
fireworks?.stop?.();
isActive = false;
});
window.addEventListener('beforeunload', () => {
fireworks.clear();
fireworks.stop?.();
fireworks?.clear?.();
fireworks?.stop?.();
});

// start setup
(function loop() {
if (isActive) {
fireworks.launch(fireworkStrength);
ensureFireworks();
fireworks?.launch?.(fireworkStrength);
if (fireworkStrength > 1) fireworkStrength--;
}
setTimeout(loop, isActive ? fireworkDelay : 1500);
})();

type Mode = 'SWISS' | 'ELIMINATION' | 'QUEUE';

function getFireworkStrengthFromUrlParameters(): number {
const p = new URLSearchParams(window.location.search);
const mode = (p.get('mode') || 'QUEUE').toUpperCase() as Mode;
const round = Number(p.get('round'));
const maxRounds = Number(p.get('maxRounds'));

if (mode == 'QUEUE' || !mode) return 1;
if (mode === 'QUEUE' || !mode) return 1;
fireworkDelay = 750;
if (mode == 'SWISS') return 2;
if (mode === 'SWISS') return 2;
fireworkDelay = 250;
if (mode == 'ELIMINATION' && round >= maxRounds - 1) return 30;
if (mode === 'ELIMINATION' && round >= maxRounds - 1) return 30;
fireworkDelay = 500;
if (mode == 'ELIMINATION' && round >= maxRounds - 3) return 5;
if (mode === 'ELIMINATION' && round >= maxRounds - 3) return 5;
fireworkDelay = 625;
if (mode == 'ELIMINATION') return 3;
if (mode === 'ELIMINATION') return 3;
fireworkDelay = 2000;
return 1;
}

export function setRenderFireworks(render: boolean): void {
if (renderFireworks == false && render == true) {
if (!renderFireworks && render) {
fireworkStrength = getFireworkStrengthFromUrlParameters();
fireworks.clear();
ensureFireworks();
fireworks?.clear?.();
}
renderFireworks = render;
applyActivity();
Expand Down
8 changes: 0 additions & 8 deletions src/ts/renderer/layoutRenderer.ts

This file was deleted.

Loading