3 IN 1: Theme Editor + Font Editor + Player Count
// ==UserScript==
// @name Kogama Plus
// @namespace http://tampermonkey.net/
// @version 1.01
// @author BlueEL
// @icon https://www.kogama.com/favicon.ico
// @description 3 IN 1: Theme Editor + Font Editor + Player Count
// @match https://www.kogama.com/*
// @grant none
// @license Copyright (c) 2026 BlueEL - All rights reserved.
// ==/UserScript==
(() => {
'use strict';
const APP_CONFIG = {
NAV_OPACITY: 0.1,
PANEL_ID: 'kogama-theme-panel',
PLAYER_COUNT_ID: 'kogama-player-count',
ROOT_SELECTOR: '#root-page-mobile',
CONTENT_CONTAINER_SELECTOR: '#content-container',
GAME_CARD_SELECTOR: '.gmqKr',
PLAYER_VALUE_SELECTOR: '._1ZdZA',
ADS_LINK_SELECTOR: '.MuiCollapse-root a[href*="subscription"]',
PANEL_TOP_OFFSET: 20,
PANEL_RIGHT_OFFSET: 20,
PANEL_WIDTH: 320,
OBSERVER_DEBOUNCE_DELAY: 250,
INITIAL_REFRESH_DELAY: 800,
STATS_REFRESH_INTERVAL: 2000,
FONT_RESULTS_LIMIT: 10,
PANEL_SHORTCUT_KEY: 'S',
DEFAULT_THEME: {
background: '#0e0a1f',
gradientStart: '#0e0a1f',
gradientEnd: '#003138',
textColor: '#ffffff',
fontFamily: 'Inter'
},
DEFAULT_PANEL_STATE: {
isOpen: true
},
FONT_FALLBACK: 'sans-serif',
FONT_OPTIONS: [
'Inter',
'Roboto',
'Open Sans',
'Lato',
'Montserrat',
'Poppins',
'Raleway',
'Ubuntu',
'Oswald',
'Merriweather',
'Playfair Display',
'Nunito',
'Rubik',
'Work Sans',
'Fira Sans',
'Quicksand',
'Inconsolata',
'DM Sans',
'Manrope',
'Space Grotesk',
'Plus Jakarta Sans',
'Source Sans 3',
'Outfit',
'Urbanist',
'Mulish',
'Bebas Neue',
'Anton',
'Archivo',
'Barlow',
'Barlow Condensed',
'Barlow Semi Condensed',
'Cabin',
'Cairo',
'Comfortaa',
'Cormorant',
'Cormorant Garamond',
'Dancing Script',
'Exo 2',
'Hind',
'Hind Madurai',
'IBM Plex Sans',
'IBM Plex Serif',
'Josefin Sans',
'Karla',
'Libre Baskerville',
'Libre Franklin',
'M PLUS Rounded 1c',
'Noto Sans',
'Noto Serif',
'Orbitron',
'PT Sans',
'PT Serif',
'Rajdhani',
'Red Hat Display',
'Red Hat Text',
'Sora',
'Teko',
'Titillium Web',
'Varela Round',
'Zilla Slab',
'Asap',
'Assistant',
'Baloo 2',
'Chivo',
'Encode Sans',
'Heebo',
'Jost',
'Kanit',
'Lexend',
'Maven Pro',
'Public Sans',
'Sen',
'Syne',
'Yantramanav'
],
STORAGE_KEY: 'kogama-theme-editor-state',
SELECTORS: {
toolbar: '.MuiToolbar-root',
stackHeader: '.MuiStack-root._2JO9f',
sectionShell: '._3TORb',
footer: 'footer',
heroSurface: '._1q4mD',
heroInnerSurface: '._1q4mD ._1sUGu ._1u05O',
gameTitle: '.gmqKr ._1ZdZA',
gameStatsItems: 'ul li'
},
ROUTES: {
game: 'https://www.kogama.com/games/',
build: 'https://www.kogama.com/build/'
}
};
class DomCache {
constructor() {
this.documentHead = document.head;
this.documentBody = document.body;
}
query(selector, root = document) {
return root.querySelector(selector);
}
queryAll(selector, root = document) {
return [...root.querySelectorAll(selector)];
}
getRootPage() {
return this.query(APP_CONFIG.ROOT_SELECTOR);
}
getContentContainer() {
return this.query(APP_CONFIG.CONTENT_CONTAINER_SELECTOR);
}
getGameCards() {
return this.queryAll(APP_CONFIG.GAME_CARD_SELECTOR);
}
getPlayerValueElements() {
return this.queryAll(APP_CONFIG.PLAYER_VALUE_SELECTOR);
}
getAdsLinks() {
return this.queryAll(APP_CONFIG.ADS_LINK_SELECTOR);
}
}
class StorageService {
loadRawState() {
try {
const rawValue = localStorage.getItem(APP_CONFIG.STORAGE_KEY);
if (!rawValue) {
return {};
}
const parsedValue = JSON.parse(rawValue);
return parsedValue && typeof parsedValue === 'object' ? parsedValue : {};
} catch {
return {};
}
}
saveRawState(nextState) {
try {
localStorage.setItem(APP_CONFIG.STORAGE_KEY, JSON.stringify(nextState));
} catch {}
}
loadThemeState() {
const storedState = this.loadRawState();
return {
...APP_CONFIG.DEFAULT_THEME,
background: typeof storedState.background === 'string' ? storedState.background : APP_CONFIG.DEFAULT_THEME.background,
gradientStart: typeof storedState.gradientStart === 'string' ? storedState.gradientStart : APP_CONFIG.DEFAULT_THEME.gradientStart,
gradientEnd: typeof storedState.gradientEnd === 'string' ? storedState.gradientEnd : APP_CONFIG.DEFAULT_THEME.gradientEnd,
textColor: typeof storedState.textColor === 'string' ? storedState.textColor : APP_CONFIG.DEFAULT_THEME.textColor,
fontFamily: typeof storedState.fontFamily === 'string' ? storedState.fontFamily : APP_CONFIG.DEFAULT_THEME.fontFamily
};
}
saveThemeState(themeState) {
const currentState = this.loadRawState();
this.saveRawState({
...currentState,
...themeState
});
}
loadPanelState() {
const storedState = this.loadRawState();
return {
...APP_CONFIG.DEFAULT_PANEL_STATE,
isOpen: typeof storedState.isOpen === 'boolean' ? storedState.isOpen : APP_CONFIG.DEFAULT_PANEL_STATE.isOpen
};
}
savePanelState(panelState) {
const currentState = this.loadRawState();
this.saveRawState({
...currentState,
...panelState
});
}
}
class StyleRegistry {
constructor(domCache) {
this.domCache = domCache;
this.registeredStyles = new Map();
}
mountStyle(key, cssText) {
if (this.registeredStyles.has(key)) {
this.registeredStyles.get(key).textContent = cssText;
return;
}
const styleElement = document.createElement('style');
styleElement.textContent = cssText;
this.domCache.documentHead.appendChild(styleElement);
this.registeredStyles.set(key, styleElement);
}
}
class FontCatalog {
constructor() {
this.loadedFonts = new Set();
}
searchFonts(queryText = '') {
const normalizedQuery = queryText.trim().toLowerCase();
return APP_CONFIG.FONT_OPTIONS
.filter(fontName => fontName.toLowerCase().includes(normalizedQuery))
.slice(0, APP_CONFIG.FONT_RESULTS_LIMIT);
}
loadFont(fontFamily) {
if (!fontFamily || this.loadedFonts.has(fontFamily)) {
return;
}
const fontLink = document.createElement('link');
fontLink.rel = 'stylesheet';
fontLink.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily).replace(/%20/g, '+')}:wght@400;500;600;700&display=swap`;
document.head.appendChild(fontLink);
this.loadedFonts.add(fontFamily);
}
}
class ThemeManager {
constructor(domCache, storageService, styleRegistry, fontCatalog) {
this.domCache = domCache;
this.storageService = storageService;
this.styleRegistry = styleRegistry;
this.fontCatalog = fontCatalog;
this.themeState = this.storageService.loadThemeState();
}
initialize() {
this.fontCatalog.loadFont(this.themeState.fontFamily);
this.applyStaticShellStyles();
this.applyTheme();
}
getThemeState() {
return { ...this.themeState };
}
updateTheme(patch) {
this.themeState = {
...this.themeState,
...patch
};
this.storageService.saveThemeState(this.themeState);
this.fontCatalog.loadFont(this.themeState.fontFamily);
this.applyTheme();
}
applyStaticShellStyles() {
this.styleRegistry.mountStyle('kogama-shell-overrides', `
${APP_CONFIG.SELECTORS.toolbar},
${APP_CONFIG.SELECTORS.stackHeader},
${APP_CONFIG.SELECTORS.sectionShell},
${APP_CONFIG.SELECTORS.footer},
${APP_CONFIG.SELECTORS.heroSurface},
${APP_CONFIG.SELECTORS.heroInnerSurface} {
background: rgba(0, 0, 0, ${APP_CONFIG.NAV_OPACITY}) !important;
background-image: none !important;
}
${APP_CONFIG.SELECTORS.gameTitle} {
color: #ffffff !important;
}
`);
}
applyTheme() {
const rootPage = this.domCache.getRootPage();
if (!rootPage) {
return;
}
const { background, gradientStart, gradientEnd, textColor, fontFamily } = this.themeState;
rootPage.style.backgroundColor = background;
rootPage.style.backgroundImage = `linear-gradient(135deg, ${gradientStart}, ${gradientEnd})`;
rootPage.style.color = textColor;
document.body.style.fontFamily = `"${fontFamily}", ${APP_CONFIG.FONT_FALLBACK}`;
}
}
class NumberFormatter {
formatCompactValue(textValue) {
const sanitizedText = String(textValue).trim();
const compactPattern = /([\d.,]+)\s*([kKmM])/;
const match = sanitizedText.match(compactPattern);
if (!match) {
return sanitizedText;
}
const numericValue = Number.parseFloat(match[1].replace(/\s/g, '').replace(',', '.'));
if (Number.isNaN(numericValue)) {
return sanitizedText;
}
const roundedValue = Math.round(numericValue * 10) / 10;
const outputValue = roundedValue.toFixed(1).replace(/\.0$/, '');
return `${outputValue}${match[2].toUpperCase()}`;
}
}
class StatsManager {
constructor(domCache, numberFormatter) {
this.domCache = domCache;
this.numberFormatter = numberFormatter;
}
refreshGameStats() {
const gameCards = this.domCache.getGameCards();
if (!gameCards.length) {
return;
}
gameCards.forEach(card => this.refreshCardStats(card));
}
refreshCardStats(cardElement) {
const statItems = this.domCache.queryAll(APP_CONFIG.SELECTORS.gameStatsItems, cardElement);
statItems.forEach(statItem => this.refreshStatItem(statItem));
}
refreshStatItem(statItem) {
const textNode = [...statItem.childNodes].reverse().find(node => node.nodeType === Node.TEXT_NODE);
if (!textNode) {
return;
}
const nextValue = this.numberFormatter.formatCompactValue(textNode.textContent);
textNode.textContent = ` ${nextValue}`;
}
}
class AdsManager {
constructor(domCache) {
this.domCache = domCache;
}
removeSubscriptionAds() {
const adLinks = this.domCache.getAdsLinks();
if (!adLinks.length) {
return;
}
adLinks.forEach(link => {
const collapsibleContainer = link.closest('.MuiCollapse-root');
if (collapsibleContainer) {
collapsibleContainer.remove();
}
});
}
}
class PlayerCountManager {
constructor(domCache) {
this.domCache = domCache;
}
refreshPlayerCount() {
if (!this.isSupportedRoute()) {
this.removePlayerCountBadge();
return;
}
const totalPlayers = this.calculatePlayerCount();
const badgeElement = this.getOrCreatePlayerCountBadge();
if (badgeElement) {
badgeElement.textContent = `Players Online · ${totalPlayers}`;
}
}
isSupportedRoute() {
const currentUrl = window.location.href;
return (
currentUrl.startsWith(APP_CONFIG.ROUTES.game) ||
currentUrl.startsWith(APP_CONFIG.ROUTES.build) ||
currentUrl === "https://www.kogama.com/" ||
currentUrl === "https://www.kogama.com"
);
}
calculatePlayerCount() {
return this.domCache.getPlayerValueElements().reduce((total, element) => {
const value = Number.parseInt(element.textContent.trim(), 10);
return Number.isNaN(value) ? total : total + value;
}, 0);
}
getOrCreatePlayerCountBadge() {
const existingBadge = document.getElementById(APP_CONFIG.PLAYER_COUNT_ID);
if (existingBadge) {
return existingBadge;
}
const targetContainer = this.domCache.getContentContainer();
if (!targetContainer) {
return null;
}
const badgeElement = document.createElement('div');
badgeElement.id = APP_CONFIG.PLAYER_COUNT_ID;
badgeElement.textContent = 'Players Online · 0';
targetContainer.prepend(badgeElement);
return badgeElement;
}
removePlayerCountBadge() {
const badgeElement = document.getElementById(APP_CONFIG.PLAYER_COUNT_ID);
if (badgeElement) {
badgeElement.remove();
}
}
}
class PanelView {
constructor(themeManager, fontCatalog) {
this.themeManager = themeManager;
this.fontCatalog = fontCatalog;
this.elements = {};
}
mount() {
this.render();
this.bindElements();
this.populateForm();
this.renderFontResults();
}
bindThemeChange(handler) {
const colorInputs = [
this.elements.gradientStartInput,
this.elements.gradientEndInput
];
colorInputs.forEach(input => {
input.addEventListener('input', () => {
handler({
gradientStart: this.elements.gradientStartInput.value,
gradientEnd: this.elements.gradientEndInput.value
});
});
});
this.elements.fontSearchInput.addEventListener('input', () => {
this.renderFontResults(this.elements.fontSearchInput.value);
});
this.elements.fontResultsList.addEventListener('click', event => {
const fontOption = event.target.closest('[data-font-family]');
if (!fontOption) {
return;
}
const selectedFont = fontOption.dataset.fontFamily;
this.setSelectedFont(selectedFont);
handler({ fontFamily: selectedFont });
});
}
bindPanelClose(handler) {
this.elements.closeButton.addEventListener('click', handler);
}
render() {
const panelElement = document.createElement('aside');
panelElement.id = APP_CONFIG.PANEL_ID;
panelElement.setAttribute('aria-label', 'Theme Editor');
panelElement.innerHTML = `
<div class="kte-panel__shell">
<div class="kte-panel__header">
<div class="kte-panel__eyebrow">Appearance</div>
<div class="kte-panel__title-row">
<h2 class="kte-panel__title">Theme Editor</h2>
<button
id="kte-panel-close"
class="kte-panel__close"
type="button"
aria-label="Hide theme editor"
title="Hide theme editor (Shift+S to show)"
>
<span class="kte-panel__close-icon" aria-hidden="true">✕</span>
</button>
</div>
<p class="kte-panel__subtitle">Made by BlueEL.</p>
</div>
<section class="kte-panel__section">
<div class="kte-panel__section-header">
<h3 class="kte-panel__section-title">Colors</h3>
<span class="kte-panel__section-caption">Gradient</span>
</div>
<div class="kte-color-grid">
<label class="kte-field kte-field--color">
<span class="kte-field__label">Gradient Start</span>
<input id="kte-gradient-start" class="kte-field__input kte-field__input--color" type="color">
</label>
<label class="kte-field kte-field--color">
<span class="kte-field__label">Gradient End</span>
<input id="kte-gradient-end" class="kte-field__input kte-field__input--color" type="color">
</label>
</div>
</section>
<section class="kte-panel__section">
<div class="kte-panel__section-header">
<h3 class="kte-panel__section-title">Typography</h3>
<span class="kte-panel__section-caption">Search and apply fonts</span>
</div>
<label class="kte-field">
<span class="kte-field__label">Font Family</span>
<input id="kte-font-search" class="kte-field__input" type="text" placeholder="Search fonts...">
</label>
<div id="kte-font-results" class="kte-font-results" role="listbox" aria-label="Font results"></div>
</section>
</div>
`;
document.body.appendChild(panelElement);
}
bindElements() {
this.elements.panel = document.getElementById(APP_CONFIG.PANEL_ID);
this.elements.closeButton = document.getElementById('kte-panel-close');
this.elements.gradientStartInput = document.getElementById('kte-gradient-start');
this.elements.gradientEndInput = document.getElementById('kte-gradient-end');
this.elements.fontSearchInput = document.getElementById('kte-font-search');
this.elements.fontResultsList = document.getElementById('kte-font-results');
}
populateForm() {
const themeState = this.themeManager.getThemeState();
this.elements.gradientStartInput.value = themeState.gradientStart;
this.elements.gradientEndInput.value = themeState.gradientEnd;
this.elements.fontSearchInput.value = '';
}
renderFontResults(searchQuery = '') {
const availableFonts = this.fontCatalog.searchFonts(searchQuery);
const activeFont = this.themeManager.getThemeState().fontFamily;
this.elements.fontResultsList.innerHTML = availableFonts.map(fontFamily => {
const isActive = fontFamily === activeFont;
return `
<button
type="button"
class="kte-font-option${isActive ? ' is-active' : ''}"
data-font-family="${fontFamily}"
style="font-family: '${fontFamily}', ${APP_CONFIG.FONT_FALLBACK};"
>
<span class="kte-font-option__name">${fontFamily}</span>
${isActive ? '<span class="kte-font-option__badge">Active</span>' : ''}
</button>
`;
}).join('');
}
setSelectedFont() {
this.renderFontResults(this.elements.fontSearchInput.value);
}
show() {
this.elements.panel.classList.remove('is-hidden');
this.elements.panel.setAttribute('aria-hidden', 'false');
}
hide() {
this.elements.panel.classList.add('is-hidden');
this.elements.panel.setAttribute('aria-hidden', 'true');
}
isVisible() {
return !this.elements.panel.classList.contains('is-hidden');
}
}
class PanelController {
constructor(panelView, storageService) {
this.panelView = panelView;
this.storageService = storageService;
this.handleKeydown = this.handleKeydown.bind(this);
this.handleCloseButtonClick = this.handleCloseButtonClick.bind(this);
}
initialize() {
this.panelView.bindPanelClose(this.handleCloseButtonClick);
document.addEventListener('keydown', this.handleKeydown);
if (!this.storageService.loadPanelState().isOpen) {
this.panelView.hide();
}
}
handleCloseButtonClick() {
this.hidePanel();
}
handleKeydown(event) {
const pressedKey = String(event.key || '').toUpperCase();
if (!event.shiftKey || pressedKey !== APP_CONFIG.PANEL_SHORTCUT_KEY) {
return;
}
this.showPanel();
}
showPanel() {
if (this.panelView.isVisible()) {
return;
}
this.panelView.show();
this.storageService.savePanelState({ isOpen: true });
}
hidePanel() {
if (!this.panelView.isVisible()) {
return;
}
this.panelView.hide();
this.storageService.savePanelState({ isOpen: false });
}
}
class PanelStyles {
constructor(styleRegistry) {
this.styleRegistry = styleRegistry;
}
mount() {
this.styleRegistry.mountStyle('kogama-theme-panel', `
#${APP_CONFIG.PANEL_ID} {
position: fixed;
top: ${APP_CONFIG.PANEL_TOP_OFFSET}px;
right: ${APP_CONFIG.PANEL_RIGHT_OFFSET}px;
width: ${APP_CONFIG.PANEL_WIDTH}px;
z-index: 999999;
color: rgba(255, 255, 255, 0.96);
font-family: Inter, ${APP_CONFIG.FONT_FALLBACK};
transition:
opacity 180ms ease,
transform 220ms ease,
visibility 220ms ease;
transform-origin: top right;
}
#${APP_CONFIG.PANEL_ID}.is-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(-8px) scale(0.985);
}
#${APP_CONFIG.PANEL_ID} *,
#${APP_CONFIG.PLAYER_COUNT_ID} * {
box-sizing: border-box;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__shell {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06)),
rgba(12, 16, 28, 0.78);
border: 1px solid rgba(255, 255, 255, 0.14);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 22px;
padding: 18px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.38),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__header {
margin-bottom: 18px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__eyebrow {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.52);
margin-bottom: 8px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__title {
margin: 0;
font-size: 21px;
line-height: 1.1;
font-weight: 700;
letter-spacing: -0.03em;
padding-top: 2px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__close {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin: -2px -2px 0 0;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04)),
rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.86);
cursor: pointer;
outline: none;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 8px 20px rgba(0, 0, 0, 0.14);
transition:
transform 160ms ease,
background-color 160ms ease,
border-color 160ms ease,
color 160ms ease,
box-shadow 160ms ease;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__close:hover {
transform: translateY(-1px);
color: rgba(255, 255, 255, 0.96);
border-color: rgba(255, 255, 255, 0.2);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.06)),
rgba(255, 255, 255, 0.06);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 12px 24px rgba(0, 0, 0, 0.18);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__close:focus-visible {
border-color: rgba(138, 180, 248, 0.72);
box-shadow:
0 0 0 4px rgba(138, 180, 248, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__close-icon {
font-size: 14px;
line-height: 1;
transform: translateY(-0.5px);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__subtitle {
margin: 10px 0 0;
font-size: 13px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.68);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__section {
padding: 14px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__section + .kte-panel__section {
margin-top: 12px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
#${APP_CONFIG.PANEL_ID} .kte-panel__section-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.94);
}
#${APP_CONFIG.PANEL_ID} .kte-panel__section-caption {
font-size: 11px;
color: rgba(255, 255, 255, 0.48);
}
#${APP_CONFIG.PANEL_ID} .kte-color-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
#${APP_CONFIG.PANEL_ID} .kte-field {
display: flex;
flex-direction: column;
gap: 7px;
}
#${APP_CONFIG.PANEL_ID} .kte-field__label {
font-size: 12px;
font-weight: 500;
color: rgba(255, 255, 255, 0.72);
}
#${APP_CONFIG.PANEL_ID} .kte-field__input {
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.96);
border-radius: 12px;
padding: 12px 14px;
outline: none;
transition:
border-color 160ms ease,
background-color 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
#${APP_CONFIG.PANEL_ID} .kte-field__input::placeholder {
color: rgba(255, 255, 255, 0.34);
}
#${APP_CONFIG.PANEL_ID} .kte-field__input:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.16);
}
#${APP_CONFIG.PANEL_ID} .kte-field__input:focus {
border-color: rgba(138, 180, 248, 0.72);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 4px rgba(138, 180, 248, 0.16);
}
#${APP_CONFIG.PANEL_ID} .kte-field__input--color {
min-height: 48px;
padding: 6px;
cursor: pointer;
}
#${APP_CONFIG.PANEL_ID} .kte-field__input--color::-webkit-color-swatch-wrapper {
padding: 0;
}
#${APP_CONFIG.PANEL_ID} .kte-field__input--color::-webkit-color-swatch {
border: none;
border-radius: 9px;
}
#${APP_CONFIG.PANEL_ID} .kte-font-results {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 260px;
overflow: auto;
padding-right: 2px;
}
#${APP_CONFIG.PANEL_ID} .kte-font-results::-webkit-scrollbar {
width: 8px;
}
#${APP_CONFIG.PANEL_ID} .kte-font-results::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
}
#${APP_CONFIG.PANEL_ID} .kte-font-option {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: left;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.94);
cursor: pointer;
transition:
transform 160ms ease,
background-color 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease;
}
#${APP_CONFIG.PANEL_ID} .kte-font-option:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.14);
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.18);
}
#${APP_CONFIG.PANEL_ID} .kte-font-option.is-active {
background: linear-gradient(180deg, rgba(138, 180, 248, 0.18), rgba(138, 180, 248, 0.1));
border-color: rgba(138, 180, 248, 0.45);
box-shadow: 0 10px 28px rgba(138, 180, 248, 0.14);
}
#${APP_CONFIG.PANEL_ID} .kte-font-option__name {
font-size: 14px;
font-weight: 500;
line-height: 1.3;
}
#${APP_CONFIG.PANEL_ID} .kte-font-option__badge {
flex-shrink: 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 5px 8px;
border-radius: 999px;
color: rgba(255, 255, 255, 0.92);
background: rgba(138, 180, 248, 0.18);
border: 1px solid rgba(138, 180, 248, 0.28);
}
#${APP_CONFIG.PLAYER_COUNT_ID} {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 14px 0 4px;
margin-left: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(15, 20, 32, 0.72);
color: rgba(255, 255, 255, 0.96);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.01em;
width: fit-content;
}
@media (max-width: 980px) {
#${APP_CONFIG.PANEL_ID} {
top: 12px;
right: 12px;
width: min(calc(100vw - 24px), ${APP_CONFIG.PANEL_WIDTH}px);
}
}
@media (max-width: 640px) {
#${APP_CONFIG.PANEL_ID} .kte-panel__shell {
padding: 14px;
border-radius: 18px;
}
}
`);
}
}
class MutationCoordinator {
constructor(callback) {
this.callback = callback;
this.observer = null;
this.debounceTimer = null;
}
start() {
this.observer = new MutationObserver(() => this.schedule());
this.observer.observe(document.body, {
childList: true,
subtree: true
});
}
schedule() {
window.clearTimeout(this.debounceTimer);
this.debounceTimer = window.setTimeout(() => {
this.callback();
}, APP_CONFIG.OBSERVER_DEBOUNCE_DELAY);
}
}
class KogamaCustomizerApp {
constructor() {
this.domCache = new DomCache();
this.storageService = new StorageService();
this.styleRegistry = new StyleRegistry(this.domCache);
this.fontCatalog = new FontCatalog();
this.themeManager = new ThemeManager(this.domCache, this.storageService, this.styleRegistry, this.fontCatalog);
this.numberFormatter = new NumberFormatter();
this.statsManager = new StatsManager(this.domCache, this.numberFormatter);
this.adsManager = new AdsManager(this.domCache);
this.playerCountManager = new PlayerCountManager(this.domCache);
this.panelStyles = new PanelStyles(this.styleRegistry);
this.panelView = new PanelView(this.themeManager, this.fontCatalog);
this.panelController = new PanelController(this.panelView, this.storageService);
this.mutationCoordinator = new MutationCoordinator(() => this.refreshDynamicUi());
this.statsIntervalId = null;
}
start() {
this.panelStyles.mount();
this.themeManager.initialize();
this.panelView.mount();
this.panelView.bindThemeChange(themePatch => this.handleThemeChange(themePatch));
this.panelController.initialize();
this.mutationCoordinator.start();
this.startStatsPolling();
this.runInitialRefresh();
}
handleThemeChange(themePatch) {
this.themeManager.updateTheme(themePatch);
}
refreshDynamicUi() {
this.adsManager.removeSubscriptionAds();
this.playerCountManager.refreshPlayerCount();
this.themeManager.applyTheme();
}
startStatsPolling() {
this.statsIntervalId = window.setInterval(() => {
this.statsManager.refreshGameStats();
}, APP_CONFIG.STATS_REFRESH_INTERVAL);
}
runInitialRefresh() {
window.setTimeout(() => {
this.statsManager.refreshGameStats();
this.refreshDynamicUi();
}, APP_CONFIG.INITIAL_REFRESH_DELAY);
}
}
new KogamaCustomizerApp().start();
})();