Визуальное обновление CatWar'а, и не только...
// ==UserScript==
// @name CatWar UwU
// @namespace http://tampermonkey.net/
// @version v1.42.0-12.25
// @description Визуальное обновление CatWar'а, и не только...
// @author Ibirtem / Затменная ( https://catwar.net/cat1477928 )
// @copyright 2025, Ibirtem (https://openuserjs.org/users/Ibirtem)
// @supportURL https://catwar.net/cat1477928
// @homepageURL https://openuserjs.org/scripts/Ibirtem/CatWar_UwU
// @match http*://*.catwar.net/*
// @match http*://*.catwar.su/*
// @license MIT
// @iconURL https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/partly_sunny_rain.png
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// ==/UserScript==
"use strict"; // Делаю вид что крутой.
// ====================================================================================================================
// . . . ЕДИНОЕ ХРАНИЛИЩЕ . . .
// ====================================================================================================================
let useUnifiedStorage = false;
(() => {
if (typeof GM_getValue === "undefined") {
return;
}
try {
const gmSettingsRaw = GM_getValue("uwu_settings");
if (gmSettingsRaw) {
const gmSettings = JSON.parse(gmSettingsRaw);
useUnifiedStorage = !!gmSettings.unifiedStorage;
return;
}
} catch (e) {
// Ошибка парсинга GM JSON, продолжаем с localStorage
}
try {
const localSettingsRaw = localStorage.getItem("uwu_settings");
if (localSettingsRaw) {
const localSettings = JSON.parse(localSettingsRaw);
if (localSettings.unifiedStorage) {
useUnifiedStorage = true;
}
}
} catch (e) {}
})();
const uwuStorage = {
setItem: (key, value) => {
const stringValue = JSON.stringify(value);
if (useUnifiedStorage) {
GM_setValue(key, stringValue);
} else {
localStorage.setItem(key, stringValue);
}
},
getItem: (key) => {
const rawValue = useUnifiedStorage
? GM_getValue(key)
: localStorage.getItem(key);
if (rawValue === null || typeof rawValue === "undefined") {
return null;
}
try {
return JSON.parse(rawValue);
} catch (e) {
return rawValue;
}
},
removeItem: (key) => {
if (useUnifiedStorage) {
GM_deleteValue(key);
} else {
localStorage.removeItem(key);
}
},
migrateAllToGM: () => {
console.log("UwU | Перенос данных из localStorage в единое хранилище...");
const keysToMigrate = Object.keys(localStorage);
for (const key of keysToMigrate) {
if (key.startsWith("uwu_")) {
const value = localStorage.getItem(key);
GM_setValue(key, value);
}
}
console.log("UwU | Перенос завершен.");
},
};
// ====================================================================================================================
// . . . DEFAULT НАСТРОЙКИ . . .
// ====================================================================================================================
const current_uwu_version = "1.42.0";
// ✨🦐✨🦐✨
const uwuDefaultSettings = {
settingsTheme: "dark",
weatherEnabled: false,
weatherDrops: false,
lowPerformanceMode: false,
minecraftStyle: false,
alwaysDay: false,
manualWeatherPanel: false,
skyInHeader: false,
auroraPos: "1",
weatherZIndex: "0",
backgroundRepeat: false,
backgroundUser: false,
backgroundUserImageURL: "",
gameFieldBackgroundUser: false,
gameFieldBackgroundUserImageURL: "",
userTheme: false,
userThemeKns: false,
showOtherCatsList: "2",
commentsAvatars: false,
chatHeight: "275",
newChat: false,
addCommaAfterNick: false,
reverseChat: false,
newChatInput: false,
showChatCharCounter: false,
namesForNotification: "",
redesignCostumsSettings: false,
showDefectsEnabled: false,
defectsStyle: "default",
defectsQuality: "high",
notificationPM: false,
notificationPMSound: "notificationSound1",
notificationPMVolume: 5,
notificationActionEnd: false,
notificationActionEndSound: "notificationSound1",
notificationActionEndVolume: 5,
notificationInMouth: false,
notificationInMouthSound: "notificationSound1",
notificationInMouthVolume: 5,
notificationInFightMode: false,
notificationInFightModeSound: "notificationSound1",
notificationInFightModeVolume: 5,
notificationBlock: false,
notificationBlockSound: "notificationBlockSound1",
notificationBlockVolume: 5,
showHintWhenToSniff: false,
duplicateTimeInBrowserTab: false,
cellsBorders: false,
cellsBordersThickness: "1",
cellsBordersColor: "#ffffff",
cellsNumbers: false,
fastStyles: false,
displayParametersPercentages: false,
compactMouth: false,
showMoreCatInfo: false,
showParametersDetails: false,
draggingFightPanel: false,
compactFightLog: false,
fightPanelAdjustableHeight: false,
fightPanelHeight: "70",
fightTeams: false,
fightTeamsColors: {
team1: ["#41cd70", "#cd4141"],
team2: ["#c968ff", "#cd4141"],
team3: ["#44bcff", "#cd4141"],
team4: ["#FFFF00", "#cd4141"],
},
fightTeamsPanelHight: "100",
highlightResources: false,
highlightResourcesStyle: "background",
showClock: false,
clockStyle: "compact",
clockFontSize: "14",
clockPosition: "fly",
intervalTimerEnabled: false,
intervalTimerSound: "notificationSound1",
intervalTimerVolume: 5,
describeHuntingSmell: false,
huntingVirtualJoystick: false,
sizeHuntingVirtualJoystick: "150",
climbingPanel: false,
climbingPanelOrientation: "vertical",
climbingPanelInputsStyle: "keyboard",
climbingNotificationsNumbers: false,
climbingRefreshNotification: false,
climbingRefreshNotificationSound: "notificationSound1",
climbingRefreshNotificationVolume: "5",
cleaningLog: false,
cleaningLogStyle: "smart",
cleaningLogShowID: false,
cleaningLogHeight: "120",
catchingLog: false,
catchingLogHeight: "120",
myNameNotificationSound: "notificationSound2",
notificationMyNameVolume: "5",
userQuickLinks: "",
historyHeight: "215",
itemListHeight: "180",
parametersColors: {
dream: ["#008000", "#008000", "#ff0000", "#ff0000"],
hunger: ["#008000", "#008000", "#ff0000", "#ff0000"],
thirst: ["#008000", "#008000", "#ff0000", "#ff0000"],
need: ["#008000", "#008000", "#ff0000", "#ff0000"],
health: ["#008000", "#008000", "#ff0000", "#ff0000"],
clean: ["#008000", "#008000", "#ff0000", "#ff0000"],
smell: ["#008000", "#008000", "#cccccc", "#cccccc"],
dig: ["#008000", "#008000", "#cccccc", "#cccccc"],
swim: ["#008000", "#008000", "#cccccc", "#cccccc"],
might: ["#008000", "#008000", "#cccccc", "#cccccc"],
tree: ["#008000", "#008000", "#cccccc", "#cccccc"],
observ: ["#008000", "#008000", "#cccccc", "#cccccc"],
other: ["#008000", "#008000", "#cccccc", "#cccccc"],
},
parametersBackgroundImage: false,
parametersUserBackgroundImage: false,
parametersUserBackgroundImageURL: "",
restoreBlogCreation: false,
moreBBCodes: false,
commentPreview: false,
moreCommentButtons: false,
lsWrapPreview: false,
calculators: false,
savingLS: false,
extendedSettingsPanel: false,
showUpdateNotification: false,
showSplashScreens: false,
extendedHints: true,
GMbetaTest: false,
personalCostumes: false,
showCostumesButtons: false,
blockItemDrop: false,
unifiedStorage: false,
};
// ====================================================================================================================
// . . . ТАРГЕТНЫЕ ССЫЛКИ . . .
// ====================================================================================================================
const targetCW3 = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/cw3(?:\/)?(?:\?.*)?$/;
const targetCW3Hunt =
/^https?:\/\/\w?\.?catwar\.(?:net|su)\/cw3\/jagd(?:\/)?(?:\?.*)?$/;
const targetCW3Kns =
/^https?:\/\/(\w+\.)?catwar\.(net|su)\/cw3\/kns\/?(\?.*)?$/;
const targetSettings = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/settings/;
const targetMainProfile = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/$/;
const targetProfile = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/cat\d+$/;
const targetLs = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/ls/;
const targetLsNew = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/ls\?new(=.*)?$/;
const targetChats = /^https?:\/\/\w?\.?catwar\.(?:net|su)\/chat/;
const targetBlog =
/^https?:\/\/\w?\.?catwar\.(?:net|su)\/(?:blog\d+|blogs)(?:$|[/?#])/i;
const targetBlogsCreation =
/^https?:\/\/\w?\.?catwar\.(?:net|su)\/blogs\?creation/;
const targetSniff =
/^https?:\/\/\w?\.?catwar\.(?:net|su)\/sniff(?:\d+|)(?:$|[/?#])/i;
const targetSniffCreation =
/^https?:\/\/\w?\.?catwar\.(?:net|su)\/sniff\?creation/;
// ====================================================================================================================
// . . . СТАНДАРТНЫЕ ЦВЕТОВЫЕ ТЕМЫ . . .
// ====================================================================================================================
const defaultThemes = {
"Тёмная Тема": {
colors: {
backgroundColor: "#161616",
blocksColor: "#242424",
chatColor: "#242424",
textColor: "#d5d5d5",
catTooltipBackground: "#242424",
fightPanelBackground: "#242424",
linkColor: "#d5d5d5",
accentColor1: "#111111",
accentColor2: "#2e2e2e82",
accentColor3: "#fc872a",
moveNameColor: "#d5d5d5",
moveNameBackground: "#242424",
climbingPanelBackground: "#242424",
},
},
};
// ====================================================================================================================
// . . . HTML ПАНЕЛЬ НАСТРОЕК . . .
// ====================================================================================================================
const uwusettings =
/* HTML */
`
<div id="uwusettings">
<div id="uwusettings-header">
<div id="uwusettings-header-glass">
<div class="main-settings-container">
<div id="settingsTheme" class="custom-select">
<label for="settingsTheme">Тема настроек:</label>
<div class="select-selected">Классическая</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<h1>Настройки CatWar UwU</h1>
<div class="link-container">
<a
href="https://vk.com/catwar_uwu"
target="_blank"
rel="noopener noreferrer"
title="ВК Группа по Скрипту/Моду"
style="margin-right: 10px; text-decoration: none;"
>
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/VK_logo.png"
alt="VK"
width="40"
height="40"
/>
</a>
<a
href="https://github.com/Ibirtem/CatWar"
target="_blank"
rel="noopener noreferrer"
title="GitHub Репозиторий"
>
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/icon_github.png"
alt="GitHub"
width="40"
height="40"
/>
</a>
</div>
</div>
<hr id="uwu-hr" class="uwu-hr-head" />
<div id="button-container">
<button id="effects-button" class="active">
<h2>
Природные эффекты
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/partly_sunny_rain.png"
alt="Иконка"
width="24"
height="24"
/>
</h2>
</button>
<button id="theme-button">
<h2>
Оформление
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/sparkles.png"
alt="Иконка"
width="24"
height="24"
/>
</h2>
</button>
<button id="utility-button">
<h2>
Инструментарий
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/flashlight.png"
alt="Иконка"
width="24"
height="24"
/>
</h2>
</button>
<button id="modules-button">
<h2>
Надстройки
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/construction.png"
alt="Иконка"
width="24"
height="24"
/>
</h2>
</button>
<button id="personal-costumes-button">
<h2>
Личные костюмы
<img
src="https://raw.githubusercontent.com/Arisamiga/CatWar/refs/heads/Personal-Costumes/images/costumes.png"
alt="Иконка"
width="24"
height="24"
/>
</h2>
</button>
</div>
<hr id="uwu-hr" class="uwu-hr-head" />
</div>
</div>
<div id="uwusettings-main">
<div id="effects-panel">
<h2>Природа и окружение</h2>
<div>
<p>
Включает генерацию Динамичной погоды в Игровой, такие как дождь,
снегопады или Северные Сияния.
</p>
<input
type="checkbox"
id="weather-enabled"
data-setting="weatherEnabled"
/>
<label for="weather-enabled">Показывать природные эффекты</label>
</div>
<div>
<p>
Сокращает количество частиц динамичной погоды, увеличивая тем
самым производительность на слабых устройствах.
</p>
<input
type="checkbox"
id="low-Performance-Mode"
data-setting="lowPerformanceMode"
/>
<label for="low-Performance-Mode"
>Режим низкой производительности</label
>
</div>
<div>
<p>
Может немного повлиять на производительность из-за возрастания
количества частиц на экране.
</p>
<input
type="checkbox"
id="weather-drops"
data-setting="weatherDrops"
/>
<label for="weather-drops">Эффекты приземления частиц</label>
</div>
<div>
<p>
Замена стандартных частиц на знакомые всеми пиксельные частицы.
</p>
<input
type="checkbox"
id="minecraft-style"
data-setting="minecraftStyle"
/>
<label for="minecraft-style-enabled">Minecraft частицы</label>
</div>
<div>
<p>Убирает затемнение Игрового поля.</p>
<input type="checkbox" id="always-day" data-setting="alwaysDay" />
<label for="always-day">Всегда день/ярко</label>
</div>
<div>
<p>
Отображает панель Ручного управления погодой в ⚙️Панели
Расширенных Настройках Игровой. Выключает натуральную генерацию
погоды.
</p>
<input
type="checkbox"
id="manual-Weather-Panel"
data-setting="manualWeatherPanel"
/>
<label for="manual-Weather-Panel">Ручное управление погоды</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<p>Расположение Северного Сияния</p>
<div id="auroraPanel">
<input
type="range"
min="1"
max="2"
value="1"
class="uwu-range-slider"
id="aurora-pos"
list="auroraStep"
data-setting="auroraPos"
/>
<datalist id="auroraStep">
<option value="1">Верх</option>
<option value="2">Низ</option>
</datalist>
</div>
<div>
<p>
Делает небо шапкой страницы, пряча под игровую, а так же по факту
чинит его потерю при Редизайне игровой. Будет выглядеть не очень
на широкоформатных мониторах из-за растягивания изображения.
</p>
<input
type="checkbox"
id="sky-in-the-sky"
data-setting="skyInHeader"
/>
<label for="sky-in-the-sky">Небо в небе.</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<p>
Z-index Погоды. Позволяет настроить, будут ли эффекты отображаться
поверх или позади игровых элементов.
</p>
<div id="weatherZIndexPanel">
<input
type="range"
min="-1"
max="1"
value="0"
class="uwu-range-slider"
id="weather-z-index"
list="weatherZIndexStep"
data-setting="weatherZIndex"
/>
<datalist id="weatherZIndexStep">
<option value="-1">За блоками</option>
<option value="0">Стандарт</option>
<option value="1">Перед блоками</option>
</datalist>
</div>
</div>
<div id="theme-panel">
<h2>Поле Игровой</h2>
<div>
<p>
Заменяет все фоны игровых локаций на выбранный вами фон. Помните,
что для правильного отображения нужно изображение 1000х1000 px.
</p>
<input
type="checkbox"
id="game-Field-background-User"
data-setting="gameFieldBackgroundUser"
/>
<label for="game-Field-background-User-enabled"
>Статичный фон локации:</label
>
<input
type="text"
id="gameFieldSettingImageURLField"
placeholder="Вставьте URL"
data-setting="gameFieldBackgroundUserImageURL"
/>
<button id="SettingSaveButton1" class="uwu-button install-button">
Сохранить
</button>
</div>
<div>
<p>Отрисовывает границы клеток Игрового поля.</p>
<input
type="checkbox"
id="cells-Borders"
data-setting="cellsBorders"
/>
<label for="cells-Borders">Границы клеток</label>
</div>
<p>Толщина/Яркость границ</p>
<div id="step-slider">
<input
type="range"
min="1"
max="9"
value="1"
id="cells-Borders-Thickness"
class="uwu-range-slider"
list="ThicknessStep"
data-setting="cellsBordersThickness"
/>
<datalist id="ThicknessStep">
<option value="1">0.1</option>
<option value="5">0.5</option>
<option value="9">0.9</option>
</datalist>
</div>
<div>
<label for="cells-Borders-Color">Цвет границы клеток</label>
<input
type="color"
id="cells-Borders-Color"
data-setting="cellsBordersColor"
value="#ffffff"
/>
</div>
<div>
<p>Обозначает клетки Игрового поля числами.</p>
<input
type="checkbox"
id="cells-Numbers"
data-setting="cellsNumbers"
/>
<label for="cells-Numbers">Нумерация клеток</label>
</div>
<div>
<p>
Ставит на страницу фон, повторяющий фон Игровой локации, а так же
размывает и затемняет его.
</p>
<input
type="checkbox"
id="background-repeat"
data-setting="backgroundRepeat"
/>
<label for="weather-enabled">Фон страницы из локации</label>
</div>
<div>
<p>Ставит на страницу фон из предоставленной ссылки.</p>
<input
type="checkbox"
id="background-user"
data-setting="backgroundUser"
/>
<label for="background-user-enabled">Свой фон страницы:</label>
<input
type="text"
id="SettingImageURLField"
placeholder="Вставьте URL"
data-setting="backgroundUserImageURL"
/>
<button id="SettingSaveButton1" class="uwu-button install-button">
Сохранить
</button>
</div>
<div>
<p>
Позволяет быстро сменять полезные стили в ⚙️Панели Расширенных
настроек в Игровой.
</p>
<input type="checkbox" id="fast-Styles" data-setting="fastStyles" />
<label for="fast-Styles">Быстрые стили</label>
<label
id="uwu-what-this"
title="
— Не показывать всплывающее окно 'О коте'
— Скрыть Игровое поле
— Скрыть фон Игрового Поля
— Скрыть Небо
— Всегда день/ярко
— Границы клеток
"
>[?]</label
>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Дефекты</h2>
<div>
<p>Добавляет котам иконки их дефектов (раны, грязь и т.д.).</p>
<input
type="checkbox"
id="show-defects-enabled"
data-setting="showDefectsEnabled"
/>
<label for="show-defects-enabled">Показывать иконки дефектов</label>
</div>
<div>
<p>Выберите стиль отображения иконок дефектов.</p>
<label>Стиль иконок:</label>
<div class="custom-select" id="defectsStyle">
<div class="select-selected">Стандартный</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
</div>
<div>
<p>Выберите качество иконок дефектов.</p>
<label>Качество иконок:</label>
<div class="custom-select" id="defectsQuality">
<div class="select-selected">Высокое/Новое</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Темы и цвета Игровой</h2>
<p>
Здесь вы можете выставить собственные цвета для игровой. Принимаются
"HEX" значения (Пример: #000) с поддержкой прозрачности. Будьте
аккуратны и не забывайте выключать другие цвета/темы в других
скриптах/модах. Очистите поле чтобы вернуться к стандартным цветам.
</p>
<input type="checkbox" id="user-theme" data-setting="userTheme" />
<label for="user-theme-enabled">Использовать свои цвета</label>
<div id="theme-selector" class="uwu-select">
<label for="theme-select">Выберите тему:</label>
<select id="theme-select" class="uwu-select-selected"></select>
<button id="addThemeButton" class="uwu-button install-button">
Добавить тему
</button>
<button
id="removeThemeButton"
style="display: none;"
class="uwu-button remove-button"
>
Удалить тему
</button>
</div>
<div id="color-picker">
<div id="color-picker-input">
<input
type="text"
id="backgroundColorField"
placeholder="Вставьте HEX код"
data-color="backgroundColor"
/>
<label>Цвет фона</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="blocksColorField"
placeholder="Вставьте HEX код"
data-color="blocksColor"
/>
<label>Основной цвет блоков</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="chatColorField"
placeholder="Вставьте HEX код"
data-color="chatColor"
/>
<label>Основной цвет чата</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="SettingTextColorField"
placeholder="Вставьте HEX код"
data-color="textColor"
/>
<label>Цвет текста</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="colorField"
placeholder="Вставьте HEX код"
data-color="linkColor"
/>
<label>Цвет ссылок</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="catTooltipBackgroundField"
placeholder="Вставьте HEX код"
data-color="catTooltipBackground"
/>
<label>Цвет фона подсказки "О Коте"</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="settingFightPanelBackgroundField"
placeholder="Вставьте HEX код"
data-color="fightPanelBackground"
/>
<label>Цвет панели Боевого режима</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="settingsMoveNameColorField"
placeholder="Вставьте HEX код"
data-color="moveNameColor"
/>
<label>Цвет текста перехода</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="settingsMoveNameBackgroundField"
placeholder="Вставьте HEX код"
data-color="moveNameBackground"
/>
<label>Цвет фона перехода</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="settingsclimbingPanelBackgroundField"
placeholder="Вставьте HEX код"
data-color="climbingPanelBackground"
/>
<label>Цвет фона Минного Поля</label>
</div>
<div id="color-picker-input">
<input
type="text"
id="accentColorField1"
placeholder="Вставьте HEX код"
data-color="accentColor1"
/>
<label
title="В основном всякие кнопки, слайдеры и строки ввода + цвет букв упоминания вас в Чате. Старайтесь пока делать просто оттенки чёрного цвета."
>[?] Акценты 1</label
>
</div>
<div id="color-picker-input">
<input
type="text"
id="accentColorField2"
placeholder="Вставьте HEX код"
data-color="accentColor2"
/>
<label
title="Линии в чате и некоторых других частях, кружочек слайдера громкости."
>[?] Акценты 2</label
>
</div>
<div id="color-picker-input">
<input
type="text"
id="accentColorField3"
placeholder="Вставьте HEX код"
data-color="accentColor3"
/>
<label title="Цвет уведомлений. Например ЛС и вашего имени в Чате"
>[?] Акценты 3</label
>
</div>
<div style="flex: 0 0 100%">
<button id="saveThemeButton" class="uwu-button install-button">
Сохранить
</button>
<p>
Отличный сайт для выбора цветов с поддержкой прозрачности:
<a href="https://get-color.ru/transparent/" target="_blank"
>https://get-color.ru/transparent/</a
>
</p>
</div>
</div>
<div>
<p>Применяет вашу тему и на конструктор окрасов.</p>
<input
type="checkbox"
id="user-theme"
data-setting="userThemeKns"
/>
<label for="user-theme-enabled">Цвета в конструкторе окрасов</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Шрифты и текст</h2>
<div>
<p>Кастомная настройка шрифтов в Игровой</p>
<input
type="checkbox"
id="use-User-Fonts"
data-setting="useUserFonts"
/>
<label for="use-User-Fonts">Свой шрифт</label>
</div>
<div>
<input
type="text"
id="font-Size-Body"
placeholder="14"
data-font-size="fontSizeBody"
/>
<label for="font-Size-Body">px; Размер общего шрифт</label>
</div>
<div>
<input
type="text"
id="font-Size-Small"
placeholder="12"
data-font-size="fontSizeSmall"
/>
<label for="font-Size-Small"
>px; Размер шрифта быстрых ссылок</label
>
</div>
<div>
<input
type="text"
id="font-Size-Location"
placeholder="14"
data-font-size="fontSizeLocation"
/>
<label for="font-Size-Location">px; Размер шрифта локации</label>
</div>
<div>
<p>
Подгрузка шрифта идёт автоматически. Для поиска возможных шрифтов,
воспользуйтесь сайтом:
<a href="https://fonts.google.com/?lang=ru_Cyrl" target="_blank"
>https://fonts.google.com/?lang=ru_Cyrl</a
>
</p>
<input
type="text"
id="font-Family-Body"
placeholder="Verdana"
data-font-size="fontFamilyBody"
/>
<label for="font-Family-Body">Название вида шрифта</label>
</div>
<details>
<summary
style="cursor: pointer; font-size: 16px; font-weight: bold;"
>
Настройка шрифта громкости сообщений в чате
</summary>
<div>
<input
type="text"
id="vlm0"
placeholder="10"
data-font-size="vlm0"
/>
<label for="vlm0">px; Громкость 0 (Самый тихий)</label>
</div>
<div>
<input
type="text"
id="vlm1"
placeholder="11"
data-font-size="vlm1"
/>
<label for="vlm1">px; Громкость 1</label>
</div>
<div>
<input
type="text"
id="vlm2"
placeholder="11.5"
data-font-size="vlm2"
/>
<label for="vlm2">px; Громкость 2</label>
</div>
<div>
<input
type="text"
id="vlm3"
placeholder="12"
data-font-size="vlm3"
/>
<label for="vlm3">px; Громкость 3</label>
</div>
<div>
<input
type="text"
id="vlm4"
placeholder="12.5"
data-font-size="vlm4"
/>
<label for="vlm4">px; Громкость 4</label>
</div>
<div>
<input
type="text"
id="vlm5"
placeholder="13"
data-font-size="vlm5"
/>
<label for="vlm5">px; Громкость 5 (Стандартная громкость)</label>
</div>
<div>
<input
type="text"
id="vlm6"
placeholder="15"
data-font-size="vlm6"
/>
<label for="vlm6">px; Громкость 6</label>
</div>
<div>
<input
type="text"
id="vlm7"
placeholder="17"
data-font-size="vlm7"
/>
<label for="vlm7">px; Громкость 7</label>
</div>
<div>
<input
type="text"
id="vlm8"
placeholder="19"
data-font-size="vlm8"
/>
<label for="vlm8">px; Громкость 8</label>
</div>
<div>
<input
type="text"
id="vlm9"
placeholder="21"
data-font-size="vlm9"
/>
<label for="vlm9">px; Громкость 9</label>
</div>
<div>
<input
type="text"
id="vlm10"
placeholder="23"
data-font-size="vlm10"
/>
<label for="vlm10">px; Громкость 10 (Самая громкая)</label>
</div>
</details>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Редизайны Игровой</h2>
<p>
Тот самый знаменитный редизайн, но с почти более расширенной
кастомизацией.
</p>
<input
type="checkbox"
id="custom-layout"
data-setting="customLayout"
/>
<label for="custom-layout">Компактный редизайн</label>
<div id="layout-customizer">
<div id="layout-preview">
<div class="column left">
<!-- Левая колонка -->
</div>
<div class="column center">
<!-- Центральная колонка -->
<div class="block center-block">Поле Игровой</div>
</div>
<div class="column right">
<!-- Правая колонка -->
</div>
<ul id="block-list">
<!-- Элементы списка блоков -->
</ul>
</div>
<button id="reset-layout-button" class="uwu-button remove-button">
Сбросить
</button>
</div>
<div>
<input
type="text"
id="chat-height"
placeholder="Вставьте значение"
data-setting="chatHeight"
/>
<label for="chat-height">px; Высота Чата</label>
</div>
<div>
<input
type="text"
id="history-height"
placeholder="Вставьте значение"
data-setting="historyHeight"
/>
<label for="history-height">px; Высота Истории</label>
</div>
<div>
<input
type="text"
id="item-list-height"
placeholder="Вставьте значение"
data-setting="itemListHeight"
/>
<label for="item-list-height">px; Высота инвентаря</label>
</div>
<label>Отображать Душевых котов:</label>
<div class="custom-select" id="showOtherCatsList">
<div class="select-selected">
Выберите стиль отображения Душевых котов
</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div>
<p>
Визуальное разделение блока "Информация" на меньшие блоки
"Параметров, Истории и Родственные связи".
</p>
<input
type="checkbox"
id="slice-info-block"
data-setting="sliceInfoBlock"
/>
<label for="slice-info-block">Разделить блок Информации</label>
</div>
<div>
<p>Скругляет края блоков в Игровой.</p>
<input
type="checkbox"
id="edge-trim-blocks"
data-setting="edgeTrimBlocks"
/>
<label for="edge-trim-blocks">Скругление блоков</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Остальные редизайны</h2>
<div>
<p>
Добавляет изображение костюмов в строки для наглядного отображения
и упрощённого поиска. Вы можете вытянуть высоту столбцов за их
стрелочки в нижнем правом краю!
</p>
<input
type="checkbox"
id="redesign-Costums-Settings"
data-setting="redesignCostumsSettings"
/>
<label for="redesign-Costums-Settings"
>Редизайн Настройки костюмов</label
>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Общение</h2>
<div>
<p>
Добавляет аватар с профиля отправителя на его комментарий в лентах
и блогах.
</p>
<input
type="checkbox"
id="comments-avatars"
data-setting="commentsAvatars"
/>
<label for="comments-avatars">Аватарки в комментариях</label>
</div>
<div>
<p>
Более функциональный Чат: допись ID отправителя и звуковое
уведомление при вашем упоминании.
</p>
<input type="checkbox" id="new-chat" data-setting="newChat" />
<label for="new-chat">Современный Чат</label>
</div>
<div>
<p>
При клике на имя кота в строку чата будет выставляться его имя с
запятой.
</p>
<input
type="checkbox"
id="add-comma-after-nick"
data-setting="addCommaAfterNick"
/>
<label for="add-comma-after-nick">Обращение с запятой</label>
</div>
<div>
<p>
Работает только с "Современным чатом". Отображет чат снизу вверх,
а так же смещает окно ввода сообщения под чат.
</p>
<input
type="checkbox"
id="reverse-Chat"
data-setting="reverseChat"
/>
<label for="reverse-Chat">Инверсия чата</label>
</div>
<div id="myNameNotificationSoundContainer">
<div class="custom-select" id="myNameNotificationSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div id="notification-volume">
<p>Громкость</p>
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notification-MyName-Volume"
list="volumeStep"
data-setting="notificationMyNameVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</div>
<div>
<p>
Ваши собственные имена и клички на упоминания в чате. Просто
пропишите их через запятую. Пример: Мяу, Мяуич, МяуВкин
</p>
<input
type="text"
id="names-For-Notification"
placeholder=". . ."
data-setting="namesForNotification"
/>
</div>
<div>
<p>
Более удобная строка ввода сообщений над чатом с возможностью
растягивания. Пока что насильно берёт цвета с "Использовать свои
цвета".
</p>
<input
type="checkbox"
id="new-chat-input"
data-setting="newChatInput"
/>
<label for="new-chat-input"
>Альтернативная строка ввода сообщений</label
>
</div>
<div>
<p>Отображает счётчик символов в строке ввода Современного Чата.</p>
<input
type="checkbox"
id="show-chat-char-counter"
data-setting="showChatCharCounter"
/>
<label for="show-chat-char-counter"
>Показывать счётчик символов в чате</label
>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Параметры и навыки</h2>
<!--
<div>
<p>Параметр наглядно отображает рядом с собой свой процент.</p>
<input type="checkbox" id="display-Parameters-Percentages" data-setting="displayParametersPercentages" />
<label for="display-Parameters-Percentages">Отображать проценты Параметров</label>
</div>
-->
<div>
<p>Заменяет стандартное оформление Параметров и Навыков на ваш.</p>
<input
type="checkbox"
id="user-Parameters-Theme"
data-setting="userParametersTheme"
/>
<label for="user-Parameters-Theme"
>Использовать своё оформление</label
>
</div>
<div id="parameters-color-settings" class="parameters-color-settings">
<table class="parameters-color-table">
<thead>
<tr>
<th class="parameters-color-table--header">Градиент</th>
<th class="parameters-color-table--header">От</th>
<th class="parameters-color-table--header">До</th>
<th class="parameters-color-table--header">От</th>
<th class="parameters-color-table--header">До</th>
</tr>
</thead>
<tbody
id="color-settings-body"
class="parameters-color-table--body"
>
<tr>
<th class="parameters-color-table--cell" colspan="5">
Параметры
</th>
</tr>
<tr>
<td class="parameters-color-table--cell">Сон</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dream"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dream"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dream"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dream"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Голод</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="hunger"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="hunger"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="hunger"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="hunger"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Жажда</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="thirst"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="thirst"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="thirst"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="thirst"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Нужда</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="need"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="need"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="need"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="need"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Здоровье</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="health"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="health"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="health"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="health"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Чистота</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="clean"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="clean"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="clean"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="clean"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<th class="parameters-color-table--cell" colspan="5">
Навыки
</th>
</tr>
<tr>
<td class="parameters-color-table--cell">Запах</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="smell"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="smell"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="smell"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="smell"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Копание</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dig"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dig"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dig"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="dig"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Плавание</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="swim"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="swim"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="swim"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="swim"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">БУ</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="might"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="might"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="might"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="might"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Лазание</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="tree"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="tree"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="tree"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="tree"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<td class="parameters-color-table--cell">Зоркость</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="observ"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="observ"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="observ"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="observ"
data-color-type="bg-to"
/>
</td>
</tr>
<tr>
<th class="parameters-color-table--cell" colspan="5">
Уникальные навыки
</th>
</tr>
<tr>
<td class="parameters-color-table--cell"></td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="other"
data-color-type="bar-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="other"
data-color-type="bar-to"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="other"
data-color-type="bg-from"
/>
</td>
<td class="parameters-color-table--cell">
<input
type="color"
data-param="other"
data-color-type="bg-to"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<p>Импорт/Экспорт настроек цветов параметров и навыков.</p>
<input
type="text"
id="param-colors-export-field"
placeholder="Экспорт"
readonly
/>
<input
type="text"
id="param-colors-import-field"
placeholder="Импорт"
/>
<button
id="param-colors-import-btn"
class="uwu-button install-button"
>
Вставить
</button>
</div>
<div>
<p>Накладывает поверх цветов изображение с узорами.</p>
<input
type="checkbox"
id="parameters-Background-Image"
data-setting="parametersBackgroundImage"
/>
<label for="parameters-Background-Image">Узоры</label>
</div>
<div>
<p>Накладывает поверх уже ваше изображение.</p>
<input
type="checkbox"
id="parameters-User-Background-Image"
data-setting="parametersUserBackgroundImage"
/>
<label for="parameters-User-Background-Image">Свои узоры:</label>
<input
type="text"
id="parametersUserBackgroundImageField"
placeholder="Вставьте URL"
data-setting="parametersUserBackgroundImageURL"
/>
<button id="SettingSaveButton1" class="uwu-button install-button">
Сохранить
</button>
</div>
</div>
<div id="utility-panel">
<h2>Подсветка</h2>
<div>
<p>Подсвечивает обводкой клетки полезные, и не очень, ресурсы</p>
<input
type="checkbox"
id="highlight-Resources"
data-setting="highlightResources"
/>
<label for="highlight-Resources">Подсветка ресурсов</label>
</div>
<label>Стиль подсветки предметов:</label>
<div class="custom-select" id="highlightResourcesStyle">
<div class="select-selected">
Выберите стиль подсветки предметов
</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<label
id="uwu-what-this"
title="Стиль 'Фон' подсвечивает всю клетку и не нагружает Игровую. Стиль 'Свечение' дублирует содержимое ячейки и стилизует его, что может нагружать Игровую."
>[?]</label
>
<table class="uwu-table-highlight-Resources">
<thead>
<tr>
<th>Название</th>
<th>Цвет</th>
<th>Подсвечивать?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Травы</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Травы"
value="#90EE90"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Травы"
/>
</td>
</tr>
<tr>
<td>Мох</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Мох"
value="#90EE90"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Мох"
/>
</td>
</tr>
<tr>
<td>Паутина</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Паутина"
value="#90EE90"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Паутина"
/>
</td>
</tr>
<tr>
<td>Пыль</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Пыль"
value="#DDA0DD"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Пыль"
/>
</td>
</tr>
<tr>
<td>Ветки, вьюнки, костоправы</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Ветки, вьюнки, костоправы"
value="#90EE90"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Ветки, вьюнки, костоправы"
/>
</td>
</tr>
<tr>
<td>Травящие предметы</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Травящие предметы"
value="#FF0000"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Травящие предметы"
/>
</td>
</tr>
<tr>
<td>Шаманские штучки</td>
<td>
<input
type="color"
class="uwu-color-picker"
data-resource="Шаманские штучки"
value="#00BFFF"
/>
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
class="uwu-highlight-checkbox"
data-resource="Шаманские штучки"
/>
</td>
</tr>
</tbody>
</table>
<hr class="uwu-hr-head" />
<h2>Шаблоны</h2>
<div>
<p>
Позволяет создавать и быстро использовать собственные шаблоны
сообщений.
</p>
<input
type="checkbox"
id="show-Templates"
data-setting="showTemplates"
/>
<label for="show-Templates">Отображать шаблоны</label>
</div>
<table id="uwu-table-templates">
<thead>
<tr>
<th>Сообщения</th>
<th>Чаты</th>
<th>Блоги и Лента</th>
</tr>
</thead>
<tbody>
<tr>
<td class="uwu-checkbox-cell">
<input type="checkbox" data-setting="templatesInLs" />
</td>
<td class="uwu-checkbox-cell">
<input type="checkbox" data-setting="templatesInChats" />
</td>
<td class="uwu-checkbox-cell">
<input
type="checkbox"
data-setting="templatesInBlogsAndSniffs"
/>
</td>
</tr>
</tbody>
</table>
<hr class="uwu-hr-head" />
<h2>Боевой режим</h2>
<div>
<p>Позволяет перетаскивать панель Боевого режима за штучку.</p>
<input
type="checkbox"
id="dragging-Fight-Panel"
data-setting="draggingFightPanel"
/>
<label for="dragging-Fight-Panel"
>Перетаскивание панели Боевого режима</label
>
</div>
<div>
<p>Сокращает и прописывает количество повторяющихся ударов.</p>
<input
type="checkbox"
id="compact-Fight-Log"
data-setting="compactFightLog"
/>
<label for="compact-Fight-Log">Компактный боевой лог</label>
</div>
<div>
<p>Возможность растягивать высоту панели и её начальная высота.</p>
<input
type="checkbox"
id="fight-Panel-Adjustable-Height"
data-setting="fightPanelAdjustableHeight"
/>
<label for="fight-Panel-Adjustable-Height"
>Настраиваемая высота панели</label
>
<input
type="text"
id="fightPanelHeightField"
placeholder=". . ."
data-setting="fightPanelHeight"
/>
<label>px; - Начальная высота панели</label>
</div>
<div>
<p>
Возможность перекрашивать и создавать команды в Панели Боевого
Режима.
</p>
<input type="checkbox" id="Fight-Teams" data-setting="fightTeams" />
<label for="fight-Teams">Команды в Боевом Режиме</label>
<input
type="text"
id="fightTeamsPanelHightField"
placeholder=". . ."
data-setting="fightTeamsPanelHight"
/>
<label>px; - Начальная высота панели Командного Боя</label>
</div>
<div>
<p>
Звуковое уведомление при нажатии/отжатии кнопки блокировании
удара.
</p>
<input
type="checkbox"
id="notification-Block"
data-setting="notificationBlock"
/>
<label for="notification-Block">Звук блокирования</label>
<div id="notificationBlockSoundContainer">
<div class="custom-select" id="notificationBlockSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
<div id="notification-volume">
<p>Громкость</p>
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notificationBlockVolume"
list="volumeStep"
data-setting="notificationBlockVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
<div id="notificationBlockContainer"></div>
</div>
</div>
<table id="colorSettingsTable">
<thead>
<tr>
<th></th>
<th>Энергия</th>
<th>Снесено</th>
</tr>
</thead>
<tbody>
<tr>
<td>Команда 1</td>
<td>
<input
type="color"
data-team="1"
data-part="green"
value="#41cd70"
/>
</td>
<td>
<input
type="color"
data-team="1"
data-part="red"
value="#cd4141"
/>
</td>
</tr>
<tr>
<td>Команда 2</td>
<td>
<input
type="color"
data-team="2"
data-part="green"
value="#c968ff"
/>
</td>
<td>
<input
type="color"
data-team="2"
data-part="red"
value="#cd4141"
/>
</td>
</tr>
<tr>
<td>Команда 3</td>
<td>
<input
type="color"
data-team="3"
data-part="green"
value="#44bcff"
/>
</td>
<td>
<input
type="color"
data-team="3"
data-part="red"
value="#cd4141"
/>
</td>
</tr>
<tr>
<td>Команда 4</td>
<td>
<input
type="color"
data-team="4"
data-part="green"
value="#FFFF00"
/>
</td>
<td>
<input
type="color"
data-team="4"
data-part="red"
value="#cd4141"
/>
</td>
</tr>
</tbody>
</table>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Часы</h2>
<div>
<p>Показывать ли часы в Игровой?</p>
<input
type="checkbox"
id="describe-show-clock"
data-setting="showClock"
/>
<label for="describe-show-clock">Часы в Игровой</label>
</div>
<label>Стиль часов:</label>
<div class="custom-select" id="clockStyle">
<div class="select-selected">Выберите стиль часов</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div><!-- Деление --></div>
<label>Где вставлять часы:</label>
<div class="custom-select" id="clockPosition">
<div class="select-selected">Выберите положение часов</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div>
<input
type="checkbox"
id="describe-clock-Moscow-Time"
data-setting="clockMoscowTime"
/>
<label for="describe-clock-Moscow-Time">Московское время</label>
</div>
<div>
<p>Размер шрифта часов</p>
<input
type="text"
id="clock-Font-Size"
placeholder=". . ."
data-setting="clockFontSize"
/>
</div>
<div>
<button id="resetClockPosition" class="uwu-button remove-button">
Сброс позиции часов
</button>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Таймер-напоминалка</h2>
<div>
<p>
Включает перетаскиваемое окно с таймером, который будет циклично
воспроизводить звуковой сигнал через заданный интервал времени.
</p>
<input
type="checkbox"
id="interval-timer-enabled"
data-setting="intervalTimerEnabled"
/>
<label for="interval-timer-enabled"
>Включить таймер-напоминалку</label
>
</div>
<div id="intervalTimerContainer">
<div class="custom-select" id="intervalTimerSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
<div id="notification-volume">
<p>Громкость</p>
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="intervalTimerVolume"
list="volumeStep"
data-setting="intervalTimerVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Рот (инвентарь)</h2>
<div>
<p>
Добавляет чекбокс для блокировки опускания предметов, сохраняя
возможность его использовать.
</p>
<input
type="checkbox"
id="block-item-drop"
data-setting="blockItemDrop"
/>
<label for="block-item-drop">Блокировка опускания предмета</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Охота</h2>
<div>
<p>
Дописывает на запахе, во время охоты, приближаетесь вы или
отдаляетесь от цели, а так же включает таймер.
</p>
<input
type="checkbox"
id="describe-Hunting-Smell"
data-setting="describeHuntingSmell"
/>
<label for="describe-Hunting-Smell">Подсказки на запахе</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Джойстики</h2>
<div>
<p>
Отображает Виртуальную сенсорную Джойстиковую кнопку для мобильных
устройств во время охоты для более удобного управления.
</p>
<input
type="checkbox"
id="hunting-Virtual-Joystick"
data-setting="huntingVirtualJoystick"
/>
<label for="hunting-Virtual-Joystick"
>Виртуальный джойстик для охоты</label
>
<input
type="text"
id="sizeHuntingVirtualJoystickField"
placeholder=". . ."
data-setting="sizeHuntingVirtualJoystick"
/>
<label>px; - Размер Джойстика. Стандартный размер - 150 px;</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>"О котах"</h2>
<div>
<p>
Добавляет во всплывающее окно "О коте" кнопку "Подробнее" для
просмотра большей полезной информации.
</p>
<input
type="checkbox"
id="show-More-Cat-Info"
data-setting="showMoreCatInfo"
/>
<label for="show-More-Cat-Info">Больше информации о Коте</label>
</div>
<div>
<p>
Сокращает и прописывает количество повторяющихся предметов в "О
коте".
</p>
<input
type="checkbox"
id="compact-Mouth"
data-setting="compactMouth"
/>
<label for="compact-Mouth">Компактные инвентари</label>
</div>
<div>
<p>
Добавляет над собственными параметрами кнопку "Подробнее" для
просмотра большей полезной информации.
</p>
<input
type="checkbox"
id="show-Parameter-Details"
data-setting="showParametersDetails"
/>
<label for="show-Parameter-Details">Подробные параметры</label>
</div>
<div>
<p>
Показывает дополнительную информацию в профиле кота, например БУ
цифрой.
</p>
<input
type="checkbox"
id="more-Profile-Info"
data-setting="moreProfileInfo"
/>
<label for="more-Profile-Info">Больше информации в профиле</label>
</div>
<div>
<p>Добавляет полезные калькуляторы для вычислений в профиля.</p>
<input
type="checkbox"
id="calculators"
data-setting="calculators"
/>
<label for="calculators">Калькуляторы активностей и лун.</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Минное поле</h2>
<div>
<p>Включает окно для расчерчивания минного поля в Игровой.</p>
<input
type="checkbox"
id="climbing-panel"
data-setting="climbingPanel"
/>
<label for="climbing-panel">Минное поле</label>
<p>
Здесь вы можете добавить/удалить Вкладки для хранения Таблиц и
количество самих таблиц в выбранной вкладке.
</p>
<h4>Вкладки</h4>
<div id="uwu-buttonRow1-settings"></div>
<h4>Локации / Таблицы</h4>
<div id="uwu-buttonRow2-settings"></div>
</div>
<label>Дизайн окна минного поля:</label>
<div class="custom-select" id="climbingPanelOrientation">
<div class="select-selected">Вертикальный</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<p>
Как вводить с клавиатуры: ЛКМ - выбрать клетку. С клавиатуры мины
ставятся от "0" до "7". Знак "минус" ( - ) равняется красной клетке,
а "равно" ( = ) ставит более яркую клетку, например для переходов,
которая не будет очищаться при "Очистить всё поле/таблицу". Два раза
ЛКМ на ячейку, чтобы очистить её значение.
</p>
<label>Вид ввода в минное поле:</label>
<div class="custom-select" id="climbingPanelInputsStyle">
<div class="select-selected">Клавиатура</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div>
<p>
Дописывает в чате громкость уведомлений числом. В случае с
лазательными локациями - количество опасных клеток вокруг вас.
</p>
<input
type="checkbox"
id="climbing-Notifications-Numbers"
data-setting="climbingNotificationsNumbers"
/>
<label for="climbing-Notifications-Numbers"
>Подписывать громкость уведомления</label
>
</div>
<div>
<p>Звуковое уведомление, когда карта локации обновляется.</p>
<input
type="checkbox"
id="climbing-Refresh-Notification"
data-setting="climbingRefreshNotification"
/>
<label for="climbing-Refresh-Notification"
>Уведомлять об перестановке</label
>
</div>
<div id="climbingRefreshNotificationSoundContainer">
<div class="custom-select" id="climbingRefreshNotificationSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<div id="notification-volume">
<p>Громкость</p>
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="climbing-Refresh-Notification-Volume"
list="volumeStep"
data-setting="climbingRefreshNotificationVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>BETA 🚧 Лог чистильщика 🚧 BETA</h2>
<div>
<p>
Упрощённое и удобное дублирование блока истории для любителей
чистить локации, в котором отображаются только поднятия и
опускания котов.
</p>
<input
type="checkbox"
id="cleaning-Log"
data-setting="cleaningLog"
/>
<label for="cleaning-Log">Включить лог чистильщика</label>
</div>
<label>Вид отображения Лога:</label>
<div class="custom-select" id="cleaningLogStyle">
<div class="select-selected">Выберите вид Лога</div>
<div class="select-items">
<!-- Опции будут добавлены сюда -->
</div>
</div>
<label
id="uwu-what-this"
title="
Умный - группирование множественных действий в более удобный, краткий и читаемый вид.
Ещё тут был 'стандартный', более привычный старый вид, но его съели росомахи.
"
>[?]</label
>
<details>
<summary
style="cursor: pointer; font-size: 16px; font-weight: bold;"
>
Как работает?
</summary>
<hr id="uwu-hr" class="uwu-hr" />
<p>1. Проверьте кота такими действиями, как:</p>
<p>— Потереться нос о нос</p>
<p>— Потереться щекой о щёку</p>
<p>— Помурлыкать вместе</p>
<p>— Обнюхать</p>
<p>
Вам выведится, можно ли поднять кота. Если он "Проверен", можете
смело...
</p>
<p>2. Поднять кота!</p>
<p>
Если же кот "Не спит", или перед поднятием вы его не проверили, то
Лог просто не запишет его.
</p>
<p>
— Больше настроек, например подсветка надписей или игнорирование
статуса кота, будет в будущем!
</p>
<p>
Если вы видите или вам кажется, что логика проверок и объединений,
странны и нелогичны, или даже что-то теряется, то можете сообщить
о проблеме в группу ВК!
</p>
<p>3. Попросились отпуститься?</p>
<p>
— Вы можете удалить из Лога последнего опущенного вами кота
кнопкой справа от "Очистить лог" в виде корзинки! Например, если
он выпросился погулять и он больше не актуален. Ещё разок!
Опускаете кота и он становится целью кнопки "Удалить"!
</p>
<hr id="uwu-hr" class="uwu-hr" />
</details>
<div>
<p>
При последующих проверках так же будет писаться ID кота. Не
добавляет ID к уже существущему тексту в логе.
</p>
<input
type="checkbox"
id="cleaning-Log"
data-setting="cleaningLogShowID"
/>
<label for="cleaning-Log">Записывать ID</label>
</div>
<div>
<input
type="text"
id="cleaning-Log-Height"
placeholder=". . ."
data-setting="cleaningLogHeight"
/>
<label>px; - Начальная высота Лога</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>BETA 🚧 Лог ловли 🚧 BETA</h2>
<div>
<p>
Аналогично Логу чистильщика, но для отслеживания результатов
ныряния и ловли в ущелье. Группирует последовательные попытки в
один блок.
</p>
<input
type="checkbox"
id="catching-Log"
data-setting="catchingLog"
/>
<label for="catching-Log">Включить лог ловли</label>
</div>
<details>
<summary
style="cursor: pointer; font-size: 16px; font-weight: bold;"
>
Как работает?
</summary>
<hr id="uwu-hr" class="uwu-hr" />
<p>
— Лог создаёт отдельные карточки для каждого типа действия
(Ныряние, Осмотр).
</p>
<p>
— Карточка считается активной 2 часа с момента последней попытки.
</p>
<p>
— Если вы вернётесь к тому же действию спустя 2 часа, создастся
новая карточка, а не дополнится старая.
</p>
<hr id="uwu-hr" class="uwu-hr" />
</details>
<div>
<input
type="text"
id="catching-Log-Height"
placeholder=". . ."
data-setting="catchingLogHeight"
/>
<label>px; - Начальная высота Лога</label>
</div>
<div>
<p>
Здесь вы можете добавить свои собственные названия для предметов
по их ID. Каждая запись должна быть на новой строке в формате
"ID=Название". Например: <code>3966=Рыбка</code>. Этот список
имеет приоритет над встроенным.
</p>
<textarea
id="catching-log-custom-items"
rows="10"
style="width: 100%"
placeholder="3966=Рыба 3967=Рыба побольше"
></textarea>
<button
id="save-custom-items-btn"
class="uwu-button install-button"
>
Сохранить
</button>
</div>
<div>
<p>Импорт/Экспорт вашего списка названий предметов.</p>
<input
type="text"
id="custom-items-export-field"
placeholder="Экспорт"
readonly
/>
<input
type="text"
id="custom-items-import-field"
placeholder="Импорт"
/>
<button
id="custom-items-import-btn"
class="uwu-button install-button"
>
Вставить
</button>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Быстрые ссылки</h2>
<p>Быстрые ссылки в Игровой.</p>
<div>
<input type="checkbox" id="quick-Link1" data-setting="quickLink1" />
<label for="quick-Link1">Настройки</label>
</div>
<div>
<input type="checkbox" id="quick-Link2" data-setting="quickLink2" />
<label for="quick-Link2">Памятка</label>
</div>
<div>
<input type="checkbox" id="quick-Link3" data-setting="quickLink3" />
<label for="quick-Link3">Блоги</label>
</div>
<div>
<input type="checkbox" id="quick-Link4" data-setting="quickLink4" />
<label for="quick-Link4">Лента</label>
</div>
<div>
<p>
Ваши ссылки. Вставляете ссылку, пробел и пишите название. Для
множества просто пишите через запятую. Пример: https://мяу Котики,
https://мяу2 Больше-котиков
</p>
<input
type="text"
id="users-quick-Links"
placeholder=". . ."
data-setting="userQuickLinks"
/>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Уведомления</h2>
<p>Уведомлять звуком, когда:</p>
<table class="notification-table">
<tbody>
<tr>
<td>
<input
type="checkbox"
id="notification-PM"
data-setting="notificationPM"
/>
</td>
<td>
<div class="custom-select" id="notificationPMSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
</td>
<td>
<div class="volume-control">
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notificationPMVolume"
list="volumeStep"
data-setting="notificationPMVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</td>
<td id="notificationPMContainer"></td>
<td><label for="notification-PM">Новое ЛС</label></td>
</tr>
<tr>
<td>
<input
type="checkbox"
id="notification-Action-End"
data-setting="notificationActionEnd"
/>
</td>
<td>
<div class="custom-select" id="notificationActionEndSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
</td>
<td>
<div class="volume-control">
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notificationActionEndVolume"
list="volumeStep"
data-setting="notificationActionEndVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</td>
<td id="notificationActionEndContainer"></td>
<td>
<label for="notification-Action-End"
>Действие закончилось</label
>
</td>
</tr>
<tr>
<td>
<input
type="checkbox"
id="notification-In-Mouth"
data-setting="notificationInMouth"
/>
</td>
<td>
<div class="custom-select" id="notificationInMouthSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
</td>
<td>
<div class="volume-control">
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notificationInMouthVolume"
list="volumeStep"
data-setting="notificationInMouthVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</td>
<td id="notificationInMouthContainer"></td>
<td>
<label for="notification-In-Mouth">Кто-то меня поднял</label>
</td>
</tr>
<tr>
<td>
<input
type="checkbox"
id="notification-In-Fight-Mode"
data-setting="notificationInFightMode"
/>
</td>
<td>
<div class="custom-select" id="notificationInFightModeSound">
<div class="select-selected">Выберите звук</div>
<div class="select-items"></div>
</div>
</td>
<td>
<div class="volume-control">
<input
type="range"
min="1"
max="10"
value="5"
class="uwu-range-slider"
id="notificationInFightModeVolume"
list="volumeStep"
data-setting="notificationInFightModeVolume"
/>
<datalist id="volumeStep">
<option value="1">10%</option>
<option value="10">100%</option>
</datalist>
</div>
</td>
<td id="notificationInFightModeContainer"></td>
<td>
<label for="notification-In-Fight-Mode"
>Ввели в стойку (Т+2/Т+3)</label
>
</td>
</tr>
</tbody>
</table>
<div>
<p>Дублирует время действий на название браузерной вкладки.</p>
<input
type="checkbox"
id="duplicate-Time-In-Browser-Tab"
data-setting="duplicateTimeInBrowserTab"
/>
<label for="duplicate-Time-In-Browser-Tab"
>Показывать время действия на вкладке</label
>
</div>
<div>
<p>Подсказывает оставшееся время до возможности понюхать.</p>
<input
type="checkbox"
id="show-Hint-When-To-Sniff"
data-setting="showHintWhenToSniff"
/>
<label for="show-Hint-When-To-Sniff">Когда нюхать?</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Общение</h2>
<div>
<p>
Позволяет сохранять личные сообщения локально в браузере для
офлайн-доступа.
</p>
<input type="checkbox" id="saving-LS" data-setting="savingLS" />
<label for="saving-LS">Сохранение Личных сообщений</label>
</div>
<div>
<p>
Автоматически находит и переносит ваши сохранённые ЛС из
подходящих модов и скриптов в хранилище UwU.
</p>
<button
id="import-ls-from-other-mods"
class="uwu-button install-button"
>
Импортировать ЛС из других модов и скриптов
</button>
</div>
<div>
<p>
Индивидуальный импорт/экспорт только сохранённых личных сообщений.
</p>
<input
type="text"
id="ls-export-field"
placeholder="Экспорт ЛС"
readonly
/>
<input type="text" id="ls-import-field" placeholder="Импорт ЛС" />
<button id="ls-import-btn" class="uwu-button install-button">
Вставить
</button>
</div>
<div>
<p>
Автоматически сохраняет и восстанавливает редактируемый текст
блога. Теперь вы не потеряете его случайно.
</p>
<input
type="checkbox"
id="restore-Blog-Creation"
data-setting="restoreBlogCreation"
/>
<label for="restore-Blog-Creation"
>Восстановление содержимого Блога</label
>
</div>
<div>
<p>Говорит само за себя.</p>
<input
type="checkbox"
id="more-BB-Codes"
data-setting="moreBBCodes"
/>
<label for="more-BB-Codes">Дополнительные BB-Коды</label>
</div>
<div>
<p>
Позволяет предпросматривать отправляемые сообщения в лентах и
блогах.
</p>
<input
type="checkbox"
id="comment-Preview"
data-setting="commentPreview"
/>
<label for="comment-Preview">Предпросмотр сообщений</label>
</div>
<div>
<p>
Позволяет "отвечать" и "цитировать" сообщения в лентах и блогах.
При цитировании вы можете выделить кусочек текста на который
хотите ответить.
</p>
<input
type="checkbox"
id="more-Comment-Buttons"
data-setting="moreCommentButtons"
/>
<label for="more-Comment-Buttons"
>Кнопки "Отправить" и "Цитировать"</label
>
</div>
<div>
<p>
Оборачивает предпросмотр письма в оболочку, похожую на ту которая
во "Входящие".
</p>
<input
type="checkbox"
id="ls-Wrap-Preview"
data-setting="lsWrapPreview"
/>
<label for="ls-Wrap-Preview">Наглядный предпросмотр письма</label>
</div>
</div>
<div id="modules-panel">
<h2>Главное</h2>
<div>
<p>
Постоянное отображание Панели Расширенных Настроек в Игровой. Сама
по себе пустая.
</p>
<input
type="checkbox"
id="extended-settings-Panel"
data-setting="extendedSettingsPanel"
/>
<label for="extended-settings-Panel"
>⚙️Панель Расширенных Настроек</label
>
</div>
<div>
<p>
Отображает уведомление в ⚙️Панели Расширенных настроек в Игровой.
</p>
<input
type="checkbox"
id="show-Update-Notification"
data-setting="showUpdateNotification"
/>
<label for="show-Update-Notification"
>Уведомлять об обновлении Скрипта/Мода UwU</label
>
</div>
<div>
<p>
⚙️Панели Расширенных Настроек не будет так скучно с рандомными
фразами.
</p>
<input
type="checkbox"
id="show-Splash-Screens"
data-setting="showSplashScreens"
/>
<label for="show-Splash-Screens">Показывать Splash надписи.</label>
</div>
<div>
<p>
Скрывать или отображать расширенные подсказки к настройкам.
Привет, я та самая расширенная подсказка. Делает Настройки CatWar
UwU очень компактным на вид.
</p>
<input
type="checkbox"
id="extended-Hints"
data-setting="extendedHints"
/>
<label for="extended-Hints">Расширенные подсказки</label>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Сборник стилей</h2>
<p>Онлайн сборник стилей от Разработчика.</p>
<hr id="uwu-hr" class="uwu-hr" />
<div id="module-info">
<!-- Сюда модули -->
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Единое Хранилище</h2>
<div>
<p>
Включает синхронизацию скрипта/мода между разными доменами CatWar
через хранилище 'monkey плагинов.
</p>
<p>
<b>ВНИМАНИЕ:</b> При первом включении этой опции, ваши текущие
локальные настройки и данные с этого сайта (например, catwar.net)
будут скопированы в единое хранилище и станут основными.
Убедитесь, что вы включаете эту опцию на том сайте, настройки и
данные которого хотите сохранить.
</p>
<input
type="checkbox"
id="unified-storage"
data-setting="unifiedStorage"
/>
<label for="unified-storage"
>Использовать единое хранилище (.net & .su)</label
>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h2>Импорт/Экспорт</h2>
<div>
<p>Импорт/Экспорт всех настроек.</p>
<input type="text" id="exportSettings" placeholder="Экспорт" />
<input type="text" id="importSettings" placeholder="Импорт" />
<button id="importSettingsButton" class="uwu-button install-button">
Вставить
</button>
</div>
<div>
<p>
Удаляет все настройки. В очень редких случаях может помочь при
проблемных проблемах.
</p>
<button id="resetAllSaves" class="uwu-button remove-button">
Сброс сохранений
</button>
</div>
</div>
<div id="personal-costumes-panel">
<h2>Личные костюмы</h2>
<p>
Здесь вы можете управлять костюмами для всех ваших котов. Костюмы
привязываются к конкретному коту.
</p>
<div>
<input
type="checkbox"
id="personal-costume-panel"
data-setting="personalCostumes"
/>
<label for="personal-costume-panel"
>⚙️Включить персональные костюмы</label
>
</div>
<p>Добавляет кнопку «Сохранить костюм» при наведении на игрока.</p>
<div>
<input
type="checkbox"
id="show-costumes"
data-setting="showCostumesButtons"
/>
<label for="show-costumes">Сохранять костюмы других игроков</label>
</div>
<br />
<hr id="uwu-hr" class="uwu-hr" />
<div class="costume-flex-box disabled">
<div class="costumeSettings" style="flex: 2;">
<div
id="cat-selector-container"
style="margin-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 15px;"
>
<label>Выберите кота:</label>
<div
style="display: flex; gap: 5px; align-items: center; margin-top: 5px;"
>
<select
id="current-cat-select"
class="uwu-select-selected"
style="width: 100%; margin: 0;"
></select>
<button
id="delete-cat-btn"
class="uwu-button remove-button"
title="Удалить этого кота из списка"
>
🗑️
</button>
</div>
<label style="display: block; margin-top: 10px;"
>Выберите позу для редактирования:</label
>
<select
id="current-pose-select"
class="uwu-select-selected"
style="width: 100%; margin: 5px 0 0 0;"
>
<!-- Пу-пу-пу -->
</select>
<div style="font-size: 0.8em; opacity: 0.6; margin-top: 2px;">
* Позы сохраняются автоматически, когда вы меняете положение в
Игровой.
</div>
</div>
<div>
<h3>Изменить Костюм:</h3>
<div
style="font-size: 0.9em; opacity: 0.8; margin-bottom: 10px;"
>
Загрузите изображение (желательно 200x300 или 2:3). Оно будет
подогнано под размер.
</div>
</div>
<input
type="file"
id="costume-file"
accept="image/png"
class="uwu-button"
style="width: 100%; box-sizing: border-box;"
/>
<br />
<div
style="display: flex; flex-direction: row; flex-wrap: wrap; gap: 10px; margin-top: 10px;"
>
<button
class="uwu-button install-button"
id="changeCostume"
style="flex: 1; white-space: normal; min-width: 120px;"
>
Надеть на кота
</button>
<button
class="uwu-button install-button"
id="saveCostumeToNewSlot"
style="flex: 1; white-space: normal; min-width: 120px;"
>
Сохранить в библиотеку
</button>
</div>
</div>
<div
id="cat-preview-wrapper"
style="flex: 1; display: flex; flex-direction: column; align-items: center;"
>
<div
id="cat-image-container"
style="width: 100px; height: 150px; position: relative; border: 1px solid rgba(255,255,255,0.2); border-radius: 10px;"
></div>
<p
id="cat-preview-name"
style="margin-top: 5px; margin-bottom: 5px; font-weight: bold; text-align: center;"
>
...
</p>
<a
class="uwu-button remove-button"
id="removeCostume"
style="font-size: 0.8em; padding: 2px 10px; text-decoration: none; color: inherit; margin-top: 5px;"
title="Снять текущий костюм с этого кота"
>Снять костюм</a
>
</div>
</div>
<div
id="no-cats-warning"
style="display: none; background: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); padding: 15px; border-radius: 10px; margin-top: 10px; text-align: center;"
>
<h3>🔍 Коты не найдены</h3>
<p>
Чтобы настроить костюм, нужно, чтобы скрипт "увидел" вашего
персонажа.
</p>
<p>
1. Убедитесь, что галочка "Включить персональные костюмы" стоит.
</p>
<p>
2. Зайдите в <a href="/cw3/" target="_blank">Игровую</a> хотя бы
один раз.
</p>
<p>3. Вернитесь сюда и обновите страницу.</p>
</div>
<hr id="uwu-hr" class="uwu-hr" />
<h3>Библиотека костюмов (общая):</h3>
<div class="costume-flex-box" id="costume-gallery"></div>
<hr id="uwu-hr" class="uwu-hr-head" />
</div>
</div>
</div>
`;
// ====================================================================================================================
// . . . HTML БЛОК НОВОСТЕЙ . . .
// ====================================================================================================================
const newsPanel =
/* HTML */
`
<div id="news-panel">
<button id="news-button">
v${current_uwu_version} - Перепись Библиотеки Личных Костюмов! Всякие
правки БР и прочая мелочь.
</button>
<div id="news-list" style="display: none">
<h3>Главное</h3>
<p>
— Библиотека Личных Костюмов теперь поддерживает множество ваших котов
и их разные позы (вроде). Эм. Мяу-мяу-мяу.
</p>
<hr id="uwu-hr" class="uwu-hr" />
<h3>Внешний вид</h3>
<p>— Кнопка "Обновить команды" в БР логе теперь не вылезает за края.</p>
<p>— Убран неприятный горизонтальный слайдер в таблицах команд.</p>
<p>
— Кнопки команд теперь не улетают на следующие строки, делая табицу
высокой и неудобной.
</p>
<p>— Возможный фикс улетающего текста из БР.</p>
<p>
— Кнопки сохранения и удаления сообщения в письмах теперь появляются и
на мобильной версии сайта.
</p>
<p>— Добавлена кнопочка на GitHub Скрипта/Мода, чтобы не терялось.</p>
<hr id="uwu-hr" class="uwu-hr" />
<h3>Изменения кода</h3>
<p>— Возможно чёта, что я уже забыл.</p>
<hr id="uwu-hr" class="uwu-hr" />
<p>Дата выпуска: 13.12.25</p>
</div>
</div>
`;
// ====================================================================================================================
// . . . HTML ПАНЕЛЬ РАСШИРЕННЫХ НАСТРОЕК . . .
// ====================================================================================================================
const extendedSettingsButton =
/* HTML */
`
<div id="uwu-extended-settings">
<button type="button" id="extended-settings-button">
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/partly_sunny_rain.png"
alt="Иконка"
width="36"
height="36"
/>
</button>
<div id="extended-settings-container">
<div id="splash-screen-panel"></div>
</div>
</div>
`;
// ====================================================================================================================
// . . . HTML БЛОК РУЧНОГО УПРАВЛЕНИЯ ПОГОДЫ . . .
// ====================================================================================================================
const manualWeatherPanel =
/* HTML */
`
<div id="manual-weather-panel">
<p>Изменения, сделанные в этой панели, носят временный характер и не сохраняются.</p>
<h3>Переключить погоду</h3>
<input type="range" min="1" max="3" value="1" class="uwu-range-slider" id="manualWeather" list="WeatherStep">
<datalist id="WeatherStep">
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/sunny.png" width="36" height="36" option
value="1"></option>
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/rain_cloud.png" width="36" height="36"
option value="2"></option>
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/snow_cloud.png" width="36" height="36"
option value="3"></option>
</datalist>
<div id="temperature-container">
<p id="temperature"
title="На это умножается скорость частиц и делится их размер. В будущем будет возможность сохранять и изменять это значение под свой вкус.">
[?] Текущий модификатор: ...уточнение...</p>
</div>
<h3>Северное Сияние</h3>
<div class="button-container-1">
<button type="button" id="manualAurora-Off" class="uwu-button-round">
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/icons8-nothern-lights-96.png"
alt="Иконка" width="48" height="48">
</button>
<button type="button" id="manualAurora-B" class="uwu-button-round">
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/icons8-nothern-lights-96_blue.png"
alt="Иконка" width="48" height="48">
</button>
<button type="button" id="manualAurora-G" class="uwu-button-round">
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/icons8-nothern-lights-96_green.png"
alt="Иконка" width="48" height="48">
</button>
</div>
<h3>Светлячки</h3>
<div class="button-container-2">
<button type="button" id="manualFirefly-On" class="uwu-button-round">
<img src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/firefly.png" alt="Иконка" width="48"
height="48" title="Включает/Выключает">
</button>
</div>
</div>
<div id="aurora-settings-panel">
<p>Изменения, сделанные в этой панели, сохранятся!</p>
<h5>Здесь будет возможность переместить Северное Сияние в реальном времени, исключать локации из генерации погоды,
либо запрещать
определённой погоде существовать на выбранной локации. Но это всё пока что лишь мечта...</h5>
</div>
`;
// ====================================================================================================================
// . . . ГЛАВНЫЙ CSS СТИЛЬ . . .
// ====================================================================================================================
// TODO - Унифицировать шрифты, цвета текстов, прозрачность, закруглённость штучек ну кароче всё как надо чтобы не сделать в итоге лабиринт.
const css_uwu_main = `
#uwu-settings {
margin-top: 10px;
margin-bottom: 10px;
}
#uwusettings {
font-family: "Montserrat", sans-serif;
margin: 0 auto;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.main-settings-container {
padding: 10px 15px 10px 15px;
}
#uwusettings-main {
padding: 0px 15px 0px 15px;
}
#news-panel {
padding: 5px 15px 15px 15px;
}
#uwu-what-this {
color: #83e5ff;
font: caption;
}
.main-settings-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
#uwusettings h1,
#uwusettings h2 {
font-family: "Montserrat", sans-serif;
margin-top: 10px;
margin-bottom: 15px;
text-align: center;
}
#uwusettings h4 {
margin-top: 5px;
margin-left: 5px;
margin-bottom: 5px;
}
#uwusettings p {
margin-bottom: 0px;
}
#uwusettings label {
font-size: 16px;
}
#uwusettings ul {
font-family: "Montserrat", sans-serif;
list-style-type: "+ ";
}
.uwu-hr-head {
border: rgba(255, 255, 255, 0.1) solid;
border-radius: 0px;
}
.uwu-hr {
border: rgba(255, 255, 255, 0.1) solid;
border-radius: 15px;
}
#uwusettings .parameters-color-table,
#uwusettings .parameters-color-table tr,
#uwusettings .parameters-color-table td {
border: 1px #383838 solid;
}
#colorSettingsTable,
#colorSettingsTable tr,
#colorSettingsTable td {
border: 1px #383838 solid;
}
.uwu-table-highlight-Resources,
#uwu-table-templates {
margin-top: 5px;
}
.uwu-table-highlight-Resources th, .uwu-table-highlight-Resources td,
#uwu-table-templates th, #uwu-table-templates td {
border: 1px solid #383838;
}
.uwu-color-picker {
border: none;
vertical-align: middle;
}
.uwu-checkbox-cell {
text-align: center;
vertical-align: middle;
}
#uwusettings .parameters-color-table,
#colorSettingsTable {
margin-top: 8px;
}
.header-rounded-image {
background-repeat: repeat;
background-attachment: fixed;
border-radius: 20px 20px 0px 0px;
}
.main-rounded-image {
background-repeat: repeat;
background-attachment: fixed;
border-radius: 20px;
}
#button-container-1 {
display: flex;
justify-content: space-evenly;
width: 100%;
}
#button-container button {
background-color: transparent;
border: none;
color: #ffffff57;
padding: 10px 20px;
cursor: pointer;
transition: box-shadow 0.4s ease;
}
#button-container button.active {
box-shadow: inset 0 -2px 0 0 #ffffff4d;
transition: box-shadow 0.4s ease;
}
#button-container button.active h2 {
color: #ffffff;
transition: color 0.4s ease;
}
#modules-panel {
display: none;
}
.module-container {
width: 300px;
min-height: 150px;
position: relative;
box-sizing: border-box;
margin: 10px;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
}
.module-info {
flex-grow: 1;
margin-bottom: 10px;
}
.module-panel {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 10px;
left: 10px;
right: 10px;
}
#module-info {
flex-grow: 1;
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
flex-basis: 100%;
}
.module-container label {
margin-top: 10px;
}
#private-module-input {
margin: 10px;
}
.module-container button {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 5px;
}
.install-button {
background-color: #78c8ff87 !important;
}
.remove-button {
background-color: #ff787887 !important;
}
#module-info input[type="checkbox"] {
margin: 10px;
}
#color-picker {
display: flex;
flex-wrap: wrap;
}
#color-picker-input {
flex: 30%;
}
#auroraPanel {
width: 120px;
}
#weatherZIndexPanel {
width: 320px;
}
.notification-table {
border-collapse: collapse;
}
.notification-table td {
padding: 5px;
vertical-align: middle;
}
#notification-volume,
#step-slider {
width: 150px;
}
details {
margin-top: 5px;
}
#layout-preview button {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 5px;
}
#layout-customizer #layout-preview {
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
#layout-customizer .column {
width: 200px;
border: 1px solid #ffffff1a;
border-radius: 10px;
padding: 5px;
margin: 0 5px;
}
#layout-customizer .block {
border-radius: 10px;
background-color: #ffffff08;
padding: 5px;
margin-bottom: 5px;
}
#layout-customizer .center-block {
height: 100%;
box-sizing: border-box;
border-radius: 10px;
background-color: #ffffff08;
}
#uwu-buttonRow1-settings,
#uwu-buttonRow2-settings {
display: flex;
margin-top: 3px;
}
#uwu-buttonRow1-settings button,
#uwu-buttonRow2-settings button {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2px 10px;
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 3px;
margin-left: 0px;
}
#uwu-buttonRow1-settings > div > button.tab-button.active, #uwu-buttonRow2-settings > div > button.table-button.active {
background-color: #abf6ffb0;
}
#uwu-buttonRow1-settings > .tab-container,
#uwu-buttonRow2-settings > .table-container {
border-radius: 15px;
background-color: #54545473;
margin-right: 5px;
padding-left: 4px;
padding-right: 2px;
padding-top: 2px;
border-bottom-width: 2px;
padding-bottom: 2px;
}
#uwu-global-container {
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
#uwu-main-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
pointer-events: none;
}
.weatherCanvas {
pointer-events: none;
position: fixed;
}
#extended-settings-button {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
backdrop-filter: blur(16px);
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
cursor: pointer;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 2em;
font-weight: bold;
color: #ff00ff;
}
#extended-settings-container {
z-index: 10;
font-family: "Montserrat", sans-serif;
color: white;
font-size: 15px;
text-align: center;
position: fixed;
bottom: 100px;
right: 30px;
width: 400px;
height: 400px;
backdrop-filter: blur(16px);
border-radius: 10px;
display: none;
pointer-events: auto;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
display: grid;
place-items: center;
padding: 15px;
box-sizing: border-box;
overflow-y: auto;
}
.extended-settings-block {
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px;
margin-bottom: 8px;
}
#news {
margin-top: 20px;
}
#manual-weather-panel,
#news,
#news-button {
width: 100%;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-around;
box-sizing: border-box;
}
#color-picker {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 10px;
padding: 15px;
border-radius: 10px;
}
#news-button,
#news-list {
font-family: "Montserrat", sans-serif;
font-size: 15px;
cursor: pointer;
}
#news-list h3 {
margin-left: 40px;
}
#news-list p {
margin-top: 3px;
margin-bottom: 3px;
margin-left: 20px;
}
#aurora-settings-panel {
width: 100%;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 15px;
margin-top: 20px;
box-sizing: border-box;
}
#WeatherStep,
#auroraStep,
#volumeStep,
#ThicknessStep,
.uwu-range-step,
#weatherZIndexStep {
margin-top: 10px;
display: flex;
justify-content: space-between;
width: 100%;
}
#extended-settings-container::-webkit-scrollbar {
width: 10px;
}
#extended-settings-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
#extended-settings-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
}
#extended-settings-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
#button-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
width: 100%;
}
.button-container-1 {
display: flex;
justify-content: space-between;
width: 100%;
}
.button-container-2 {
display: flex;
justify-content: space-evenly;
width: 100%;
}
.uwu-button-round {
width: 60px;
height: 60px;
cursor: pointer;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#extended-settings-button:hover,
.uwu-button-round:hover {
background-color: rgba(255, 255, 255, 0.15);
}
@property --gradient-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes aurora-spin {
0% {
--gradient-angle: 0deg;
}
100% {
--gradient-angle: 360deg;
}
}
@keyframes auroraFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes auroraFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.firefly {
position: fixed;
background-color: rgba(255, 255, 153, 1);
border-radius: 50%;
filter: blur(5px);
pointer-events: none;
animation: fadeIn 6s ease-in-out;
}
.firefly-glow {
position: fixed;
background-color: rgba(255, 255, 153, 0.2);
border-radius: 50%;
filter: blur(40px);
pointer-events: none;
animation: fadeIn 6s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.firefly-disappearing {
animation: fadeOut 6s ease-in-out forwards;
}
.custom-select {
position: relative;
display: inline-block;
}
.select-selected, .uwu-select-selected {
margin-top: 10px;
width: fit-content;
border-radius: 10px;
color: white;
background-color: #5c5c5c;
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
padding: 10px;
cursor: pointer;
}
.uwu-select-selected {
width: fit-content;
}
.select-items, uwu-select-items {
margin-top: 5px;
display: none;
position: absolute;
border-radius: 10px;
width: max-content;
color: white;
background-color: #5c5c5c;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
z-index: 1;
}
.select-items div {
padding: 8px 16px;
cursor: pointer;
}
.select-items div:hover {
background-color: #757575;
}
.custom-select.active .select-items {
display: block;
}
#climbingRefreshNotificationSoundContainer button,
#myNameNotificationSoundContainer button {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2px 10px;
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 3px;
margin-left: 0px;
}
#climbingRefreshNotificationSoundContainer,
#myNameNotificationSoundContainer,
#notificationBlockSoundContainer {
gap: 5px;
display: flex;
align-items: center;
}
.update-notification {
background-color: #78c8ff69;
padding: 10px;
border-radius: 10px;
margin-bottom: 10px;
}
.new-update::before {
content: "•";
color: #78c8ff;
font-size: 2em;
position: absolute;
top: -20px;
right: -5px;
}
.random-phrase-block {
margin-bottom: 10px;
width: 100%;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
padding: 5px;
}
.costume-flex-box {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
margin-top: 10px;
margin-bottom: 10px;
gap: 3rem;
}
.costume-flex-box.disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.costume-flex-box div{
flex: 0;
}
#cat-image {
margin: 0 auto;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: flex-end;
font-weight: bold;
gap: 0.5rem;
}
#cat-image-container {
box-shadow: 0px 0px 7px 0px white;
}
#costume-gallery {
gap: 1rem;
flex-wrap: wrap;
justify-content: flex-start;
}
#costume-gallery > div {
flex: 1;
}
.costume-gallery-box {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
flex: 0 1 250px;
max-width: 250px;
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
box-sizing: border-box;
position: relative;
}
.costume-slot-delete-button {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
padding: 4px 8px !important;
line-height: 1;
background-color: transparent !important;
}
.save-costume-button {
margin-top: 10px;
}
.costume-flex-grid {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
justify-content: flex-start;
margin-top: 1rem;
}
.costume-flex-item {
flex: 0 1 calc(33.333% - 1rem);
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1rem;
max-width: 180px;
}
.costume-style {
width: 100px;
height: 150px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.costume-slot {
width: 100px;
height: 150px;
background-position: center;
background-repeat: no-repeat;
}
.costume-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
align-items: stretch;
}
.uwu-slot-select {
padding: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.03);
font-family: "Montserrat", sans-serif;
font-size: 14px;
color: black;
cursor: pointer;
transition: background-color 0.3s ease;
}
#show-costumes:disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
`;
document.head.insertAdjacentHTML(
"beforeend",
`<style id="css-uwu-main">${css_uwu_main}</style>`
);
// ====================================================================================================================
// . . . ПРОЗРАЧНЫЙ CSS СТИЛЬ . . .
// ====================================================================================================================
// Glassmorphism вперёд Glassmorphism вперёд Glassmorphism вперёд Glassmorphism вперёд Glassmorphism вперёд
const css_uwu_glass =
// css
`
#uwusettings {
backdrop-filter: blur(16px);
background-color: rgba(255, 255, 255, 0.03);
}
.uwu-button {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 5px;
margin-left: 0px;
color: #d5d5d5;
}
.uwu-button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.uwu-range-slider {
width: 100%;
cursor: pointer;
-webkit-appearance: none;
background-color: rgba(255, 255, 255, 0.06) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 10px;
height: 10px;
outline: none;
}
.uwu-range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.uwu-range-slider::-webkit-slider-thumb {
transform: translateY(-35%);
}
#uwusettings input[type="checkbox"] {
margin-right: 8px;
appearance: none;
transform: translate(-10%, 30%);
width: 35px;
height: 18px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
#uwusettings input[type="checkbox"]:checked {
background-color: #90ff78a8;
}
#uwusettings input[type="checkbox"]:not(:checked) {
background-color: rgba(255, 255, 255, 0.1);
}
#uwusettings input[type="checkbox"]:before {
content: "";
position: absolute;
top: 50%;
left: 4px;
transform: translate(-50%, -50%);
width: 25px;
height: 25px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.2s ease-in-out, left 0.2s ease-in-out;
}
#uwusettings input[type="checkbox"]:checked:before {
left: calc(100% - 4px);
}
#uwusettings input[type="text"] {
width: 150px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 10px;
outline: none;
margin: 5px;
margin-left: 0px;
}
`;
// ====================================================================================================================
// . . . ТЁМНАЯ ТЕМА . . .
// ====================================================================================================================
const css_uwu_dark =
/* CSS */
`
#uwusettings {
background-color: #242424;
color: #dddddd;
}
#uwusettings-header-glass {
border-radius: 20px 20px 0px 0px;
backdrop-filter: blur(16px) brightness(0.9);
}
#news-button {
color: #dddddd;
}
.uwu-button {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 5px;
margin-left: 0px;
}
.uwu-button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.uwu-range-slider {
width: 100%;
cursor: pointer;
-webkit-appearance: none;
background-color: rgba(255, 255, 255, 0.06) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 10px;
height: 10px;
outline: none;
}
.uwu-range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.uwu-range-slider::-webkit-slider-thumb {
transform: translateY(-35%);
}
#uwusettings input[type="checkbox"] {
margin-right: 8px;
appearance: none;
transform: translate(-10%, 30%);
width: 35px;
height: 18px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
#uwusettings input[type="checkbox"]:checked {
background-color: #90ff78a8;
}
#uwusettings input[type="checkbox"]:not(:checked) {
background-color: rgba(255, 255, 255, 0.1);
}
#uwusettings input[type="checkbox"]:before {
content: "";
position: absolute;
top: 50%;
left: 4px;
transform: translate(-50%, -50%);
width: 25px;
height: 25px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.2s ease-in-out, left 0.2s ease-in-out;
}
#uwusettings input[type="checkbox"]:checked:before {
left: calc(100% - 4px);
}
#uwusettings input[type="text"] {
width: 150px;
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 10px;
outline: none;
margin: 5px;
margin-left: 0px;
}
`;
// ====================================================================================================================
// . . . КЛАССИЧЕСКАЯ ТЕМА . . .
// ====================================================================================================================
// лол а где
const css_uwu_classic = `
`;
// ====================================================================================================================
// . . . топовой шрифт кто не согласен тому в глаз 👅👅👅👅👅👅бе бе бе мяу мяу мяу мяу мяу мяу . . .
// ====================================================================================================================
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://fonts.googleapis.com/css?family=Montserrat";
document.head.appendChild(link);
// TODO - автоматически подкачивать шрифты нужные пользователю по названию в "Название вида шрифта" в Настройках
// ====================================================================================================================
// . . . СОХРАНЕНИЯ И ЗАГРУЗКА НАСТРОЕК . . .
// ====================================================================================================================
let settings;
function saveSettings() {
try {
uwuStorage.setItem("uwu_settings", settings);
} catch (error) {
console.error("Не удалось сохранить настройки:", error);
}
}
function loadSettings() {
const storedSettings = uwuStorage.getItem("uwu_settings");
if (storedSettings) {
try {
if (typeof storedSettings === "object" && storedSettings !== null) {
settings = { ...uwuDefaultSettings, ...storedSettings };
} else {
throw new Error("Сохраненные настройки не являются объектом.");
}
} catch (error) {
console.error(
"Ошибка обработки uwu_settings, будут применены настройки по умолчанию.",
error
);
settings = { ...uwuDefaultSettings };
}
} else {
settings = { ...uwuDefaultSettings };
}
}
// ====================================================================================================================
// . . . ДИНАМИЧНЫЕ ОБОЗРЕВАТЕЛИ . . .
// ====================================================================================================================
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Когда нужно вставить прослушку на какой-то элемент, который ещё не успел появиться.
async function setupMutationObserver(
selector,
callback,
options = { attributes: true, attributeFilter: ["style"] },
maxAttempts = 8,
delay = 500,
debounceTime = 100
) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const element = document.querySelector(selector);
if (element) {
const observer = new MutationObserver(debounce(callback, debounceTime));
observer.observe(element, options);
// console.log(`Наблюдатель установлен для элемента с селектором "${selector}".`);
callback();
return;
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
// console.warn(
// `Элемент с селектором "${selector}" не найден после ${maxAttempts} попыток.`
// );
}
// Когда нужно вставить что-то в какой-то элемент, который ещё не успел появиться.
async function setupSingleCallback(
selector,
callback,
maxAttempts = 8,
delay = 500
) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const element = document.querySelector(selector);
if (element) {
callback();
return;
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
// console.warn(
// `Элемент с селектором "${selector}" не найден после ${maxAttempts} попыток.`
// );
}
// ====================================================================================================================
// . . . СОХРАНЕНИЕ И РАБОТА С ЦВЕТОВЫМИ ТЕМАМИ . . .
// ====================================================================================================================
function getThemes() {
const storedThemes = uwuStorage.getItem("uwu_colorThemes");
const userThemes = storedThemes || {};
return { ...userThemes, ...defaultThemes };
}
function saveThemes(themes) {
const themesToSave = Object.keys(themes)
.filter((themeName) => !isDefaultTheme(themeName))
.reduce((obj, key) => {
obj[key] = themes[key];
return obj;
}, {});
uwuStorage.setItem("uwu_colorThemes", themesToSave);
}
function getCurrentThemeName() {
return uwuStorage.getItem("uwu_currentTheme") || "Тёмная Тема";
}
function setCurrentThemeName(themeName) {
uwuStorage.setItem("uwu_currentTheme", themeName);
}
function isDefaultTheme(themeName) {
return Object.keys(defaultThemes).includes(themeName);
}
function updateSaveButtonState() {
saveThemeButton.disabled = isDefaultTheme(currentThemeName);
}
// ====================================================================================================================
// . . . ВНЕШНИЙ ВИД ПАНЕЛИ НАСТРОЕК . . .
// ====================================================================================================================
function applyBackgroundImage(element, backgroundImage) {
element.style.backgroundImage = backgroundImage;
}
function createSettingsBlock(blockId, content) {
const siteTable = document.querySelector("#site_table");
const isMobile = siteTable.getAttribute("data-mobile") === "0";
const settingsElement = document.createElement("div");
settingsElement.id = blockId;
settingsElement.innerHTML = content;
const settingsContainer = isMobile
? document.querySelector("#branch")
: siteTable;
settingsContainer.appendChild(settingsElement);
}
// ====================================================================================================================
// . . . РАБОТА ПАНЕЛИ НАСТРОЕК . . .
// ====================================================================================================================
if (targetSettings.test(window.location.href)) {
createSettingsBlock("uwu-settings", uwusettings);
const uwuSettingsElement = document.getElementById("uwusettings");
if (uwuSettingsElement) {
uwuSettingsElement.insertAdjacentHTML("beforeend", newsPanel);
}
loadSettings();
if (!settings.extendedHints) {
const uwuHideHints = document.createElement("style");
uwuHideHints.innerHTML = `
#uwusettings p {
display: none;
}
`;
document.head.appendChild(uwuHideHints);
}
document
.querySelectorAll("#uwusettings [data-setting]")
.forEach((element) => {
const setting = element.dataset.setting;
if (element.type === "checkbox") {
element.checked = settings[setting];
} else {
element.value = settings[setting];
}
});
// ====================================================================================================================
// . . . ТЕМА UWU . . .
// ====================================================================================================================
function applySettingsTheme(theme) {
let css;
const settingsBlock = document.getElementById("uwu-settings");
const settingsHeader = document.getElementById("uwusettings-header");
switch (theme) {
case "classic":
css = css_uwu_classic;
break;
case "dark":
css = css_uwu_dark;
const backgroundImageDark = window.getComputedStyle(
document.body
).backgroundImage;
applyBackgroundImage(settingsHeader, backgroundImageDark);
settingsHeader.classList.add("header-rounded-image");
break;
case "glass":
css = css_uwu_glass;
const backgroundImageGlass = window.getComputedStyle(
document.body
).backgroundImage;
applyBackgroundImage(settingsBlock, backgroundImageGlass);
settingsBlock.classList.add("main-rounded-image");
break;
default:
css = css_uwu_classic;
break;
}
const oldStyle = document.getElementById("css-uwu-theme");
if (oldStyle) {
oldStyle.remove();
}
document.head.insertAdjacentHTML(
"beforeend",
`<style id="css-uwu-theme">${css}</style>`
);
}
applySettingsTheme(settings.settingsTheme);
// ====================================================================================================================
// . . . ШРИФТ ГРОМКОСТИ ЧАТА . . .
// ====================================================================================================================
function saveFontSettings() {
let fontSize = {};
document.querySelectorAll("input[data-font-size]").forEach((input) => {
fontSize[input.dataset.fontSize] = input.value;
});
uwuStorage.setItem("uwu_fontSize", fontSize);
}
function loadFontSettings() {
let defaultFontSize = {
vlm0: "10",
vlm1: "11",
vlm2: "11.5",
vlm3: "12",
vlm4: "12.5",
vlm5: "13",
vlm6: "15",
vlm7: "17",
vlm8: "19",
vlm9: "21",
vlm10: "23",
fontSizeBody: "14",
fontSizeSmall: "12",
fontSizeLocation: "14",
fontFamilyBody: "Verdana",
};
let fontSize = uwuStorage.getItem("uwu_fontSize") || defaultFontSize;
document.querySelectorAll("input[data-font-size]").forEach((input) => {
input.value = fontSize[input.dataset.fontSize] || "";
});
saveFontSettings();
}
document.querySelectorAll("input[data-font-size]").forEach((input) => {
input.addEventListener("input", saveFontSettings);
});
loadFontSettings();
// ====================================================================================================================
// . . . ПАРАМЕТРЫ КОСТЮМА . . .
// ====================================================================================================================
/**
* Изменяет размер изображения до соотношения сторон 2:3.
* @param {string} dataUrl - Изображение в формате Data URL.
* @param {number} aspectRatio - Целевое соотношение сторон (ширина / высота).
* @returns {Promise<string>} - Новый Data URL изображения с измененным размером.
*/
async function resizeImageToAspectRatio(dataUrl, aspectRatio = 2 / 3) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
try {
const canvas = document.createElement("canvas");
canvas.height = img.height;
canvas.width = img.height * aspectRatio;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const resizedDataUrl = canvas.toDataURL("image/png");
resolve(resizedDataUrl);
} catch (error) {
reject(error);
}
};
img.onerror = () => {
reject(
new Error("Не удалось загрузить изображение для изменения размера.")
);
};
img.src = dataUrl;
});
}
const costumeCheckbox = document.getElementById("personal-costume-panel");
function updateCostumeFlexBoxState() {
const costumeFlexBox = document.querySelectorAll(".costume-flex-box");
const showCostumesCheckbox = document.getElementById("show-costumes");
if (!costumeFlexBox || !showCostumesCheckbox) return;
if (costumeCheckbox.checked) {
costumeFlexBox.forEach((box) => {
box.classList.remove("disabled");
});
showCostumesCheckbox.disabled = false;
} else {
costumeFlexBox.forEach((box) => {
box.classList.add("disabled");
});
settings.showCostumesButtons = false;
showCostumesCheckbox.checked = false;
showCostumesCheckbox.disabled = true;
}
}
costumeCheckbox.addEventListener("change", updateCostumeFlexBoxState);
updateCostumeFlexBoxState();
function applyCostumeFromSlot(slotIndex) {
let data = uwuStorage.getItem("uwu_personal") || {};
if (
!data.costumes ||
!data.costumes.slots ||
!data.costumes.slots[slotIndex]
) {
alert("В этом слоте нет костюма.");
return;
}
const costumeImage = data.costumes.slots[slotIndex].base;
if (!costumeImage) {
alert("В этом слоте нет изображения костюма.");
return;
}
data.costumes.base = costumeImage;
uwuStorage.setItem("uwu_personal", data);
alert("Костюм успешно применен!");
loadCostume();
}
function removeCostumeFromSlot(slotIndex) {
if (!confirm("Вы уверены, что хотите удалить этот слот с костюмом?")) {
return;
}
let data = uwuStorage.getItem("uwu_personal") || {};
if (
data.costumes &&
data.costumes.slots &&
data.costumes.slots[slotIndex]
) {
data.costumes.slots.splice(slotIndex, 1);
data.costumes.slots = data.costumes.slots.filter((slot) => slot);
uwuStorage.setItem("uwu_personal", data);
alert("Слот " + (slotIndex + 1) + " успешно удален.");
loadCostume();
} else {
alert("Не удалось найти костюм для удаления.");
}
}
function loadCostume() {
let data = uwuStorage.getItem("uwu_personal") || {};
document.getElementById("cat-image-preview")?.remove();
if (!data || !data.catImg) {
// Нет данных для костюма или изображение не найдено
return;
}
if (data.costumes && data.costumes.base) {
const costumeImg = document.getElementById("cat-image-container");
const imgElement = document.createElement("img");
imgElement.id = "cat-image-preview";
imgElement.style.backgroundColor = "transparent";
imgElement.style.backgroundImage = "url(" + data.costumes.base + ")";
imgElement.style.backgroundSize = data.catImg.size;
imgElement.style.position = "absolute";
imgElement.classList.add("costume-style");
costumeImg.appendChild(imgElement);
}
const galleryArray = document.getElementById("costume-gallery");
galleryArray.innerHTML = "";
if (data.costumes && Array.isArray(data.costumes.slots)) {
data.costumes.slots.forEach((slot, i) => {
if (!slot) return;
const boxContainer = document.createElement("div");
boxContainer.className = "costume-gallery-box";
const deleteButton = document.createElement("button");
deleteButton.innerText = "❌";
deleteButton.classList.add(
"uwu-button",
"remove-button",
"costume-slot-delete-button"
);
deleteButton.title = "Удалить слот";
deleteButton.addEventListener("click", () => removeCostumeFromSlot(i));
boxContainer.appendChild(deleteButton);
const slotNumber = document.createElement("div");
slotNumber.innerText = `${i + 1}.`;
slotNumber.style.position = "absolute";
slotNumber.style.top = "10px";
slotNumber.style.left = "10px";
boxContainer.appendChild(slotNumber);
const imageContainer = document.createElement("div");
imageContainer.style.display = "flex";
imageContainer.style.justifyContent = "center";
const imageBox = document.createElement("div");
imageBox.style.backgroundImage = `url(${data.catImg.src})`;
imageBox.style.backgroundSize = data.catImg.size;
imageBox.classList.add("costume-slot");
imageBox.style.flex = "1";
imageBox.style.maxWidth = "100px";
imageContainer.appendChild(imageBox);
if (slot.base) {
const costumeBox = document.createElement("div");
costumeBox.style.backgroundImage = `url(${slot.base})`;
costumeBox.style.backgroundSize = data.catImg.size;
costumeBox.classList.add("costume-slot");
costumeBox.style.position = "absolute";
costumeBox.id = "costume";
imageContainer.appendChild(costumeBox);
}
const applyButton = document.createElement("button");
applyButton.classList = "uwu-button install-button";
applyButton.style.margin = "0.5rem";
applyButton.innerText = "Применить костюм";
applyButton.addEventListener("click", () => applyCostumeFromSlot(i));
boxContainer.appendChild(imageContainer);
boxContainer.appendChild(applyButton);
galleryArray.appendChild(boxContainer);
});
}
const addSlotButton = document.createElement("div");
addSlotButton.className = "costume-gallery-box";
addSlotButton.style.display = "flex";
addSlotButton.style.alignItems = "center";
addSlotButton.style.justifyContent = "center";
addSlotButton.style.fontSize = "5rem";
addSlotButton.style.cursor = "pointer";
addSlotButton.style.minWidth = "150px";
addSlotButton.style.minHeight = "220px";
addSlotButton.innerText = "+";
addSlotButton.title = "Добавить новый костюм из файла";
addSlotButton.addEventListener("click", () => {
document.getElementById("costume-file").click();
});
galleryArray.appendChild(addSlotButton);
}
function readImageFileAsDataURL(inputId, onSuccess, onError) {
const imgInput = document.getElementById(inputId);
const file = imgInput.files[0];
if (!file) {
alert("Пожалуйста, выберите изображение для костюма.");
return;
}
const reader = new FileReader();
reader.onerror = function () {
if (onError) onError("Ошибка при чтении файла. Попробуйте еще раз.");
};
reader.onload = function (e) {
if (!e.target.result.startsWith("data:image/")) {
if (onError) onError("Пожалуйста, выберите изображение для костюма.");
return;
}
if (onSuccess) onSuccess(e.target.result);
};
reader.readAsDataURL(file);
}
if (settings.personalCostumes) {
let rawData = uwuStorage.getItem("uwu_personal") || {};
let dataChanged = false;
if (!rawData.cats && rawData.id) {
rawData.cats = {};
rawData.cats[rawData.id] = {
id: rawData.id,
name: "Мой кот",
img: rawData.catImg?.src || "",
size: rawData.catImg?.size || "contain",
costume: rawData.costumes?.base || "",
};
if (rawData.costumes?.slots) rawData.slots = rawData.costumes.slots;
delete rawData.id;
delete rawData.catImg;
delete rawData.costumes;
dataChanged = true;
}
if (!rawData.cats) {
rawData.cats = {};
dataChanged = true;
}
if (!rawData.slots) {
rawData.slots = [];
dataChanged = true;
}
Object.keys(rawData.cats).forEach((catId) => {
let cat = rawData.cats[catId];
if (!cat.poses) {
cat.poses = {};
if (cat.costume && cat.img) {
const fileName = cat.img.split("/").pop() || "unknown";
cat.poses[fileName] = cat.costume;
}
delete cat.costume;
dataChanged = true;
}
});
if (dataChanged) uwuStorage.setItem("uwu_personal", rawData);
let currentEditCatId = null;
let currentPoseKey = null;
const catSelect = document.getElementById("current-cat-select");
const poseSelect = document.getElementById("current-pose-select");
const warningBox = document.getElementById("no-cats-warning");
const flexBox = document.querySelector(".costume-flex-box");
const previewContainer = document.getElementById("cat-image-container");
const previewName = document.getElementById("cat-preview-name");
function updateCatSelector() {
let data = uwuStorage.getItem("uwu_personal") || { cats: {}, slots: [] };
if (catSelect) catSelect.innerHTML = "";
const catIds = Object.keys(data.cats);
if (catIds.length === 0) {
if (warningBox) warningBox.style.display = "block";
if (flexBox) flexBox.classList.add("disabled");
return;
}
if (warningBox) warningBox.style.display = "none";
if (costumeCheckbox.checked && flexBox)
flexBox.classList.remove("disabled");
catIds.forEach((id) => {
const cat = data.cats[id];
const option = document.createElement("option");
option.value = id;
option.textContent = `${cat.name} (ID: ${id})`;
if (catSelect) catSelect.appendChild(option);
});
if (currentEditCatId && data.cats[currentEditCatId]) {
if (catSelect) catSelect.value = currentEditCatId;
} else {
currentEditCatId = catIds[0];
if (catSelect) catSelect.value = currentEditCatId;
}
updatePoseSelector();
}
function updatePoseSelector() {
let data = uwuStorage.getItem("uwu_personal");
if (!currentEditCatId || !data.cats[currentEditCatId]) return;
const cat = data.cats[currentEditCatId];
if (poseSelect) poseSelect.innerHTML = "";
const poses = Object.keys(cat.poses);
if (poses.length === 0 && cat.img) {
const fileName = cat.img.split("/").pop();
cat.poses[fileName] = "";
poses.push(fileName);
uwuStorage.setItem("uwu_personal", data);
}
poses.forEach((poseFile, index) => {
const option = document.createElement("option");
option.value = poseFile;
const hasCostume = cat.poses[poseFile] ? "✅" : "❌";
option.textContent = `${index + 1}. ${poseFile} [${hasCostume}]`;
if (poseSelect) poseSelect.appendChild(option);
});
if (poses.length > 0) {
currentPoseKey = poses[0];
if (poseSelect) poseSelect.value = currentPoseKey;
} else {
currentPoseKey = null;
}
renderPreview();
}
function renderPreview() {
let data = uwuStorage.getItem("uwu_personal");
if (
!currentEditCatId ||
!data.cats[currentEditCatId] ||
!currentPoseKey
) {
if (previewContainer) previewContainer.innerHTML = "";
if (previewName) previewName.textContent = "...";
return;
}
const cat = data.cats[currentEditCatId];
const currentCostume = cat.poses[currentPoseKey];
if (previewName) previewName.textContent = cat.name;
if (previewContainer) {
previewContainer.innerHTML = "";
previewContainer.style.minWidth = "100px";
previewContainer.style.minHeight = "150px";
previewContainer.style.flexShrink = "0";
if (!cat.img) {
previewContainer.innerHTML = `
<div style="
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 0.8em;
color: #ffaaaa;
padding: 5px;
box-sizing: border-box;
">
Внешность не загружена.<br>Зайдите в Игровую!
</div>
`;
return;
}
let bgUrl = "";
if (cat.img.includes(currentPoseKey)) {
bgUrl = cat.img;
} else {
bgUrl = `/cw3/composited/${currentPoseKey}`;
}
// 1. Слой кота
const catImg = document.createElement("div");
catImg.style.width = "100px";
catImg.style.height = "150px";
catImg.style.position = "absolute";
catImg.style.top = "0";
catImg.style.left = "0";
catImg.style.backgroundImage = `url(${bgUrl})`;
catImg.style.backgroundSize = cat.size || "contain";
catImg.style.backgroundPosition = "center";
catImg.style.backgroundRepeat = "no-repeat";
previewContainer.appendChild(catImg);
// 2. Слой костюма
if (currentCostume) {
const costumeImg = document.createElement("div");
costumeImg.style.width = "100px";
costumeImg.style.height = "150px";
costumeImg.style.position = "absolute";
costumeImg.style.top = "0";
costumeImg.style.left = "0";
costumeImg.style.backgroundImage = `url(${currentCostume})`;
costumeImg.style.backgroundSize = cat.size || "contain";
costumeImg.style.backgroundPosition = "center";
costumeImg.style.backgroundRepeat = "no-repeat";
previewContainer.appendChild(costumeImg);
}
}
loadGallery();
}
function loadGallery() {
const gallery = document.getElementById("costume-gallery");
if (!gallery) return;
gallery.innerHTML = "";
let data = uwuStorage.getItem("uwu_personal") || { slots: [] };
const addBtn = document.createElement("div");
addBtn.className = "costume-gallery-box";
addBtn.style.justifyContent = "center";
addBtn.style.cursor = "pointer";
addBtn.style.minWidth = "140px";
addBtn.style.minHeight = "230px";
addBtn.innerHTML =
"<div style='font-size: 3em;'>+</div><div>Из файла</div>";
addBtn.addEventListener("click", () =>
document.getElementById("costume-file").click()
);
gallery.appendChild(addBtn);
data.slots.forEach((slotBase64, index) => {
const box = document.createElement("div");
box.className = "costume-gallery-box";
box.style.minWidth = "140px";
const slotPreview = document.createElement("div");
slotPreview.style.width = "100px";
slotPreview.style.height = "150px";
slotPreview.style.minWidth = "100px";
slotPreview.style.minHeight = "150px";
slotPreview.style.flexShrink = "0";
slotPreview.style.position = "relative";
slotPreview.style.border = "1px solid rgba(255,255,255,0.1)";
slotPreview.style.borderRadius = "5px";
if (currentEditCatId && data.cats[currentEditCatId] && currentPoseKey) {
const cat = data.cats[currentEditCatId];
let bgUrl = cat.img.includes(currentPoseKey)
? cat.img
: `/cw3/composited/${currentPoseKey}`;
const catLayer = document.createElement("div");
catLayer.style.position = "absolute";
catLayer.style.top = "0";
catLayer.style.left = "0";
catLayer.style.width = "100%";
catLayer.style.height = "100%";
catLayer.style.backgroundImage = `url(${bgUrl})`;
catLayer.style.backgroundSize = cat.size || "contain";
catLayer.style.backgroundRepeat = "no-repeat";
catLayer.style.backgroundPosition = "center";
slotPreview.appendChild(catLayer);
}
const suitDiv = document.createElement("div");
suitDiv.style.position = "absolute";
suitDiv.style.top = "0";
suitDiv.style.left = "0";
suitDiv.style.width = "100%";
suitDiv.style.height = "100%";
suitDiv.style.backgroundImage = `url(${slotBase64})`;
suitDiv.style.backgroundSize =
currentEditCatId && data.cats[currentEditCatId]
? data.cats[currentEditCatId].size
: "contain";
suitDiv.style.backgroundRepeat = "no-repeat";
suitDiv.style.backgroundPosition = "center";
slotPreview.appendChild(suitDiv);
box.appendChild(slotPreview);
const applyBtn = document.createElement("button");
applyBtn.className = "uwu-button install-button";
applyBtn.innerText = "Надеть";
applyBtn.style.width = "100%";
applyBtn.style.marginTop = "10px";
applyBtn.addEventListener("click", () => {
if (!currentEditCatId || !currentPoseKey)
return alert("Не выбран кот или поза!");
let d = uwuStorage.getItem("uwu_personal");
d.cats[currentEditCatId].poses[currentPoseKey] = slotBase64;
uwuStorage.setItem("uwu_personal", d);
updatePoseSelector();
renderPreview();
});
const delBtn = document.createElement("button");
delBtn.className =
"costume-slot-delete-button uwu-button remove-button";
delBtn.innerText = "✖";
delBtn.title = "Удалить слот";
delBtn.addEventListener("click", () => {
if (confirm("Удалить этот костюм из библиотеки?")) {
let d = uwuStorage.getItem("uwu_personal");
d.slots.splice(index, 1);
uwuStorage.setItem("uwu_personal", d);
loadGallery();
}
});
box.appendChild(applyBtn);
box.appendChild(delBtn);
gallery.appendChild(box);
});
}
if (catSelect) {
catSelect.addEventListener("change", (e) => {
currentEditCatId = e.target.value;
updatePoseSelector();
});
}
if (poseSelect) {
poseSelect.addEventListener("change", (e) => {
currentPoseKey = e.target.value;
renderPreview();
});
}
const deleteCatBtn = document.getElementById("delete-cat-btn");
if (deleteCatBtn) {
deleteCatBtn.addEventListener("click", () => {
if (!currentEditCatId) return;
if (confirm("Удалить этого кота из списка?")) {
let data = uwuStorage.getItem("uwu_personal");
delete data.cats[currentEditCatId];
uwuStorage.setItem("uwu_personal", data);
currentEditCatId = null;
updateCatSelector();
}
});
}
const changeBtn = document.getElementById("changeCostume");
if (changeBtn) {
changeBtn.addEventListener("click", () => {
readImageFileAsDataURL(
"costume-file",
async (dataUrl) => {
if (!currentEditCatId || !currentPoseKey)
return alert("Сначала выберите кота и позу.");
try {
const resized = await resizeImageToAspectRatio(dataUrl);
let data = uwuStorage.getItem("uwu_personal");
data.cats[currentEditCatId].poses[currentPoseKey] = resized;
uwuStorage.setItem("uwu_personal", data);
updatePoseSelector();
renderPreview();
alert("Костюм надет на выбранную позу!");
} catch (e) {
console.error(e);
alert("Ошибка обработки");
}
},
alert
);
});
}
const removeBtn = document.getElementById("removeCostume");
if (removeBtn) {
removeBtn.addEventListener("click", () => {
if (!currentEditCatId || !currentPoseKey) return;
let data = uwuStorage.getItem("uwu_personal");
data.cats[currentEditCatId].poses[currentPoseKey] = "";
uwuStorage.setItem("uwu_personal", data);
updatePoseSelector();
renderPreview();
});
}
const saveSlotBtn = document.getElementById("saveCostumeToNewSlot");
if (saveSlotBtn) {
saveSlotBtn.addEventListener("click", () => {
readImageFileAsDataURL(
"costume-file",
async (dataUrl) => {
try {
const resized = await resizeImageToAspectRatio(dataUrl);
let data = uwuStorage.getItem("uwu_personal") || {
cats: {},
slots: [],
};
if (!data.slots) data.slots = [];
data.slots.push(resized);
uwuStorage.setItem("uwu_personal", data);
loadGallery();
alert("Костюм сохранен!");
} catch (e) {
console.error(e);
alert("Ошибка");
}
},
alert
);
});
}
updateCatSelector();
}
// ====================================================================================================================
// . . . ИМПОРТ ЛС ИЗ ДРУГИХ МОДОВ (ВАРОМОДА) . . .
// ====================================================================================================================
function importLsFromVarmod() {
try {
const varmodLsRaw = localStorage.getItem("cwmod_ls");
if (!varmodLsRaw) {
alert(
"Сохранённые ЛС из других модов или скриптов не найдены в вашем браузере."
);
return;
}
const varmodLs = JSON.parse(varmodLsRaw);
const uwuLs = uwuStorage.getItem("uwu_saved_ls") || {};
let importedCount = 0;
let updatedCount = 0;
for (const lsId in varmodLs) {
if (uwuLs.hasOwnProperty(lsId)) {
const varmodDate = new Date(varmodLs[lsId].savedate);
const uwuDate = new Date(uwuLs[lsId].savedate);
if (varmodDate > uwuDate) {
uwuLs[lsId] = varmodLs[lsId];
updatedCount++;
}
} else {
uwuLs[lsId] = varmodLs[lsId];
importedCount++;
}
}
uwuStorage.setItem("uwu_saved_ls", uwuLs);
alert(
`Импорт успешно завершён!\nНовых переписок импортировано: ${importedCount}\nСуществующих переписок обновлено: ${updatedCount}`
);
} catch (error) {
console.error("UwU | Ошибка при импорте ЛС из других модов:", error);
alert(
"Произошла ошибка во время импорта. Возможно, данные других модов повреждены."
);
}
}
const importLsButton = document.getElementById("import-ls-from-other-mods");
if (importLsButton) {
importLsButton.addEventListener("click", importLsFromVarmod);
}
/**
* Настраивает логику для индивидуального импорта/экспорта сохраненных ЛС.
*/
function setupLsImportExport() {
const exportField = document.getElementById("ls-export-field");
const importField = document.getElementById("ls-import-field");
const importButton = document.getElementById("ls-import-btn");
if (!exportField || !importField || !importButton) return;
function updateLsExportField() {
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
exportField.value = JSON.stringify(savedLs);
}
importButton.addEventListener("click", () => {
const jsonString = importField.value;
if (!jsonString.trim()) {
alert("Поле для импорта пустое.");
return;
}
try {
const importedLs = JSON.parse(jsonString);
if (typeof importedLs !== "object" || importedLs === null) {
throw new Error("Импортируемые данные не являются объектом.");
}
uwuStorage.setItem("uwu_saved_ls", importedLs);
alert("Сохранённые ЛС успешно импортированы!");
importField.value = "";
updateLsExportField();
} catch (error) {
alert(
"Ошибка! Не удалось импортировать ЛС. Проверьте корректность вставленных данных."
);
console.error("UwU | Ошибка импорта ЛС:", error);
}
});
updateLsExportField();
}
setupLsImportExport();
// ====================================================================================================================
// . . . ТЕМЫ И ЦВЕТА ИГРОВОЙ . . .
// ====================================================================================================================
const colorInputs = document.querySelectorAll(
"#color-picker input[type='text']"
);
const saveThemeButton = document.getElementById("saveThemeButton");
const themeSelect = document.getElementById("theme-select");
const addThemeButton = document.getElementById("addThemeButton");
const removeThemeButton = document.getElementById("removeThemeButton");
let currentThemeName = getCurrentThemeName();
let allThemes = getThemes();
function loadThemeToInputs(themeName) {
const theme = allThemes[themeName]?.colors;
colorInputs.forEach((input) => {
const colorKey = input.dataset.color;
input.value = theme?.[colorKey] || "";
});
}
function saveThemeFromInputs() {
const themeData = { colors: {} };
colorInputs.forEach((input) => {
const colorKey = input.dataset.color;
themeData.colors[colorKey] = input.value;
});
allThemes[currentThemeName] = themeData;
saveThemes(allThemes);
console.log(`Тема "${currentThemeName}" сохранена!`);
}
function updateThemeSelect() {
themeSelect.innerHTML = "";
Object.keys(allThemes).forEach((name) => {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
themeSelect.appendChild(option);
});
themeSelect.value = currentThemeName;
removeThemeButton.style.display = Object.keys(defaultThemes).includes(
currentThemeName
)
? "none"
: "inline";
}
themeSelect.addEventListener("change", (event) => {
currentThemeName = event.target.value;
setCurrentThemeName(currentThemeName);
loadThemeToInputs(currentThemeName);
updateThemeSelect();
updateSaveButtonState();
});
addThemeButton.addEventListener("click", () => {
const newThemeName = prompt("Введите название новой темы:");
if (newThemeName && !allThemes[newThemeName]) {
allThemes[newThemeName] = { colors: {} };
saveThemes(allThemes);
updateThemeSelect();
themeSelect.value = newThemeName;
currentThemeName = newThemeName;
setCurrentThemeName(currentThemeName);
loadThemeToInputs(currentThemeName);
}
});
removeThemeButton.addEventListener("click", () => {
if (!Object.keys(defaultThemes).includes(currentThemeName)) {
delete allThemes[currentThemeName];
saveThemes(allThemes);
currentThemeName = "Тёмная Тема";
setCurrentThemeName(currentThemeName);
updateThemeSelect();
loadThemeToInputs(currentThemeName);
}
});
saveThemeButton.addEventListener("click", () => {
if (isDefaultTheme(currentThemeName)) {
alert(
"Вы не можете изменять стандартные темы. Пожалуйста, создайте свою собственную тему."
);
} else {
saveThemeFromInputs();
}
});
colorInputs.forEach((input) => {
input.addEventListener("input", () => {
if (isDefaultTheme(currentThemeName)) {
alert(
"Вы не можете изменять стандартные темы. Пожалуйста, создайте свою собственную тему."
);
loadThemeToInputs(currentThemeName);
} else {
saveThemeFromInputs();
}
});
});
updateThemeSelect();
loadThemeToInputs(currentThemeName);
// ====================================================================================================================
// . . . РАБОТА ЦВЕТОВ НАВЫКОВ И ПАРАМЕТРОВ . . .
// ====================================================================================================================
document
.querySelectorAll('#parameters-color-settings input[type="color"]')
.forEach((element) => {
element.addEventListener("change", () => {
const paramId = element.dataset.param;
const colorType = element.dataset.colorType;
const colorValue = element.value;
if (!settings.parametersColors[paramId]) {
settings.parametersColors[paramId] = [];
}
const colorIndex =
colorType === "bar-from"
? 0
: colorType === "bar-to"
? 1
: colorType === "bg-from"
? 2
: 3;
settings.parametersColors[paramId][colorIndex] = colorValue;
saveSettings();
});
});
function restoreColorPickers() {
for (const paramId in settings.parametersColors) {
const colors = settings.parametersColors[paramId];
const barFromInput = document.querySelector(
`#parameters-color-settings input[type="color"][data-param="${paramId}"][data-color-type="bar-from"]`
);
const barToInput = document.querySelector(
`#parameters-color-settings input[type="color"][data-param="${paramId}"][data-color-type="bar-to"]`
);
const bgFromInput = document.querySelector(
`#parameters-color-settings input[type="color"][data-param="${paramId}"][data-color-type="bg-from"]`
);
const bgToInput = document.querySelector(
`#parameters-color-settings input[type="color"][data-param="${paramId}"][data-color-type="bg-to"]`
);
if (barFromInput) barFromInput.value = colors[0];
if (barToInput) barToInput.value = colors[1];
if (bgFromInput) bgFromInput.value = colors[2];
if (bgToInput) bgToInput.value = colors[3];
}
}
restoreColorPickers();
function setupParameterColorImportExport() {
const exportField = document.getElementById("param-colors-export-field");
const importField = document.getElementById("param-colors-import-field");
const importButton = document.getElementById("param-colors-import-btn");
if (!exportField || !importField || !importButton) return;
function updateParamColorsExportField() {
try {
exportField.value = JSON.stringify(settings.parametersColors);
} catch (error) {
console.error("Ошибка при обновлении поля экспорта цветов:", error);
exportField.value = "Ошибка экспорта.";
}
}
importButton.addEventListener("click", () => {
const jsonString = importField.value;
if (!jsonString.trim()) {
alert("Поле для импорта пустое.");
return;
}
try {
const importedColors = JSON.parse(jsonString);
if (
typeof importedColors !== "object" ||
importedColors === null ||
!importedColors.dream
) {
throw new Error("Неверный формат данных.");
}
settings.parametersColors = importedColors;
saveSettings();
restoreColorPickers();
updateParamColorsExportField();
alert("Настройки цветов успешно импортированы!");
importField.value = "";
} catch (error) {
alert(
"Ошибка! Не удалось импортировать настройки. Проверьте корректность вставленных данных."
);
console.error("Ошибка импорта цветов параметров:", error);
}
});
updateParamColorsExportField();
}
setupParameterColorImportExport();
// ====================================================================================================================
// . . . ПОДСВЕТКА РЕСУРСОВ . . .
// ====================================================================================================================
function saveHighlightSettings() {
const highlightResources = [];
document
.querySelectorAll(".uwu-table-highlight-Resources tbody tr")
.forEach((row) => {
const resourceName =
row.querySelector(".uwu-color-picker").dataset.resource;
const colorPicker = row.querySelector(".uwu-color-picker");
const checkbox = row.querySelector(".uwu-highlight-checkbox");
const resource = {
name: resourceName,
color: colorPicker.value,
highlight: checkbox.checked,
};
highlightResources.push(resource);
});
uwuStorage.setItem("uwu_highlightResources", highlightResources);
}
function restoreHighlightSettings() {
const highlightResources = uwuStorage.getItem("uwu_highlightResources");
if (highlightResources) {
highlightResources.forEach((resource) => {
const colorPicker = document.querySelector(
`.uwu-color-picker[data-resource="${resource.name}"]`
);
const checkbox = document.querySelector(
`.uwu-highlight-checkbox[data-resource="${resource.name}"]`
);
if (colorPicker) colorPicker.value = resource.color;
if (checkbox) checkbox.checked = resource.highlight;
});
}
}
restoreHighlightSettings();
document.querySelectorAll(".uwu-color-picker").forEach((element) => {
element.addEventListener("input", saveHighlightSettings);
});
document.querySelectorAll(".uwu-highlight-checkbox").forEach((element) => {
element.addEventListener("change", saveHighlightSettings);
});
// ====================================================================================================================
// . . . ЦВЕТА КОМАНДНЫХ БОЁВ . . .
// ====================================================================================================================
document
.querySelectorAll('#colorSettingsTable input[type="color"]')
.forEach((element) => {
element.addEventListener("change", () => {
const team = `team${element.dataset.team}`;
const part = element.dataset.part === "green" ? 0 : 1;
const colorValue = element.value;
settings.fightTeamsColors[team][part] = colorValue;
saveSettings();
});
});
function restoreColorTeamsPickers() {
document
.querySelectorAll('#colorSettingsTable input[type="color"]')
.forEach((element) => {
element.addEventListener("change", () => {
const team = `team${element.dataset.team}`;
const part = element.dataset.part === "green" ? 0 : 1;
const colorValue = element.value;
settings.fightTeamsColors[team][part] = colorValue;
saveSettings();
});
});
}
restoreColorTeamsPickers();
// ====================================================================================================================
// . . . СБРОС НАСТРОЕК . . .
// ====================================================================================================================
const settingsKeys = [
"uwu_settings",
"uwu_version",
"uwu_layoutSettings",
"uwu_climbingPanelState",
"uwu_moduleStates",
"uwu_fightPanelPosition",
"uwu_climbingPanelStatus",
"uwu_privateModules",
"uwu_colorThemes",
"uwu_currentTheme",
"uwu_fontSize",
"uwu_clock",
"uwu_templates",
"uwu_highlightResources",
"uwu_saved_ls",
"uwu_activity",
"uwu_fastStyles",
"uwu_fastStyles_hideCatTooltip",
"uwu_fightTeamsCats",
"uwu_catchingLog_customItems",
];
function resetAllSaves() {
const confirmReset = confirm(
"Точно сбросить ВСЕ UwU Настройки? Это удалить абсолютно всё по UwU скрипту/моду, даже ваши карты Минных полей, темы и многое другое!"
);
if (confirmReset) {
settingsKeys.forEach((key) => {
uwuStorage.removeItem(key);
console.log(`Удалено ${key}`);
});
console.log("Все настройки сброшены");
} else {
console.log("Сброс настроек отменен");
}
}
document
.getElementById("resetAllSaves")
.addEventListener("click", resetAllSaves);
// ====================================================================================================================
// . . . ЕДИНОЕ ХРАНИЛИЩЕ . . .
// ====================================================================================================================
const unifiedStorageCheckbox = document.querySelector(
'[data-setting="unifiedStorage"]'
);
if (
typeof GM_setValue === "undefined" ||
typeof GM_getValue === "undefined" ||
typeof GM_listValues === "undefined" ||
typeof GM_deleteValue === "undefined"
) {
const container = unifiedStorageCheckbox.parentElement;
container.innerHTML =
'<p style="color: #ff7878;"><b>Единое хранилище недоступно.</b><br>Вероятнее всего, ваш менеджер скриптов (например, Tampermonkey) не предоставляет необходимый API для кросс-доменной синхронизации :(</p>';
} else {
unifiedStorageCheckbox.addEventListener("change", () => {
if (unifiedStorageCheckbox.checked) {
const confirmation = confirm(
"Вы уверены, что хотите включить единое хранилище?\n\n" +
"Все ваши текущие настройки и данные с этого сайта будут скопированы в общее хранилище и заменят любые данные, которые могли там быть.\n\n" +
"Убедитесь, что вы находитесь на сайте (.net или .su) с теми настройками и данными, которые хотите сделать основными."
);
if (confirmation) {
settings.unifiedStorage = true;
uwuStorage.migrateAllToGM();
GM_setValue("uwu_settings", JSON.stringify(settings));
alert(
"Единое хранилище включено. Настройки и данные с этого сайта были скопированы. Страница будет перезагружена."
);
location.reload();
} else {
unifiedStorageCheckbox.checked = false;
}
} else {
const confirmation = confirm(
"Вы уверены, что хотите отключить единое хранилище?\n\n" +
"Текущие настройки и данные из единого хранилища скопируются в локальное для этого сайта.\n\n" +
"Скрипт снова будет использовать отдельное хранилище для этого домена (localStorage)."
);
if (confirmation) {
const keysToProcess = GM_listValues();
keysToProcess.forEach((key) => {
if (key.startsWith("uwu_")) {
const value = GM_getValue(key);
localStorage.setItem(key, value);
}
});
settings.unifiedStorage = false;
GM_setValue("uwu_settings", JSON.stringify(settings));
localStorage.setItem("uwu_settings", JSON.stringify(settings));
alert(
"Единое хранилище отключено. Ваши настройки и данные были скопированы в локальное хранилище для этого сайта. Страница будет перезагружена."
);
location.reload();
} else {
unifiedStorageCheckbox.checked = true;
}
}
});
}
// ====================================================================================================================
// . . . ВЗАИМОИСКЛЮЧАЮЩИЕСЯ ЧЕКБОКСЫ . . .
// ====================================================================================================================
const exclusiveCheckboxGroups = [
["backgroundRepeat", "backgroundUser"],
["parametersBackgroundImage", "parametersUserBackgroundImage"],
];
document
.querySelectorAll("#uwusettings [data-setting]")
.forEach((element) => {
const setting = element.dataset.setting;
element.addEventListener("change", () => {
if (element.type === "checkbox") {
const group = exclusiveCheckboxGroups.find((g) =>
g.includes(setting)
);
if (group) {
group.forEach((s) => {
if (s !== setting) {
settings[s] = false;
document.querySelector(
`#uwusettings [data-setting="${s}"]`
).checked = false;
}
});
}
settings[setting] = element.checked;
} else {
settings[setting] = element.value;
}
saveSettings();
});
});
// ====================================================================================================================
// . . . НАСТРОЙКИ ЛОГА ЛОВЛИ - СВОИ НАЗВАНИЯ ПРЕДМЕТОВ . . .
// ====================================================================================================================
function setupCatchingLogCustomItems() {
const textarea = document.getElementById("catching-log-custom-items");
const saveBtn = document.getElementById("save-custom-items-btn");
const exportField = document.getElementById("custom-items-export-field");
const importField = document.getElementById("custom-items-import-field");
const importBtn = document.getElementById("custom-items-import-btn");
if (!textarea || !saveBtn || !exportField || !importField || !importBtn)
return;
function parseTextareaToObject(text) {
const lines = text.split("\n");
const obj = {};
lines.forEach((line) => {
if (line.includes("=")) {
const parts = line.split("=");
const id = parts[0].trim();
const name = parts.slice(1).join("=").trim();
if (id && name) {
obj[id] = name;
}
}
});
return obj;
}
function objectToTextarea(obj) {
return Object.entries(obj)
.map(([id, name]) => `${id}=${name}`)
.join("\n");
}
function loadAndDisplay() {
const customItems =
uwuStorage.getItem("uwu_catchingLog_customItems") || {};
textarea.value = objectToTextarea(customItems);
exportField.value = JSON.stringify(customItems);
}
saveBtn.addEventListener("click", () => {
const customItemsObject = parseTextareaToObject(textarea.value);
uwuStorage.setItem("uwu_catchingLog_customItems", customItemsObject);
alert("Список названий сохранён!");
loadAndDisplay();
});
importBtn.addEventListener("click", () => {
const jsonString = importField.value;
if (!jsonString.trim()) {
alert("Поле для импорта пустое.");
return;
}
try {
const importedItems = JSON.parse(jsonString);
if (typeof importedItems !== "object" || importedItems === null) {
throw new Error("Неверный формат данных.");
}
uwuStorage.setItem("uwu_catchingLog_customItems", importedItems);
alert("Список названий успешно импортирован!");
importField.value = "";
loadAndDisplay();
} catch (error) {
alert(
"Ошибка! Не удалось импортировать список. Проверьте корректность вставленных данных."
);
console.error("UwU | Ошибка импорта названий предметов:", error);
}
});
exportField.addEventListener("click", function () {
this.select();
});
loadAndDisplay();
}
setupCatchingLogCustomItems();
// ====================================================================================================================
// . . . СОЗДАНИЕ ВЫПАДАЮЩИХ СПИСКОВ ПРИ ПОМОЩИ ФУНКЦИИ createCustomSelect . . .
// ====================================================================================================================
loadSettings();
// Звуки звуки звуки, вуху.
const notificationSounds = [
{ name: "Звук 1", id: "notificationSound1" },
{ name: "Звук 2", id: "notificationSound2" },
{ name: "Звук 3", id: "notificationSound3" },
{ name: "Блокирование", id: "notificationBlockSound1" },
];
createCustomSelect("climbingRefreshNotificationSound", notificationSounds);
createCustomSelect("myNameNotificationSound", notificationSounds);
createCustomSelect("notificationPMSound", notificationSounds);
createCustomSelect("notificationActionEndSound", notificationSounds);
createCustomSelect("notificationInMouthSound", notificationSounds);
createCustomSelect("notificationInFightModeSound", notificationSounds);
createCustomSelect("notificationBlockSound", notificationSounds);
createCustomSelect("intervalTimerSound", notificationSounds);
// ==============================================================================
const howShowOtherCatsList = [
{ name: "Не отображать", id: "1" },
{ name: "Компактно", id: "2" },
{ name: "Целиком", id: "3" },
];
createCustomSelect("showOtherCatsList", howShowOtherCatsList);
// ==============================================================================
const themeOptions = [
{ id: "classic", name: "Классическая" },
{ id: "dark", name: "Тёмная" },
{ id: "glass", name: "Стеклянная" },
];
createCustomSelect("settingsTheme", themeOptions);
// ==============================================================================
const climbingPanelOrientations = [
{ id: "vertical", name: "Вертикальный" },
{ id: "horizontal", name: "Горизонтальный" },
];
createCustomSelect("climbingPanelOrientation", climbingPanelOrientations);
// ==============================================================================
const clockStyles = [
{ id: "compact", name: "Компактный" },
{ id: "standard", name: "Стандартный" },
{ id: "string", name: "Строчный" },
];
createCustomSelect("clockStyle", clockStyles);
// ==============================================================================
const clockPositions = [
{ id: "fly", name: "Свободно" },
{ id: "tos", name: "В блоке погоды" },
];
createCustomSelect("clockPosition", clockPositions);
// ==============================================================================
const highlightResourcesStyles = [
{ id: "background", name: "Фон / Быстро" },
{ id: "glow", name: "Свечение / Медленно" },
];
createCustomSelect("highlightResourcesStyle", highlightResourcesStyles);
// ==============================================================================
const cleaningLogStyles = [
{ id: "smart", name: "Умный" },
// { id: "standart", name: "Стандартный" },
];
createCustomSelect("cleaningLogStyle", cleaningLogStyles);
// ==============================================================================
const defectsStyles = [{ id: "default", name: "Стандартный" }];
createCustomSelect("defectsStyle", defectsStyles);
// ==============================================================================
const defectsQualities = [
{ id: "low", name: "Низкое/Старое (100x150)" },
{ id: "high", name: "Высокое/Новое (200x300)" },
];
createCustomSelect("defectsQuality", defectsQualities);
// ==============================================================================
const climbingPanelInputsStyles = [
{ id: "keyboard", name: "Клавиатура" },
{ id: "standart", name: "Галочки + Клавиатура" },
];
createCustomSelect("climbingPanelInputsStyle", climbingPanelInputsStyles);
// ====================================================================================================================
// . . . СОЗДАНИЕ ВЫПАДАЮЩИХ СПИСКОВ . . .
// ====================================================================================================================
function createCustomSelect(selectId, options) {
const selectContainer = document.getElementById(selectId);
const selectedElement = selectContainer.querySelector(".select-selected");
const optionsContainer = selectContainer.querySelector(".select-items");
if (settings && settings[selectId] !== undefined) {
const selectedOption = options.find(
(option) => option.id === settings[selectId]
);
if (selectedOption) {
selectedElement.textContent = selectedOption.name;
}
}
options.forEach((option, index) => {
const optionElement = document.createElement("div");
optionElement.textContent = option.name;
optionElement.dataset.id = option.id;
optionElement.addEventListener("click", () => {
selectedElement.textContent = option.name;
settings[selectId] = option.id;
saveSettings();
selectContainer.classList.remove("active");
});
optionsContainer.appendChild(optionElement);
});
selectedElement.addEventListener("click", () => {
selectContainer.classList.toggle("active");
});
}
// ====================================================================================================================
// . . . КНОПКА НОВОСТЕЙ . . .
// ====================================================================================================================
window.addEventListener("load", () => {
const newsButton = document.getElementById("news-button");
const newsList = document.getElementById("news-list");
if (newsButton && newsList) {
newsButton.addEventListener("click", () => {
if (newsList.style.display === "none") {
newsList.style.display = "block";
} else {
newsList.style.display = "none";
}
});
}
});
// ====================================================================================================================
// . . . КНОПКА ТЕСТА ЗВУКОВ . . .
// ====================================================================================================================
function addSoundTestButton(
containerId,
settingsKeyForSound,
settingsKeyForVolume
) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`Контейнер с ID ${containerId} не найден.`);
return;
}
const testButton = document.createElement("button");
testButton.textContent = "Тест звука";
testButton.addEventListener("click", () => {
const selectedSoundId = settings[settingsKeyForSound];
const volume = settings[settingsKeyForVolume] || 5;
if (selectedSoundId) {
soundManager.playSound(selectedSoundId, volume);
} else {
console.error(
`Выбранный звук для контейнера ${containerId} не найден.`
);
}
});
container.appendChild(testButton);
}
addSoundTestButton(
"notificationPMContainer",
"notificationPMSound",
"notificationPMVolume"
);
addSoundTestButton(
"notificationActionEndContainer",
"notificationActionEndSound",
"notificationActionEndVolume"
);
addSoundTestButton(
"notificationInMouthContainer",
"notificationInMouthSound",
"notificationInMouthVolume"
);
addSoundTestButton(
"notificationInFightModeContainer",
"notificationInFightModeSound",
"notificationInFightModeVolume"
);
addSoundTestButton(
"climbingRefreshNotificationSoundContainer",
"climbingRefreshNotificationSound",
"climbingRefreshNotificationVolume"
);
addSoundTestButton(
"myNameNotificationSoundContainer",
"myNameNotificationSound",
"notificationMyNameVolume"
);
addSoundTestButton(
"notificationBlockContainer",
"notificationBlockSound",
"notificationBlockVolume"
);
addSoundTestButton(
"intervalTimerContainer",
"intervalTimerSound",
"intervalTimerVolume"
);
// ====================================================================================================================
// . . . СБРОС ПОЗИЦИИ ЧАСИКОВ . . .
// ====================================================================================================================
document
.getElementById("resetClockPosition")
.addEventListener("click", () => {
const defaultPosition = { x: 10, y: 10 };
uwuStorage.setItem("uwu_clock", defaultPosition);
});
// ====================================================================================================================
// . . . ИМПОРТ / ЭКСПОРТ ВСЕХ НАСТРОЕК . . .
// ====================================================================================================================
const settingsAllKeys = [
"uwu_settings",
"uwu_version",
"uwu_layoutSettings",
"uwu_climbingPanelState",
"uwu_moduleStates",
"uwu_fightPanelPosition",
"uwu_climbingPanelStatus",
"uwu_privateModules",
"uwu_colorThemes",
"uwu_currentTheme",
"uwu_fontSize",
"uwu_clock",
"uwu_templates",
"uwu_highlightResources",
"uwu_saved_ls",
"uwu_activity",
"uwu_fastStyles",
"uwu_fightTeamsCats",
];
const importButton = document.getElementById("importSettingsButton");
const importSettingsInput = document.getElementById("importSettings");
const exportSettingsInput = document.getElementById("exportSettings");
importButton.addEventListener("click", () => {
const importedSettings = importSettingsInput.value;
try {
const parsedSettings = JSON.parse(importedSettings);
settingsAllKeys.forEach((key) => {
if (parsedSettings[key] !== undefined) {
uwuStorage.setItem(key, parsedSettings[key]);
}
});
console.log("Настройки импортированы:", parsedSettings);
} catch (error) {
console.error("Ошибка при импорте настроек:", error);
}
updateExportField();
});
function updateExportField() {
const settingsObject = getSpecificLocalStorageItems();
const settingsToExport = JSON.stringify(settingsObject, null, 2);
exportSettingsInput.value = settingsToExport;
}
function getSpecificLocalStorageItems() {
const items = {};
settingsAllKeys.forEach((key) => {
const value = uwuStorage.getItem(key);
if (value !== null) {
items[key] = value;
}
});
return items;
}
loadSettings();
updateExportField();
// ====================================================================================================================
// . . . РЕДИЗАЙН НАСТРОЕК КОСТЮМОВ . . .
// ====================================================================================================================
if (settings.redesignCostumsSettings) {
function addStyles() {
const style = document.createElement("style");
style.innerHTML =
// css
`
.list-group-item {
display: grid !important;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
align-items: center;
margin-bottom: 10px;
width: 280px !important;
}
.list-group-item img {
margin-right: 10px;
width: 50px;
height: 80px;
}
.costume-image-container {
grid-column: 1;
grid-row: 1 / span 2;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.list-group {
resize: vertical;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.list-group-item span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.costume-id {
font-weight: bold;
margin-bottom: 5px;
grid-column: 2;
grid-row: 1;
}
.costume-text {
grid-column: 2;
grid-row: 2;
}
.col-3 {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.col-3 > button {
height: 22px;
}
`;
document.head.appendChild(style);
}
function addCostumePreview() {
const items = document.querySelectorAll(".list-group-item");
items.forEach((item) => {
const costumeId = item.textContent.trim().split(" ")[0];
const imageContainer = item.querySelector(".costume-image-container");
const img = imageContainer ? imageContainer.querySelector("img") : null;
if (
!imageContainer ||
!img ||
img.getAttribute("data-costume-id") !== costumeId
) {
const imgUrl = `https://catwar.net/cw3/cats/0/costume/${costumeId}.png`;
if (!imageContainer) {
const newImageContainer = document.createElement("div");
newImageContainer.classList.add("costume-image-container");
const newImg = document.createElement("img");
newImg.src = imgUrl;
newImg.alt = `Costume ${costumeId}`;
newImg.setAttribute("data-costume-id", costumeId);
newImageContainer.appendChild(newImg);
item.insertBefore(newImageContainer, item.firstChild);
} else {
img.src = imgUrl;
img.setAttribute("data-costume-id", costumeId);
}
}
});
}
addStyles();
setupMutationObserver(".double-container", addCostumePreview, {
childList: true,
subtree: true,
});
}
// ====================================================================================================================
// . . . МАКЕТ КАСТОМИЗАЦИИ ИГРОВОЙ . . .
// ====================================================================================================================
const blockNames = {
tr_info: "Информация",
tr_tos: "Погода",
tr_chat: "Чат",
tr_actions: "Действия",
tr_mouth: "Во рту",
// 'tr_sky': 'Небо',
};
const leftColumn = document.querySelector("#layout-customizer .column.left");
const rightColumn = document.querySelector(
"#layout-customizer .column.right"
);
function saveLayoutSettings() {
const leftBlocks = Array.from(leftColumn.querySelectorAll(".block")).map(
(block) => block.classList[1]
);
const rightBlocks = Array.from(rightColumn.querySelectorAll(".block")).map(
(block) => block.classList[1]
);
const layoutSettings = {
leftBlocks,
rightBlocks,
};
uwuStorage.setItem("uwu_layoutSettings", layoutSettings);
}
function createBlockElement(blockId) {
const blockElement = document.createElement("div");
blockElement.classList.add("block", blockId);
const blockName = document.createElement("span");
blockName.textContent = blockNames[blockId];
blockElement.appendChild(blockName);
const controlsWrapper = document.createElement("div");
controlsWrapper.classList.add("controls");
if (blockId === "tr_info") {
const moveInfoButton = document.createElement("button");
moveInfoButton.textContent = "⏪Переместить⏩";
moveInfoButton.classList.add("move-info", "install-button");
moveInfoButton.addEventListener("click", () => {
swapColumns(blockElement);
saveLayoutSettings();
});
controlsWrapper.appendChild(moveInfoButton);
} else {
const moveUpButton = document.createElement("button");
moveUpButton.textContent = "🔼Вверх";
moveUpButton.classList.add("move-up", "install-button");
moveUpButton.addEventListener("click", () => {
const previousBlock = blockElement.previousElementSibling;
if (previousBlock) {
blockElement.parentNode.insertBefore(blockElement, previousBlock);
saveLayoutSettings();
}
});
controlsWrapper.appendChild(moveUpButton);
const moveDownButton = document.createElement("button");
moveDownButton.textContent = "🔽Вниз";
moveDownButton.classList.add("move-down", "install-button");
moveDownButton.addEventListener("click", () => {
const nextBlock = blockElement.nextElementSibling;
if (nextBlock) {
blockElement.parentNode.insertBefore(nextBlock, blockElement);
saveLayoutSettings();
}
});
controlsWrapper.appendChild(moveDownButton);
}
blockElement.appendChild(controlsWrapper);
return blockElement;
}
function swapColumns(blockElement) {
if (blockElement.parentNode === leftColumn) {
const rightColumnBlocks = Array.from(rightColumn.children);
rightColumn.innerHTML = "";
rightColumn.appendChild(blockElement);
rightColumnBlocks.forEach((block) => leftColumn.appendChild(block));
} else {
const leftColumnBlocks = Array.from(leftColumn.children);
leftColumn.innerHTML = "";
leftColumn.appendChild(blockElement);
leftColumnBlocks.forEach((block) => rightColumn.appendChild(block));
}
saveLayoutSettings();
}
const resetLayoutButton = document.getElementById("reset-layout-button");
resetLayoutButton.addEventListener("click", () => {
const confirmReset = confirm(
"Вы уверены, что хотите сбросить расположение блоков?"
);
if (confirmReset) {
const defaultSettings = getDefaultLayoutSettings();
uwuStorage.setItem("uwu_layoutSettings", defaultSettings);
location.reload();
}
});
function getDefaultLayoutSettings() {
return {
leftBlocks: ["tr_info"],
rightBlocks: ["tr_tos", "tr_chat", "tr_actions", "tr_mouth"],
};
}
function loadLayoutSettings() {
try {
const savedSettings = uwuStorage.getItem("uwu_layoutSettings");
if (savedSettings) {
const { leftBlocks, rightBlocks } = savedSettings;
leftColumn.innerHTML = "";
rightColumn.innerHTML = "";
leftBlocks.forEach((blockId) => {
const blockElement = createBlockElement(blockId);
leftColumn.appendChild(blockElement);
});
rightBlocks.forEach((blockId) => {
const blockElement = createBlockElement(blockId);
rightColumn.appendChild(blockElement);
});
} else {
const defaultSettings = getDefaultLayoutSettings();
uwuStorage.setItem("uwu_layoutSettings", defaultSettings);
defaultSettings.leftBlocks.forEach((blockId) => {
leftColumn.appendChild(createBlockElement(blockId));
});
defaultSettings.rightBlocks.forEach((blockId) => {
rightColumn.appendChild(createBlockElement(blockId));
});
}
} catch (error) {
console.error("Ошибка при загрузке настроек макета:", error);
}
}
loadLayoutSettings();
// ====================================================================================================================
// . . . РЕДАКТОР ВКЛАДОК И ТАБЛИЦ МИННОГО ПОЛЯ . . .
// ====================================================================================================================
// как же я ненавижу минное поле как же я ненавижу минное поле как же я ненавижу минное поле
const tabManager = {
tabs: [],
currentTabIndex: 0,
createTab(name) {
const newTab = {
name: name,
tables: [],
currentTableId: 0,
};
this.tabs.push(newTab);
this.render();
this.switchTab(this.tabs.length - 1);
},
createTable(
tableName = `Локация ${this.tabs[this.currentTabIndex].tables.length + 1}`
) {
const currentTab = this.tabs[this.currentTabIndex];
currentTab.tables.push({ name: tableName });
this.saveState();
this.render();
},
removeTable(tableIndex) {
const currentTab = this.tabs[this.currentTabIndex];
if (currentTab && currentTab.tables[tableIndex]) {
currentTab.tables.splice(tableIndex, 1);
if (currentTab.currentTableId === tableIndex) {
currentTab.currentTableId = Math.max(
0,
currentTab.currentTableId - 1
);
}
this.saveState();
this.render();
}
},
removeTab(index) {
this.tabs.splice(index, 1);
if (index === this.currentTabIndex) {
this.currentTabIndex = Math.max(0, this.currentTabIndex - 1);
}
this.saveState();
this.render();
},
switchTab(index) {
this.currentTabIndex = index;
this.render();
},
switchTable(tableIndex) {
const currentTab = this.tabs[this.currentTabIndex];
if (currentTab) {
currentTab.currentTableId = tableIndex;
this.saveState();
this.render();
}
},
renameTab(index) {
const newName = prompt(
"Введите новое имя вкладки:",
this.tabs[index].name
);
if (newName) {
this.tabs[index].name = newName;
this.saveState();
this.render();
}
},
renameTable(tableIndex) {
const currentTab = this.tabs[this.currentTabIndex];
if (currentTab) {
const newName = prompt(
"Введите новое имя поля:",
currentTab.tables[tableIndex].name
);
if (newName) {
currentTab.tables[tableIndex].name = newName;
this.saveState();
this.render();
}
}
},
saveState() {
uwuStorage.setItem("uwu_climbingPanelState", this);
},
render() {
this.renderTabs();
this.renderTables();
},
renderTabs() {
const tabRow = document.getElementById("uwu-buttonRow1-settings");
tabRow.innerHTML = "";
this.tabs.forEach((tab, index) => {
const tabButton = document.createElement("button");
tabButton.textContent = tab.name;
tabButton.classList.add("tab-button");
if (index === this.currentTabIndex) {
tabButton.classList.add("active");
}
tabButton.addEventListener("click", () => this.switchTab(index));
const removeButton = document.createElement("button");
removeButton.textContent = "X";
removeButton.classList.add("remove-button");
removeButton.addEventListener("click", () => this.removeTab(index));
const renameButton = document.createElement("button");
renameButton.textContent = "✎";
renameButton.classList.add("rename-button");
renameButton.addEventListener("click", () => this.renameTab(index));
const tabContainer = document.createElement("div");
tabContainer.classList.add("tab-container");
tabContainer.appendChild(tabButton);
tabContainer.appendChild(renameButton);
tabContainer.appendChild(removeButton);
tabRow.appendChild(tabContainer);
});
const addTabButton = document.createElement("button");
addTabButton.textContent = "+";
addTabButton.classList.add("add-button");
addTabButton.addEventListener("click", () => {
const tabName = prompt("Введите имя вкладки:");
if (tabName) {
this.createTab(tabName);
}
});
tabRow.appendChild(addTabButton);
},
renderTables() {
const tableRow = document.getElementById("uwu-buttonRow2-settings");
tableRow.innerHTML = "";
const currentTab = this.tabs[this.currentTabIndex];
if (currentTab) {
currentTab.tables.forEach((table, index) => {
const tableButton = document.createElement("button");
tableButton.textContent = table.name;
tableButton.classList.add("table-button");
tableButton.addEventListener("click", () => this.switchTable(index));
const removeButton = document.createElement("button");
removeButton.textContent = "X";
removeButton.classList.add("remove-button");
removeButton.addEventListener("click", () => this.removeTable(index));
const renameButton = document.createElement("button");
renameButton.textContent = "✎";
renameButton.classList.add("rename-button");
renameButton.addEventListener("click", () => this.renameTable(index));
const tableContainer = document.createElement("div");
tableContainer.classList.add("table-container");
tableContainer.appendChild(tableButton);
tableContainer.appendChild(renameButton);
tableContainer.appendChild(removeButton);
tableRow.appendChild(tableContainer);
});
const addTableButton = document.createElement("button");
addTableButton.textContent = "+";
addTableButton.classList.add("add-button");
addTableButton.addEventListener("click", () => {
const tableName = prompt("Введите имя поля:");
if (tableName) {
this.createTable(tableName);
}
});
tableRow.appendChild(addTableButton);
}
},
};
const savedState = uwuStorage.getItem("uwu_climbingPanelState");
if (!savedState) {
tabManager.createTab("Вкладка 1");
for (let i = 0; i < 5; i++) {
tabManager.createTable(`Поле ${i + 1}`);
}
tabManager.createTab("Вкладка 2");
for (let i = 0; i < 5; i++) {
tabManager.createTable(`Поле ${i + 1}`);
}
tabManager.saveState();
} else {
const state = savedState;
Object.assign(tabManager, state);
}
tabManager.render();
/**
* Добавляет функционал автоматического выделения текста при клике на поля для экспорта.
*/
function setupExportFieldSelection() {
const exportFields = document.querySelectorAll(
"#exportSettings, #param-colors-export-field, #ls-export-field"
);
exportFields.forEach((field) => {
field.addEventListener("click", function () {
this.select();
});
});
}
setupExportFieldSelection();
}
// ====================================================================================================================
// . . . ВКЛАДКИ ГЛАВНЫХ НАСТРОЕК . . .
// ====================================================================================================================
if (targetSettings.test(window.location.href)) {
const buttonContainer = document.getElementById("button-container");
buttonContainer.addEventListener("click", (event) => {
const clickedElement = event.target;
const clickedButton = clickedElement.closest("button");
if (!clickedButton) return;
const panelId = clickedButton.id.replace("button", "panel");
const targetPanel = document.getElementById(panelId);
buttonContainer.querySelectorAll("button").forEach((button) => {
const correspondingPanelId = button.id.replace("button", "panel");
const correspondingPanel = document.getElementById(correspondingPanelId);
correspondingPanel.style.display =
correspondingPanel === targetPanel ? "block" : "none";
button.classList.toggle("active", button === clickedButton);
});
});
const defaultButton = buttonContainer.querySelector("button");
const defaultPanelId = defaultButton.id.replace("button", "panel");
const defaultPanel = document.getElementById(defaultPanelId);
buttonContainer.querySelectorAll("button").forEach((button) => {
const correspondingPanelId = button.id.replace("button", "panel");
const correspondingPanel = document.getElementById(correspondingPanelId);
if (correspondingPanel !== defaultPanel) {
correspondingPanel.style.display = "none";
}
});
defaultPanel.style.display = "block";
defaultButton.classList.add("active");
}
// ====================================================================================================================
// . . . ОНЛАЙН МАГАЗИН СТИЛЕЙ . . .
// ====================================================================================================================
// буду вечно задаваться вопросом, а зачем я это вообще сделал..................
const moduleStates = {};
const defaultModules = [
// "style.css",
// ...
];
const privateModules = {};
function loadModuleStates() {
const storedModuleStates = uwuStorage.getItem("uwu_moduleStates");
if (storedModuleStates) {
Object.assign(moduleStates, storedModuleStates);
} else {
for (const moduleName of defaultModules) {
moduleStates[moduleName] = true;
}
}
const storedPrivateModules = uwuStorage.getItem("uwu_privateModules");
if (storedPrivateModules) {
Object.assign(privateModules, storedPrivateModules);
}
}
async function loadModuleListOnSettings() {
const url =
"https://raw.githubusercontent.com/Ibirtem/CatWar/main/modules/modules.txt";
const targetSettings = /^https:\/\/catwar\.net\/settings/;
if (!targetSettings.test(window.location.href)) {
return;
}
try {
const response = await fetch(url);
const moduleList = await response.text();
const modules = moduleList.split("\n").filter((line) => line.trim() !== "");
const moduleInfoContainer = document.getElementById("module-info");
if (!moduleInfoContainer) {
console.error("Контейнер модулей не найден!");
return;
}
for (const moduleInfo of modules) {
const [moduleName, description, version] = moduleInfo.split("|");
const isOnlineModule = !uwuStorage.getItem(moduleName);
const moduleContainer = createModuleContainer(
moduleName,
description,
version,
isOnlineModule
);
moduleInfoContainer.appendChild(moduleContainer);
if (moduleStates[moduleName]) {
loadModule(moduleName, description, version);
}
}
for (const [moduleName, moduleInfo] of Object.entries(privateModules)) {
const { description, version } = moduleInfo;
const isPrivateModule = true;
const moduleContainer = createModuleContainer(
moduleName,
description,
version,
false,
isPrivateModule
);
moduleInfoContainer.appendChild(moduleContainer);
if (moduleStates[moduleName]) {
loadModule(moduleName, description, version);
}
}
} catch (error) {
console.error("Ошибка при загрузке списка модулей:", error);
}
}
async function activateModules() {
const url =
"https://raw.githubusercontent.com/Ibirtem/CatWar/main/modules/modules.txt";
try {
const response = await fetch(url);
const moduleList = await response.text();
const modules = moduleList.split("\n").filter((line) => line.trim() !== "");
for (const moduleInfo of modules) {
const [moduleName, description, version] = moduleInfo.split("|");
const isOnlineModule = !uwuStorage.getItem(moduleName);
if (moduleStates[moduleName]) {
loadModule(moduleName, description, version);
}
}
for (const [moduleName, moduleInfo] of Object.entries(privateModules)) {
const { description, version } = moduleInfo;
if (moduleStates[moduleName]) {
loadModule(moduleName, description, version);
}
}
} catch (error) {
console.error("Ошибка при активации модулей:", error);
}
}
function createModuleContainer(
moduleName,
description,
version,
isOnlineModule = false,
isPrivateModule = false
) {
const moduleContainer = document.createElement("div");
moduleContainer.classList.add("module-container");
const moduleInfo = document.createElement("div");
moduleInfo.classList.add("module-info");
moduleInfo.textContent = `${description}`;
const modulePanel = document.createElement("div");
modulePanel.classList.add("module-panel");
const versionInfo = document.createElement("span");
versionInfo.textContent = `Версия: ${version}`;
modulePanel.appendChild(versionInfo);
if (isOnlineModule) {
const installButton = document.createElement("button");
installButton.textContent = "Установить";
installButton.classList.add("install-button");
installButton.addEventListener("click", () => {
loadModule(moduleName, description, version);
moduleContainer.remove();
createModuleContainer(
moduleName,
description,
version,
false,
isPrivateModule
);
});
modulePanel.appendChild(installButton);
} else {
const checkboxContainer = document.createElement("div");
checkboxContainer.classList.add("checkbox-container");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = moduleName;
checkbox.checked = moduleStates[moduleName] || false;
checkboxContainer.appendChild(checkbox);
const removeButton = document.createElement("button");
removeButton.textContent = "Удалить";
removeButton.classList.add("remove-button");
removeButton.addEventListener("click", () => {
unloadModule(moduleName);
moduleContainer.remove();
});
modulePanel.appendChild(removeButton);
checkbox.addEventListener("change", () => {
moduleStates[moduleName] = checkbox.checked;
uwuStorage.setItem("uwu_moduleStates", moduleStates);
if (checkbox.checked) {
loadModule(moduleName, description, version);
}
});
moduleInfo.appendChild(checkboxContainer);
}
moduleContainer.appendChild(moduleInfo);
moduleContainer.appendChild(modulePanel);
return moduleContainer;
}
async function loadModule(moduleName, description, version) {
const cachedModule = uwuStorage.getItem(moduleName);
if (cachedModule) {
activateModule(cachedModule, moduleName, description, version);
} else {
const url = `https://raw.githubusercontent.com/Ibirtem/CatWar/main/modules/${moduleName}`;
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.text();
uwuStorage.setItem(moduleName, data);
activateModule(data, moduleName, description, version);
moduleStates[moduleName] = true;
uwuStorage.setItem("uwu_moduleStates", moduleStates);
createModuleContainer(moduleName, description, version, false);
loadModuleStates();
clearModuleInfoContainer();
loadModuleListOnSettings();
} else {
console.error(
`Ошибка при загрузке модуля "${moduleName}": ${response.status} ${response.statusText}`
);
}
} catch (error) {
console.error("Ошибка при загрузке модуля:", error);
}
}
}
function addStyle(css) {
const style = document.createElement("style");
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
function activateModule(data, moduleName, description, version) {
if (moduleName.endsWith(".css")) {
addStyle(data);
} else if (moduleName.endsWith(".js")) {
try {
new Function(data);
eval(data);
} catch (error) {
console.error(`Ошибка при активации модуля "${moduleName}":`, error);
}
}
}
function unloadModule(moduleName) {
uwuStorage.removeItem(moduleName);
delete moduleStates[moduleName];
uwuStorage.setItem("uwu_moduleStates", moduleStates);
if (privateModules[moduleName]) {
delete privateModules[moduleName];
uwuStorage.setItem("uwu_privateModules", privateModules);
}
loadModuleStates();
clearModuleInfoContainer();
loadModuleListOnSettings();
}
function clearModuleInfoContainer() {
const moduleInfoContainer = document.getElementById("module-info");
while (moduleInfoContainer.firstChild) {
moduleInfoContainer.removeChild(moduleInfoContainer.firstChild);
}
}
loadModuleStates();
loadModuleListOnSettings();
window.addEventListener("load", activateModules);
// ====================================================================================================================
// . . . ЗАГРУЗКА НАСТРОЕК . . .
// ====================================================================================================================
loadSettings();
// ====================================================================================================================
// . . . АВАТАРЫ В КОММЕНТАРИЯХ . . .
// ====================================================================================================================
if (!targetCW3.test(window.location.href)) {
if (settings.commentsAvatars) {
const styleElement = document.createElement("style");
styleElement.textContent = `
.avatar-img {
width: 100px;
height: 100px;
object-fit: cover;
float: left;
margin: 5px;
border: black solid 1px;
}
`;
document.head.appendChild(styleElement);
startCheckingForComments();
}
function startCheckingForComments() {
setupMutationObserver("#view_comments", insertAvatars, {
childList: true,
});
}
function insertAvatars() {
const comments = document.querySelectorAll(".view-comment");
comments.forEach((comment) => {
if (!comment.querySelector(".avatar-img")) {
const authorLink = comment.querySelector(".author");
const catId = authorLink
? authorLink.getAttribute("href").match(/\/cat(\d+)/)?.[1]
: null;
const avatarImg = document.createElement("img");
avatarImg.alt = "Аватар пользователя";
avatarImg.classList.add("avatar-img");
if (!catId) {
avatarImg.src = "https://e.catwar.net/avatar/0.jpg";
} else {
loadAvatar(catId, (avatarUrl) => {
avatarImg.src = avatarUrl || "https://e.catwar.net/avatar/0.jpg";
});
}
comment.insertBefore(avatarImg, comment.firstChild);
}
});
}
function loadAvatar(catId, callback) {
const formats = ["png", "jpg", "gif"];
let currentFormat = 0;
function tryNextFormat() {
const url = `https://e.catwar.net/avatar/${catId}.${formats[currentFormat]}`;
const img = new Image();
img.onload = () => callback(url);
img.onerror = () => {
currentFormat++;
if (currentFormat < formats.length) {
tryNextFormat();
} else {
callback(null);
}
};
img.src = url;
}
tryNextFormat();
}
}
// ====================================================================================================================
// . . . МЕНЕДЖЕР ЗВУКОВ . . .
// ====================================================================================================================
function createSoundManager() {
const sounds = {};
let isUserInteracted = false;
let pendingSounds = {};
function loadSound(id, url) {
const audio = new Audio(url);
sounds[id] = audio;
}
function playSound(id, volume) {
return new Promise((resolve, reject) => {
if (sounds[id]) {
sounds[id].currentTime = 0;
sounds[id].volume = volume / 10;
sounds[id]
.play()
.then(resolve)
.catch((error) => {
if (!isUserInteracted) {
console.warn(
"Политика браузера заблокировала звук. Ждём взаимодействия со стороны пользователя для новой попытки."
);
pendingSounds[id] = { id, volume, resolve };
} else {
console.warn("Ошибка воспроизведения звука:", error);
reject(error);
}
});
} else {
reject(new Error(`Звук с ID ${id} не найден.`));
}
});
}
function playSoundNow(id, volume, resolve) {
sounds[id]
.play()
.then(resolve)
.catch((error) => {
console.error(`Не удалось воспроизвести звук с ID ${id}:`, error);
resolve();
});
}
function handleUserInteraction() {
isUserInteracted = true;
document.removeEventListener("mousedown", handleUserInteraction);
document.removeEventListener("touchstart", handleUserInteraction);
document.removeEventListener("keydown", handleUserInteraction);
for (const id in pendingSounds) {
const { volume, resolve } = pendingSounds[id];
playSoundNow(id, volume, resolve);
}
pendingSounds = {};
}
document.addEventListener("mousedown", handleUserInteraction);
document.addEventListener("touchstart", handleUserInteraction);
document.addEventListener("keydown", handleUserInteraction);
return {
loadSound,
playSound,
};
}
const soundManager = createSoundManager();
// ===================== СПИСОК ДОСТУПНЫХ ЗВУКОВ =====================
soundManager.loadSound(
"notificationSound1",
"https://github.com/Ibirtem/CatWar/raw/main/sounds/notification_1.mp3"
);
soundManager.loadSound(
"notificationSound2",
"https://github.com/Ibirtem/CatWar/raw/main/sounds/notification_2.mp3"
);
soundManager.loadSound(
"notificationSound3",
"https://github.com/Ibirtem/CatWar/raw/main/sounds/notification_3.mp3"
);
soundManager.loadSound(
"notificationBlockSound1",
"https://github.com/Ibirtem/CatWar/raw/main/sounds/block_1.mp3"
);
if (targetCW3Kns.test(window.location.href)) {
// ====================================================================================================================
// . . . ПОДГРУЗКА ЦВЕТОВЫХ ТЕМ . . .
// ====================================================================================================================
const currentThemeName = getCurrentThemeName();
const allThemes = getThemes();
const theme = allThemes[currentThemeName]?.colors || {};
// ====================================================================================================================
// . . . ПОЛЬЗОВАТЕЛЬСКИЕ ТЕМЫ / ЦВЕТА . . .
// ====================================================================================================================
function applyTheme() {
const newStyle = document.createElement("style");
newStyle.innerHTML =
/* CSS */
`
body {
background: ${theme?.backgroundColor || ""};
}
#cages_overflow {
background: black;
}
#blocks {
background-color: ${theme?.blocksColor || ""};
}
::-webkit-scrollbar-track {
background-color: ${theme?.blocksColor || ""};
}
::-webkit-scrollbar-thumb {
background-color: ${theme?.accentColor3 || ""};
}
body, input, select, .ui-slider-handle {
color: ${theme?.textColor || ""};
}
input, select, .ui-slider-horizontal {
background-color: ${theme?.accentColor1 || ""};
background: ${theme?.accentColor1 || ""};
border: solid 1px ${theme?.accentColor2 || ""};
}
.ui-widget-content .ui-state-default {
background: ${theme?.accentColor2 || ""};
border: solid 1px ${theme?.accentColor2 || ""};
}
hr {
border: solid 1px ${theme?.accentColor2 || ""};
}
a, a:hover {
color: ${theme?.linkColor || ""};
}
`;
document.head.appendChild(newStyle);
}
if (settings.userThemeKns) {
applyTheme();
}
}
// ====================================================================================================================
// . . . ЗАГРУЗКА КОДА В ИГРОВОЙ . . .
// ====================================================================================================================
// Игровая ли... Я чё знаю?
if (targetCW3.test(window.location.href)) {
const containerElement = document.querySelector("body");
const globalContainerElement = document.createElement("div");
globalContainerElement.id = "uwu-global-container";
containerElement.appendChild(globalContainerElement);
const mainContainerElement = document.createElement("div");
mainContainerElement.id = "uwu-main-container";
globalContainerElement.appendChild(mainContainerElement);
// ====================================================================================================================
// . . . ПОДГРУЗКА ЦВЕТОВЫХ ТЕМ . . .
// ====================================================================================================================
const currentThemeName = getCurrentThemeName();
const allThemes = getThemes();
const theme = allThemes[currentThemeName]?.colors || {};
// ====================================================================================================================
// . . . ПОЛЬЗОВАТЕЛЬСКИЕ ТЕМЫ / ЦВЕТА . . .
// ====================================================================================================================
function applyTheme() {
const newStyle = document.createElement("style");
newStyle.innerHTML =
/* CSS */
`
body {
background: ${theme?.backgroundColor || ""};
}
#cages_overflow {
background: black;
}
#tr_actions > td, #tr_mouth > td, #location, .small {
background-color: ${theme?.blocksColor || ""};
}
#history_block > div {
background-color: unset !important;
}
#main_table, #tr_mouth, #tr_actions, #info_main {
background-color: unset;
background: none;
}
#tr_chat {
background-color: ${theme?.chatColor || ""};
}
body, input, select, .ui-slider-handle, .hotkey {
color: ${theme?.textColor || ""};
}
input, select, .ui-slider-horizontal {
background-color: ${theme?.accentColor1 || ""};
background: ${theme?.accentColor1 || ""};
border: solid 1px ${theme?.accentColor2 || ""};
}
.ui-widget-content .ui-state-default {
background: ${theme?.accentColor2 || ""};
border: solid 1px ${theme?.accentColor2 || ""};
}
hr {
border: solid 1px ${theme?.accentColor2 || ""};
}
.myname {
color: ${theme?.accentColor1 || ""};
background: ${theme?.accentColor3 || ""};
}
span.cat_tooltip {
background: ${theme?.catTooltipBackground || ""} !important;
color: ${theme?.textColor || ""} !important;
border: 2px solid ${theme?.accentColor2 || ""} !important;
}
span.cat_tooltip > span.online {
filter: brightness(2) contrast(150%);
}
.cat:hover .cat_tooltip a, .other_cats_list > a {
color: ${theme?.linkColor || ""};
}
.move_name {
color: ${theme?.moveNameColor || ""};
background-color: ${theme?.moveNameBackground || ""} !important;
}
a, a:hover {
color: ${theme?.linkColor || ""};
}
#fightPanel {
background-color: ${theme?.fightPanelBackground || ""};
}
.hotkey {
background-color: ${theme?.accentColor1 || ""};
}
#newchat, #newls {
color: ${theme?.accentColor3 || ""};
}
.cat-info {
background-color: ${theme?.catTooltipBackground || ""} !important;
color: ${theme?.textColor || ""} !important;
}
`;
document.head.appendChild(newStyle);
}
if (settings.userTheme) {
applyTheme();
}
// ====================================================================================================================
// . . . ДЕФЕКТИКИ И СТИЛИ . . .
// ====================================================================================================================
function applyDefectsStyle() {
if (!settings.showDefectsEnabled) return;
const qualityFolder =
settings.defectsQuality === "high" ? "assets_200_300" : "assets_100_150";
const baseUrl = `https://raw.githubusercontent.com/Ibirtem/CatWar/refs/heads/main/images/${qualityFolder}`;
const defectsCss = /* CSS */ `
/*грязь_1*/
#tr_field [style*='dirt/base/1/1'], #tr_field [style*='dirt/base/2/1']
{content: url(${baseUrl}/dirt_1.png)
!important;}
/*грязь_2*/
#tr_field [style*='dirt/base/1/2'], #tr_field [style*='dirt/base/2/2']
{content: url(${baseUrl}/dirt_2.png)
!important;}
/*грязь_3*/
#tr_field [style*='dirt/base/1/3'], #tr_field [style*='dirt/base/2/3']
{content: url(${baseUrl}/dirt_3.png)
!important;}
/*грязь_4*/
#tr_field [style*='dirt/base/1/4'], #tr_field [style*='dirt/base/2/4']
{content: url(${baseUrl}/dirt_4.png)
!important;}
/*ушибы_1*/
#tr_field [style*='trauma/1']
{content: url(${baseUrl}/trauma_1.png)
!important;}
/*ушибы_2*/
#tr_field [style*='trauma/2']
{content: url(${baseUrl}/trauma_2.png)
!important;}
/*ушибы_3*/
#tr_field [style*='trauma/3']
{content: url(${baseUrl}/trauma_3.png)
!important;}
/*ушибы_4*/
#tr_field [style*='trauma/4']
{content: url(${baseUrl}/trauma_4.png)
!important;}
/*отравление_1*/
#tr_field [style*='poisoning/1']
{content: url(${baseUrl}/poisoning_1.png)
!important;}
/*отравление_2*/
#tr_field [style*='poisoning/2']
{content: url(${baseUrl}/poisoning_2.png)
!important;}
/*отравление_3*/
#tr_field [style*='poisoning/3']
{content: url(${baseUrl}/poisoning_3.png)
!important;}
/*отравление_4*/
#tr_field [style*='poisoning/4']
{content: url(${baseUrl}/poisoning_4.png)
!important;}
/*переломы_1*/
#tr_field [style*='drown/1']
{content: url(${baseUrl}/drown_1.png)
!important;}
/*переломы_2*/
#tr_field [style*='drown/2']
{content: url(${baseUrl}/drown_2.png)
!important;}
/*переломы_3*/
#tr_field [style*='drown/3']
{content: url(${baseUrl}/drown_3.png)
!important;}
/*переломы_4*/
#tr_field [style*='drown/4']
{content: url(${baseUrl}/drown_4.png)
!important;}
/*раны_1*/
#tr_field [style*='wound/1']
{content: url(${baseUrl}/wound_1.png)
!important;}
/*раны_2*/
#tr_field [style*='wound/2']
{content: url(${baseUrl}/wound_2.png)
!important;}
/*раны_3*/
#tr_field [style*='wound/3']
{content: url(${baseUrl}/wound_3.png)
!important;}
/*раны_4*/
#tr_field [style*='wound/4']
{content: url(${baseUrl}/wound_4.png)
!important;}
/*кашель*/
#tr_field [style*='disease/1']
{content: url(${baseUrl}/disease.png)
!important;}
`;
const styleElement = document.createElement("style");
styleElement.id = "uwu-defects-style";
styleElement.textContent = defectsCss;
document.head.appendChild(styleElement);
}
applyDefectsStyle();
// ====================================================================================================================
// . . . РАСШИРЕННЫЕ НАСТРОЙКИ . . .
// ====================================================================================================================
const extendedSettingsButtonElement = document.createElement("div");
extendedSettingsButtonElement.innerHTML = extendedSettingsButton;
mainContainerElement.appendChild(extendedSettingsButtonElement);
const panel = extendedSettingsButtonElement.querySelector(
"#uwu-extended-settings"
);
const extendedSettingsContainer = extendedSettingsButtonElement.querySelector(
"#extended-settings-container"
);
const button = extendedSettingsButtonElement.querySelector(
"#extended-settings-button"
);
extendedSettingsContainer.style.display = "none";
const shouldShowPanel = () => {
return (
settings.extendedSettingsPanel ||
settings.showSplashScreens ||
settings.showUpdateNotification ||
settings.manualWeatherPanel ||
settings.fastStyles
);
};
if (shouldShowPanel()) {
panel.style.display = "block";
} else {
panel.style.display = "none";
}
button.addEventListener("click", () => {
extendedSettingsContainer.style.display =
extendedSettingsContainer.style.display === "none" ? "block" : "none";
button.classList.remove("new-update");
});
// ====================================================================================================================
// . . . СПЛЕШ СКРИН . . .
// ====================================================================================================================
if (settings.showSplashScreens) {
const randomPhraseBlock = document.createElement("div");
const splashPanel = extendedSettingsButtonElement.querySelector(
"#splash-screen-panel"
);
randomPhraseBlock.classList.add("random-phrase-block");
splashPanel.appendChild(randomPhraseBlock);
function loadRandomPhrase(url) {
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`Ошибка загрузки: ${response.status}`);
}
return response.text();
})
.then((text) => {
const phrases = text.split("\n").filter((line) => line.trim() !== "");
const randomIndex = Math.floor(Math.random() * phrases.length);
randomPhraseBlock.innerHTML = parseColorCodes(phrases[randomIndex]);
})
.catch((error) => {
console.error("Ошибка при загрузке случайной фразы:", error);
randomPhraseBlock.textContent = "Не удалось загрузить фразу :(";
});
}
function parseColorCodes(text) {
const colorMap = {
"&0": "</span>", // - Сброс -
"&1": "<span style='color: blue;'>", // Синий
"&2": "<span style='color: green;'>", // Зеленый
"&3": "<span style='color: aqua;'>", // Бирюзовый
"&4": "<span style='color: red;'>", // Красный
"&5": "<span style='color: #dc00dc;'>", // Фиолетовый
"&6": "<span style='color: gold;'>", // Золотой
"&7": "<span style='color: pink;'>", // Розовый
"&8": "<span style='color: white;'>", // Белый
"&9": "<span style='color: black;'>", // Черный
};
text = "<b>" + text;
for (const code in colorMap) {
text = text.replace(new RegExp(code, "g"), colorMap[code]);
}
return text;
}
window.addEventListener("load", () => {
loadRandomPhrase(
"https://raw.githubusercontent.com/Ibirtem/CatWar/main/texts/text.txt"
);
});
}
// ====================================================================================================================
// . . . ПЕРСОНАЛЬНЫЕ КОСТЮМЫ . . .
// ====================================================================================================================
async function personalCostumes() {
if (settings.personalCostumes) {
const match = window.location.hostname.match(/catwar\.(net|su)/);
if (!match) return;
function applyCostumeStyles() {
let items = uwuStorage.getItem("uwu_personal") || {};
if (!items.cats) return;
let styleElement = document.getElementById(
"uwu-personal-costume-style"
);
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "uwu-personal-costume-style";
document.head.appendChild(styleElement);
}
let cssRules = "";
Object.values(items.cats).forEach((cat) => {
if (cat.poses) {
Object.keys(cat.poses).forEach((poseFile) => {
const costumeData = cat.poses[poseFile];
if (costumeData) {
cssRules += `
.cat:has(a[href='/cat${
cat.id
}']) .d:has(.first[style*='${poseFile}'])::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('${costumeData}');
background-size: ${cat.size || "contain"};
background-repeat: no-repeat;
pointer-events: none;
z-index: 1;
}
`;
}
});
}
});
styleElement.innerHTML = cssRules;
}
let lastKnownPoseUrl = "";
function checkMyPose() {
let personalData = uwuStorage.getItem("uwu_personal");
if (!personalData || !personalData.lastActiveId) return;
const currentId = personalData.lastActiveId;
const cages = document.getElementById("cages");
if (!cages) return;
const link = cages.querySelector("a[href='/cat" + currentId + "']");
if (!link) return;
const catNode = link.closest(".cat");
const img = catNode ? catNode.querySelector(".first") : null;
if (img) {
const rawBg = img.style.backgroundImage;
const src = rawBg.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "");
const fileName = src.split("/").pop();
if (src === lastKnownPoseUrl) return;
lastKnownPoseUrl = src;
const size = img.style.backgroundSize;
personalData = uwuStorage.getItem("uwu_personal") || {};
if (!personalData.cats) personalData.cats = {};
if (!personalData.cats[currentId]) {
personalData.cats[currentId] = {
id: currentId,
name: "Кот " + currentId,
poses: {},
};
}
const cat = personalData.cats[currentId];
if (!cat.poses) cat.poses = {};
cat.img = src;
cat.size = size;
if (!cat.poses.hasOwnProperty(fileName)) {
console.log(`UwU | Найдена новая поза: ${fileName}`);
cat.poses[fileName] = "";
uwuStorage.setItem("uwu_personal", personalData);
applyCostumeStyles();
} else {
uwuStorage.setItem("uwu_personal", personalData);
}
}
}
applyCostumeStyles();
setupMutationObserver(
"#cages",
() => {
setTimeout(checkMyPose, 100);
},
{ childList: true, subtree: true },
10,
500
);
setupMutationObserver(
"#tr_actions",
() => {
setTimeout(checkMyPose, 100);
},
{ childList: true, subtree: true, characterData: true }
);
}
}
function saveCostumeToSlot(dataUrl, choice) {
return new Promise(async (resolve, reject) => {
try {
const resizedDataUrl = await resizeImageToAspectRatio(dataUrl);
let data = uwuStorage.getItem("uwu_personal") || {
cats: {},
slots: [],
};
if (!data.slots) data.slots = [];
if (choice === "new") {
data.slots.push(resizedDataUrl);
alert("Костюм успешно сохранен в новый слот.");
} else {
const slotIndex = parseInt(choice, 10);
if (data.slots[slotIndex]) {
if (!confirm("Этот слот уже занят. Вы хотите перезаписать его?")) {
return resolve();
}
}
data.slots[slotIndex] = resizedDataUrl;
alert(`Костюм успешно сохранен в слот ${slotIndex + 1}.`);
}
uwuStorage.setItem("uwu_personal", data);
resolve();
} catch (error) {
console.error("Ошибка при сохранении костюма:", error);
alert("Ошибка при сохранении костюма.");
reject(error);
}
});
}
function createCostumeSavePopup(costumes) {
let { catInfoElement, contentContainer } = createCatInfoContainer();
let data = uwuStorage.getItem("uwu_personal") || {};
const savedSlots =
data.costumes && data.costumes.slots ? data.costumes.slots : [];
catInfoElement.style.width = "600px";
let slotOptions = '<option value="new">В новый слот</option>';
savedSlots.forEach((slot, i) => {
if (slot) {
slotOptions += `<option value="${i}">Перезаписать слот ${
i + 1
}</option>`;
}
});
contentContainer.innerHTML = `
<div class="cat-details">
<p>Выберите костюм для сохранения:</p>
<div class="costume-flex-grid">
${costumes
.map(
(costumeUrl, idx) => `
<div class="costume-flex-item">
<div class="costume-style" style="background-image: url('${costumeUrl}');"></div>
<div class="costume-actions">
<select class="uwu-slot-select" data-costume-idx="${idx}">
${slotOptions}
</select>
<button class="uwu-button install-button" data-costume-idx="${idx}">Сохранить</button>
</div>
</div>
`
)
.join("")}
</div>
</div>
`;
contentContainer
.querySelectorAll("button[data-costume-idx]")
.forEach((btn) => {
btn.addEventListener("click", async (e) => {
const idx = parseInt(btn.getAttribute("data-costume-idx"), 10);
const select = contentContainer.querySelector(
`select[data-costume-idx="${idx}"]`
);
const slotChoice = select.value;
contentContainer.style.pointerEvents = "none";
contentContainer.style.opacity = "0.5";
await saveCostumeToSlot(costumes[idx], slotChoice);
contentContainer.style.pointerEvents = "auto";
contentContainer.style.opacity = "1";
});
});
globalContainer.appendChild(catInfoElement);
}
// ====================================================================================================================
// . . . ИНВЕНТАРЬ . . .
// ====================================================================================================================
if (settings.blockItemDrop) {
function getLockedItems() {
return uwuStorage.getItem("uwu_lockedItems") || "[]";
}
function setLockedItems(lockedItems) {
uwuStorage.setItem("uwu_lockedItems", lockedItems);
}
function checkIfIdIsLocked(itemId) {
return getLockedItems().includes(itemId);
}
function changePutButtonState() {
const putButton = document.getElementById("put");
if (!putButton) return;
const item = document.getElementsByClassName("active_thing")[0];
const lockedItems = getLockedItems();
if (item && lockedItems.includes(item.id)) {
putButton.style.pointerEvents = "none";
putButton.style.opacity = "0.5";
putButton.style.userSelect = "none";
} else {
putButton.style.pointerEvents = "auto";
putButton.style.opacity = "1";
putButton.style.userSelect = "auto";
}
}
function createLockCheckbox() {
const item = document.getElementsByClassName("active_thing")[0];
if (!item || !item.id) return;
let input = document.getElementById("lock-put-button");
let label = document.getElementById("lock-put-label");
if (!input) {
input = document.createElement("input");
input.type = "checkbox";
input.id = "lock-put-button";
input.style.marginRight = "5px";
input.style.marginBottom = "10px";
input.style.cursor = "pointer";
document.getElementById("thdey").appendChild(input);
label = document.createElement("label");
label.id = "lock-put-label";
label.style.marginLeft = "10px";
label.style.fontSize = "14px";
document.getElementById("thdey").appendChild(label);
input.addEventListener("change", () => {
const itemId = document.getElementsByClassName("active_thing")[0].id;
let lockedItems = getLockedItems();
const idx = lockedItems.indexOf(itemId);
if (input.checked && idx === -1) {
lockedItems.push(itemId);
} else if (!input.checked && idx !== -1) {
lockedItems.splice(idx, 1);
}
setLockedItems(lockedItems);
changePutButtonState();
});
}
input.checked = checkIfIdIsLocked(item.id);
label.innerText = `Блокировка опускания предмета с ID ${item.id}`;
}
setupMutationObserver(
"#thdey",
() => {
createLockCheckbox();
changePutButtonState();
},
{ attributes: true, attributeFilter: ["style"] }
);
createLockCheckbox();
}
// ====================================================================================================================
// . . . УВЕДОМЛЕНИЕ ОБ ОБНОВЛЕНИИ . . .
// ====================================================================================================================
function showUpdateNotification(oldVersion) {
const panel = document.getElementById("extended-settings-container");
const notificationBlock = document.createElement("div");
notificationBlock.classList.add("update-notification");
notificationBlock.innerHTML = `
<p>Скрипт/Мод UwU был обновлен с версии v${
oldVersion || "неизвестной"
} до версии v${current_uwu_version}!</p>
<p>Можете посетить <a href="https://catwar.net/settings" target="_blank">Настройки</a> для ознакомления с изменениями.</p>
`;
panel.appendChild(notificationBlock);
const button = extendedSettingsButtonElement.querySelector("button");
button.classList.add("new-update");
}
window.addEventListener("load", () => {
const savedVersion = uwuStorage.getItem("uwu_version");
if (savedVersion !== current_uwu_version) {
uwuStorage.setItem("uwu_version", current_uwu_version);
}
if (
settings.showUpdateNotification &&
savedVersion !== current_uwu_version
) {
showUpdateNotification(savedVersion);
}
personalCostumes();
});
// ====================================================================================================================
// . . . РУЧНОЕ УПРАВЛЕНИЕ ПОГОДОЙ . . .
// ====================================================================================================================
if (settings.manualWeatherPanel) {
const panel = extendedSettingsButtonElement.querySelector(
"#extended-settings-container"
);
panel.innerHTML += manualWeatherPanel;
const manualAuroraOffButton = document.getElementById("manualAurora-Off");
const manualAuroraBButton = document.getElementById("manualAurora-B");
const manualAuroraGButton = document.getElementById("manualAurora-G");
const fireflyOnButton = document.getElementById("manualFirefly-On");
manualAuroraOffButton.addEventListener("click", () => {
for (const auroraElement of auroras) {
removeAurora(auroraElement);
}
});
manualAuroraBButton.addEventListener("click", () => {
createAurora("blue");
});
manualAuroraGButton.addEventListener("click", () => {
createAurora("green");
});
fireflyOnButton.addEventListener("click", () => {
toggleFireflies();
});
}
// ====================================================================================================================
// . . . ТАЙМЕР-НАПОМИНАЛКА . . .
// ====================================================================================================================
if (settings.intervalTimerEnabled) {
const intervalTimerPanelHTML =
/* HTML */
`
<div id="uwu-interval-timer-main-panel">
<div id="uwu-interval-timer-button">
<div class="left-content">
<h2 class="timer-title">Таймер</h2>
<span id="timer-header-countdown" style="display: none;"
>00:00</span
>
</div>
<div class="right-content">
<span id="uwu-interval-timer-toggle">▼</span>
</div>
</div>
<div id="uwu-interval-timer-container">
<div id="uwu-interval-timer-content">
<div class="timer-inputs">
<input
type="number"
id="timer-minutes-input"
min="0"
placeholder="Мин"
/>
<span>:</span>
<input
type="number"
id="timer-seconds-input"
min="0"
max="59"
placeholder="Сек"
/>
</div>
<button
id="timer-start-stop-btn"
class="uwu-button install-button"
>
Старт
</button>
<div id="timer-countdown-display">00:00</div>
</div>
</div>
</div>
`;
const globalContainer = document.getElementById("uwu-global-container");
globalContainer.insertAdjacentHTML("beforeend", intervalTimerPanelHTML);
const timerStyles = document.createElement("style");
timerStyles.innerHTML = /* CSS */ `
#uwu-interval-timer-main-panel {
z-index: 11;
pointer-events: auto;
width: 180px;
position: absolute;
top: 100px;
left: 100px;
background-color: ${theme?.climbingPanelBackground || "#ffffff08"};
border: 1px solid #ffffff1a;
backdrop-filter: blur(20px);
border-radius: 10px;
color: ${theme?.textColor || "#d5d5d5"};
}
#uwu-interval-timer-button {
height: 31px;
cursor: grab;
background-color: #00000026;
border-radius: 10px;
border: 1px solid #ffffff1a;
display: flex;
align-items: center;
justify-content: space-around;
}
#uwu-interval-timer-button .left-content {
pointer-events: none;
width: 85%;
display: flex;
justify-content: center;
align-items: center;
}
#uwu-interval-timer-button .right-content {
pointer-events: none;
width: 15%;
text-align: right;
}
#uwu-interval-timer-button h2 {
display: flex;
margin-top: 2px;
margin-bottom: 2px;
justify-content: center;
pointer-events: none;
}
#timer-start-stop-btn {
width: -webkit-fill-available;
}
#uwu-interval-timer-toggle {
cursor: pointer;
font-size: 18px;
margin-right: 8px;
}
#uwu-interval-timer-container {
display: block;
padding: 5px;
}
#uwu-interval-timer-container.collapsed {
display: none;
}
#uwu-interval-timer-content {
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.timer-inputs {
display: flex;
align-items: center;
gap: 5px;
}
.timer-inputs input {
width: 50px;
text-align: center;
}
#timer-countdown-display,
#timer-header-countdown {
font-size: 1.5em;
font-weight: bold;
}
`;
document.head.appendChild(timerStyles);
const timerPanel = document.getElementById("uwu-interval-timer-main-panel");
const header = document.getElementById("uwu-interval-timer-button");
const container = document.getElementById("uwu-interval-timer-container");
const toggleBtn = document.getElementById("uwu-interval-timer-toggle");
const minutesInput = document.getElementById("timer-minutes-input");
const secondsInput = document.getElementById("timer-seconds-input");
const startStopBtn = document.getElementById("timer-start-stop-btn");
const countdownDisplay = document.getElementById("timer-countdown-display");
const headerCountdown = document.getElementById("timer-header-countdown");
const timerTitle = document.querySelector("h2.timer-title");
let timerId = null;
let totalSeconds = 0;
let remainingSeconds = 0;
let isRunning = false;
let isDragging = false;
let wasDragging = false;
let offsetX, offsetY;
header.addEventListener("mousedown", (e) => {
isDragging = true;
wasDragging = false;
offsetX = e.clientX - timerPanel.offsetLeft;
offsetY = e.clientY - timerPanel.offsetTop;
header.style.cursor = "grabbing";
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
wasDragging = true;
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = timerPanel.offsetWidth;
const panelHeight = timerPanel.offsetHeight;
if (newX < 0) newX = 0;
if (newX + panelWidth > windowWidth) newX = windowWidth - panelWidth;
if (newY < 0) newY = 0;
if (newY + panelHeight > windowHeight)
newY = windowHeight - panelHeight;
timerPanel.style.left = `${newX}px`;
timerPanel.style.top = `${newY}px`;
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
header.style.cursor = "grab";
saveTimerState();
}
});
function togglePanel() {
container.classList.toggle("collapsed");
const isCollapsed = container.classList.contains("collapsed");
toggleBtn.textContent = isCollapsed ? "▶" : "▼";
timerTitle.style.display = isCollapsed && isRunning ? "none" : "inline";
headerCountdown.style.display =
isCollapsed && isRunning ? "inline" : "none";
saveTimerState();
}
header.addEventListener("click", () => {
if (!wasDragging) {
togglePanel();
}
});
function startTimer() {
const minutes = parseInt(minutesInput.value) || 0;
const seconds = parseInt(secondsInput.value) || 0;
totalSeconds = minutes * 60 + seconds;
if (totalSeconds <= 0) {
alert("Пожалуйста, введите корректное время.");
return;
}
isRunning = true;
startStopBtn.textContent = "Стоп";
startStopBtn.classList.remove("install-button");
startStopBtn.classList.add("remove-button");
remainingSeconds = totalSeconds;
updateDisplay();
playSound();
timerId = setInterval(() => {
remainingSeconds--;
if (remainingSeconds < 0) {
remainingSeconds = totalSeconds;
playSound();
}
updateDisplay();
}, 1000);
if (container.classList.contains("collapsed")) {
timerTitle.style.display = "none";
headerCountdown.style.display = "inline";
}
saveTimerState();
}
function stopTimer() {
isRunning = false;
startStopBtn.textContent = "Старт";
startStopBtn.classList.remove("remove-button");
startStopBtn.classList.add("install-button");
clearInterval(timerId);
timerId = null;
remainingSeconds = 0;
updateDisplay();
timerTitle.style.display = "inline";
headerCountdown.style.display = "none";
}
function updateDisplay() {
const mins = Math.floor(remainingSeconds / 60);
const secs = remainingSeconds % 60;
const timeString = `${String(mins).padStart(2, "0")}:${String(
secs
).padStart(2, "0")}`;
countdownDisplay.textContent = timeString;
headerCountdown.textContent = timeString;
}
function playSound() {
soundManager.playSound(
settings.intervalTimerSound,
settings.intervalTimerVolume
);
}
startStopBtn.addEventListener("click", () => {
if (isRunning) {
stopTimer();
} else {
startTimer();
}
});
function saveTimerState() {
const state = {
x: timerPanel.offsetLeft,
y: timerPanel.offsetTop,
collapsed: container.classList.contains("collapsed"),
minutes: minutesInput.value,
seconds: secondsInput.value,
};
uwuStorage.setItem("uwu_intervalTimerState", state);
}
function checkAndResetPosition() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = timerPanel.offsetWidth;
const panelHeight = timerPanel.offsetHeight;
let currentX = timerPanel.offsetLeft;
let currentY = timerPanel.offsetTop;
if (
currentX + panelWidth > windowWidth ||
currentY + panelHeight > windowHeight ||
currentX < 0 ||
currentY < 0
) {
timerPanel.style.left = `100px`;
timerPanel.style.top = `100px`;
saveTimerState();
}
}
function loadTimerState() {
const state = uwuStorage.getItem("uwu_intervalTimerState");
if (state) {
timerPanel.style.left = `${state.x}px`;
timerPanel.style.top = `${state.y}px`;
if (state.collapsed) {
container.classList.add("collapsed");
toggleBtn.textContent = "▶";
}
minutesInput.value = state.minutes || "";
secondsInput.value = state.seconds || "";
}
updateDisplay();
checkAndResetPosition();
}
minutesInput.addEventListener("change", saveTimerState);
secondsInput.addEventListener("change", saveTimerState);
loadTimerState();
}
// ====================================================================================================================
// . . . ЧАСЫ . . .
// ====================================================================================================================
if (settings.showClock) {
const style = document.createElement("style");
style.textContent =
/* CSS */
`
#uwu-clock {
border-radius: 10px;
width: min-content;
height: min-content;
background-color: ${theme?.blocksColor || "#242424"};
color: ${theme?.textColor || "#d5d5d5"};
border: 1px solid #ffffff1a;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
align-items: center;
justify-content: space-between;
font-family: Arial, sans-serif;
text-align: center;
cursor: move;
pointer-events: auto;
position: absolute;
z-index: 10;
padding: 5px;
font-size: ${settings.clockFontSize || 14}px;
}
#uwu-clock .time {
font-size: 2em;
}
#uwu-clock .icon {
cursor: help;
}
.compact #uwu-clock {
column-gap: 5px;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
}
.compact #uwu-clock .time {
grid-column: 1 / 2;
grid-row: 1 / 3;
}
.compact #uwu-clock .icon {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
.compact #uwu-clock .date {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
.standard #uwu-clock {
grid-template-columns: auto auto;
grid-template-rows: auto auto;
}
.standard #uwu-clock .time {
text-align: start;
grid-column: 1 / 2;
grid-row: 1 / 2;
}
.standard #uwu-clock .icon {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
.standard #uwu-clock .date {
font-size: 1.2em;
grid-column: 1 / 3;
grid-row: 2 / 3;
width: max-content;
}
.string #uwu-clock {
column-gap: 5px;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
}
.string #uwu-clock .date {
font-size: 2em;
grid-column: 2 / 3;
}
.string #uwu-clock .icon {
grid-column: 3 / 3;
grid-row: 1 / 2;
}
`;
document.head.appendChild(style);
const tosStyle = document.createElement("style");
tosStyle.textContent = `
#uwu-clock {
position: relative;
}
`;
const flyStyle = document.createElement("style");
flyStyle.textContent = `
`;
const container = document.getElementById("uwu-global-container");
const clockElement = document.createElement("div");
clockElement.id = "uwu-clock";
const timeElement = document.createElement("span");
timeElement.className = "time";
clockElement.appendChild(timeElement);
const iconElement = document.createElement("span");
iconElement.className = "icon";
clockElement.appendChild(iconElement);
const dateElement = document.createElement("span");
dateElement.className = "date";
clockElement.appendChild(dateElement);
timeElement.textContent = "00:00:00";
dateElement.textContent = "00.00.00";
iconElement.textContent = "?";
iconElement.title = "Загрузка...";
if (settings.clockPosition === "fly") {
container.appendChild(clockElement);
document.head.appendChild(flyStyle);
} else if (settings.clockPosition === "tos") {
const trTos = document.getElementById("tr_tos").querySelector("tbody tr");
const newTd = document.createElement("td");
newTd.appendChild(clockElement);
trTos.appendChild(newTd);
document.head.appendChild(tosStyle);
}
let useInternetTime = false;
let isDragging = false;
let offsetX, offsetY;
let internetTime = null;
let timerInterval = null;
let lastSyncTimestamp = 0;
const SYNC_COOLDOWN_MS = 3 * 60 * 1000;
let isFetchingTime = false;
function updateClock(timeSource = new Date()) {
const hours = String(timeSource.getHours()).padStart(2, "0");
const minutes = String(timeSource.getMinutes()).padStart(2, "0");
const seconds = String(timeSource.getSeconds()).padStart(2, "0");
const day = String(timeSource.getDate()).padStart(2, "0");
const month = String(timeSource.getMonth() + 1).padStart(2, "0");
const year = String(timeSource.getFullYear());
timeElement.textContent = `${hours}:${minutes}:${seconds}`;
if (
settings.clockStyle === "compact" ||
settings.clockStyle === "string"
) {
dateElement.textContent = `${day}.${month}.${year.slice(-2)}`;
} else if (settings.clockStyle === "standard") {
const dayOfWeek = ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"][
timeSource.getDay()
];
const monthNames = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
const monthName = monthNames[timeSource.getMonth()];
dateElement.textContent = `${day} (${dayOfWeek}), ${monthName}, ${year}`;
}
if (useInternetTime) {
iconElement.textContent = "🌍︎";
iconElement.title = "Точное онлайн время";
if (settings.clockMoscowTime) {
iconElement.textContent += " MSK";
}
} else {
iconElement.textContent = "⌨";
iconElement.title =
"Не удалось получить точное онлайн время! Используется локальное время устройства";
if (settings.clockMoscowTime) {
iconElement.textContent += " MSK";
iconElement.title =
"Не удалось получить точное онлайн время! Используется локальное время устройства, сконвертированное в Московское.";
}
}
}
async function fetchInternetTime() {
const timeProviders = [
// Увы, вариант со Сбером ультра рабочий, но требует работы @grant GM_xmlhttpRequest из-за CORS политики,
// но тогда пользователь испугается всяких предупреждений. На будущее оставил,
// если всё сломается вообще, но потребует потом дработки в духе новой fetchWithGM функции.
// {
// name: "Sber",
// buildUrl: (isMoscow) => {
// const tz = isMoscow ? "Europe/Moscow" : "Etc/UTC";
// return `https://smartapp-code.sberdevices.ru/tools/api/now?tz=${tz}`;
// },
// parseResponse: async (response) => {
// const data = await response.json();
// return new Date(data.timestamp);
// },
// },
{
name: "timeapi.io",
buildUrl: (isMoscow) => {
const userTimezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
const tz = isMoscow ? "Europe/Moscow" : userTimezone;
return `https://timeapi.io/api/Time/current/zone?timeZone=${tz}`;
},
parseResponse: async (response) => {
const data = await response.json();
return new Date(data.dateTime);
},
},
{
name: "worldtimeapi.org",
buildUrl: (isMoscow) => {
const userTimezone =
Intl.DateTimeFormat().resolvedOptions().timeZone;
const tz = isMoscow ? "Europe/Moscow" : userTimezone;
return `https://worldtimeapi.org/api/timezone/${tz}`;
},
parseResponse: async (response) => {
const data = await response.json();
return new Date(data.datetime);
},
},
];
try {
for (const provider of timeProviders) {
try {
const url = provider.buildUrl(settings.clockMoscowTime);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response not OK: ${response.status}`);
}
internetTime = await provider.parseResponse(response);
useInternetTime = true;
lastSyncTimestamp = Date.now();
// console.log(`Время успешно получено от ${provider.name}.`);
break;
} catch (error) {
// console.warn(
// `Не удалось получить время от ${provider.name}, пробую следующий источник.`,
// error
// );
useInternetTime = false;
}
}
if (useInternetTime) {
updateClockWithInternetTime();
} else {
// console.warn(
// "Не удалось получить время от всех онлайн-источников, используется локальное время."
// );
useInternetTime = false;
updateClockWithLocalTime();
}
startTimer();
} finally {
isFetchingTime = false;
}
}
function updateClockWithInternetTime() {
if (internetTime) {
internetTime.setSeconds(internetTime.getSeconds() + 1);
updateClock(internetTime);
if (settings.clockMoscowTime) {
iconElement.textContent = "🌍︎ MSK";
}
}
}
function updateClockWithLocalTime() {
if (settings.clockMoscowTime) {
const now = new Date();
const utcTime = now.getTime() + now.getTimezoneOffset() * 60000;
const moscowTime = new Date(utcTime + 3600000 * 3); // UTC+3
updateClock(moscowTime);
} else {
updateClock();
}
}
function startTimer() {
if (timerInterval) {
clearInterval(timerInterval);
}
timerInterval = setInterval(() => {
if (useInternetTime) {
updateClockWithInternetTime();
} else {
updateClockWithLocalTime();
}
}, 1000);
}
if (settings.clockPosition === "fly") {
clockElement.addEventListener("mousedown", (e) => {
isDragging = true;
offsetX = e.clientX - clockElement.offsetLeft;
offsetY = e.clientY - clockElement.offsetTop;
document.body.style.userSelect = "none";
});
clockElement.addEventListener("touchstart", (e) => {
e.preventDefault();
isDragging = true;
const touch = e.touches[0];
offsetX = touch.clientX - clockElement.offsetLeft;
offsetY = touch.clientY - clockElement.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
clockElement.style.left = `${e.clientX - offsetX}px`;
clockElement.style.top = `${e.clientY - offsetY}px`;
}
});
document.addEventListener("touchmove", (e) => {
e.preventDefault();
if (isDragging) {
const touch = e.touches[0];
clockElement.style.left = `${touch.clientX - offsetX}px`;
clockElement.style.top = `${touch.clientY - offsetY}px`;
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
document.body.style.userSelect = "auto";
saveClockPosition();
});
document.addEventListener("touchend", () => {
isDragging = false;
document.body.style.userSelect = "auto";
saveClockPosition();
});
}
function saveClockPosition() {
const clockPosition = {
x: clockElement.offsetLeft,
y: clockElement.offsetTop,
};
uwuStorage.setItem("uwu_clock", clockPosition);
}
function loadClockPosition() {
const storedPosition = uwuStorage.getItem("uwu_clock");
if (storedPosition) {
const clockPosition = storedPosition;
clockElement.style.left = `${clockPosition.x}px`;
clockElement.style.top = `${clockPosition.y}px`;
}
}
function handleFocusOrVisibilityChange() {
if (document.hidden || isFetchingTime) {
return;
}
const now = Date.now();
if (now - lastSyncTimestamp > SYNC_COOLDOWN_MS) {
isFetchingTime = true;
fetchInternetTime().finally(() => {
isFetchingTime = false;
});
}
}
document.addEventListener(
"visibilitychange",
handleFocusOrVisibilityChange
);
window.addEventListener("focus", handleFocusOrVisibilityChange);
fetchInternetTime();
if (settings.clockPosition === "fly") {
loadClockPosition();
}
document.body.classList.add(settings.clockStyle);
}
// ====================================================================================================================
// . . . ДЕЙСТВИЯ ПРИ НАВОДКЕ НА .cat . . .
// ====================================================================================================================
document.addEventListener("mouseover", (event) => {
const catElement = event.target.closest(".cat");
if (catElement) {
const catTooltip = catElement.querySelector(".cat_tooltip");
if (
settings.showMoreCatInfo &&
!catTooltip.querySelector(".more-info-link")
) {
const moreInfoLink = document.createElement("a");
moreInfoLink.classList.add("more-info-link");
moreInfoLink.textContent = "Подробнее";
moreInfoLink.addEventListener("click", () => {
showCatInfo(catElement);
});
const moreInfoContainer = document.createElement("div");
moreInfoContainer.classList.add("more-info-container");
moreInfoContainer.appendChild(moreInfoLink);
const onlineSpan = catTooltip.querySelector("span.online");
onlineSpan.parentNode.insertBefore(moreInfoContainer, onlineSpan);
}
if (settings.compactMouth) {
compactInventory(catElement);
}
if (
settings.personalCostumes &&
settings.showCostumesButtons &&
!catTooltip.querySelector(".save-costume-button")
) {
const costumeDivs = catElement.querySelectorAll(
"div[data-v-59afe5e8]:not(.first)"
);
const matchingCostumes = Array.from(costumeDivs).filter((div) =>
div.style.backgroundImage
.slice(5, -2)
.startsWith("/cw3/cats/0/costume/")
);
if (matchingCostumes.length > 0) {
const saveCostume = document.createElement("button");
saveCostume.textContent = "Сохранить костюм";
saveCostume.classList.add("save-costume-button");
saveCostume.addEventListener("click", () => {
const costumeImages = matchingCostumes.map((costume) =>
costume.style.backgroundImage.slice(5, -2)
);
createCostumeSavePopup(costumeImages);
});
catTooltip.appendChild(saveCostume);
}
}
}
});
// ====================================================================================================================
// . . . КОМПАКТНЫЙ РОТ АХХАХХА . . .
// ====================================================================================================================
function compactInventory(cat) {
const originalMouth = cat.querySelector(".cat_tooltip .mouth");
if (originalMouth) {
const existingSortedMouths = cat.querySelectorAll(".mouth.uwu-sorted");
existingSortedMouths.forEach((mouth) => mouth.remove());
const newMouth = document.createElement("ol");
newMouth.classList.add("mouth", "uwu-sorted");
originalMouth.parentNode.insertBefore(
newMouth,
originalMouth.nextSibling
);
originalMouth.style.display = "none";
const inventory = new Map();
const cats = [];
[...originalMouth.querySelectorAll("li img")].forEach((img) => {
const itemSrc = img.getAttribute("src");
inventory.set(itemSrc, (inventory.get(itemSrc) || 0) + 1);
});
[...originalMouth.querySelectorAll("li")].forEach((item) => {
if (!item.querySelector("img")) {
cats.push(item.innerHTML);
}
});
newMouth.innerHTML = "";
for (const [itemSrc, count] of inventory) {
const listItem = document.createElement("li");
const itemImage = document.createElement("img");
itemImage.setAttribute("src", itemSrc);
listItem.appendChild(itemImage);
if (count > 1) {
const countSpan = document.createElement("span");
countSpan.textContent = `x${count}`;
listItem.appendChild(countSpan);
}
newMouth.appendChild(listItem);
}
cats.forEach((catHtml) => {
const listItem = document.createElement("li");
listItem.innerHTML = catHtml;
newMouth.appendChild(listItem);
});
}
}
// ====================================================================================================================
// . . . ИНФОРМАЦИОННЫЙ КОНТЕЙНЕР . . .
// ====================================================================================================================
let globalContainer = document.getElementById("uwu-global-container");
if (!globalContainer) {
globalContainer = document.createElement("div");
globalContainer.id = "uwu-global-container";
globalContainer.style.display = "none";
document.body.appendChild(globalContainer);
}
function createCatInfoContainer() {
const catInfoElement = document.createElement("div");
catInfoElement.classList.add("cat-info");
const contentContainer = document.createElement("div");
contentContainer.classList.add("content-container");
catInfoElement.appendChild(contentContainer);
const closeButton = document.createElement("button");
closeButton.textContent = "Закрыть";
closeButton.classList.add("close-info");
closeButton.addEventListener("click", () => {
globalContainer.removeChild(catInfoElement);
});
catInfoElement.appendChild(closeButton);
const css_catDefects = document.createElement("style");
css_catDefects.innerHTML =
// css
`
.cat-info {
pointer-events: auto;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px #00000033;
z-index: 5;
width: 300px;
text-align: center;
display: block;
background-color: white;
color: black;
}
.other-cat-info-container {
display: grid;
grid-template-columns: 1fr 2fr;
}
.close-info-container {
text-align: right;
}
.close-info {
cursor: pointer;
}
.more-info-container {
cursor: pointer;
}
.parameter-details-container {
text-align: left;
}
.cat-details {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 8px;
}
.cat-details > p,
.cat-details > div > p {
margin-top: 5px;
margin-bottom: 5px;
}
#uwu-global-container > div.cat-info > div > div > div.cat-details > div {
margin-top: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
`;
document.head.appendChild(css_catDefects);
return { catInfoElement, contentContainer };
}
// ====================================================================================================================
// . . . БОЛЬШЕ ИНФОРМАЦИИ В "О КОТЕ" . . .
// ====================================================================================================================
const defectsInfo = {
wound: {
name: "Раны",
states: {
1: "царапины",
2: "лёгкие раны",
3: "глубокие раны",
4: "смертельные раны",
},
},
poisoning: {
name: "Отравление",
states: {
1: "недомогание",
2: "лёгкое отравление",
3: "сильное отравление",
4: "смертельное отравление",
},
},
drown: {
name: "Травмы от утопления",
states: {
1: "ссадины",
2: "небольшие кровоподтёки",
3: "сильные травмы",
4: "смертельные травмы",
},
},
disease: {
name: "Болезнь",
states: {
1: "кашель",
2: "кашель",
3: "кашель",
4: "кашель",
},
},
trauma: {
name: "Переломы",
states: {
1: "синяки",
2: "лёгкие ушибы",
3: "сильные ушибы",
4: "смертельные ушибы",
},
},
dirt: {
name: "Грязь",
states: {
1: "грязные лапы",
2: "грязевые пятна",
3: "клещи",
4: "блохи",
},
},
};
function showCatInfo(cat) {
const catName = cat.querySelector(".cat_tooltip a").textContent;
const catSize = cat.querySelector(".d .first").style.backgroundSize;
const catImage = cat
.querySelector(".d .first")
.style.backgroundImage.slice(5, -2);
const defectElements = Array.from(
cat.querySelectorAll(".d > div:not(.first)")
);
const uniqueDefects = new Set();
const catDefects = defectElements
.map((element) => {
const defectUrl = element.style.backgroundImage;
if (defectUrl.includes("/defects/")) {
const defectParts = defectUrl.split("/");
const lastPart = defectParts.pop();
const defectLevel = parseInt(lastPart.split("/")[0]);
const defectType = defectParts[5];
const defectKey = `${defectType}-${defectLevel}`;
if (!uniqueDefects.has(defectKey)) {
uniqueDefects.add(defectKey);
return { type: defectType, level: defectLevel };
}
}
return null;
})
.filter(Boolean);
const catId = cat
.querySelector(".cat_tooltip a")
.getAttribute("href")
.slice(4);
let { catInfoElement, contentContainer } = createCatInfoContainer();
contentContainer.innerHTML =
// html
`
<h2>${catName}</h2>
<div class="other-cat-info-container">
<div>
<img src="${catImage}" class="cat-image">
</div>
<div class="cat-details">
<p><strong>ID</strong>: ${catId}</p>
<p><strong>Размер</strong>: ${catSize}</p>
</div>
</div>
`;
const defectsContainer = document.createElement("div");
if (catDefects.length > 0) {
defectsContainer.innerHTML = "<p>Дефекты:</p>";
catDefects.forEach((defect) => {
const defectData = defectsInfo[defect.type];
if (defectData) {
const defectState = defectData.states[defect.level] || "";
const defectLine = document.createElement("p");
const defectNameSpan = document.createElement("strong");
defectNameSpan.textContent = defectData.name;
defectLine.appendChild(defectNameSpan);
defectLine.insertAdjacentHTML(
"beforeend",
` (${defect.level} стадия, ${defectState})`
);
defectsContainer.appendChild(defectLine);
}
});
contentContainer
.querySelector(".cat-details")
.appendChild(defectsContainer);
} else {
contentContainer.querySelector(".cat-details").innerHTML +=
"<p><strong>Здоровый</strong></p>";
}
globalContainer.appendChild(catInfoElement);
}
// ====================================================================================================================
// . . . СОБСТВЕННЫЙ ФОН ЛОКАЦИИ ИГРОВОЙ . . .
// ====================================================================================================================
if (settings.gameFieldBackgroundUser) {
const css_gameField = document.createElement("style");
css_gameField.textContent = `
#cages_div {
background-image: url(${settings.gameFieldBackgroundUserImageURL}) !important;
}
`;
document.head.appendChild(css_gameField);
}
// ====================================================================================================================
// . . . ГРАНИЦЫ ЯЧЕЕК . . . cellsNumbers
// ====================================================================================================================
function updateCellsBordersStyle(checked) {
let styleElement = document.getElementById("cellsBordersStyle");
const cellsBordersStyle = `
.cage {
box-shadow: inset 0 0 0 0.${settingsMap.uwu_settings.cellsBordersThickness}px ${settings.cellsBordersColor};
}
`;
if (checked) {
if (!styleElement) {
styleElement = document.createElement("style");
styleElement.id = "cellsBordersStyle";
styleElement.innerHTML = cellsBordersStyle;
document.head.appendChild(styleElement);
}
} else {
if (styleElement) {
document.head.removeChild(styleElement);
}
}
}
// ====================================================================================================================
// . . . НУМЕРАЦИЯ ЯЧЕЕК . . .
// ====================================================================================================================
if (settings.cellsNumbers) {
function createCellNumbers(style) {
let css =
/* CSS */
`
#cages_div { position: relative; }
#cages > tbody > tr > td { position: relative; }
#cages > tbody > tr > td::after {
content: attr(data-cell-num);
position: absolute;
z-index: 2;
top: 5px;
right: 5px;
color: ${style.color || "#000"};
opacity: ${style.opacity || 0.4};
font-size: 16px;
font-weight: bold;
}
`;
let cagesNums = document.createElement("style");
cagesNums.id = "cages_nums";
cagesNums.innerHTML = css;
document.head.appendChild(cagesNums);
let table = document.getElementById("cages");
if (!table) return;
let rows = table.querySelectorAll("tbody > tr");
for (let i = 0; i < rows.length; i++) {
let cells = rows[i].querySelectorAll("td");
for (let j = 0; j < cells.length; j++) {
cells[j].setAttribute("data-cell-num", (j + 1).toString());
}
}
}
createCellNumbers({
color: "white",
opacity: 0.8,
});
}
// ====================================================================================================================
// . . . ПРОЦЕНТЫ ПАРАМЕТРОВ . . .
// ====================================================================================================================
// if (settings.displayParametersPercentages) {
// const parameterTableIds = [
// "dream_table",
// "hunger_table",
// "thirst_table",
// "need_table",
// "health_table",
// "clean_table",
// ];
// function updateParameterPercentages(tableId) {
// const table = document.getElementById(tableId);
// if (table) {
// const row = table.querySelector("tbody tr");
// if (!row) {
// console.warn(`Строка не найдена в таблице с ID "${tableId}".`);
// return;
// }
// const greenBar = row.querySelector(
// "td[style*='background-color: green;']"
// );
// const redBar = row.querySelector("td[style*='background-color: red;']");
// if (!greenBar || !redBar) {
// console.warn(`Бары не найдены в строке таблицы с ID "${tableId}".`);
// return;
// }
// const greenBarWidth = parseInt(greenBar.style.width, 10);
// const redBarWidth = parseInt(redBar.style.width, 10);
// const totalWidth = greenBarWidth + redBarWidth;
// let percentage = (greenBarWidth / totalWidth) * 100;
// percentage =
// percentage % 1 !== 0 ? percentage.toFixed(2) : Math.round(percentage);
// let percentageCell = row.querySelector(".percentage-cell");
// if (!percentageCell) {
// percentageCell = document.createElement("td");
// percentageCell.classList.add("percentage-cell");
// row.appendChild(percentageCell);
// }
// percentageCell.textContent = `${percentage}%`;
// } else {
// console.warn(`Таблица с ID "${tableId}" не найдена.`);
// }
// }
// async function setupTableObservers() {
// for (const tableId of parameterTableIds) {
// const tableSelector = `#${tableId}`;
// const rowSelector = `${tableSelector} tbody tr`;
// const greenBarSelector = `${rowSelector} td[style*='background-color: green;']`;
// const redBarSelector = `${rowSelector} td[style*='background-color: red;']`;
// await setupMutationObserver(tableSelector, () =>
// updateParameterPercentages(tableId)
// );
// await setupMutationObserver(greenBarSelector, () =>
// updateParameterPercentages(tableId)
// );
// await setupMutationObserver(redBarSelector, () =>
// updateParameterPercentages(tableId)
// );
// }
// }
// window.addEventListener("load", setupTableObservers);
// }
// ====================================================================================================================
// . . . ПОДРОБНЕЕ О ПАРАМЕТРАХ (И НАВЫКОВ?) . . .
// ====================================================================================================================
function createMoreInfoButton() {
const parametersBlock = document.getElementById("parameters_skills_block");
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("button-container");
buttonContainer.style.paddingBottom = "5px";
const moreInfoLink = document.createElement("a");
moreInfoLink.href = "#";
moreInfoLink.textContent = "Подробнее";
moreInfoLink.classList.add("more-info-link");
moreInfoLink.addEventListener("click", (event) => {
event.preventDefault();
showParameterDetails();
});
buttonContainer.appendChild(moreInfoLink);
parametersBlock.insertBefore(buttonContainer, parametersBlock.firstChild);
}
// const parameters = [
// {
// id: "dream_table",
// name: "Сонливость",
// timePerPixel: 20,
// formula: null,
// },
// {
// id: "hunger_table",
// name: "Голод",
// timePerPixel: null,
// formula: (red) => Math.ceil((red / 150) * 9) * 15,
// },
// { id: "thirst_table", name: "Жажда", timePerPixel: 60, formula: null },
// { id: "need_table", name: "Нужда", timePerPixel: 30, formula: null },
// {
// id: "health_table",
// name: "Здоровье",
// timePerPixel: null,
// formula: null,
// },
// {
// id: "clean_table",
// name: "Чистота",
// timePerPixel: null,
// formula: (red) => {
// red = red % 3 ? red : red - 0.5;
// return ((red - 1) / 1.5) * 100 + 100;
// },
// },
// ];
const maxWidthPx = 150;
function showParameterDetails() {
const parameters = [
{
id: "dream",
name: "Бодрость",
formula: (redPixels) => {
if (redPixels <= 0) return 0;
const percentageLoss = Math.round(redPixels / 1.5);
let totalTime = 0;
for (let i = 1; i <= percentageLoss; i++) {
if (i <= 2) {
// Первые два процента добавляют по 20 секунд
totalTime += 20;
} else if (i % 2 !== 0) {
// Нечётные проценты (3-й, 5-й и т.д.) добавляют 40 секунд
totalTime += 40;
} else {
// Чётные проценты (4-й, 6-й и т.д.) добавляют 20 секунд
totalTime += 20;
}
}
return totalTime;
},
},
{
id: "hunger",
name: "Голод",
formula: (percentage) => Math.ceil(((100 - percentage) / 150) * 9) * 15,
},
{ id: "thirst", name: "Жажда", timePerPixel: 60, formula: null },
{ id: "need", name: "Нужда", timePerPixel: 30, formula: null },
{
id: "health",
name: "Здоровье",
formula: null,
},
{
id: "clean",
name: "Чистота",
timePerPixel: null,
formula: (redPixels) => {
if (redPixels <= 0) return 0;
return (200 / 3) * redPixels;
},
},
];
let { catInfoElement, contentContainer } = createCatInfoContainer();
contentContainer.classList.add("parameter-details-container");
parameters.forEach(({ id, name, formula, timePerPixel }) => {
const parameterElement = document.getElementById(id);
if (parameterElement) {
const barFill = parameterElement.querySelector(".bar-fill");
const barData = parameterElement.querySelector(".bar-data");
if (!barFill || !barData) {
console.warn(`Элементы бара не найдены для параметра с ID "${id}".`);
return;
}
const percentageString = barData.textContent.match(/\d+%$/);
if (!percentageString) {
console.warn(
`Не удалось извлечь процент из текста для параметра с ID "${id}".`
);
return;
}
const percentage = parseInt(percentageString[0], 10);
const effectivePercentage = ["hunger", "thirst", "need"].includes(id)
? 100 - percentage
: percentage;
const reversePercentage = 100 - effectivePercentage;
const pixelWidth = (effectivePercentage / 100) * maxWidthPx;
const reversePixelWidth = maxWidthPx - pixelWidth;
let timeInfo = "";
let totalTimeSeconds;
if (formula) {
totalTimeSeconds = formula(reversePixelWidth);
} else if (timePerPixel) {
totalTimeSeconds = reversePixelWidth * timePerPixel;
}
if (totalTimeSeconds !== undefined) {
const hours = Math.floor(totalTimeSeconds / 3600);
const minutes = Math.floor((totalTimeSeconds % 3600) / 60);
const seconds = Math.ceil(totalTimeSeconds % 60);
if (hours > 0) {
timeInfo = ` (> ${hours} ч ${minutes} мин)`;
} else if (minutes > 0) {
timeInfo = ` (${minutes} мин ${seconds} сек)`;
} else {
timeInfo = ` (${seconds} сек)`;
}
}
const detailLine = document.createElement("p");
detailLine.innerHTML = `<strong>${name}:</strong> <span style="color: #00cc00;">${effectivePercentage}%</span> / <span style="color: red;">${reversePercentage}%</span>`;
detailLine.style.marginBottom = "0";
contentContainer.appendChild(detailLine);
if (timeInfo) {
const detailLineTime = document.createElement("p");
detailLineTime.innerHTML = `≈${timeInfo}`;
detailLineTime.style.marginTop = "0";
contentContainer.appendChild(detailLineTime);
}
} else {
console.warn(`Параметр с ID "${id}" не найден.`);
}
});
globalContainer.appendChild(catInfoElement);
}
if (settings.showParametersDetails) {
setupSingleCallback("#parameters_skills_block", createMoreInfoButton);
}
// ====================================================================================================================
// . . . ЧИСЛОВАЯ ГРОМКОСТЬ УВЕДОМЛЕНИЙ . . .
// ====================================================================================================================
if (settings.climbingNotificationsNumbers) {
function addClimbingNotificationsStyles() {
const styles = Array.from(
{ length: 11 },
(_, i) => `
.vlm${i} > .nick[style*="italic"]:after {
content: " [${i}]";
}
`
).join("");
const styleElement = document.createElement("style");
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
addClimbingNotificationsStyles();
}
// ====================================================================================================================
// . . . ЗВУКОВОЕ УВЕДОМЛЕНИЕ ПРИ ОБНОВЛЕНИИ КЛЕТОК . . .
// ====================================================================================================================
// TODO - debounceTimer, если не сработает решение со сравнением историй. P.S. Вроде работает.
if (settings.climbingRefreshNotification) {
function handleClimbingRefresh() {
const refreshRegex = /Услышала? оглушительн/;
let previousHistory = "";
const updateHistory = () => {
const istElement = document.getElementById("ist");
const currentHistory = istElement.innerHTML;
if (currentHistory !== previousHistory) {
previousHistory = currentHistory;
const entries = currentHistory.split(".");
const lastEntry = entries[entries.length - 2];
if (lastEntry !== undefined && refreshRegex.test(lastEntry)) {
const lastPlayedEntry = entries[entries.length - 3];
if (!lastPlayedEntry || !refreshRegex.test(lastPlayedEntry)) {
soundManager.playSound(
settings.climbingRefreshNotificationSound,
settings.climbingRefreshNotificationVolume
);
}
}
}
};
const historyBlock = document.getElementById("history_block");
const observer = new MutationObserver(() => {
updateHistory();
});
const config = {
childList: true,
subtree: true,
characterData: true,
};
observer.observe(historyBlock, config);
}
handleClimbingRefresh();
}
// ====================================================================================================================
// . . . МИННОЕ ПОЛЕ . . .
// ====================================================================================================================
// Вторая по ненависти работа с кодами. Но уже к самому себе а не к сайту.........
// чат уже ничего не перебьёт....... наверно????????????
// TODO - Переписать всё это мессиво к чертям, это кошмар какой-то. Как оно вообще ещё работает?????? Что я употреблял?????????????????????
if (settings.climbingPanel) {
let isDragging = false;
let initialX;
let initialY;
let currentX;
let currentY;
let wasDragging = false;
function saveClimbingPanelStatus() {
const status = {
x: currentX,
y: currentY,
isOpen: climbingPanelContainer.classList.contains("open"),
isChecked: transferCheckbox.checked,
currentTabIndex: tabManager.currentTabIndex,
currentTableId: tabManager.currentTableId,
};
uwuStorage.setItem("uwu_climbingPanelStatus", status);
}
function loadClimbingPanelStatus() {
const savedStatus = uwuStorage.getItem("uwu_climbingPanelStatus");
const arrow = document.getElementById("uwu-arrow");
if (savedStatus) {
const status = savedStatus;
currentX = status.x;
currentY = status.y;
climbingPanelContainer.classList.toggle("open", status.isOpen);
if (status.isOpen) {
arrow.textContent = "▼";
} else {
arrow.textContent = "▶";
}
transferCheckbox.checked = status.isChecked;
tabManager.currentTabIndex = status.currentTabIndex;
if (
status.currentTableId !== null &&
tabManager.tabs[status.currentTabIndex].tables[status.currentTableId]
) {
tabManager.currentTableId = status.currentTableId;
}
tabManager.render();
if (status.isChecked) {
transferColors();
}
} else {
tabManager.render();
arrow.textContent = "▶";
}
checkAndResetPanelPosition();
}
function updateCell(cell, value) {
cell.dataset.value = value || "";
cell.textContent = value === "mine" || value === "transit" ? "" : value;
switch (value) {
case "mine":
cell.style.backgroundColor = "#5b000073";
break;
case "transit":
cell.style.backgroundColor = "#ffffff87";
break;
default:
cell.style.backgroundColor = "";
}
}
function transferColors() {
const transferCheckbox = document.getElementById("uwu-transferCheckbox");
let styleTag = document.getElementById(
"uwu-climbing-panel-dynamic-styles"
);
if (!styleTag) {
styleTag = document.createElement("style");
styleTag.id = "uwu-climbing-panel-dynamic-styles";
document.head.appendChild(styleTag);
}
if (!transferCheckbox.checked) {
styleTag.innerHTML = "";
return;
}
const climbingPanelCells = Array.from(
document.querySelectorAll("#uwu-climbingPanel td")
);
let newCssRules = "";
const numCols = 10;
climbingPanelCells.forEach((cell, i) => {
const color = getComputedStyle(cell).backgroundColor;
if (color && color !== "rgba(0, 0, 0, 0)" && color !== "transparent") {
const rowIndex = Math.floor(i / numCols) + 1;
const colIndex = (i % numCols) + 1;
newCssRules += `#cages > tbody > tr:nth-of-type(${rowIndex}) > td:nth-of-type(${colIndex})::before { background-color: ${color}; }\n`;
}
});
styleTag.innerHTML = newCssRules;
}
function clearColors() {
const styleTag = document.getElementById(
"uwu-climbing-panel-dynamic-styles"
);
if (styleTag) {
styleTag.innerHTML = "";
}
}
let lastClickedCell = null;
function handleCellClick(event) {
const cell = event.target.closest("td");
if (!cell || !cell.closest("#uwu-climbingPanel")) return;
if (settings.climbingPanelInputsStyle === "standart") {
updateCell(cell, activeInputValue);
saveTableData(tabManager.currentTableId);
transferColors();
} else {
if (lastClickedCell === cell) {
updateCell(cell, "");
saveTableData(tabManager.currentTableId);
transferColors();
lastClickedCell = null;
} else {
lastClickedCell = cell;
}
}
}
function handleKeyDown(event) {
const keyPressed = event.key;
const activeElement = document.activeElement;
if (
activeElement &&
activeElement.tagName === "TD" &&
activeElement.closest("#uwu-climbingPanel")
) {
switch (keyPressed) {
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
updateCell(activeElement, keyPressed);
break;
case "-":
updateCell(activeElement, "mine");
break;
case "=":
updateCell(activeElement, "transit");
break;
default:
return;
}
saveTableData(tabManager.currentTableId);
transferColors();
}
}
function handleTransferCheckboxChange(event) {
event.target.checked ? transferColors() : clearColors();
saveClimbingPanelStatus();
}
let activeInputValue = "0";
const uwuClimbingPanelContainer =
/* HTML */
`
<div id="uwu-climbingMainPanel">
<div id="uwu-climbingPanelButton">
<div class="left-content">
<h2>Минное поле</h2>
</div>
<div class="right-content">
<span id="uwu-arrow">▶</span>
</div>
</div>
<div id="uwu-climbingPanelContainer">
<div id="uwu-climbingPanelContent">
<div id="uwu-buttonContainer">
<div id="uwu-inputButtons" style="display: none;">
<button value="0">0</button>
<button value="1">1</button>
<button value="2">2</button>
<button value="3">3</button>
<button value="4">4</button>
<button value="5">5</button>
<button value="6">6</button>
<button value="7">7</button>
<button value="transit">Переход</button>
<button value="mine">Мина</button>
<button value="">Очистить</button>
</div>
<h3>Вкладка</h3>
<div id="uwu-buttonRow1"></div>
<hr id="uwu-hr" />
<h3>Локация</h3>
<div id="uwu-buttonRow2"></div>
</div>
<div id="uwu-functionButtonsContainer">
<input type="checkbox" id="uwu-transferCheckbox" />
<label for="uwu-transferCheckbox"
>Перенос на Игровое поле</label
>
</div>
<div id="uwu-tableContainer"></div>
</div>
</div>
</div>
`;
function createClimbingPanel() {
const globalContainer = document.getElementById("uwu-global-container");
globalContainer.insertAdjacentHTML(
"beforeend",
uwuClimbingPanelContainer
);
const transferCheckbox = document.getElementById("uwu-transferCheckbox");
if (settings.climbingPanelInputsStyle === "standart") {
setupInputButtons();
}
document.addEventListener("keydown", handleKeyDown);
transferCheckbox.addEventListener("change", handleTransferCheckboxChange);
}
function setupInputButtons() {
const inputButtonsContainer = document.getElementById("uwu-inputButtons");
inputButtonsContainer.style.display = "flex";
inputButtonsContainer.style.flexWrap = "wrap";
const inputButtons = inputButtonsContainer.querySelectorAll("button");
inputButtons.forEach((button) => {
button.addEventListener("click", () => {
activeInputValue = button.value;
updateInputButtonsStyle();
});
});
updateInputButtonsStyle();
}
function updateInputButtonsStyle() {
const inputButtons = document.querySelectorAll(
"#uwu-inputButtons button"
);
inputButtons.forEach((button) => {
button.classList.toggle("active", button.value === activeInputValue);
});
}
function saveTableData(tableIndex) {
const climbingPanel = document.getElementById("uwu-climbingPanel");
if (!climbingPanel) return;
const tableData = getTableData(climbingPanel.id);
const currentTab = tabManager.tabs[tabManager.currentTabIndex];
currentTab.tables[tableIndex] = {
name: currentTab.tables[tableIndex].name,
data: tableData,
};
tabManager.saveState();
}
function clearTable() {
const climbingPanel = document.getElementById("uwu-climbingPanel");
if (!climbingPanel) return;
const cells = Array.from(climbingPanel.querySelectorAll("td"));
cells.forEach((cell) => {
if (cell.dataset.value !== "transit") {
updateCell(cell, "");
}
});
const currentTab = tabManager.tabs[tabManager.currentTabIndex];
currentTab.tables[tabManager.currentTableId] = {
name: currentTab.tables[tabManager.currentTableId].name,
data: getTableData(climbingPanel.id),
};
tabManager.saveState();
transferColors();
}
const tabManager = {
tabs: [],
currentTabIndex: 0,
currentTableId: 0,
createTab(name) {
const newTab = {
name: name,
tables: [],
};
this.tabs.push(newTab);
this.render();
this.switchTab(this.tabs.length - 1);
},
switchTab(index) {
this.currentTabIndex = index;
const currentTab = this.tabs[this.currentTabIndex];
this.currentTableId =
currentTab && currentTab.tables.length > 0 ? 0 : null;
if (currentTab && currentTab.tables.length === 0) {
this.renderNoTableMessage();
} else {
this.render();
}
transferColors();
saveClimbingPanelStatus();
},
switchTable(tableIndex) {
this.currentTableId = tableIndex;
this.render();
transferColors();
saveClimbingPanelStatus();
},
saveState() {
uwuStorage.setItem("uwu_climbingPanelState", this);
},
render() {
this.renderTabs();
this.renderTables();
if (this.currentTableId !== null) {
this.renderTable(this.currentTableId);
}
},
renderTabs() {
const tabRow = document.getElementById("uwu-buttonRow1");
tabRow.innerHTML = "";
this.tabs.forEach((tab, index) => {
const tabButton = document.createElement("button");
tabButton.textContent = tab.name;
tabButton.classList.add("tab-button");
if (index === this.currentTabIndex) {
tabButton.classList.add("active");
}
tabButton.addEventListener("click", () => this.switchTab(index));
const tabContainer = document.createElement("div");
tabContainer.classList.add("tab-container");
tabContainer.appendChild(tabButton);
tabRow.appendChild(tabContainer);
});
},
renderTables() {
const tableRow = document.getElementById("uwu-buttonRow2");
tableRow.innerHTML = "";
const currentTab = this.tabs[this.currentTabIndex];
if (currentTab) {
currentTab.tables.forEach((table, index) => {
const tableButton = document.createElement("button");
tableButton.textContent = table.name || `Локация ${index + 1}`;
tableButton.classList.add("table-button");
if (index === this.currentTableId) {
tableButton.classList.add("active");
}
tableButton.addEventListener("click", () =>
this.switchTable(index)
);
const tableContainer = document.createElement("div");
tableContainer.classList.add("table-container");
tableContainer.appendChild(tableButton);
tableRow.appendChild(tableContainer);
});
}
},
renderTable(tableIndex) {
const tableContainer = document.getElementById("uwu-tableContainer");
tableContainer.innerHTML = "";
const currentTab = this.tabs[this.currentTabIndex];
const climbingPanel = document.createElement("table");
climbingPanel.id = "uwu-climbingPanel";
for (let i = 0; i < 6; i++) {
const row = document.createElement("tr");
for (let j = 0; j < 10; j++) {
const cell = document.createElement("td");
cell.setAttribute("tabindex", "0");
cell.addEventListener("click", handleCellClick);
row.appendChild(cell);
}
climbingPanel.appendChild(row);
}
const tableData = currentTab.tables[tableIndex]?.data;
if (tableData) {
tableData.forEach((rowData, i) => {
rowData.forEach((cellData, j) => {
updateCell(climbingPanel.rows[i].cells[j], cellData.value);
});
});
}
tableContainer.appendChild(climbingPanel);
const clearButton = document.createElement("button");
clearButton.textContent = "Очистить всё поле/таблицу";
clearButton.id = "button-clear-table";
clearButton.addEventListener("click", clearTable);
tableContainer.appendChild(clearButton);
},
renderNoTableMessage() {
const tableContainer = document.getElementById("uwu-tableContainer");
tableContainer.innerHTML = "";
const message = document.createElement("div");
message.textContent = "Добавьте поле/таблицу в настройках";
message.style.textAlign = "center";
message.style.marginTop = "20px";
tableContainer.appendChild(message);
this.renderTabs();
this.renderTables();
},
};
function loadSavedState() {
const savedState = uwuStorage.getItem("uwu_climbingPanelState");
if (savedState) {
const state = savedState;
Object.assign(tabManager, state);
tabManager.currentTabIndex = 0;
const currentTab = tabManager.tabs[tabManager.currentTabIndex];
if (currentTab && currentTab.tables.length > 0) {
if (tabManager.currentTableId >= currentTab.tables.length) {
tabManager.currentTableId = 0;
}
} else {
tabManager.currentTableId = null;
}
}
}
loadSavedState();
createClimbingPanel();
tabManager.render();
function getTableData(tableId) {
const table = document.getElementById(tableId);
if (!table) {
console.error(`Таблица с id ${tableId} не найдена`);
return [];
}
const tableData = [];
for (let i = 0; i < table.rows.length; i++) {
const rowData = [];
for (let j = 0; j < table.rows[i].cells.length; j++) {
const cell = table.rows[i].cells[j];
rowData.push({
value: cell.dataset.value || "",
});
}
tableData.push(rowData);
}
return tableData;
}
// ===================== ПЕРЕТАСКИВАНИЕ =====================
const climbingMainPanel = document.getElementById("uwu-climbingMainPanel");
const climbingPanelButton = document.getElementById(
"uwu-climbingPanelButton"
);
const climbingPanelContainer = document.getElementById(
"uwu-climbingPanelContainer"
);
const transferCheckbox = document.getElementById("uwu-transferCheckbox");
let touchStartTime;
let touchStartX;
let touchStartY;
const CLICK_THRESHOLD = 200;
const MOVE_THRESHOLD = 10;
function handleTouchStart(e) {
touchStartTime = Date.now();
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
dragStart(e);
}
function handleTouchEnd(e) {
dragEnd(e);
// Проверяем, был ли это клик
const touchEndTime = Date.now();
const touchDuration = touchEndTime - touchStartTime;
if (e.changedTouches && e.changedTouches[0]) {
const touch = e.changedTouches[0];
const moveDistance = Math.sqrt(
Math.pow(touch.clientX - touchStartX, 2) +
Math.pow(touch.clientY - touchStartY, 2)
);
if (
touchDuration < CLICK_THRESHOLD &&
moveDistance < MOVE_THRESHOLD &&
!wasDragging
) {
togglePanelContainer(e);
}
}
}
function dragStart(e) {
const touch = e.touches ? e.touches[0] : e;
const savedStatus = uwuStorage.getItem("uwu_climbingPanelStatus");
initialX =
touch.clientX -
(savedStatus ? savedStatus.x : climbingMainPanel.offsetLeft);
initialY =
touch.clientY -
(savedStatus ? savedStatus.y : climbingMainPanel.offsetTop);
if (e.target === climbingPanelButton) {
isDragging = true;
wasDragging = false;
}
if (e.type === "touchstart") {
e.preventDefault();
}
}
function drag(e) {
if (isDragging) {
const touch = e.touches ? e.touches[0] : e;
currentX = touch.clientX - initialX;
currentY = touch.clientY - initialY;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = climbingMainPanel.offsetWidth;
const panelHeight = climbingMainPanel.offsetHeight;
const maxX = windowWidth - panelWidth;
currentX = Math.max(0, Math.min(currentX, maxX));
const maxY = windowHeight - panelHeight;
currentY = Math.max(0, Math.min(currentY, maxY));
setPosition(currentX, currentY, climbingMainPanel);
wasDragging = true;
e.preventDefault();
}
}
function dragEnd(e) {
if (isDragging) {
saveClimbingPanelStatus();
}
isDragging = false;
}
function setPosition(x, y, el) {
el.style.left = `${x}px`;
el.style.top = `${y}px`;
}
function togglePanelContainer(e) {
if (!wasDragging) {
const arrow = document.getElementById("uwu-arrow");
climbingPanelContainer.classList.toggle("open");
saveClimbingPanelStatus();
if (climbingPanelContainer.classList.contains("open")) {
arrow.textContent = "▼";
} else {
arrow.textContent = "▶";
}
}
wasDragging = false;
}
function checkAndResetPanelPosition() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = climbingMainPanel.offsetWidth;
const panelHeight = climbingMainPanel.offsetHeight;
const savedStatus = uwuStorage.getItem("uwu_climbingPanelStatus");
if (savedStatus) {
currentX = savedStatus.x;
currentY = savedStatus.y;
} else {
currentX = 0;
currentY = 0;
}
if (
currentX + panelWidth > windowWidth ||
currentY + panelHeight > windowHeight
) {
currentX = 0;
currentY = 0;
saveClimbingPanelStatus();
}
setPosition(currentX, currentY, climbingMainPanel);
}
climbingPanelButton.addEventListener("mousedown", dragStart);
document.addEventListener("mouseup", dragEnd);
document.addEventListener("mousemove", drag);
climbingPanelButton.addEventListener("click", togglePanelContainer);
climbingPanelButton.addEventListener("touchstart", handleTouchStart, {
passive: false,
});
document.addEventListener("touchend", handleTouchEnd);
document.addEventListener("touchmove", drag, { passive: false });
setTimeout(loadClimbingPanelStatus, 10);
const climbingPanelContent = document.getElementById(
"uwu-climbingPanelContent"
);
const buttonContainer = document.getElementById("uwu-buttonContainer");
const inputButtonsContainer = document.getElementById("uwu-inputButtons");
const buttonRow1 = document.getElementById("uwu-buttonRow1");
const buttonRow2 = document.getElementById("uwu-buttonRow2");
const functionButtonsContainer = document.getElementById(
"uwu-functionButtonsContainer"
);
const tableContainer = document.getElementById("uwu-tableContainer");
const clearTableButton = document.getElementById("button-clear-table");
if (
settings.climbingPanelOrientation === "horizontal" &&
settings.climbingPanelInputsStyle === "standart"
) {
climbingMainPanel.classList.add("horizontal-keyboard");
climbingPanelContent.classList.add("horizontal-keyboard");
buttonContainer.classList.add("horizontal-keyboard");
inputButtonsContainer.classList.add("horizontal-keyboard");
buttonRow1.classList.add("horizontal-keyboard");
buttonRow2.classList.add("horizontal-keyboard");
functionButtonsContainer.classList.add("horizontal-keyboard");
tableContainer.classList.add("horizontal-keyboard");
clearTableButton.classList.add("horizontal-keyboard");
}
const uwuClimbingPanel = document.createElement("style");
uwuClimbingPanel.innerHTML =
/* CSS */
`
#cages > tbody > tr > td.cage {
position: relative;
}
#cages > tbody > tr > td.cage::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
#uwu-climbingPanelContainer {
background-color: "";
display: none;
padding: 5px;
}
#uwu-climbingPanelContainer.open {
display: block;
}
#uwu-climbingMainPanel {
z-index: 2;
pointer-events: auto;
width: 260px;
position: absolute;
background-color: ${theme?.climbingPanelBackground || "#ffffff08"};
border: 1px solid #ffffff1a;
backdrop-filter: blur(20px);
border-radius: 10px;
}
#uwu-climbingPanelButton .left-content {
pointer-events: none;
width: 90%;
}
#uwu-climbingPanelButton .right-content {
pointer-events: none;
width: 10%;
text-align: right;
}
#uwu-arrow {
font-size: 18px;
margin-right: 8px;
}
#uwu-inputButtons button.active {
background-color: #abf6ffb0;
}
#uwu-climbingPanelButton {
cursor: grab;
background-color: #00000026;
border-radius: 10px;
border: 1px solid #ffffff1a;
display: flex;
align-items: center;
justify-content: space-around;
}
#uwu-climbingPanelButton h2 {
display: flex;
margin-top: 2px;
margin-bottom: 2px;
justify-content: center;
pointer-events: none;
}
#uwu-climbingPanel {
font-size: 24px;
border-collapse: collapse;
width: 250px;
height: 190px;;
background-color: #ffffff1a;
border: 2px solid black;
table-layout: fixed;
}
#uwu-climbingPanel > tr > td {
height: calc(100% / 6);
width: calc(100% / 10);
aspect-ratio: 1;
padding: 0;
border: 1px solid black;
text-align: center;
cursor: pointer;
pointer-events: auto;
position: relative;
}
@media (max-width: 500px) {
#uwu-climbingPanel {
font-size: 20px;
}
}
@media (max-width: 400px) {
#uwu-climbingPanel {
font-size: 16px;
}
}
#uwu-climbingPanelContainer h3 {
margin-top: 5px;
margin-bottom: 5px;
}
#uwu-functionButtonsContainer {
height: 25px;
}
#uwu-climbingPanel > tr > td:focus {
outline: 2px solid white;
}
#uwu-climbingPanel > tr > td:not(:empty) {
background-color: #cccccc4d;
}
#uwu-transferCheckbox, #uwu-transferValuesCheckbox {
pointer-events: auto;
cursor: pointer;
}
#uwu-buttonRow1,
#uwu-buttonRow2 {
display: flex;
flex-wrap: wrap;
}
#uwu-climbingPanel > tab-container, #uwu-climbingPanel > table-container {
display: inline-block;
margin-right: 10px;
}
#uwu-climbingPanelContainer button {
background-color: #ffffff4d;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2px 10px;
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s ease;
margin: 3px;
margin-left: 0px;
}
#uwu-buttonRow1 > div > button.tab-button.active,
#uwu-buttonRow2 > div > button.table-button.active {
background-color: #abf6ffb0;
}
#button-clear-table {
margin-top: 5px !important;
width: 100%;
border-radius: 5px !important;
}
`;
document.head.appendChild(uwuClimbingPanel);
const uwuClimbingPanelHorizontal = document.createElement("style");
uwuClimbingPanelHorizontal.innerHTML =
/* CSS */
`
#uwu-climbingMainPanel {
width: 390px !important;
}
#uwu-climbingPanelContent {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto 1fr;
height: calc(100% - 40px);
}
#uwu-buttonContainer {
overflow-y: auto;
grid-column: 1 / 2;
grid-row: 1 / 3;
}
#uwu-functionButtonsContainer {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
#uwu-tableContainer {
grid-column: 2 / 3;
grid-row: 2 / 3;
overflow: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
/* Стили для горизонтальной ориентации с кнопочным вводом */
#uwu-climbingMainPanel.horizontal-keyboard {
width: 420px !important;
}
#uwu-climbingPanelContent.horizontal-keyboard {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto 1fr;
max-height: 250px;
}
#uwu-buttonContainer.horizontal-keyboard {
grid-column: 1 / 2;
grid-row: 1 / 3;
display: flex;
flex-direction: column;
overflow-y: auto;
}
#uwu-inputButtons.horizontal-keyboard {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
#uwu-buttonRow2.horizontal-keyboard {
margin-top: 10px;
}
#uwu-functionButtonsContainer.horizontal-keyboard {
grid-column: 2 / 3;
grid-row: 1 / 2;
align-self: start;
}
#uwu-buttonRow1.horizontal-keyboard {
grid-column: 2 / 3;
grid-row: 1 / 2;
margin-bottom: 10px;
}
#uwu-tableContainer.horizontal-keyboard {
grid-column: 2 / 3;
grid-row: 2 / 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
overflow: hidden;
}
`;
if (settings.climbingPanelOrientation === "horizontal") {
document.head.appendChild(uwuClimbingPanelHorizontal);
}
}
// ====================================================================================================================
// . . . БЫСТРЫЕ СТИЛИ . . .
// ====================================================================================================================
const settingsContainer = document.getElementById(
"extended-settings-container"
);
if (!settingsContainer) {
console.error("Контейнер #extended-settings-container не найден");
return;
}
const checkboxes = [
{
label: "Не показывать всплывающее окно 'О коте'",
key: "hideCatTooltip",
storageKey: "uwu_fastStyles",
style: ".cat_tooltip { display: none !important; }",
callback: function (checked) {
if (checked) {
const style = document.createElement("style");
style.innerHTML = this.style;
document.head.appendChild(style);
} else {
const styles = document.head.querySelectorAll("style");
styles.forEach((style) => {
if (style.innerHTML === this.style) {
document.head.removeChild(style);
}
});
}
},
},
{
label: "Скрыть Игровое поле",
key: "hideGameField",
storageKey: "uwu_fastStyles",
style: "#cages_overflow { visibility: hidden !important; }",
callback: function (checked) {
if (checked) {
const style = document.createElement("style");
style.innerHTML = this.style;
document.head.appendChild(style);
} else {
const styles = document.head.querySelectorAll("style");
styles.forEach((style) => {
if (style.innerHTML === this.style) {
document.head.removeChild(style);
}
});
}
},
},
{
label: "Скрыть фон Игрового Поля",
key: "hideGameFieldBackground",
storageKey: "uwu_fastStyles",
style: "#cages_div { background-image: none !important; }",
callback: function (checked) {
if (checked) {
const style = document.createElement("style");
style.innerHTML = this.style;
document.head.appendChild(style);
} else {
const styles = document.head.querySelectorAll("style");
styles.forEach((style) => {
if (style.innerHTML === this.style) {
document.head.removeChild(style);
}
});
}
},
},
{
label: "Скрыть Небо",
key: "hideSky",
storageKey: "uwu_fastStyles",
style: "#tr_sky { display: none !important; }",
callback: function (checked) {
if (checked) {
const style = document.createElement("style");
style.innerHTML = this.style;
document.head.appendChild(style);
} else {
const styles = document.head.querySelectorAll("style");
styles.forEach((style) => {
if (style.innerHTML === this.style) {
document.head.removeChild(style);
}
});
}
},
},
{
label: "Всегда день/ярко",
key: "alwaysDay",
storageKey: "uwu_settings",
callback: function (checked) {
updateAlwaysDayStyle(checked);
},
},
{
label: "Границы клеток",
key: "cellsBorders",
storageKey: "uwu_settings",
callback: function (checked) {
updateCellsBordersStyle(checked);
},
},
];
const loadSettings = (storageKey) => {
const savedSettings = uwuStorage.getItem(storageKey);
return savedSettings ? savedSettings : {};
};
const saveSettings = (storageKey, settings) => {
uwuStorage.setItem(storageKey, settings);
};
const settingsMap = {
uwu_fastStyles: loadSettings("uwu_fastStyles"),
uwu_settings: loadSettings("uwu_settings"),
};
const applyStyles = () => {
checkboxes.forEach((checkbox) => {
if (settingsMap[checkbox.storageKey][checkbox.key] === true) {
checkbox.callback.call(checkbox, true);
}
});
};
if (settings.fastStyles) {
const settingsDiv = document.createElement("div");
settingsDiv.id = "fast-Styles-container";
settingsDiv.classList.add("extended-settings-block");
checkboxes.forEach((checkbox) => {
const label = document.createElement("div");
const input = document.createElement("input");
input.type = "checkbox";
input.name = checkbox.key;
const storedValue = settingsMap[checkbox.storageKey][checkbox.key];
if (storedValue === true) {
input.checked = true;
checkbox.callback.call(checkbox, true);
}
input.addEventListener("change", function () {
settingsMap[checkbox.storageKey][checkbox.key] = this.checked;
saveSettings(checkbox.storageKey, settingsMap[checkbox.storageKey]);
checkbox.callback.call(checkbox, this.checked);
});
label.appendChild(input);
label.appendChild(document.createTextNode(checkbox.label));
settingsDiv.appendChild(label);
});
settingsContainer.appendChild(settingsDiv);
const style = document.createElement("style");
style.innerHTML = `
.extended-settings-block {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.extended-settings-block div {
display: flex;
align-items: center;
}
`;
document.head.appendChild(style);
} else {
applyStyles();
}
// ====================================================================================================================
// . . . БЫСТРЫЕ ССЫЛКИ В ИГРОВОЙ . . .
// ====================================================================================================================
const quickLinks = {
quickLink1: {
href: "/settings",
text: "Настройки",
},
quickLink2: {
href: "/ls?id=0",
text: "Памятка",
},
quickLink3: {
href: "/blogs",
text: "Блоги",
},
quickLink4: {
href: "/sniff",
text: "Лента",
},
};
const spanElement = document.querySelector("span.small");
Object.entries(quickLinks).forEach(([key, link]) => {
if (settings[key]) {
const newLink = document.createElement("a");
newLink.href = link.href;
newLink.textContent = link.text;
const pipe = document.createTextNode(" | ");
spanElement.appendChild(pipe);
spanElement.appendChild(newLink);
}
});
if (settings.userQuickLinks) {
const userLinksArray = settings.userQuickLinks.split(", ");
userLinksArray.forEach((userLink) => {
const [href, text] = userLink.trim().split(" ");
const newLink = document.createElement("a");
newLink.href = href;
newLink.textContent = text;
const pipe = document.createTextNode(" | ");
spanElement.appendChild(pipe);
spanElement.appendChild(newLink);
});
}
// ====================================================================================================================
// . . . ПОДСВЕТКА РЕСУРСОВ . . .
// ====================================================================================================================
if (settings.highlightResources) {
function hexToRGBA(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
const ITEM_MAP = {
Травы: [
"13",
"15",
"17",
"19",
"21",
"23",
"25",
"26",
"106",
"108",
"109",
"110",
"111",
"112",
"115",
"116",
"119",
"655",
"2613",
"2614",
],
Мох: ["75", "78", "95"],
Паутина: ["20"],
Пыль: ["94", "385", "386", "387", "388", "389", "390", "391", "392"],
"Ветки, вьюнки, костоправы": ["565", "566", "562", "563", "3993"],
"Травящие предметы": [
"985",
"986",
"987",
"988",
"989",
"44",
"180",
"77",
"7801",
"7802",
"7803",
"7804",
"7805",
"7806",
],
"Шаманские штучки": [
"120",
"121",
"122",
"123",
"124",
"125",
"128",
"129",
"130",
"131",
"132",
],
};
function generateHighlightStyles(cageItem) {
const savedSettings = uwuStorage.getItem("uwu_highlightResources");
if (!savedSettings) return;
const uwu_highlightResources = savedSettings;
if (settings.highlightResourcesStyle === "background") {
const styleElement =
document.getElementById("resourcesStyle") ||
document.createElement("style");
styleElement.id = "resourcesStyle";
styleElement.textContent = "";
uwu_highlightResources.forEach((resource) => {
if (resource.highlight) {
const rgbaColor = hexToRGBA(resource.color, 0.4);
let cssRules = "";
const items = ITEM_MAP[resource.name];
if (!items) {
console.warn("Неизвестный ресурс:", resource.name);
return;
}
items.forEach((itemName) => {
cssRules += `
.cage_items[style*='things/${itemName}.png'] {
background-color: ${rgbaColor} !important;
}`;
});
if (cssRules) {
styleElement.textContent += cssRules;
}
}
});
document.head.appendChild(styleElement);
} else if (settings.highlightResourcesStyle === "glow") {
const style = cageItem.getAttribute("style");
if (!style) return;
const oldHighlights = cageItem.querySelectorAll(
"style.uwu_itemHighlight"
);
oldHighlights.forEach((oldHighlight) => oldHighlight.remove());
cageItem.style.position = "relative";
uwu_highlightResources.forEach((resource) => {
if (resource.highlight) {
const rgbaColor = hexToRGBA(resource.color, 1);
let highlightedItems = [];
const items = ITEM_MAP[resource.name];
if (!items) {
console.warn("Неизвестный ресурс:", resource.name);
return;
}
items.forEach((itemName) => {
const backgroundImages =
style.match(
/url\("things\/(.*?)\.png"\) (\d+)% (\d+)% no-repeat/g
) || [];
backgroundImages.forEach((backgroundImage) => {
if (backgroundImage.includes(`things/${itemName}.png`)) {
const positionMatch = backgroundImage.match(
/(url\("things\/(.*?)\.png"\)) (\d+)% (\d+)% no-repeat/
);
const imageUrl = positionMatch ? positionMatch[1] : "";
const positionX = positionMatch ? positionMatch[3] : "0";
const positionY = positionMatch ? positionMatch[4] : "0";
highlightedItems.push(
`${imageUrl} ${positionX}% ${positionY}% no-repeat`
);
}
});
});
if (highlightedItems.length > 0) {
const styleBody = `
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: ${highlightedItems.join(", ")};
filter: drop-shadow(0 0 8px ${rgbaColor}) drop-shadow(0 0 8px ${rgbaColor});
`;
const styleElement = document.createElement("style");
styleElement.classList.add("uwu_itemHighlight");
styleElement.textContent = `
.cage_items[style*='${style}']::before {
${styleBody}
}
`;
cageItem.appendChild(styleElement);
}
}
});
}
}
function setupMutationObserver(targetNode, callback, config) {
const observer = new MutationObserver((mutationsList, observer) => {
for (let mutation of mutationsList) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "style"
) {
callback(targetNode);
}
}
});
observer.observe(targetNode, config);
}
document.querySelectorAll(".cage_items").forEach((cageItem) => {
generateHighlightStyles(cageItem);
setupMutationObserver(cageItem, generateHighlightStyles, {
attributes: true,
childList: true,
subtree: true,
});
});
}
// ====================================================================================================================
// . . . ПОЛЬЗОВАТЕЛЬКИЙ ФОН . . .
// ====================================================================================================================
const cagesDiv = document.querySelector("#cages_div");
function createBackgroundDiv() {
const backgroundDiv = document.createElement("div");
backgroundDiv.style.position = "fixed";
backgroundDiv.style.top = "-1%";
backgroundDiv.style.left = "-1%";
backgroundDiv.style.width = "102%";
backgroundDiv.style.height = "102%";
backgroundDiv.style.zIndex = "-2";
backgroundDiv.style.overflow = "hidden";
return backgroundDiv;
}
function updateBackgroundImage(backgroundDiv, imageUrl) {
if (imageUrl) {
backgroundDiv.style.backgroundImage = `url(${imageUrl})`;
backgroundDiv.style.backgroundSize = "cover";
backgroundDiv.style.backgroundPosition = "center";
backgroundDiv.style.backgroundRepeat = "no-repeat";
} else {
backgroundDiv.style.backgroundImage = "none";
}
}
if (settings.backgroundRepeat) {
const backgroundDiv = createBackgroundDiv();
backgroundDiv.style.filter = "blur(16px)";
backgroundDiv.style.backgroundBlendMode = "overlay";
backgroundDiv.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
const backgroundImageStyle =
window.getComputedStyle(cagesDiv).backgroundImage;
const url = backgroundImageStyle.match(/url\("?(.+?)"?\)/);
const backgroundImageUrl = url ? url[1] : null;
updateBackgroundImage(backgroundDiv, backgroundImageUrl);
globalContainerElement.appendChild(backgroundDiv);
setupMutationObserver(
"#cages_div",
() => {
const backgroundImageStyle =
window.getComputedStyle(cagesDiv).backgroundImage;
const url = backgroundImageStyle.match(/url\("?(.+?)"?\)/);
const backgroundImageUrl = url ? url[1] : null;
updateBackgroundImage(backgroundDiv, backgroundImageUrl);
},
{ attributes: true, attributeFilter: ["style"] },
8,
500,
10
);
}
if (settings.backgroundUser) {
const backgroundDiv = createBackgroundDiv();
updateBackgroundImage(backgroundDiv, settings.backgroundUserImageURL);
globalContainerElement.appendChild(backgroundDiv);
}
// ====================================================================================================================
// . . . ПОЛЬЗОВАТЕЛЬСКИЕ ЦВЕТА НАВЫКОВ И ПАРАМЕТРОВ . . .
// ====================================================================================================================
const defaultBackgroundImageUrl =
"https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/parametersBackgroundImageURL.png";
function generateParameterStyles() {
let cssStyles = "";
const otherColors = settings.parametersColors.other || [
"#cccccc",
"#cccccc",
"#cccccc",
"#cccccc",
];
const otherFirstCellBackground = `linear-gradient(to right, ${otherColors[0]}, ${otherColors[1]})`;
const otherLastCellBackground = `linear-gradient(to right, ${otherColors[2]}, ${otherColors[3]})`;
cssStyles += `#parameters_skills_block .bar-fill { background: ${otherFirstCellBackground}; }\n`;
cssStyles += `#parameters_skills_block .bar { background: ${otherLastCellBackground}; }\n`;
const backgroundImageURL = settings.parametersUserBackgroundImage
? settings.parametersUserBackgroundImageURL
: defaultBackgroundImageUrl;
const useBackgroundImage =
settings.parametersBackgroundImage ||
settings.parametersUserBackgroundImage;
for (const paramId in settings.parametersColors) {
if (paramId === "other") continue;
const colors = settings.parametersColors[paramId] || [
"#cccccc",
"#cccccc",
"#cccccc",
"#cccccc",
];
const firstCellBackground = useBackgroundImage
? `url(${backgroundImageURL}), linear-gradient(to right, ${colors[0]}, ${colors[1]})`
: `linear-gradient(to right, ${colors[0]}, ${colors[1]})`;
const lastCellBackground = useBackgroundImage
? `url(${backgroundImageURL}), linear-gradient(to right, ${colors[2]}, ${colors[3]})`
: `linear-gradient(to right, ${colors[2]}, ${colors[3]})`;
cssStyles += `#${paramId} .bar-fill { background: ${firstCellBackground} !important; }\n`;
cssStyles += `#${paramId} .bar { background: ${lastCellBackground} !important; }\n`;
}
return cssStyles;
}
function applyParameterColors() {
const existingStyleTag = document.getElementById("custom-parameter-styles");
if (existingStyleTag) {
existingStyleTag.remove();
}
const cssStyles = generateParameterStyles();
const styleTag = document.createElement("style");
styleTag.id = "custom-parameter-styles";
styleTag.innerHTML = cssStyles;
document.head.appendChild(styleTag);
}
if (settings.userParametersTheme) {
applyParameterColors();
}
// ====================================================================================================================
// . . . ПОЛЬЗОВАТЕЛЬСКИЙ ШРИФТ . . .
// ====================================================================================================================
let fontSize = uwuStorage.getItem("uwu_fontSize");
function applyFonts() {
// Создаем элемент <link> для подключения шрифта
const fontFamily = fontSize?.fontFamilyBody;
if (fontFamily) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `https://fonts.googleapis.com/css?family=${encodeURIComponent(
fontFamily
)}`;
document.head.appendChild(link);
}
// Создаем элемент <style> для применения стилей
const newFontStyle = document.createElement("style");
newFontStyle.innerHTML = `
body {
font-size: ${fontSize?.fontSizeBody}px;
font-family: ${
fontFamily ? `'${fontFamily}', sans-serif` : "sans-serif"
};
}
.small {
font-size: ${fontSize?.fontSizeSmall}px;
}
#location {
font-size: ${fontSize?.fontSizeLocation}px !important;
}
.vlm0 {
font-size: ${fontSize?.vlm0}px;
}
.vlm1 {
font-size: ${fontSize?.vlm1}px;
}
.vlm2 {
font-size: ${fontSize?.vlm2}px;
}
.vlm3 {
font-size: ${fontSize?.vlm3}px;
}
.vlm4 {
font-size: ${fontSize?.vlm4}px;
}
.vlm5 {
font-size: ${fontSize?.vlm5}px;
}
.vlm6 {
font-size: ${fontSize?.vlm6}px;
}
.vlm7 {
font-size: ${fontSize?.vlm7}px;
}
.vlm8 {
font-size: ${fontSize?.vlm8}px;
}
.vlm9 {
font-size: ${fontSize?.vlm9}px;
}
.vlm10 {
font-size: ${fontSize?.vlm10}px;
}
`;
document.head.appendChild(newFontStyle);
}
if (settings.useUserFonts) {
applyFonts();
}
// ====================================================================================================================
// . . . РЕДИЗАЙН ИГРОВОЙ . . .
// ====================================================================================================================
if (settings.customLayout) {
// ==================================================================
function prependOtherCatsListContent() {
const otherCatsList = document.querySelector(".other_cats_list");
const smallContainer = document.querySelector(".small");
if (!otherCatsList || !smallContainer) return;
const catsListContent = otherCatsList.innerHTML;
switch (settings.showOtherCatsList) {
case "1":
break;
case "2":
const clickableBlockHTML =
'<span style="display: inline; cursor: pointer;"><a href="#" style="display: inline; pointer-events: none;">Душевые коты</a></span>';
smallContainer.insertAdjacentHTML(
"afterbegin",
clickableBlockHTML + " || "
);
const clickableBlock = smallContainer.firstChild;
const catsListContainer = document.createElement("span");
catsListContainer.id = "catsListContainer";
catsListContainer.innerHTML = ": " + catsListContent;
catsListContainer.style.display = "none";
smallContainer.insertBefore(
catsListContainer,
smallContainer.firstChild.nextSibling
);
clickableBlock.addEventListener("click", (event) => {
event.preventDefault();
if (catsListContainer.style.display === "none") {
catsListContainer.style.display = "inline";
} else {
catsListContainer.style.display = "none";
}
});
break;
case "3":
smallContainer.insertAdjacentHTML(
"afterbegin",
catsListContent + " || "
);
break;
default:
break;
}
}
setupSingleCallback(".other_cats_list", prependOtherCatsListContent);
// ==================================================================
function applyLayoutSettings() {
const savedSettings = uwuStorage.getItem("uwu_layoutSettings");
if (savedSettings) {
const { leftBlocks, rightBlocks } = savedSettings;
const mainTable = document.getElementById("main_table");
const tbody = mainTable.getElementsByTagName("tbody")[0];
const blocks = Array.from(tbody.children);
resetBlockStyles(tbody);
const gridAreaTemplate = generateGridTemplate(leftBlocks, rightBlocks);
// console.log(gridAreaTemplate);
tbody.style.display = "grid";
tbody.style.gridTemplateAreas = gridAreaTemplate;
tbody.style.gridTemplateColumns = "1fr auto 1fr";
tbody.style.gridTemplateRows = generateGridRowStyles(
leftBlocks,
rightBlocks
);
blocks.forEach((block) => {
if (block.id) {
block.style.gridArea = block.id;
}
});
}
}
function generateGridRowStyles(leftBlocks, rightBlocks) {
const numRows = Math.max(leftBlocks.length, rightBlocks.length);
let rowStyles = [];
for (let i = 0; i < numRows; i++) {
let rowHeight = "auto";
rowStyles.push(rowHeight);
}
const rowStylesString = rowStyles.join(" ");
return rowStylesString;
}
function generateGridTemplate(leftBlocks, rightBlocks) {
const numRows = Math.max(leftBlocks.length, rightBlocks.length);
let template = "";
let lastLeftBlockId = "";
let lastRightBlockId = "";
let isFirstRow = true;
for (let i = 0; i < numRows; i++) {
const leftBlockId = leftBlocks[i] || lastLeftBlockId;
const rightBlockId = rightBlocks[i] || lastRightBlockId;
if (isFirstRow) {
template += `"${leftBlockId} tr_field ${rightBlockId}" `;
isFirstRow = false;
} else {
template += `"${
leftBlockId === lastLeftBlockId ? "." : leftBlockId
} . ${rightBlockId === lastRightBlockId ? "." : rightBlockId}" `;
}
if (leftBlockId) {
lastLeftBlockId = leftBlockId;
}
if (rightBlockId) {
lastRightBlockId = rightBlockId;
}
}
return template;
}
function resetBlockStyles(parent) {
const blocks = parent.querySelectorAll("tr > *");
blocks.forEach((block) => {
block.style.gridArea = "";
});
}
// Больше фикс стилей.
const fixStyle = document.createElement("style");
fixStyle.innerHTML =
/* CSS */
`
#main_table {
width: 100%;
max-width: unset;
height: 100%;
background: none;
border-spacing: 0px !important;
margin-top: 0px !important;
}
#app > br {
display: none;
}
#app {
width: 100%;
height: 100%;
display: flex !important;
flex-direction: column;
gap: 5px;
}
#chat_msg, #cws_chat_msg {
height: ${settings.chatHeight}px;
width: auto;
}
#history_block > div {
visibility: hidden;
}
#history_block {
display: block;
height: ${settings.historyHeight}px;
overflow-y: auto;
resize: vertical;
}
#family {
display: block;
overflow-y: auto;
resize: vertical;
}
.infos {
width: auto;
}
#cages_overflow {
background: black;
}
.chat_text {
width: auto !important;
overflow-wrap: anywhere;
}
#chat_form {
margin: unset;
margin: 5px;
}
#volume {
margin: 5px;
}
#app > p:last-of-type {
position: fixed;
bottom: 0px;
margin: 8px;
}
h2 {
margin-top: 5px;
margin-bottom: 10px;
}
#itemList {
overflow-y: auto;
max-height: ${settings.itemListHeight || 180}px;
display: flex;
flex-wrap: wrap;
}
#location {
visibility: visible;
position: fixed;
right: 0px;
top: 0px;
font-size: 1.5rem;
background-color: ${theme?.blocksColor};
z-index: 1;
}
.small {
width: fit-content;
position: relative;
left: 0px;
top: 0px;
font-size: ${fontSize?.fontSizeSmall || 16}px;
z-index: 1;
}
body {
overflow-y: scroll;
}
#tr_chat, #tr_actions > td, #tr_mouth > td, #location, .small, #info_main > tbody > tr > td {
padding: 5px !important;
}
#tr_chat > td {
display: contents;
}
#chat_msg, #cws_chat_msg {
height: ${theme?.chatHeight}px;
resize: vertical;
}
#tr_field, #tr_info {
height: 10px;
}
#newchat, #newls {
background-color: transparent;
}
.other_cats_list {
display: none;
}
#deys {
width: auto !important;
}
#block_deys {
flex-wrap: wrap;
justify-content: space-between;
}
#deys_mit {
width: min-content !important;
}
#mit {
width: auto !important;
}
`;
document.head.appendChild(fixStyle);
applyLayoutSettings();
const paragraph = document.querySelector("#app > p > b");
paragraph.textContent = "ТБ:";
function applyLayoutSettingsForInfoMain() {
const infoMainTable = document.getElementById("info_main");
if (!infoMainTable) {
return;
}
const tableRow = infoMainTable.querySelector("tr");
if (!tableRow) {
return;
}
const tds = tableRow.getElementsByTagName("td");
if (tds.length < 3) {
return;
}
for (const td of tds) {
td.style.gridArea = "";
}
tableRow.style.display = "grid";
// хахахах поглядите на смешного строчного
tableRow.style.gridTemplateAreas = `"parameter"
"history"
"family"`;
tds[0].style.gridArea = "family";
tds[1].style.gridArea = "history";
tds[2].style.gridArea = "parameter";
}
applyLayoutSettingsForInfoMain();
}
// ====================================================================================================================
// . . . ПОДСКАЗЫВАТЬ ОСТАВШЕЕСЯ ВРЕМЯ ДО НЮХА . . .
// ====================================================================================================================
if (settings.showHintWhenToSniff) {
let smellTimerInterval = null;
let visualTimerStartTime = null;
let visualInitialTimerValue = 0;
const sniffCheckpointKey = "uwu_sniffCheckpoint";
const visualTimerStateKey = "uwu_sniffVisualTimerState";
const smellCooldowns = {
0: 3600,
1: 3600,
2: 3600,
3: 3600,
4: 1800,
5: 1200,
6: 900,
7: 720,
8: 600,
9: 0,
};
function formatTime(seconds) {
if (seconds <= 0) return "";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.ceil(seconds % 60);
return `${hours ? `${hours} ч ` : ""}${
minutes ? `${minutes} мин ` : ""
}${remainingSeconds} с`;
}
function updateVisualTimerDisplay() {
const timerElement = document.getElementById("uwu_sniff_timer");
if (!timerElement || visualTimerStartTime === null) return;
const currentTime = Date.now();
const elapsedTime = (currentTime - visualTimerStartTime) / 1000;
let remainingTime = visualInitialTimerValue - elapsedTime;
if (remainingTime <= 0) {
stopVisualTimer();
timerElement.textContent = "";
} else {
timerElement.setAttribute("value", Math.ceil(remainingTime));
timerElement.textContent = ` | Нюх через: ${formatTime(remainingTime)}`;
saveVisualTimerState();
}
}
function stopVisualTimer() {
if (smellTimerInterval) {
clearInterval(smellTimerInterval);
smellTimerInterval = null;
}
visualTimerStartTime = null;
visualInitialTimerValue = 0;
uwuStorage.removeItem(visualTimerStateKey);
uwuStorage.removeItem(sniffCheckpointKey);
const timerElement = document.getElementById("uwu_sniff_timer");
if (timerElement) {
timerElement.setAttribute("value", "0");
timerElement.textContent = "";
}
}
function saveVisualTimerState() {
if (visualTimerStartTime !== null && visualInitialTimerValue > 0) {
const remainingTime =
visualInitialTimerValue - (Date.now() - visualTimerStartTime) / 1000;
if (remainingTime > 0) {
const timerState = {
startTime: visualTimerStartTime,
initialValue: visualInitialTimerValue,
};
uwuStorage.setItem(visualTimerStateKey, timerState);
} else {
uwuStorage.removeItem(visualTimerStateKey);
}
} else {
uwuStorage.removeItem(visualTimerStateKey);
}
}
function tryRestoreTimerFromCheckpointOrState() {
if (visualTimerStartTime !== null) {
return;
}
const checkpointTimestampStr = uwuStorage.getItem(sniffCheckpointKey);
if (checkpointTimestampStr) {
const checkpointTimestamp = parseInt(checkpointTimestampStr, 10);
if (!isNaN(checkpointTimestamp)) {
const smellLevelElement = document.querySelector("#smell .level");
if (smellLevelElement) {
const smellLevel = parseInt(smellLevelElement.textContent, 10);
if (smellCooldowns.hasOwnProperty(smellLevel)) {
const totalCooldownSeconds = smellCooldowns[smellLevel];
if (totalCooldownSeconds > 0) {
const elapsedTimeSeconds =
(Date.now() - checkpointTimestamp) / 1000;
const remainingTimeSeconds =
totalCooldownSeconds - elapsedTimeSeconds;
if (remainingTimeSeconds > 0) {
visualInitialTimerValue = remainingTimeSeconds;
visualTimerStartTime = Date.now();
if (smellTimerInterval) clearInterval(smellTimerInterval);
updateVisualTimerDisplay();
smellTimerInterval = setInterval(
updateVisualTimerDisplay,
1000
);
saveVisualTimerState();
return;
} else {
stopVisualTimer();
return;
}
} else {
stopVisualTimer();
return;
}
}
}
} else {
uwuStorage.removeItem(sniffCheckpointKey);
}
}
const savedVisualStateStr = uwuStorage.getItem(visualTimerStateKey);
if (savedVisualStateStr) {
try {
const savedVisualState = savedVisualStateStr;
const elapsedTimeSinceSave =
(Date.now() - savedVisualState.startTime) / 1000;
const remainingTimeFromSave =
savedVisualState.initialValue - elapsedTimeSinceSave;
if (remainingTimeFromSave > 0) {
visualTimerStartTime = savedVisualState.startTime;
visualInitialTimerValue = savedVisualState.initialValue;
if (smellTimerInterval) clearInterval(smellTimerInterval);
updateVisualTimerDisplay();
smellTimerInterval = setInterval(updateVisualTimerDisplay, 1000);
return;
} else {
uwuStorage.removeItem(visualTimerStateKey);
}
} catch (e) {
console.error(
"Ошибка разбора сохраненного состояния визуального таймера:",
e
);
uwuStorage.removeItem(visualTimerStateKey);
}
}
}
function handleBlockMessChange() {
const blockMess = document.getElementById("block_mess");
if (blockMess && blockMess.textContent.includes("Принюхиваться")) {
stopVisualTimer();
uwuStorage.setItem(sniffCheckpointKey, Date.now().toString());
}
}
function checkActionAvailability() {
const trActions = document.getElementById("tr_actions");
if (!trActions) return;
const sniffActionLink = trActions.querySelector('a[data-id="13"]');
if (sniffActionLink) {
stopVisualTimer();
} else {
tryRestoreTimerFromCheckpointOrState();
}
}
function handleErrorChange() {
const errorElement = document.getElementById("error");
if (!errorElement || !errorElement.textContent) return;
const htmlContent = errorElement.innerHTML;
const smellCooldownMatch = htmlContent.match(
/Следующее обнюхивание будет доступно через (.*?)(\.|<br|$)/
);
const cooldownExpiredMatch = htmlContent.includes("Час уже прошёл");
if (smellCooldownMatch) {
const timeString = smellCooldownMatch[1];
let totalSeconds = 0;
const minutesMatch = timeString.match(/(\d+)\s*мин/);
const secondsMatch = timeString.match(/(\d+)\s*с/);
if (minutesMatch) totalSeconds += parseInt(minutesMatch[1], 10) * 60;
if (secondsMatch) totalSeconds += parseInt(secondsMatch[1], 10);
stopVisualTimer();
if (totalSeconds > 0) {
visualInitialTimerValue = totalSeconds;
visualTimerStartTime = Date.now();
if (smellTimerInterval) clearInterval(smellTimerInterval);
updateVisualTimerDisplay();
smellTimerInterval = setInterval(updateVisualTimerDisplay, 1000);
saveVisualTimerState();
}
} else if (cooldownExpiredMatch) {
stopVisualTimer();
}
}
function createTimerElement() {
const smallElement = document.querySelector(".small");
if (smallElement && !document.getElementById("uwu_sniff_timer")) {
smallElement.insertAdjacentHTML(
"beforeend",
'<span id="uwu_sniff_timer" value="0"></span>'
);
}
}
setupSingleCallback(".small", createTimerElement);
setupMutationObserver(
"#tr_actions",
checkActionAvailability,
{
childList: true,
subtree: true,
},
8,
500
);
setupMutationObserver(
"#block_mess",
handleBlockMessChange,
{
childList: true,
subtree: true,
characterData: true,
},
8,
500
);
setupMutationObserver(
"#error",
handleErrorChange,
{
childList: true,
subtree: true,
characterData: true,
},
8,
500
);
}
// ====================================================================================================================
// . . . ДУБЛИРОВАНИЕ ДЕЙСТВИЙ НА ВКЛАДКУ БРАУЗЕРА . . .
// ====================================================================================================================
if (settings.duplicateTimeInBrowserTab) {
const titleElement = document.querySelector("title");
let blockMess = null;
function updateTitle() {
if (!blockMess) {
blockMess = document.getElementById("block_mess");
if (!blockMess) {
titleElement.textContent = "Игровая / CatWar";
return;
}
}
const messageText = blockMess.textContent.trim();
const catNameMatch = messageText.match(/^(.+?)\s+держит/);
const catName = catNameMatch ? catNameMatch[1] : "";
if (catName) {
titleElement.textContent = `Поднят. Во рту | ${catName}`;
} else {
const timeActionMatch = messageText.match(
/^(.+?)\s+(\d+\s*(?:ч\s*)?\d+\s*мин\s*\d+\s*с|\d+\s*мин\s*\d+\s*с|\d+\s*с)\.\s*(Отменить)?$/
);
if (timeActionMatch) {
const actionText = timeActionMatch[1].trim();
const currentTime = timeActionMatch[2].trim();
titleElement.textContent = `${currentTime} | ${actionText}`;
} else {
titleElement.textContent = "Игровая / CatWar";
}
}
}
setupMutationObserver("#tr_actions", updateTitle, {
childList: true,
subtree: true,
characterData: true,
});
}
// ====================================================================================================================
// . . . ЛОГ ЧИСТИЛЬЩИКОВ . . .
// ====================================================================================================================function cleaningLogUpdate(mutationsList) {
if (settings.cleaningLog) {
let logStates = uwuStorage.getItem("uwu_logStates") || {
cleaning: false,
catching: false,
};
let lastDroppedCatInfo = null;
function saveLogStates() {
uwuStorage.setItem("uwu_logStates", logStates);
}
const relevantActions = [
{ regex: /Потёрлись носом о нос с/, type: "check" },
{ regex: /Потёрлись щекой о щёку/, type: "check" },
{ regex: /Помурлыкал(а)? вместе с/, type: "check" },
{ regex: /Обнюхал(а)? /, type: "check" },
{ regex: /Поднял(а)? /, type: "pickup" },
{ regex: /Опустил(а)? на землю /, type: "putdown" },
];
let cleaningLogBuffer = "";
let catNamesAndIds = [];
function cleaningLogUpdate() {
const historyBlock = document.querySelector("#history");
const ist = historyBlock.querySelector("#ist");
const locationSpan = historyBlock.querySelector("#location");
const currentLocation = locationSpan.textContent.trim();
if (currentLocation === "[ Загружается… ]") {
return;
}
let cleaningLogBlock = historyBlock.querySelector("#uwu-cleaningLog");
if (!cleaningLogBlock) {
createCleaningLogBlock(historyBlock);
cleaningLogBlock = historyBlock.querySelector("#uwu-cleaningLog");
}
const istOuterHtml = ist.outerHTML;
const actions = istOuterHtml
.split(".")
.map((action) => action.trim())
.filter((action) => action);
const lastAction = actions[actions.length - 2];
const cleaningLogContent = cleaningLogBlock.querySelector(
"#uwu-cleaningLog-content"
);
if (lastAction) {
if (settings.cleaningLogStyle === "smart") {
processSmartAction(lastAction, currentLocation, cleaningLogContent);
} else {
processStandardAction(
lastAction,
currentLocation,
cleaningLogContent
);
}
let storageKey;
switch (settings.cleaningLogStyle) {
case "smart":
storageKey = "uwu_cleaningLogSmart";
break;
default:
storageKey = "uwu_cleaningLogStandard";
break;
}
uwuStorage.setItem(storageKey, {
log: cleaningLogBuffer,
catNamesAndIds,
counters: {
pickup: parseInt(
document.getElementById("uwu-cleaningLog-counter-pickup")
.textContent
),
putdown: parseInt(
document.getElementById("uwu-cleaningLog-counter-putdown")
.textContent
),
},
});
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
}
}
function createCleaningLogBlock(historyBlock) {
const cleaningLogTemplate =
/* HTML */
`
<div id="uwu-cleaningLog">
<h2>
<a href="#" id="uwu-cleaningLog-toggle" class="toggle"
>Лог чистильщика</a
>
</h2>
<div id="uwu-cleaningLog-content-wrapper">
<div id="uwu-cleaningLog-content"></div>
<div id="uwu-cleaningLog-counters">
<span
>Успешно поднятых:
<span id="uwu-cleaningLog-counter-pickup">0</span></span
>
<span
>Опущенных:
<span id="uwu-cleaningLog-counter-putdown">0</span></span
>
</div>
<div id="uwu-cleaningLog-actions">
<a href="#" id="uwu-cleaningLog-clear">Очистить лог</a>
<a
href="#"
id="uwu-cleaningLog-delete-last"
class="disabled"
title="Удалить последнего опущенного персонажа"
>
<img
src="https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/wastebasket.png"
alt="Удалить"
style="width: 18px; height: 18px; vertical-align: middle;"
/>
</a>
</div>
</div>
</div>
`;
historyBlock.insertAdjacentHTML("beforeend", cleaningLogTemplate);
const hr = document.createElement("hr");
historyBlock.insertBefore(
hr,
historyBlock.querySelector("#uwu-cleaningLog")
);
const cleaningLogContent = historyBlock.querySelector(
"#uwu-cleaningLog-content"
);
const savedLog = uwuStorage.getItem("uwu_cleaningLogSmart");
if (savedLog) {
const savedData = savedLog;
cleaningLogBuffer = savedData.log;
catNamesAndIds = savedData.catNamesAndIds;
if (savedData.counters) {
document.getElementById(
"uwu-cleaningLog-counter-pickup"
).textContent = savedData.counters.pickup;
document.getElementById(
"uwu-cleaningLog-counter-putdown"
).textContent = savedData.counters.putdown;
}
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
}
const clearButton = historyBlock.querySelector("#uwu-cleaningLog-clear");
clearButton.addEventListener("click", (e) => {
e.preventDefault();
if (confirm("Вы уверены, что хотите очистить лог чистильщика?")) {
cleaningLogBuffer = "";
catNamesAndIds = [];
document.getElementById(
"uwu-cleaningLog-counter-pickup"
).textContent = "0";
document.getElementById(
"uwu-cleaningLog-counter-putdown"
).textContent = "0";
cleaningLogContent.innerHTML = "";
uwuStorage.removeItem("uwu_cleaningLogSmart");
lastDroppedCatInfo = null;
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.add("disabled");
}
});
const deleteLastButton = historyBlock.querySelector(
"#uwu-cleaningLog-delete-last"
);
deleteLastButton.addEventListener("click", (e) => {
e.preventDefault();
if (!deleteLastButton.classList.contains("disabled")) {
deleteLastDroppedEntry();
}
});
const toggleButton = historyBlock.querySelector(
"#uwu-cleaningLog-toggle"
);
const contentWrapper = historyBlock.querySelector(
"#uwu-cleaningLog-content-wrapper"
);
if (logStates.cleaning) {
contentWrapper.style.display = "none";
}
toggleButton.addEventListener("click", (e) => {
e.preventDefault();
logStates.cleaning = !logStates.cleaning;
contentWrapper.style.display = logStates.cleaning ? "none" : "block";
saveLogStates();
});
}
function addCatLinksToLog(log, catNamesAndIds) {
let logWithLinks = log;
const nameToIdMap = new Map(
catNamesAndIds.map((cat) => [cat.name, cat.id])
);
logWithLinks = logWithLinks.replace(/\[([^\]]+)\]/g, (match, content) => {
const names = content.split(", ");
const linkedNames = names.map((nameWithId) => {
const name = nameWithId.split(" ")[0];
const id = nameToIdMap.get(name);
if (id) {
return nameWithId.replace(
name,
`<a href="/cat${id}" target="_blank">${name}</a>`
);
}
return nameWithId;
});
return `[${linkedNames.join(", ")}]`;
});
return logWithLinks;
}
function extractCatId(action) {
const match = action.match(/<a href="\/cat(\d+)">/);
return match ? match[1] : null;
}
function checkCatStatus(catId) {
const catTooltip = document
.querySelector(`#cages > tbody .cat_tooltip a[href="/cat${catId}"]`)
.closest(".cat_tooltip");
if (catTooltip) {
const statusSpan = catTooltip.querySelector(".online");
if (statusSpan) {
const statusText = statusSpan.textContent
.replace(/[\[\]]/g, "")
.trim();
const validStatuses = [
"Спит",
"На удалении",
"Заблокирована",
"Заблокирован",
];
return validStatuses.includes(statusText);
}
}
return false;
}
function processStandardAction(action, location, cleaningLogContent) {
for (const relevantAction of relevantActions) {
if (relevantAction.regex.test(action)) {
const catNameMatch = action.match(/<a href="\/cat\d+">([^<]+)<\/a>/);
if (!catNameMatch) {
console.error("Не удалось извлечь имя кота из действия:", action);
return;
}
const catName = catNameMatch[1];
const catId = extractCatId(action);
const actionText = action.replace(
/<a href="\/cat\d+">([^<]+)<\/a>/,
`[${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}]`
);
if (relevantAction.type === "action") {
cleaningLogBuffer += `${actionText} на локации "${location}". `;
} else {
const status = checkCatStatus(catId) ? "" : "Кот не спит. ";
cleaningLogBuffer += `Проверен [${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}] на локации "${location}". ${status}`;
}
if (!catNamesAndIds.some((cat) => cat.id === catId)) {
catNamesAndIds.push({ name: catName, id: catId });
}
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
return;
}
}
}
function processSmartAction(action, location, cleaningLogContent) {
let matched = false;
for (const relevantAction of relevantActions) {
if (relevantAction.regex.test(action)) {
matched = true;
const catNameMatch = action.match(/<a href="\/cat\d+">([^<]+)<\/a>/);
if (!catNameMatch) {
console.error("Не удалось извлечь имя кота из действия:", action);
return;
}
const catName = catNameMatch[1];
const catId = extractCatId(action);
const logLines = cleaningLogBuffer
.split(".")
.map((line) => line.trim())
.filter((line) => line);
switch (relevantAction.type) {
case "check":
processCheckAction(logLines, catName, catId, location);
break;
case "putdown":
processPutdownAction(logLines, catName, catId, location);
break;
case "pickup":
processPickupAction(logLines, catName, catId, location);
break;
}
cleaningLogBuffer =
logLines.join(". ") + (logLines.length > 0 ? "." : "");
if (!catNamesAndIds.some((cat) => cat.id === catId)) {
catNamesAndIds.push({ name: catName, id: catId });
}
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
return;
}
}
if (!matched) {
const logLines = cleaningLogBuffer
.split(".")
.map((line) => line.trim())
.filter((line) => line);
processUnmatchedAction(logLines, cleaningLogContent, action);
cleaningLogBuffer =
logLines.join(". ") + (logLines.length > 0 ? "." : "");
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
}
return null;
}
function deleteLastDroppedEntry() {
if (!lastDroppedCatInfo) return;
const { catName, catId } = lastDroppedCatInfo;
const catIdentifier = `${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}`;
let logLines = cleaningLogBuffer
.split(".")
.map((line) => line.trim())
.filter(Boolean);
let putdownRemoved = false;
let pickupRemoved = false;
for (let i = logLines.length - 1; i >= 0; i--) {
let line = logLines[i];
if (line.startsWith("Опущен") && line.includes(catIdentifier)) {
const match = line.match(/\[([^\]]+)\]/);
if (match) {
let catsInGroup = match[1].split(",").map((c) => c.trim());
if (catsInGroup.length > 1) {
catsInGroup = catsInGroup.filter((c) => c !== catIdentifier);
logLines[i] = line.replace(
/\[([^\]]+)\]/,
`[${catsInGroup.join(", ")}]`
);
} else {
logLines.splice(i, 1);
}
putdownRemoved = true;
break;
}
}
}
if (putdownRemoved) {
for (let i = logLines.length - 1; i >= 0; i--) {
let line = logLines[i];
if (
line.startsWith("Проверен и поднят") &&
line.includes(catIdentifier)
) {
const match = line.match(/\[([^\]]+)\]/);
if (match) {
let catsInGroup = match[1].split(",").map((c) => c.trim());
if (catsInGroup.length > 1) {
catsInGroup = catsInGroup.filter((c) => c !== catIdentifier);
logLines[i] = line.replace(
/\[([^\]]+)\]/,
`[${catsInGroup.join(", ")}]`
);
} else {
logLines.splice(i, 1);
}
pickupRemoved = true;
break;
}
}
}
}
if (putdownRemoved && pickupRemoved) {
cleaningLogBuffer =
logLines.join(". ") + (logLines.length > 0 ? "." : "");
const pickupCounter = document.getElementById(
"uwu-cleaningLog-counter-pickup"
);
const putdownCounter = document.getElementById(
"uwu-cleaningLog-counter-putdown"
);
pickupCounter.textContent = parseInt(pickupCounter.textContent) - 1;
putdownCounter.textContent = parseInt(putdownCounter.textContent) - 1;
const cleaningLogContent = document.getElementById(
"uwu-cleaningLog-content"
);
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
uwuStorage.setItem("uwu_cleaningLogSmart", {
log: cleaningLogBuffer,
catNamesAndIds,
counters: {
pickup: parseInt(pickupCounter.textContent),
putdown: parseInt(putdownCounter.textContent),
},
});
} else {
console.warn("UwU | Не удалось найти парные записи для удаления.");
}
lastDroppedCatInfo = null;
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.add("disabled");
}
function processCheckAction(logLines, catName, catId, location) {
const lastLogIndex = logLines.length - 1;
const isCatSleeping = checkCatStatus(catId);
if (
lastLogIndex >= 0 &&
(logLines[lastLogIndex].includes("Проверен [") ||
logLines[lastLogIndex].includes("Кот не спит") ||
logLines[lastLogIndex].includes("Вы забыли проверить кота"))
) {
logLines.splice(lastLogIndex, 1);
}
if (isCatSleeping) {
logLines.push(
`Проверен [${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}] на локации "${location}"`
);
} else {
logLines.push(
`Кот не спит [${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}]`
);
}
if (!catNamesAndIds.some((cat) => cat.id === catId)) {
catNamesAndIds.push({ name: catName, id: catId });
}
lastDroppedCatInfo = null;
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.add("disabled");
}
function processPutdownAction(logLines, catName, catId, location) {
const catPattern = new RegExp(
`\\[${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}\\]`
);
// 1. Ищем последнее и предпоследнее предложения.
const lastSentenceIndex = logLines.length - 1;
const penultimateSentenceIndex = lastSentenceIndex - 1;
// 2. Проверяем последнее предложение на наличие "Опущен" без текущего имени кота.
const lastSentence = logLines[lastSentenceIndex];
if (
lastSentence.includes(`на локации "${location}"`) &&
lastSentence.includes("Опущен")
) {
const catNamesMatch = lastSentence.match(/\[([^\]]+)\]/);
if (catNamesMatch) {
const catNames = catNamesMatch[1]
.split(",")
.map((name) => name.trim());
const currentCatNameWithId = `${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}`;
if (catNames.includes(currentCatNameWithId)) {
return;
}
}
}
// 3. Если есть, добавляем имя текущего кота к этому предложению.
if (
lastSentence.includes(`на локации "${location}"`) &&
lastSentence.includes("Опущен") &&
!catPattern.test(lastSentence)
) {
logLines[lastSentenceIndex] = lastSentence.replace(
/]/,
`, ${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}]`
);
} else {
// 4. Если нет, добавляем новое предложение с "Опущен".
logLines.push(
`Опущен [${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}] на локации "${location}"`
);
}
if (!catNamesAndIds.some((cat) => cat.id === catId)) {
catNamesAndIds.push({ name: catName, id: catId });
}
const putdownCounter = document.getElementById(
"uwu-cleaningLog-counter-putdown"
);
putdownCounter.textContent = parseInt(putdownCounter.textContent) + 1;
lastDroppedCatInfo = { catName, catId };
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.remove("disabled");
}
function processPickupAction(logLines, catName, catId, location) {
const catPattern = new RegExp(
`\\[${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}\\]`
);
// 1. Ищем последнее и предпоследнее предложения.
const lastSentenceIndex = logLines.length - 1;
const penultimateSentenceIndex = lastSentenceIndex - 1;
// 2. Проверяем последнее предложение на "Проверен и поднят" с именем кота.
const lastSentence = logLines[lastSentenceIndex];
if (
lastSentence.includes(`на локации "${location}"`) &&
lastSentence.includes("Проверен и поднят") &&
catPattern.test(lastSentence)
) {
return;
}
// 3. Проверяем последнее предложение на "Проверен" с именем кота.
let lastSentenceChecked = false;
if (
logLines[lastSentenceIndex].includes("Проверен") &&
logLines[lastSentenceIndex].includes(
`[${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}]`
) &&
logLines[lastSentenceIndex].includes(`на локации "${location}"`)
) {
lastSentenceChecked = true;
}
// 4. Если последнее предложение - "Проверен", проверяем предпоследнее на "Проверен и поднят".
if (lastSentenceChecked) {
if (
penultimateSentenceIndex >= 0 &&
logLines[penultimateSentenceIndex].includes("Проверен и поднят") &&
logLines[penultimateSentenceIndex].includes(
`на локации "${location}"`
) &&
!logLines[penultimateSentenceIndex].includes(
`[${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}]`
)
) {
const currentCatMatch =
logLines[lastSentenceIndex].match(/\[(.*?)\]/);
if (currentCatMatch) {
// Добавляем имя текущего кота к предпоследнему предложению.
const existingCatsMatch =
logLines[penultimateSentenceIndex].match(/\[(.*?)\]/);
if (existingCatsMatch) {
const existingCats = existingCatsMatch[1];
const newCatString = existingCats.trim()
? `${existingCats}, ${catName}${
settings.cleaningLogShowID ? ` ${catId}` : ""
}`
: `${catName}${settings.cleaningLogShowID ? ` ${catId}` : ""}`;
logLines[penultimateSentenceIndex] = logLines[
penultimateSentenceIndex
].replace(/\[(.*?)\]/, `[${newCatString}]`);
}
// Удаляем последнее предложение.
logLines.splice(lastSentenceIndex, 1);
}
} else {
// 5. Создаем новое предложение "Проверен и поднят".
logLines[lastSentenceIndex] = logLines[lastSentenceIndex].replace(
"Проверен",
"Проверен и поднят"
);
}
} else {
// 6. Если "Проверен" с именем кота нет.
if (logLines[lastSentenceIndex].includes("Кот не спит")) {
logLines[lastSentenceIndex] = "Вы забыли проверить кота";
} else if (
!logLines[lastSentenceIndex].includes("Вы забыли проверить кота")
) {
logLines.push("Вы забыли проверить кота");
}
}
if (!catNamesAndIds.some((cat) => cat.id === catId)) {
catNamesAndIds.push({ name: catName, id: catId });
}
const pickupCounter = document.getElementById(
"uwu-cleaningLog-counter-pickup"
);
pickupCounter.textContent = parseInt(pickupCounter.textContent) + 1;
lastDroppedCatInfo = null;
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.add("disabled");
}
function processUnmatchedAction(logLines, cleaningLogContent, action) {
const lastLogIndex = logLines.length - 1;
const isCancelAction = /Отменил(а)? /.test(action);
if (
lastLogIndex >= 0 &&
logLines[lastLogIndex].includes("Проверен [") &&
!isCancelAction
) {
logLines.splice(lastLogIndex, 1);
cleaningLogBuffer =
logLines.join(". ") + (logLines.length > 0 ? "." : "");
cleaningLogContent.innerHTML = addCatLinksToLog(
cleaningLogBuffer,
catNamesAndIds
);
}
lastDroppedCatInfo = null;
document
.getElementById("uwu-cleaningLog-delete-last")
?.classList.add("disabled");
}
setupMutationObserver("#history_block", cleaningLogUpdate, {
childList: true,
subtree: true,
});
const cleaningLogStyle = document.createElement("style");
cleaningLogStyle.innerHTML = `
#uwu-cleaningLog-content {
height: ${settings.cleaningLogHeight || 120}px;
overflow-y: auto;
resize: vertical;
}
#uwu-cleaningLog-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
}
#uwu-cleaningLog-delete-last.disabled {
pointer-events: none;
opacity: 0.5;
cursor: not-allowed;
}
`;
document.head.appendChild(cleaningLogStyle);
}
// ====================================================================================================================
// . . . ЛОГ ЛОВЛИ . . .
// ====================================================================================================================
if (settings.catchingLog) {
let logStates = uwuStorage.getItem("uwu_logStates") || {
cleaning: false,
catching: false,
};
function saveLogStates() {
uwuStorage.setItem("uwu_logStates", logStates);
}
let isWaitingForItem = false;
let mouthSnapshot = new Set();
const stopWordsRegex = /Отменил|Отменила|Пошла|Пошёл|Поднял|Подняла/;
let catchingState = {
isWaiting: false, // Находимся ли мы в процессе ожидания результата?
actionType: null, // Тип действия (diving, crevice)
actionVerb: null, // Глагол действия (Нырнул, Осмотрела)
foundItem: null, // Информация о найденном, но не подтвержденном предмете
historyGaveClear: false, // Дала ли история "зеленый свет"?
attemptCounted: false, // Гарант, что попытка засчитана только один раз
lastLoggedItemInCycle: null,
catchLoggedThisCycle: false,
};
function resetCatchingState() {
catchingState.isWaiting = false;
catchingState.actionType = null;
catchingState.actionVerb = null;
catchingState.foundItem = null;
catchingState.historyGaveClear = false;
catchingState.attemptCounted = false;
catchingState.lastLoggedItemInCycle = null;
catchingState.catchLoggedThisCycle = false;
isWaitingForItem = false;
}
function logFoundItem(itemId) {
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
const lastSession =
logData.length > 0 ? logData[logData.length - 1] : null;
if (lastSession && lastSession.type === catchingState.actionType) {
const catchTime = new Date().toLocaleTimeString("ru-RU", {
hour: "2-digit",
minute: "2-digit",
});
const newCatch = {
itemId: itemId,
time: catchTime,
};
lastSession.summary.unshift(newCatch);
uwuStorage.setItem("uwu_catchingLogData", logData);
renderCatchingLog(logData);
return newCatch;
}
return null;
}
function undoLastCatch() {
if (!catchingState.lastLoggedItemInCycle) return;
const { itemId, time } = catchingState.lastLoggedItemInCycle;
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
const lastSession =
logData.length > 0 ? logData[logData.length - 1] : null;
if (lastSession && lastSession.type === catchingState.actionType) {
const indexToRemove = lastSession.summary.findIndex(
(c) => c.itemId === itemId && c.time === time
);
if (indexToRemove > -1) {
lastSession.summary.splice(indexToRemove, 1);
uwuStorage.setItem("uwu_catchingLogData", logData);
renderCatchingLog(logData);
}
}
}
const activityTypes = {
diving: {
triggers: ["Нырнул.", "Нырнула."],
type: "diving",
title: "Ныряние",
verb: { male: "Выловил", female: "Выловила" },
style: {
backgroundColor: "rgba(173, 216, 230, 0.1)",
border: "1px solid rgba(173, 216, 230, 0.4)",
},
},
crevice: {
triggers: ["Осмотрел расщелину.", "Осмотрела расщелину."],
type: "crevice",
title: "Осмотр",
verb: { male: "Нашёл", female: "Нашла" },
style: {
backgroundColor: "rgba(144, 238, 144, 0.1)",
border: "1px solid rgba(144, 238, 144, 0.4)",
},
},
hollow: {
triggers: ["Осмотрел дупло.", "Осмотрела дупло."],
type: "hollow",
title: "Осмотр",
verb: { male: "Нашёл", female: "Нашла" },
style: {
backgroundColor: "rgba(144, 238, 144, 0.1)",
border: "1px solid rgba(144, 238, 144, 0.4)",
},
},
};
const itemNamesById = {
20: "Паутина",
21: "Целебная водоросль",
75: "Мох",
76: "Водный мох",
110: "Мед",
417: "Камень 2х местный",
418: "Камень 3х местный",
565: "Крепкая ветка",
566: "Вьюнок",
1034: "Маленький камушек",
2072: "Гнездо",
2073: "Гнездо",
2074: "Яйцо",
2075: "Черное перо",
2076: "Коричневое перо",
2077: "Голубое перо",
3956: "Рак",
3958: "Рак",
3960: "Рак",
3962: "Краснобрюхая жерлянка",
3965: "Карась",
3966: "Рыба",
3967: "Рыба",
3968: "Рыба",
3969: "Рыба",
3970: "Рыба",
3971: "Речной угорь",
3973: "Речной угорь",
3993: "Плотная водоросль",
3994: "Ракушка (+20)",
3995: "Ракушка (+30)",
3997: "Ракушка (сон)",
3998: "Ракушка (+15)",
3999: "Ракушка (+28)",
4001: "Коралл",
4002: "Коралл",
4004: "Водоросоль",
4005: "Водоросоль",
4006: "Водоросоль",
4008: "Комок",
4009: "Комок",
8021: "Тонколапый паук",
8022: "Светлый паук",
8023: "Бурый паук",
8024: "Тёмный паук",
8042: "Мышь",
8043: "Упитанная мышь",
};
let customItemNames = null;
function getItemNameById(itemId) {
if (customItemNames === null) {
customItemNames =
uwuStorage.getItem("uwu_catchingLog_customItems") || {};
}
if (customItemNames.hasOwnProperty(itemId)) {
return customItemNames[itemId];
}
if (itemNamesById.hasOwnProperty(itemId)) {
return itemNamesById[itemId];
}
return null;
}
function getActivityType(actionText) {
for (const key in activityTypes) {
if (activityTypes[key].triggers.includes(actionText)) {
return activityTypes[key];
}
}
return null;
}
function renderCatchingLog(logData) {
const contentDiv = document.getElementById("uwu-catchingLog-content");
if (!contentDiv) return;
contentDiv.innerHTML = "";
logData.forEach((session) => {
const activityConfig = activityTypes[session.type];
if (!activityConfig) return;
const card = document.createElement("div");
card.className = "uwu-catching-session";
card.style.backgroundColor = activityConfig.style.backgroundColor;
card.style.border = activityConfig.style.border;
card.style.borderRadius = "5px";
card.style.padding = "5px";
card.style.marginBottom = "5px";
const startTime = new Date(session.startTime).toLocaleTimeString(
"ru-RU",
{ hour: "2-digit", minute: "2-digit" }
);
const lastTime = new Date(session.lastActionTime).toLocaleTimeString(
"ru-RU",
{ hour: "2-digit", minute: "2-digit" }
);
const verb =
session.actionVerb === activityConfig.triggers[1]
? activityConfig.verb.female
: activityConfig.verb.male;
const emptyMessageVerb = verb.toLowerCase();
let summaryHtml = "";
if (session.summary.length > 0) {
summaryHtml = "<ul style='margin: 0; padding-left: 20px;'>";
session.summary.forEach((catchInfo) => {
const itemName = getItemNameById(catchInfo.itemId);
const itemDisplayName = itemName ? `${itemName} ` : "";
summaryHtml += `<li>${itemDisplayName}ID ${catchInfo.itemId} в ${catchInfo.time}</li>`;
});
summaryHtml += "</ul>";
} else {
summaryHtml = `<p style='margin: 2px 0; font-style: italic;'>Пока что ничего не ${emptyMessageVerb} :(</p>`;
}
card.innerHTML = `
<p style="margin: 2px 0;"><strong>${activityConfig.title}. Время с ${startTime} до ${lastTime}.</strong></p>
<p style="margin: 2px 0;"><strong>Всего попыток:</strong> ${session.totalDives}</p>
${summaryHtml}
`;
contentDiv.prepend(card);
});
}
function handleMouthChange(mutationsList) {
if (!catchingState.isWaiting) return;
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && node.id && !mouthSnapshot.has(node.id)) {
const img = node.querySelector("img");
if (img && img.src) {
const itemIdMatch = img.src.match(/things\/(\d+)\.png/);
if (itemIdMatch) {
const itemId = itemIdMatch[1];
catchingState.foundItem = itemId;
return;
}
}
}
}
}
}
}
function catchingLogUpdate() {
const ist = document.getElementById("ist");
if (!ist) return;
const actions = ist.innerText
.split(".")
.map((s) => s.trim() + ".")
.filter((s) => s.length > 2);
if (actions.length === 0) return;
const lastAction = actions[actions.length - 1];
const activityConfig = getActivityType(lastAction);
const stopWordsRegex = /Отменил|Отменила|Пошла|Пошёл|Положил|Положила/;
const pickupWordsRegex = /Поднял|Подняла/;
if (activityConfig) {
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
let lastSession =
logData.length > 0 ? logData[logData.length - 1] : null;
const currentTime = Date.now();
const twoHours = 2 * 60 * 60 * 1000;
const isContinuingSession =
lastSession &&
lastSession.type === activityConfig.type &&
currentTime - lastSession.lastActionTime < twoHours;
if (!isContinuingSession) {
const newSession = {
type: activityConfig.type,
startTime: currentTime,
lastActionTime: currentTime,
actionVerb: lastAction,
totalDives: 0,
summary: [],
};
logData.push(newSession);
uwuStorage.setItem("uwu_catchingLogData", logData);
renderCatchingLog(logData);
}
resetCatchingState();
catchingState.isWaiting = true;
catchingState.actionType = activityConfig.type;
catchingState.actionVerb = lastAction;
mouthSnapshot.clear();
const itemsInMouth = document.querySelectorAll("#itemList > div");
itemsInMouth.forEach((item) => {
if (item.id) {
mouthSnapshot.add(item.id);
}
});
return;
}
if (!catchingState.isWaiting) return;
if (pickupWordsRegex.test(lastAction)) {
if (!catchingState.catchLoggedThisCycle) {
undoLastCatch();
}
resetCatchingState();
return;
}
if (stopWordsRegex.test(lastAction)) {
if (
!/Отменил|Отменила/.test(lastAction) &&
!catchingState.attemptCounted
) {
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
let lastSession =
logData.length > 0 ? logData[logData.length - 1] : null;
if (lastSession && lastSession.type === catchingState.actionType) {
lastSession.totalDives++;
lastSession.lastActionTime = Date.now();
uwuStorage.setItem("uwu_catchingLogData", logData);
renderCatchingLog(logData);
}
}
resetCatchingState();
return;
}
if (!catchingState.attemptCounted) {
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
let lastSession =
logData.length > 0 ? logData[logData.length - 1] : null;
if (lastSession && lastSession.type === catchingState.actionType) {
lastSession.totalDives++;
lastSession.lastActionTime = Date.now();
uwuStorage.setItem("uwu_catchingLogData", logData);
renderCatchingLog(logData);
}
catchingState.attemptCounted = true;
}
if (catchingState.foundItem) {
const loggedItem = logFoundItem(catchingState.foundItem);
if (loggedItem) {
catchingState.lastLoggedItemInCycle = loggedItem;
catchingState.catchLoggedThisCycle = true;
}
catchingState.foundItem = null;
}
}
function createCatchingLogBlock() {
const historyContainer = document.getElementById("history");
if (!historyContainer || document.getElementById("uwu-catchingLog"))
return;
const logContainerHTML =
/* HTML */
`
<div id="uwu-catchingLog">
<h2>
<a href="#" id="uwu-catchingLog-toggle" class="toggle"
>Лог ловли</a
>
</h2>
<div id="uwu-catchingLog-content-wrapper">
<div id="uwu-catchingLog-content"></div>
<a href="#" id="uwu-catchingLog-clear">Очистить лог</a>
</div>
</div>
`;
const hr = document.createElement("hr");
historyContainer.appendChild(hr);
historyContainer.insertAdjacentHTML("beforeend", logContainerHTML);
const contentDiv = document.getElementById("uwu-catchingLog-content");
contentDiv.style.height = settings.catchingLogHeight
? `${settings.catchingLogHeight}px`
: "120px";
contentDiv.style.overflowY = "auto";
contentDiv.style.resize = "vertical";
const clearButton = document.getElementById("uwu-catchingLog-clear");
clearButton.addEventListener("click", (e) => {
e.preventDefault();
if (confirm("Вы уверены, что хотите очистить лог ловли?")) {
uwuStorage.removeItem("uwu_catchingLogData");
renderCatchingLog([]);
}
});
const toggleButton = document.getElementById("uwu-catchingLog-toggle");
const contentWrapper = document.getElementById(
"uwu-catchingLog-content-wrapper"
);
if (logStates.catching) {
contentWrapper.style.display = "none";
}
toggleButton.addEventListener("click", (e) => {
e.preventDefault();
logStates.catching = !logStates.catching;
contentWrapper.style.display = logStates.catching ? "none" : "block";
saveLogStates();
});
const logData = uwuStorage.getItem("uwu_catchingLogData") || [];
renderCatchingLog(logData);
}
setupSingleCallback("#history", createCatchingLogBlock);
const mouthObserver = new MutationObserver(handleMouthChange);
setupSingleCallback("#itemList", () => {
mouthObserver.observe(document.getElementById("itemList"), {
childList: true,
});
});
setupMutationObserver("#history_block", catchingLogUpdate, {
childList: true,
subtree: true,
});
}
// ====================================================================================================================
// . . . ЗВУКОВЫЕ УВЕДОМЛЕНИЯ . . .
// ====================================================================================================================
// мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу
// ====================================================================================================================
// . . . ЛИЧНЫЕ СООБЩЕНИЯ . . .
// ====================================================================================================================
let previousCount = 0;
if (settings.notificationPM) {
const newlsElement = document.getElementById("newls");
if (newlsElement) {
const observer = new MutationObserver(handleNewlsChange);
observer.observe(newlsElement, {
characterData: true,
subtree: true,
});
}
function handleNewlsChange(mutations) {
if (mutations.length > 0) {
const currentText = newlsElement.textContent;
const currentCount = parseInt(
currentText.match(/\(\d+\)/)?.[0].slice(1, -1) || 0,
10
);
if (!isNaN(currentCount) && currentCount > previousCount) {
soundManager.playSound(
settings.notificationPMSound,
settings.notificationPMVolume
);
previousCount = currentCount;
} else if (!isNaN(currentCount)) {
previousCount = currentCount;
}
}
}
}
// ====================================================================================================================
// . . . ОКОНЧАНИЕ ДЕЙСТВИЯ . . .
// ====================================================================================================================
if (settings.notificationActionEnd) {
let actionStartTime = null;
const observer = new MutationObserver(() => {
const blockMess = document.getElementById("block_mess");
if (blockMess && blockMess.innerHTML.trim() !== "" && !actionStartTime) {
actionStartTime = Date.now();
} else if (!blockMess && actionStartTime) {
const actionEndTime = Date.now();
const actionDuration = actionEndTime - actionStartTime;
if (actionDuration >= 6000) {
soundManager.playSound(
settings.notificationActionEndSound,
settings.notificationActionEndVolume
);
}
actionStartTime = null;
}
});
const targetNode = document.getElementById("tr_actions");
if (targetNode) {
observer.observe(targetNode, { childList: true, subtree: true });
}
}
// ====================================================================================================================
// . . . ПОДНЯЛИ В РОТ . . .
// ====================================================================================================================
function handleInMouthNotification() {
const blockMess = document.getElementById("block_mess");
if (!blockMess) return;
const observer = new MutationObserver(() => {
if (blockMess.innerHTML.includes("во рту. Вы не сможете выбраться")) {
soundManager.playSound(
settings.notificationInMouthSound,
settings.notificationInMouthVolume
);
}
});
observer.observe(blockMess, { childList: true, subtree: true });
}
if (settings.notificationInMouth) {
setupMutationObserver("#tr_actions", handleInMouthNotification, {
childList: true,
subtree: true,
});
}
// ====================================================================================================================
// . . . ВВЕЛИ В БОЕВУЮ СТОЙКУ . . .
// ====================================================================================================================
if (settings.notificationInFightMode) {
const attackRegex = /в боевую стойку, поскольку на меня напал/;
let previousHistory = "";
const updateHistory = () => {
const istElement = document.getElementById("ist");
const currentHistory = istElement.innerHTML;
if (currentHistory !== previousHistory) {
previousHistory = currentHistory;
const entries = currentHistory.split(".");
const lastEntry = entries[entries.length - 2];
if (lastEntry !== undefined && attackRegex.test(lastEntry)) {
soundManager.playSound(
settings.notificationInFightModeSound,
settings.notificationInFightModeVolume
);
}
}
};
const historyBlock = document.getElementById("history_block");
const observer = new MutationObserver(() => {
updateHistory();
});
const config = {
childList: true,
subtree: true,
characterData: true,
};
observer.observe(historyBlock, config);
}
// ====================================================================================================================
// мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу мяу
// ====================================================================================================================
// . . . СОВРЕМЕННЫЙ (НОВЫЙ) ЧАТ . . .
// ====================================================================================================================
// я на этом инвалиде потерял все нервы кетвар желаю тебе счастья удачи и всего хорошего 😌😌😌😌😌😌😌😌😌😌
// И ДО СИХ ПОР ТЕРЯЮ ААААА
// TODO - как-то пределать шоле
if (settings.newChat) {
const newChatContainer = document.createElement("div");
newChatContainer.id = "uwu_chat_msg";
const chatForm = document.getElementById("chat_form");
chatForm.parentNode.insertBefore(newChatContainer, chatForm.nextSibling);
newChatContainer.addEventListener("click", function (event) {
const target = event.target;
const nickElement = target.closest(".nick");
if (nickElement) {
const textArea = document.getElementById("text");
let nick = nickElement.textContent;
if (settings.addCommaAfterNick) {
nick += ", ";
}
textArea.value += nick;
textArea.focus();
return;
}
const reportButton = target.closest(".msg_report");
if (reportButton) {
const dataId = reportButton.getAttribute("data-id");
const originalReportLink = document.querySelector(
`#chat_msg .msg_report[data-id="${dataId}"]`
);
if (originalReportLink) {
originalReportLink.click();
}
return;
}
});
const chatElement = document.getElementById("chat_msg");
if (chatElement) {
const observer = new MutationObserver(handleNewChatMessage);
observer.observe(chatElement, { childList: true, subtree: true });
}
let addedSpanCount = 0;
function handleNewChatMessage(mutations) {
const addedNodes = Array.from(mutations)
.flatMap((mutation) => Array.from(mutation.addedNodes))
.filter(
(node) =>
node.nodeName === "SPAN" && node.querySelector("td > .chat_text")
);
addedSpanCount += addedNodes.length;
processChatMessages(addedSpanCount);
addedSpanCount = 0;
}
function processChatMessages(messageCount) {
const chatMessages = document.querySelectorAll("#chat_msg > span");
const messagesArray = Array.from(chatMessages);
const messagesToProcess = messagesArray.slice(0, messageCount);
messagesToProcess.reverse();
messagesToProcess.forEach((message) => {
copyMessageToNewChat(message);
});
}
function copyMessageToNewChat(chatMessage) {
const chatTextSpan = chatMessage.querySelector("td > .chat_text");
const messageSpan = chatTextSpan.querySelector("span");
const messageText = messageSpan ? messageSpan.innerHTML : "";
const nickElement = chatTextSpan.querySelector(".nick");
const nickName = nickElement ? nickElement.textContent.trim() : "";
const chatTextClasses = chatTextSpan.className;
const nickStyle = nickElement ? nickElement.getAttribute("style") : "";
let nameFound = false;
let processedText = messageText;
if (settings.namesForNotification) {
const names = settings.namesForNotification
.trim()
.split(/\s*,\s*/)
.filter((name) => name);
names.forEach((name) => {
const regex = new RegExp(
`(^|\\s|[.,!?])(${name})(?=$|\\s|[.,!?])`,
"gi"
);
processedText = processedText.replace(regex, (match, p1, p2) => {
nameFound = true;
return `${p1}<span class="myname">${p2}</span>`;
});
});
}
if (!nameFound && messageSpan && messageSpan.querySelector(".myname")) {
nameFound = true;
}
if (nameFound) {
soundManager.playSound(
settings.myNameNotificationSound,
settings.notificationMyNameVolume
);
}
const profileLink = chatMessage.querySelector('a[href^="/cat"]').href;
const catIdMatch = profileLink.match(/\/cat(\d+)/);
const catId = catIdMatch ? catIdMatch[1] : ". . .";
const reportLink = chatMessage.querySelector(".msg_report");
const dataId = reportLink ? reportLink.getAttribute("data-id") : "";
const newChatMessageHTML =
// html
`
<hr>
<div id="msg">
<div class="${chatTextClasses}">${processedText} - <b class="nick" style="${nickStyle}">${nickName}</b> <i>[${catId}]</i></div>
<div style="display: flex; width: 42px; justify-content: flex-end; margin-right: 2px;">
<a href="${profileLink}" title="Перейти в профиль" target="_blank" rel="noopener noreferrer">➝</a> |
<a href="#" title="Пожаловаться на нарушение ОПИ" class="msg_report" data-id="${dataId}">X</a>
</div>
</div>
`;
newChatContainer.insertAdjacentHTML("afterbegin", newChatMessageHTML);
}
const uwuChatMsg = document.createElement("style");
uwuChatMsg.innerHTML = `
#uwu_chat_msg {
height: ${settings.chatHeight}px;
resize: vertical;
overflow-y: auto;
display: flex;
flex-direction: ${settings.reverseChat ? "column-reverse" : "column"};
}
#chat_msg {
display: none;
}
#msg {
display: flex;
justify-content: space-between;
}
#uwu_chat_msg > hr {
width: -webkit-fill-available;
}
`;
document.head.appendChild(uwuChatMsg);
}
// ====================================================================================================================
// . . . НОВЫЙ ВВОД ЧАТА . . .
// ====================================================================================================================
const chatForm = document.getElementById("chat_form");
const trChatTd = document.querySelector("#tr_chat > td");
function updateChatFormPosition() {
if (settings.reverseChat) {
trChatTd.appendChild(chatForm);
} else {
trChatTd.prepend(chatForm);
}
}
updateChatFormPosition();
if (settings.newChatInput) {
const txtSpan = document.getElementById("txt");
const selectField = txtSpan.querySelector("select#text");
let textarea;
function initTextarea(id, value) {
const textarea = document.createElement("textarea");
textarea.id = id;
textarea.maxLength = 255;
textarea.style.height = "auto";
textarea.style.width = "100%";
textarea.style.resize = "vertical";
textarea.value = value || "";
return textarea;
}
if (selectField) {
textarea =
document.getElementById("text-hide") || initTextarea("text-hide");
textarea.style.display = "none";
} else {
const inputField = txtSpan.querySelector("input#text");
textarea = initTextarea("text", inputField ? inputField.value : "");
txtSpan.insertBefore(textarea, inputField);
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
const selectField = txtSpan.querySelector("select#text");
if (selectField) {
textarea.style.display = "none";
textarea.id = "text-hide";
} else {
textarea.style.display = "";
textarea.id = "text";
}
}
});
});
observer.observe(txtSpan, { childList: true });
// Make Enter great again!
textarea.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
if (event.shiftKey) {
event.preventDefault();
textarea.value += "\n";
} else {
event.preventDefault();
const sendButton = document.getElementById("msg_send");
sendButton.click();
}
}
});
const NewChatDesign = document.createElement("style");
NewChatDesign.innerHTML = `
input#text {
display: none;
}
#text, #text-hide {
color: ${theme?.textColor};
background: ${theme?.accentColor1};
border: solid 1px ${theme?.accentColor2};
font-family: Verdana;
}
`;
document.head.appendChild(NewChatDesign);
}
// ====================================================================================================================
// . . . СЧЁТЧИК СИМВОЛОВ В ЧАТЕ . . .
// ====================================================================================================================
if (settings.newChatInput && settings.showChatCharCounter) {
function setupCharCounter() {
const chatForm = document.getElementById("chat_form");
const textarea = chatForm.querySelector("textarea#text");
const volumeLabel = chatForm.querySelector("b");
if (
!textarea ||
!volumeLabel ||
document.getElementById("uwu-char-counter")
) {
return;
}
const counterElement = document.createElement("span");
counterElement.id = "uwu-char-counter";
counterElement.style.margin = "0 8px";
volumeLabel.parentNode.insertBefore(counterElement, volumeLabel);
volumeLabel.parentNode.insertBefore(
document.createTextNode(" | "),
volumeLabel
);
function updateCounter() {
const currentLength = textarea.value.length;
const maxLength = textarea.maxLength;
counterElement.textContent = `${currentLength}/${maxLength}`;
}
textarea.addEventListener("input", updateCounter);
updateCounter();
}
setupSingleCallback("#chat_form", setupCharCounter);
}
// ====================================================================================================================
// . . . РЕДИЗАЙНЫ + + ЗАКРУГЛЕНИЕ БЛОКОВ . . .
// ====================================================================================================================
const sliceInfoStyle = document.createElement("style");
if (settings.sliceInfoBlock) {
sliceInfoStyle.innerHTML = `
#info_main > tbody > tr > td {
background-color: ${theme?.blocksColor || ""};
margin-bottom: 5px;
}
`;
document.head.appendChild(sliceInfoStyle);
} else {
sliceInfoStyle.innerHTML = `
#tr_info > td {
background-color: ${theme?.blocksColor || ""};
}
`;
document.head.appendChild(sliceInfoStyle);
}
const edgeTrimBlocksStyle = document.createElement("style");
if (settings.edgeTrimBlocks) {
edgeTrimBlocksStyle.innerHTML =
// css
`
#info_main > tbody > tr > td {
width: fit-content;
border-radius: 10px;
margin-bottom: 10px;
}
#info_main,
#tos,
#cages_overflow,
#cages_div {
border-radius: 10px;
}
#main_table > tbody > #tr_actions,
#main_table > tbody > #tr_mouth,
#main_table > tbody > #tr_chat,
#main_table > tbody > #tr_tos,
#main_table > tbody > #tr_info {
margin: 0px 10px 10px 10px;
}
#tr_chat,
#tr_actions > td,
#tr_mouth > td,
#location,
#tr_info > td {
border-radius: 10px;
}
.small {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
}
`;
document.head.appendChild(edgeTrimBlocksStyle);
}
// ====================================================================================================================
// . . . ЗВУК БЛОКИРОВАНИЯ / ОТЖАТИЯ . . .
// ====================================================================================================================
if (settings.notificationBlock) {
function playSoundOnSrcAttributeChange() {
soundManager.playSound(
settings.notificationBlockSound,
settings.notificationBlockVolume
);
}
setupMutationObserver(
"#block",
() => {
const blockElement = document.querySelector("#block");
if (blockElement) {
const srcObserver = new MutationObserver(
playSoundOnSrcAttributeChange
);
srcObserver.observe(blockElement, {
attributes: true,
attributeFilter: ["src"],
});
} else {
console.warn("Элемент #block не найден.");
}
},
{ attributes: true },
8,
500
);
}
// ====================================================================================================================
// . . . КОМАНДЫ В БОЕВОМ РЕЖИМЕ . . .
// ====================================================================================================================
if (settings.fightTeams) {
const colors = settings.fightTeamsColors;
const uwu_fightTeamsCats = uwuStorage.getItem("uwu_fightTeamsCats") || {};
const fightPanel = document.getElementById("fightPanel");
const buttonHTML =
'<button id="updateTableButton" style="width: 100%; box-sizing: border-box;">Обновить команды</button>';
fightPanel.insertAdjacentHTML("beforeend", buttonHTML);
document.getElementById("updateTableButton").onclick = () => {
if (!document.getElementById("uwu-team-settings")) {
createTeamTable();
}
updateTeamTable();
};
function createTeamTable() {
const tableHTML =
/* HTML */
`
<div
id="uwu-team-settings"
style="height: ${settings.fightTeamsPanelHight ||
"auto"}px; box-sizing: border-box; overflow-y: scroll; overflow-x: hidden; resize: vertical;"
>
<table
id="uwu-team-settings-table"
style="width: 100%; border-collapse: collapse;"
>
<thead>
<tr>
<th style="border: 1px solid #000; padding: 5px;">Имя</th>
<th style="border: 1px solid #000; padding: 5px;">Команда</th>
</tr>
</thead>
<tbody id="teamTableBody"></tbody>
</table>
</div>
`;
const updateButton = document.getElementById("updateTableButton");
updateButton.insertAdjacentHTML("beforebegin", tableHTML);
}
function updateTeamTable() {
const tbody = document.getElementById("teamTableBody");
tbody.innerHTML = "";
const cages = document.querySelectorAll("#cages .cage");
cages.forEach((cage) => {
const catName = cage.querySelector(".cat_tooltip a")?.textContent;
const arrow = cage.querySelector(
".arrow.arrow-paws, .arrow.arrow-claws, .arrow arrow-teeth"
);
if (catName && arrow) {
const arrowId = arrow.id;
const savedTeam = uwu_fightTeamsCats[arrowId];
const buttonsHTML = Object.keys(colors)
.map((team) => {
const isSelected = savedTeam === team ? "selected" : "";
return `
<button
class="team-color-button ${isSelected}"
data-arrow-id="${arrowId}"
data-team="${team}"
style="
background-color: ${colors[team][0]};
flex: 1;
height: 20px;
border: 1px solid #333;
padding: 0;
margin: 0;
cursor: pointer;
box-sizing: border-box;
"
></button>
`;
})
.join("");
const rowHTML = `
<tr>
<td style="border: 1px solid #000; padding: 5px; vertical-align: middle;">${catName}</td>
<td style="
border: 1px solid #000;
padding: 4px;
display: flex;
justify-content: space-between;
gap: 4px;
align-items: center;
">
${buttonsHTML}
</td>
</tr>
`;
tbody.insertAdjacentHTML("beforeend", rowHTML);
if (savedTeam) {
applyTeamColors(arrowId, savedTeam);
}
}
});
const teamColorButtons = document.querySelectorAll(".team-color-button");
teamColorButtons.forEach((button) => {
button.addEventListener("click", () => {
const arrowId = button.getAttribute("data-arrow-id");
const team = button.getAttribute("data-team");
applyTeamColors(arrowId, team);
});
});
}
function applyTeamColors(arrowId, team) {
const styleElement = document.createElement("style");
const cssRule = `
#${arrowId} .arrow_green { background-color: ${colors[team][0]} !important; }
#${arrowId} .arrow_red { background-color: ${colors[team][1]} !important; }
`;
styleElement.appendChild(document.createTextNode(cssRule));
document.head.appendChild(styleElement);
uwu_fightTeamsCats[arrowId] = team;
uwuStorage.setItem("uwu_fightTeamsCats", uwu_fightTeamsCats);
}
}
// ====================================================================================================================
// . . . ПЕРЕТАСКИВАНИЕ ПАНЕЛИ БОЕВОГО РЕЖИМА . . .
// ====================================================================================================================
if (settings.draggingFightPanel) {
const dragDiv = document.createElement("div");
dragDiv.style.cursor = "move";
dragDiv.style.display = "inline-block";
const dragImage = document.createElement("img");
dragImage.src =
"https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/drag-move.png";
dragImage.style.width = "24px";
dragImage.style.height = "24px";
dragImage.style.pointerEvents = "none";
dragDiv.appendChild(dragImage);
const fightPanel = document.getElementById("fightPanel");
const firstImage = fightPanel.querySelector("img");
const parentDiv = firstImage.parentElement;
parentDiv.insertBefore(dragDiv, firstImage);
let mouseX = 0;
let mouseY = 0;
let panelX = 0;
let panelY = 0;
let isDragging = false;
function saveFightPanelPosition(x, y) {
uwuStorage.setItem("uwu_fightPanelPosition", { x, y });
}
function loadFightPanelPosition() {
const savedPosition = uwuStorage.getItem("uwu_fightPanelPosition");
if (savedPosition) {
const position = savedPosition;
panelX = position.x;
panelY = position.y;
}
}
function setFightPanelPosition(x, y) {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const panelWidth = fightPanel.offsetWidth;
const panelHeight = fightPanel.offsetHeight;
const maxX = windowWidth - panelWidth;
x = Math.max(0, Math.min(x, maxX));
const maxY = windowHeight - panelHeight;
y = Math.max(0, Math.min(y, maxY));
fightPanel.style.left = `${x}px`;
fightPanel.style.top = `${y}px`;
saveFightPanelPosition(x, y);
}
dragDiv.addEventListener("mousedown", (e) => {
e.preventDefault();
isDragging = true;
mouseX = e.clientX;
mouseY = e.clientY;
loadFightPanelPosition();
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
e.preventDefault();
const dx = e.clientX - mouseX;
const dy = e.clientY - mouseY;
setFightPanelPosition(panelX + dx, panelY + dy);
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
document.body.style.userSelect = "auto";
});
loadFightPanelPosition();
setFightPanelPosition(panelX, panelY);
}
// ====================================================================================================================
// . . . СОКРАЩЕНИЕ ЛОГА БОЕВОГО РЕЖИМА . . .
// ====================================================================================================================
// емааааа ужасное решение
// TODO - исправить переделать уничтожить пересобрать заамогусить чё за фигню я сделал
if (settings.compactFightLog) {
const compactLogStyle = document.createElement("style");
compactLogStyle.innerHTML =
/* CSS */
`
#uwu-Compacted-Fight-Log {
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
padding: 2px;
}
#uwu-Compacted-Fight-Log > div {
width: 100%;
box-sizing: border-box;
}
`;
document.head.appendChild(compactLogStyle);
function compactFightLog() {
const fightLog = document.getElementById("fightLog");
fightLog.style.display = "none";
let compactedFightLog = document.getElementById(
"uwu-Compacted-Fight-Log"
);
if (!compactedFightLog) {
compactedFightLog = document.createElement("div");
compactedFightLog.id = "uwu-Compacted-Fight-Log";
compactedFightLog.style.height = settings.fightPanelHeight + "px";
fightLog.parentNode.insertBefore(compactedFightLog, fightLog);
}
const logEntries = Array.from(fightLog.childNodes).filter(
(entry) => entry.tagName === "SPAN"
);
if (logEntries.length > 0) {
const firstEntry = logEntries[0];
const text = firstEntry.textContent.trim();
const match = text.match(/^(.*) x(\d+)$/);
const originalText = match ? match[1] : text;
const count = match ? parseInt(match[2], 10) : 1;
const latestEntry = compactedFightLog.firstElementChild;
if (latestEntry) {
const latestTextSpan = latestEntry.querySelector(".text");
if (
latestTextSpan &&
latestTextSpan.textContent.trim() === originalText
) {
const countLabel = latestEntry.querySelector(".count");
const existingCount = parseInt(
countLabel.textContent.match(/x(\d+)$/)[1],
10
);
countLabel.textContent = ` x${existingCount + count}`;
} else {
const newEntryHTML = createEntryHTML(
firstEntry.className,
originalText,
count
);
compactedFightLog.insertAdjacentHTML("afterbegin", newEntryHTML);
}
} else {
const newEntryHTML = createEntryHTML(
firstEntry.className,
originalText,
count
);
compactedFightLog.insertAdjacentHTML("afterbegin", newEntryHTML);
}
fightLog.removeChild(firstEntry);
}
}
function createEntryHTML(className, originalText, count) {
return `
<div class="${className}">
<span class="text">${originalText}</span>
<label class="count"> x${count}</label>
</div>
`;
}
setupMutationObserver(
"#fightLog",
compactFightLog,
{
attributes: true,
childList: true,
},
8,
500,
10
);
}
// ====================================================================================================================
// . . . ИЗМЕНЯЕМАЯ ВЫСОТА ПАНЕЛИ БОЕВОГО РЕЖИМА . . .
// ====================================================================================================================
if (settings.fightPanelAdjustableHeight) {
const uwuFightLog = document.createElement("style");
uwuFightLog.innerHTML = `
#fightPanel {
height: auto;
}
#fightLog {
resize: vertical;
overflow-y: scroll;
}
#uwu-Compacted-Fight-Log {
resize: vertical;
overflow-y: scroll;
}
`;
document.head.appendChild(uwuFightLog);
const fightLogElement = document.getElementById("fightLog");
if (fightLogElement) {
fightLogElement.style.height = `${settings.fightPanelHeight || 70}px`;
}
}
// ====================================================================================================================
// . . . ВСЕГДА ДЕНЬ В ИГРОВОЙ . . .
// ====================================================================================================================
// Вот бы всё писалось так кратко и легко...........
function updateAlwaysDayStyle(checked) {
const alwaysDayStyle = `
#cages_div {
opacity: 1 !important;
}
`;
const styles = document.head.querySelectorAll("style");
let styleFound = false;
styles.forEach((style) => {
if (style.innerHTML === alwaysDayStyle) {
if (!checked) {
document.head.removeChild(style);
}
styleFound = true;
}
});
if (checked && !styleFound) {
const alwaysDay = document.createElement("style");
alwaysDay.innerHTML = alwaysDayStyle;
document.head.appendChild(alwaysDay);
}
}
// ====================================================================================================================
// . . . НЕБО - ШАПКА . . .
// ====================================================================================================================
if (settings.skyInHeader) {
function getSkyUrl() {
const skyElement = document.querySelector("#sky");
if (skyElement) {
const skyStyle = skyElement.getAttribute("style");
const match = skyStyle.match(/url\((.*?)\)/);
if (match) {
return match[1].trim();
} else {
console.log("Не удалось найти URL изображения неба");
}
}
return "";
}
const skyDiv = document.createElement("div");
skyDiv.id = "skyDuplicate";
const globalContainerElement = document.getElementById(
"uwu-global-container"
);
globalContainerElement.appendChild(skyDiv);
const skyStyle = document.createElement("style");
skyStyle.innerHTML = `
#skyDuplicate {
height: 15%;
width: 100%;
mask-image: linear-gradient(to bottom,
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0.40) 50%,
rgba(0, 0, 0, 0)
);
top: 0;
left: 0;
z-index: -1;
position: absolute;
background-size: cover;
}
`;
document.head.appendChild(skyStyle);
const originalSkyStyle = document.createElement("style");
originalSkyStyle.innerHTML = `
#tr_sky {
display: none;
}
`;
document.head.appendChild(originalSkyStyle);
function updateSkyImage() {
const skyUrl = getSkyUrl();
if (skyUrl) {
skyDiv.style.backgroundImage = `url(${skyUrl})`;
}
}
updateSkyImage();
setupMutationObserver(
"#sky",
updateSkyImage,
{ attributes: true, attributeFilter: ["style"] },
8,
500,
10
);
}
// ====================================================================================================================
// . . . ОПРЕДЕЛЕНИЕ ПОГОДЫ В ИГРОВОЙ . . . 🛠️
// ====================================================================================================================
let currentWeather = "null";
let currentHour = "null";
let currentSeason = "null";
let currentTemperature = "null";
let temperatureDescription = "null";
// ахахаха глянье на этих незнающих
let weatherModifier = 1;
if (settings.manualWeatherPanel) {
const manualWeatherSlider = document.getElementById("manualWeather");
manualWeatherSlider.addEventListener("change", () => {
const selectedWeather = manualWeatherSlider.value;
if (selectedWeather === "1") {
currentWeather = "clear";
} else if (selectedWeather === "2") {
if (settings.minecraftStyle) {
currentWeather = "pixelRain";
} else {
currentWeather = "rain";
}
} else if (selectedWeather === "3") {
if (settings.minecraftStyle) {
currentWeather = "pixelSnow";
} else {
currentWeather = "snow";
}
}
});
}
function getSkyType() {
const skyElement = document.querySelector("#sky");
if (!skyElement) {
currentWeather = "unknown";
return;
}
const skyStyle = skyElement.getAttribute("style");
if (settings.weatherEnabled) {
const match = skyStyle.match(/\/(\d+)\.png/);
if (match) {
const skyNumber = parseInt(match[1]);
switch (skyNumber) {
case 2:
case 4:
currentWeather = settings.minecraftStyle ? "pixelRain" : "rain";
break;
case 7:
case 8:
currentWeather = settings.minecraftStyle ? "pixelSnow" : "snow";
break;
case 22:
currentWeather = "northernLights";
break;
default:
currentWeather = "clear";
}
} else {
currentWeather = "unknown";
}
}
}
function getTime() {
const timeElement = document.querySelector("#hour");
if (!timeElement) {
currentHour = "unknown";
return;
}
const hourImg = timeElement.querySelector("img");
if (!hourImg) {
currentHour = "unknown";
return;
}
const hourTime = hourImg.getAttribute("src");
if (!hourTime) {
currentHour = "unknown";
return;
}
if (settings.weatherEnabled) {
const match = hourTime.match(/(\d+)\.png$/);
if (match) {
const hourNumber = parseInt(match[1]);
if (hourNumber >= 6 && hourNumber <= 12) {
currentHour = "morning";
} else if (hourNumber >= 13 && hourNumber <= 18) {
currentHour = "day";
} else if (hourNumber >= 19 && hourNumber <= 21) {
currentHour = "evening";
} else {
currentHour = "night";
}
} else {
currentHour = "unknown";
}
}
}
function getSeason() {
const seasonElement = document.querySelector("img[src*='symbole/season']");
if (!seasonElement) {
currentSeason = "unknown";
return;
}
const seasonSrc = seasonElement.getAttribute("src");
if (!seasonSrc) {
currentSeason = "unknown";
return;
}
const match = seasonSrc.match(/season(\d+)\.png/);
if (match) {
const seasonNumber = parseInt(match[1]);
switch (seasonNumber) {
case 0:
currentSeason = "winter";
break;
case 1:
currentSeason = "spring";
break;
case 2:
currentSeason = "summer";
break;
case 3:
currentSeason = "autumn";
break;
default:
currentSeason = "unknown";
}
} else {
currentSeason = "unknown";
}
}
function getTemperature() {
const temperatureElement = document.querySelector("#tos");
const temperatureElementHTML = temperatureElement.outerHTML;
const backgroundValue = /background:\s*([a-zA-Z0-9#()]+);/.exec(
temperatureElementHTML
);
if (backgroundValue && backgroundValue.length > 1) {
const foundBackground = backgroundValue[1];
const temperatureRanges = [
{
description: "Очень холодно",
temperature: -3,
colors: [
"#94BDD2",
"#9DC5D8",
"#B2D8E5",
"#C3E8EF",
"#AED4E2",
"#AAD1E0",
"#A5CDDD",
],
},
{
description: "Холодно",
temperature: -2,
colors: [
"#7FAAC5",
"#76A2C0",
"#6A96B8",
"#6593B6",
"#618FB3",
"#7BA6C3",
],
},
{
description: "Прохладно",
temperature: -1,
colors: [
"#3B6C9B",
"#4C7BA6",
"#5887AE",
"#5D8BB0",
"#4777A3",
"#366899",
"#3F709E",
"#4374A1",
"#5483AB",
],
},
{
description: "Тепло",
temperature: 1,
colors: ["#FCBD8E", "#F8A37A", "#F79E77", "#FDC291", "#FCB88A"],
},
{
description: "Жарковато",
temperature: 2,
colors: [
"#F79973",
"#F6946F",
"#F58F6B",
"#F28060",
"#F38563",
"#F17A5C",
"#EF6B50",
"#F07054",
],
},
{
description: "Жарко",
temperature: 3,
colors: [
"#EE664D",
"#ED6149",
"#EB5741",
"#EB523D",
"#E73D2E",
"#E6382A",
],
},
{
description: "Засуха",
temperature: 4,
colors: ["#DF0A08", "#E3241B", "#E4291F", "#E52E22", "#E63326"],
},
];
let foundTemperature = null;
for (const range of temperatureRanges) {
if (range.colors.includes(foundBackground)) {
foundTemperature = range;
break;
}
}
if (foundTemperature) {
currentTemperature = foundTemperature.temperature;
temperatureDescription = foundTemperature.description;
} else {
currentTemperature = 1;
temperatureDescription =
"Неизвестная температура. Разработчик скорее всего уже в курсе и в скором времени выпустит правку.";
console.warn("Неизвестная температура:", foundBackground);
}
switch (currentTemperature) {
case 1:
case -1:
weatherModifier = 2;
break;
case 2:
case -2:
weatherModifier = 1.5;
break;
case 3:
case -3:
weatherModifier = 1;
break;
default:
weatherModifier = 1;
}
// console.log("Температура:", currentTemperature);
const temperatureDisplayElement = document.getElementById("temperature");
if (temperatureDisplayElement) {
temperatureDisplayElement.innerHTML = `[?] Текущий модификатор: ${weatherModifier} (${temperatureDescription})`;
}
} else {
// console.log("...я временно потерял бекграунд температуры🌡️...");
}
}
// ====================================================================================================================
if (!settings.manualWeatherPanel) {
setupMutationObserver("#sky", getSkyType);
setupSingleCallback("#hour", getTime);
setupMutationObserver(
"#hour",
getTime,
{
attributes: true,
attributeFilter: ["src"],
subtree: true,
},
8,
500,
20
);
setupMutationObserver("img[src*='symbole/season']", getSeason, {
attributes: true,
attributeFilter: ["src"],
});
}
setupMutationObserver("#tos", getTemperature, {
attributes: true,
subtree: true,
});
// ====================================================================================================================
// . . . ПОДГОТОВКА КОНТЕЙНЕРОВ / ИЗОБРАЖЕНИЙ . . . 🖼️
// ====================================================================================================================
const weatherContainer = document.getElementById("uwu-main-container");
const weatherCanvas = document.createElement("canvas");
weatherCanvas.classList.add("weatherCanvas");
weatherCanvas.style.zIndex = settings.weatherZIndex;
weatherContainer.appendChild(weatherCanvas);
const weatherCtx = weatherCanvas.getContext("2d");
function resizeCanvasElement() {
weatherCanvas.width = weatherCanvas.parentNode.offsetWidth;
weatherCanvas.height = weatherCanvas.parentNode.offsetHeight;
}
window.addEventListener("resize", resizeCanvasElement);
resizeCanvasElement();
const images = {
pixelSnow: [
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/snowflake1.png",
},
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/snowflake2.png",
},
],
pixelRain: [
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/rain1.png",
},
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/rain2.png",
},
],
pixelSplash: [
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/splash_0.png",
},
{
url: "https://raw.githubusercontent.com/Ibirtem/CatWar/main/images/splash_1.png",
},
],
sus: [
{
url: "https://firebasestorage.googleapis.com/v0/b/xwx-823ac.appspot.com/o/images%2Ftiny-red-among-us.png?alt=media&token=354b34c6-6297-4a4d-8a73-f36a903170c0",
},
],
};
async function loadImages(type) {
const imagesForType = images[type];
if (!imagesForType) {
console.error(`Чё ета...?: ${type}`);
return;
}
const promises = [];
for (const image of imagesForType) {
promises.push(
new Promise((resolve, reject) => {
const img = new Image();
img.src = image.url;
img.onload = function () {
image.image = this;
resolve();
};
img.onerror = function () {
console.error(`Чёта не скачалось: ${image.url}`);
reject();
};
})
);
}
await Promise.all(promises);
}
loadImages("pixelSnow");
loadImages("pixelRain");
loadImages("pixelSplash");
loadImages("sus");
const { raindrops } = generateRain();
const { snowflakes } = generateSnowflakes();
const { pixelRaindrops } = generatePixelRain();
const { pixelSnowflakes } = generatePixelSnow();
// ====================================================================================================================
// . . . РЕЖИМ НИЗКОЙ ПРОИЗВОДИТЕЛЬНОСТИ . . .
// ====================================================================================================================
// Может быть уже даже готовка к динамичному количеству частиц.
var rainNumParticles = 10;
var snowTimerValue = 120;
var desiredNumberOfFireflies = 10;
function setWeatherPerformanceMode() {
rainNumParticles = settings.lowPerformanceMode ? 4 : 10;
snowTimerValue = settings.lowPerformanceMode ? 240 : 120;
desiredNumberOfFireflies = settings.lowPerformanceMode ? 6 : 10;
return { rainNumParticles, snowTimerValue, desiredNumberOfFireflies };
}
setWeatherPerformanceMode();
// ====================================================================================================================
// . . . ДОЖДЬ . . . 🌧️
// ====================================================================================================================
function generateRain() {
const raindrops = [];
setInterval(() => {
if (currentWeather === "rain") {
for (let i = 0; i < rainNumParticles; i++) {
const raindrop = generateRaindrop();
if (raindrop) {
raindrops.push(raindrop);
}
}
}
}, 80);
function generateRaindrop() {
if (document.hidden) {
return;
}
const x = Math.random() * weatherCanvas.width;
const y = Math.random() * -100;
const length = (Math.random() * 20 + 40) / weatherModifier;
const width = (Math.random() * 1 + 1) / weatherModifier;
const ySpeed = length * 0.2 * weatherModifier;
const xSpeed = Math.random() * 1;
return { x, y, length, width, ySpeed, xSpeed };
}
return { raindrops };
}
function drawRaindrop(raindrop) {
weatherCtx.beginPath();
weatherCtx.ellipse(
raindrop.x,
raindrop.y,
raindrop.width,
raindrop.length,
0,
Math.PI,
2 * Math.PI
);
weatherCtx.fillStyle = "rgba(150, 150, 150, 0.4)";
weatherCtx.fill();
}
// ====================================================================================================================
// . . . СНЕГ . . . 🌨️
// ====================================================================================================================
function generateSnowflakes() {
const snowflakes = [];
const snowTimerValue = setWeatherPerformanceMode().snowTimerValue;
setInterval(() => {
if (currentWeather === "snow") {
for (let i = 0; i < 1; i++) {
const snowflake = generateSnowflake();
if (snowflake) {
snowflakes.push(snowflake);
}
}
}
}, snowTimerValue);
function generateSnowflake() {
if (document.hidden) {
return;
}
const y = Math.random() * -100;
const x = Math.random() * weatherCanvas.width;
const size = (Math.random() * 5 + 2) / weatherModifier;
const ySpeed = size * 0.1 * weatherModifier;
const xSpeed = (Math.random() - Math.random()) * 0.2;
const opacity = 1;
return { x, y, size, ySpeed, xSpeed, opacity };
}
return { snowflakes };
}
function drawSnowflake(x, y, size) {
weatherCtx.beginPath();
weatherCtx.ellipse(x, y, size, size, 0, 0, 2 * Math.PI);
weatherCtx.fillStyle = "white";
weatherCtx.fill();
}
// ====================================================================================================================
// . . . ПИКСЕЛЬНЫЙ ДОЖДЬ . . . 🌧️
// ====================================================================================================================
function generatePixelRain() {
const pixelRaindrops = [];
setInterval(() => {
if (currentWeather === "pixelRain") {
for (let i = 0; i < rainNumParticles; i++) {
const pixelRaindrop = generatePixelRaindrop();
if (pixelRaindrop) {
pixelRaindrops.push(pixelRaindrop);
}
}
}
}, 80);
function generatePixelRaindrop() {
if (document.hidden) {
return;
}
const x = Math.random() * weatherCanvas.width;
const y = Math.random() * -100;
const size = (Math.random() * 26 + 26) / Math.pow(weatherModifier, 0.5);
const ySpeed = size * 0.2 * Math.pow(weatherModifier, 0.5);
const xSpeed = Math.random() * 0.2 - 0.1;
const imageData =
images.pixelRain[Math.floor(Math.random() * images.pixelRain.length)];
const image = imageData.image;
return { x, y, size, ySpeed, xSpeed, image };
}
return { pixelRaindrops };
}
function drawPixelRaindrop(pixelRaindrop) {
const imageWidth = pixelRaindrop.image.width;
const imageHeight = pixelRaindrop.image.height;
const scaleFactor = pixelRaindrop.size / Math.max(imageWidth, imageHeight);
weatherCtx.drawImage(
pixelRaindrop.image,
pixelRaindrop.x,
pixelRaindrop.y,
imageWidth * scaleFactor,
imageHeight * scaleFactor
);
}
// ====================================================================================================================
// . . . ПИКСЕЛЬНЫЙ СНЕГ . . . 🌨️
// ====================================================================================================================
function generatePixelSnow() {
const pixelSnowflakes = [];
const snowTimerValue = setWeatherPerformanceMode().snowTimerValue;
setInterval(() => {
if (currentWeather === "pixelSnow") {
for (let i = 0; i < 1; i++) {
const pixelSnowflake = generatePixelSnowflake();
if (pixelSnowflake) {
pixelSnowflakes.push(pixelSnowflake);
}
}
}
}, snowTimerValue);
function generatePixelSnowflake() {
if (document.hidden) {
return;
}
const y = Math.random() * -100;
const x = Math.random() * weatherCanvas.width;
const size = (Math.random() * 8 + 8) / Math.pow(weatherModifier, 0.8); // TODO - Протестить, сильно ли влияет Math.pow на производительность или нет
const ySpeed = size * 0.1 * Math.pow(weatherModifier, 0.8) - 0.6;
const xSpeed = (Math.random() - Math.random()) * 0.2;
const imageData =
images.pixelSnow[Math.floor(Math.random() * images.pixelSnow.length)];
const image = imageData.image;
const opacity = 1;
return { x, y, size, ySpeed, xSpeed, image, opacity };
}
return { pixelSnowflakes };
}
function drawPixelSnowflake(x, y, size, image) {
weatherCtx.drawImage(image, x - size / 2, y - size / 2, size, size);
}
// ====================================================================================================================
// . . . АНИМАЦИЯ ПОГОДЫ / ЧАСТИЦ . . .
// ====================================================================================================================
let lastTime = 0;
function animateWeather() {
const currentTime = performance.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
const baseSpeedMultiplier = 140;
weatherCtx.clearRect(0, 0, weatherCanvas.width, weatherCanvas.height);
if (raindrops.length > 0) {
for (const raindrop of raindrops) {
raindrop.y += raindrop.ySpeed * baseSpeedMultiplier * deltaTime;
raindrop.x += raindrop.xSpeed * baseSpeedMultiplier * deltaTime;
drawRaindrop(raindrop);
}
}
if (snowflakes.length > 0) {
for (const snowflake of snowflakes) {
snowflake.y += snowflake.ySpeed * baseSpeedMultiplier * deltaTime;
snowflake.x += snowflake.xSpeed * baseSpeedMultiplier * deltaTime;
drawSnowflake(snowflake.x, snowflake.y, snowflake.size);
}
}
if (pixelSnowflakes.length > 0) {
for (const pixelSnowflake of pixelSnowflakes) {
pixelSnowflake.y +=
pixelSnowflake.ySpeed * baseSpeedMultiplier * deltaTime;
pixelSnowflake.x +=
pixelSnowflake.xSpeed * baseSpeedMultiplier * deltaTime;
drawPixelSnowflake(
pixelSnowflake.x,
pixelSnowflake.y,
pixelSnowflake.size,
pixelSnowflake.image
);
}
}
if (pixelRaindrops.length > 0) {
for (const pixelRaindrop of pixelRaindrops) {
pixelRaindrop.y +=
pixelRaindrop.ySpeed * baseSpeedMultiplier * deltaTime;
pixelRaindrop.x +=
pixelRaindrop.xSpeed * baseSpeedMultiplier * deltaTime;
drawPixelRaindrop(pixelRaindrop);
}
}
requestAnimationFrame(animateWeather);
}
if (settings.weatherEnabled || settings.manualWeatherPanel) {
animateWeather();
}
// ====================================================================================================================
// . . . СЕВЕРНОЕ СИЯНИЕ . . . 🌟
// ====================================================================================================================
const auroraColors = {
green: {
1: "#aaff9d",
2: "#00faa0",
3: "#00ff62",
},
blue: {
1: "#9DF5ED",
2: "#82BBF5",
3: "#725DFA",
},
};
const auroras = [];
function removeAurora(auroraElement) {
auroraElement.style.animation = "auroraFadeOut 6s ease-in-out";
setTimeout(() => {
if (weatherContainer.contains(auroraElement)) {
weatherContainer.removeChild(auroraElement);
const index = auroras.indexOf(auroraElement);
if (index > -1) {
auroras.splice(index, 1);
}
} else {
console.warn(
"Element to be removed is not a child of weatherContainer."
);
}
}, 6000);
}
function createAurora(color) {
for (const auroraElement of auroras) {
removeAurora(auroraElement);
}
const newAurora = document.createElement("div");
newAurora.style.cssText = `
transform: translate(0, 60%);
z-index: -1;
position: fixed;
left: 0;
width: 100%;
height: 30%;
filter: blur(4rem);
animation: aurora-spin 15s linear infinite, auroraFadeIn 6s ease-in-out;
background: conic-gradient(from var(--gradient-angle),
${auroraColors[color][1]},
${auroraColors[color][2]},
${auroraColors[color][3]},
${auroraColors[color][2]},
${auroraColors[color][1]});
`;
if (settings.auroraPos === "1") {
newAurora.style.top = "-30%";
} else if (settings.auroraPos === "2") {
newAurora.style.bottom = "0";
}
weatherContainer.appendChild(newAurora);
auroras.push(newAurora);
}
function toggleAurora() {
if (settings.manualWeatherPanel) return;
const isAuroraConditionMet =
currentWeather === "northernLights" ||
(currentWeather === "clear" &&
currentHour === "night" &&
(currentSeason === "autumn" || currentSeason === "winter"));
if (isAuroraConditionMet) {
if (auroras.length === 0) {
const auroraColor = Math.random() > 0.5 ? "green" : "blue";
createAurora(auroraColor);
}
} else {
auroras.forEach(removeAurora);
}
}
setInterval(() => {
toggleAurora();
if (!settings.manualWeatherPanel) {
generateFirefliesNaturally();
}
}, 2000);
// ====================================================================================================================
// . . . СВЕТЛЯЧКИ . . . 🪲
// ====================================================================================================================
const fireflies = [];
const glowSizeMultiplier = 12;
function generateFirefly() {
const x = Math.random() * weatherCanvas.width;
const y = Math.random() * weatherCanvas.height;
const size = Math.random() * 5 + 10;
const xSpeed = (Math.random() - 0.5) * 0.5;
const ySpeed = (Math.random() - 0.5) * 0.5;
const firefly = document.createElement("div");
firefly.className = "firefly";
firefly.style.left = x + "px";
firefly.style.top = y + "px";
firefly.style.width = size + "px";
firefly.style.height = size + "px";
const glow = document.createElement("div");
glow.className = "firefly-glow";
glow.style.left = x + "px";
glow.style.top = y + "px";
glow.style.width = size * glowSizeMultiplier + "px";
glow.style.height = size * glowSizeMultiplier + "px";
return { element: firefly, glowElement: glow, x, y, size, xSpeed, ySpeed };
}
function createNewFirefliesIfNeeded() {
const missingFireflies = desiredNumberOfFireflies - fireflies.length;
for (let i = 0; i < missingFireflies; i++) {
const newFirefly = generateFirefly();
fireflies.push(newFirefly);
weatherContainer.appendChild(newFirefly.element);
weatherContainer.appendChild(newFirefly.glowElement);
}
}
function removeFireflies() {
for (const firefly of fireflies) {
weatherContainer.removeChild(firefly.element);
weatherContainer.removeChild(firefly.glowElement);
}
fireflies.length = 0;
}
function toggleFireflies() {
if (settings.manualWeatherPanel) {
if (fireflies.length === 0) {
for (let i = 0; i < desiredNumberOfFireflies; i++) {
fireflies.push(generateFirefly());
weatherContainer.appendChild(fireflies[i].element);
weatherContainer.appendChild(fireflies[i].glowElement);
}
} else {
for (const firefly of fireflies) {
firefly.element.classList.add("firefly-disappearing");
firefly.glowElement.classList.add("firefly-disappearing");
}
setTimeout(() => {
removeFireflies();
}, 6000);
}
}
}
function generateFirefliesNaturally() {
if (
currentWeather === "clear" &&
currentHour === "night" &&
currentSeason === "summer"
) {
if (fireflies.length === 0) {
for (let i = 0; i < desiredNumberOfFireflies; i++) {
fireflies.push(generateFirefly());
weatherContainer.appendChild(fireflies[i].element);
weatherContainer.appendChild(fireflies[i].glowElement);
}
}
} else {
for (const firefly of fireflies) {
firefly.element.classList.add("firefly-disappearing");
firefly.glowElement.classList.add("firefly-disappearing");
}
setTimeout(() => {
removeFireflies();
}, 6000);
}
}
function animateFireflies() {
for (let i = fireflies.length - 1; i >= 0; i--) {
const firefly = fireflies[i];
firefly.x += firefly.xSpeed;
firefly.y += firefly.ySpeed;
if (firefly.x < 0 || firefly.x + firefly.size > weatherCanvas.width) {
firefly.xSpeed *= -1;
}
if (firefly.y < 0 || firefly.y + firefly.size > weatherCanvas.height) {
firefly.ySpeed *= -1;
}
firefly.element.style.left = firefly.x + "px";
firefly.element.style.top = firefly.y + "px";
firefly.glowElement.style.left =
firefly.x - (firefly.size * glowSizeMultiplier) / 2 + "px";
firefly.glowElement.style.top =
firefly.y - (firefly.size * glowSizeMultiplier) / 2 + "px";
createNewFirefliesIfNeeded();
}
requestAnimationFrame(animateFireflies);
}
if (settings.weatherEnabled || settings.manualWeatherPanel) {
animateFireflies();
}
// ====================================================================================================================
// . . . ПРИЗЕМЛЕНИЕ ЧАСТИЦ . . . ☔
// ====================================================================================================================
const landedSnowflakes = [];
const landedPixelSnowflakes = [];
const splashes = [];
const pixelSplashes = [];
switch (true) {
case settings.manualWeatherPanel && !settings.weatherDrops:
case settings.weatherEnabled && !settings.weatherDrops:
setInterval(() => {
checkElements(raindrops, weatherContainer);
checkElements(snowflakes, weatherContainer);
checkElements(pixelSnowflakes, weatherContainer);
checkElements(pixelRaindrops, weatherContainer);
}, 120);
break;
case settings.manualWeatherPanel && settings.weatherDrops:
case settings.weatherEnabled && settings.weatherDrops:
animateLanding();
break;
default:
break;
}
function checkElements(elements, container) {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
if (
element &&
(element.y >= container.offsetHeight ||
element.x >= container.offsetWidth ||
element.x <= 0)
) {
elements.splice(i, 1);
}
}
// console.log(`Количество элементов: ${elements.length}`)
}
function animateLanding() {
for (let i = snowflakes.length - 1; i >= 0; i--) {
const snowflake = snowflakes[i];
if (snowflake.y >= weatherCanvas.height - snowflake.size) {
snowflakes.splice(i, 1);
landedSnowflakes.push(snowflake);
}
}
for (let i = pixelSnowflakes.length - 1; i >= 0; i--) {
const pixelSnowflake = pixelSnowflakes[i];
if (pixelSnowflake.y >= weatherCanvas.height - pixelSnowflake.size) {
pixelSnowflakes.splice(i, 1);
landedPixelSnowflakes.push(pixelSnowflake);
}
}
for (let i = landedSnowflakes.length - 1; i >= 0; i--) {
const snowflake = landedSnowflakes[i];
snowflake.opacity -= 0.001;
if (snowflake.opacity <= 0) {
landedSnowflakes.splice(i, 1);
}
}
for (const snowflake of landedSnowflakes) {
weatherCtx.globalAlpha = snowflake.opacity;
drawSnowflake(snowflake.x, snowflake.y, snowflake.size);
}
for (let i = landedPixelSnowflakes.length - 1; i >= 0; i--) {
const pixelSnowflake = landedPixelSnowflakes[i];
pixelSnowflake.opacity -= 0.001;
if (pixelSnowflake.opacity <= 0) {
landedPixelSnowflakes.splice(i, 1);
}
}
for (const pixelSnowflake of landedPixelSnowflakes) {
weatherCtx.globalAlpha = pixelSnowflake.opacity;
drawPixelSnowflake(
pixelSnowflake.x,
pixelSnowflake.y,
pixelSnowflake.size,
pixelSnowflake.image
);
}
for (let i = raindrops.length - 1; i >= 0; i--) {
const raindrop = raindrops[i];
if (raindrop.y >= weatherCanvas.height - raindrop.length) {
raindrops.splice(i, 1);
splashes.push(generateSplash(raindrop.x, weatherCanvas.height));
}
}
for (let i = pixelRaindrops.length - 1; i >= 0; i--) {
const pixelRaindrop = pixelRaindrops[i];
if (pixelRaindrop.y >= weatherCanvas.height - pixelRaindrop.size) {
pixelRaindrops.splice(i, 1);
pixelSplashes.push(
generateSplash(pixelRaindrop.x, weatherCanvas.height - 24)
);
}
}
for (const splash of splashes) {
splash.x += splash.xSpeed;
splash.y += splash.ySpeed;
splash.ySpeed += 0.1;
weatherCtx.beginPath();
weatherCtx.arc(
splash.x,
splash.y,
splash.size / 1.2 / weatherModifier,
0,
Math.PI * 2
);
weatherCtx.fillStyle = "rgba(150, 150, 150, 0.4)";
weatherCtx.fill();
}
for (const pixelSplash of pixelSplashes) {
pixelSplash.x += pixelSplash.xSpeed;
pixelSplash.y += pixelSplash.ySpeed;
pixelSplash.ySpeed += 0.1;
weatherCtx.drawImage(
pixelSplash.image,
pixelSplash.x,
pixelSplash.y,
pixelSplash.size * weatherModifier * 2,
pixelSplash.size * weatherModifier * 2
);
}
checkSplashes();
checkPixelSplashes();
weatherCtx.globalAlpha = 1;
requestAnimationFrame(animateLanding);
}
function generateSplash(x, y) {
const size = Math.random() * 5 + 2;
const xSpeed = (Math.random() - 0.5) * 2;
const ySpeed = -Math.random() * 2 - 1;
const imageData =
images.pixelSplash[Math.floor(Math.random() * images.pixelSplash.length)];
const image = imageData.image;
return { x, y, size, xSpeed, ySpeed, image };
}
function checkSplashes() {
for (let i = splashes.length - 1; i >= 0; i--) {
const splash = splashes[i];
if (
splash.y >= weatherCanvas.height ||
splash.x >= weatherCanvas.width ||
splash.x <= 0
) {
splashes.splice(i, 1);
}
}
// console.log(`Количество сплешев: ${splashes.length}`)
}
function checkPixelSplashes() {
for (let i = pixelSplashes.length - 1; i >= 0; i--) {
const pixelSplash = pixelSplashes[i];
if (
pixelSplash.y >= weatherCanvas.height ||
pixelSplash.x >= weatherCanvas.width ||
pixelSplash.x <= 0
) {
pixelSplashes.splice(i, 1);
}
}
}
// ====================================================================================================================
} // Конец грандиозного, но и начало чево то нового... Зогдачно......
// ====================================================================================================================
// 🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨🦐✨
// ====================================================================================================================
// . . . ТАРГЕТИНГ ОКНА ОХОТЫ И ПОДГОТОВКА КОНТЕЙНЕРОВ . . .
// ====================================================================================================================
if (targetCW3Hunt.test(window.location.href)) {
amogusSus();
const containerElement = document.querySelector("body");
const globalContainerElement = document.createElement("div");
globalContainerElement.id = "uwu-main-container";
containerElement.appendChild(globalContainerElement);
// ====================================================================================================================
// . . . ПОДПИСЫВАТЬ ЗАПАХ . . .
// ====================================================================================================================
if (settings.describeHuntingSmell) {
const smellElement = document.getElementById("smell");
let smellText = null;
let smellTimer = null;
let previousRed = null;
let seconds = 0;
function updateHintText(currentRed) {
if (currentRed === 0) {
smellText.textContent = "Потерян";
} else if (previousRed !== null) {
if (currentRed > previousRed) {
smellText.textContent = "Ближе";
} else if (currentRed < previousRed) {
smellText.textContent = "Дальше";
}
} else {
smellText.textContent = " ";
}
previousRed = currentRed;
}
function updateTimer() {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
smellTimer.textContent = `${String(minutes).padStart(2, "0")}:${String(
remainingSeconds
).padStart(2, "0")}`;
seconds++;
}
function handleSmellChange() {
const style = window.getComputedStyle(smellElement);
const currentColor = style.backgroundColor;
if (
currentColor !== "rgba(0, 0, 0, 0)" &&
currentColor !== "transparent"
) {
if (!smellText) {
smellText = document.createElement("div");
smellText.id = "smellText";
smellTimer = document.createElement("div");
smellTimer.id = "smellTimer";
document.body.appendChild(smellText);
document.body.appendChild(smellTimer);
intervalId = setInterval(updateTimer, 1000);
}
const currentRed = parseInt(
currentColor.slice(
currentColor.indexOf("(") + 1,
currentColor.indexOf(",")
)
);
updateHintText(currentRed);
}
}
new MutationObserver(handleSmellChange).observe(smellElement, {
attributes: true,
attributeFilter: ["style"],
});
const describeHuntingSmell = document.createElement("style");
describeHuntingSmell.innerHTML = /* CSS */ `
#smellText {
font-size: 20px;
background: white;
color: black;
text-align: center;
width: 100px;
position: absolute;
z-index: 3;
bottom: 60px;
}
#smellTimer {
font-size: 18px;
background: white;
color: black;
text-align: center;
width: 100px;
position: absolute;
z-index: 3;
bottom: 40px;
}
`;
document.head.appendChild(describeHuntingSmell);
}
// ====================================================================================================================
// . . . ВИРТУАЛЬНЫЙ ДЖОЙСТИК . . .
// ====================================================================================================================
// Работаем с сайтовым обработчиком нажатий: "//e.catwar.net/js/key.js?268881668"
if (settings.huntingVirtualJoystick) {
function createJoystick() {
const joystickHTML = `
<div id="joystick-container">
<div id="joystick-base">
<div id="joystick-head"></div>
</div>
</div>
`;
const uwuContainer = document.getElementById("uwu-main-container");
uwuContainer.insertAdjacentHTML("beforeend", joystickHTML);
const css =
/* CSS */
`
#nav_buttons_wrapper {
display: none;
}
#joystick-container {
pointer-events: auto;
position: fixed;
bottom: 20px;
right: 20px;
width: ${settings.sizeHuntingVirtualJoystick}px;
height: ${settings.sizeHuntingVirtualJoystick}px;
z-index: 10;
}
#joystick-base {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(128, 128, 128, 0.5);
position: relative;
}
#joystick-head {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: ${settings.sizeHuntingVirtualJoystick / 2}px;
height: ${settings.sizeHuntingVirtualJoystick / 2}px;
border-radius: 50%;
background-color: #808080;
touch-action: none;
}
`;
const style = document.createElement("style");
style.innerHTML = css;
document.head.appendChild(style);
const joystickContainer = document.getElementById("joystick-container");
const joystickHead = document.getElementById("joystick-head");
const baseRadius = joystickContainer.offsetWidth / 2;
let activeTouchId = null;
let currentActiveKey = null;
let keys = {};
function handleTouchStart(event) {
if (activeTouchId === null) {
const touch = event.touches[0];
activeTouchId = touch.identifier;
updateJoystickPosition(touch.clientX, touch.clientY);
}
}
function handleTouchMove(event) {
event.preventDefault();
for (let i = 0; i < event.touches.length; i++) {
const touch = event.touches[i];
if (touch.identifier === activeTouchId) {
updateJoystickPosition(touch.clientX, touch.clientY);
break;
}
}
}
function handleTouchEnd(event) {
activeTouchId = null;
resetJoystick();
if (currentActiveKey) {
simulateKeyRelease(currentActiveKey);
currentActiveKey = null;
}
}
function updateJoystickPosition(x, y) {
const containerRect = joystickContainer.getBoundingClientRect();
const deltaX = x - (containerRect.left + baseRadius);
const deltaY = y - (containerRect.top + baseRadius);
const angle = Math.atan2(deltaY, deltaX);
const distance = Math.min(Math.hypot(deltaX, deltaY), baseRadius * 0.8);
joystickHead.style.left = `${
baseRadius + distance * Math.cos(angle)
}px`;
joystickHead.style.top = `${baseRadius + distance * Math.sin(angle)}px`;
const threshold = 0.3;
let newActiveKey = null;
if (distance > baseRadius * threshold) {
if (angle >= -Math.PI * 0.125 && angle < Math.PI * 0.125)
newActiveKey = "d";
else if (angle >= Math.PI * 0.125 && angle < Math.PI * 0.375)
newActiveKey = "x";
else if (angle >= Math.PI * 0.375 && angle < Math.PI * 0.625)
newActiveKey = "s";
else if (angle >= Math.PI * 0.625 && angle < Math.PI * 0.875)
newActiveKey = "z";
else if (angle >= Math.PI * 0.875 || angle < -Math.PI * 0.875)
newActiveKey = "a";
else if (angle >= -Math.PI * 0.875 && angle < -Math.PI * 0.625)
newActiveKey = "q";
else if (angle >= -Math.PI * 0.625 && angle < -Math.PI * 0.375)
newActiveKey = "w";
else if (angle >= -Math.PI * 0.375 && angle < -Math.PI * 0.125)
newActiveKey = "e";
}
if (newActiveKey !== currentActiveKey) {
if (currentActiveKey) {
simulateKeyRelease(currentActiveKey);
}
if (newActiveKey) {
simulateKeyPress(newActiveKey);
}
currentActiveKey = newActiveKey;
}
}
function resetJoystick() {
joystickHead.style.left = "50%";
joystickHead.style.top = "50%";
}
// Оставим на будущее, вдруг пригодится.
function releaseAllKeys() {
for (const key in keys) {
if (keys[key]) {
simulateKeyRelease(key);
keys[key] = false;
}
}
}
function simulateKeyPress(key) {
const keyCode = Key.dict[key];
if (keyCode && !Key.keys.includes(keyCode)) {
Key.push(keyCode);
const mockEvent = createMockEvent(keyCode);
Key.keydown(mockEvent);
}
}
function simulateKeyRelease(key) {
const keyCode = Key.dict[key];
if (keyCode) {
const mockEvent = createMockEvent(keyCode);
Key.keyup(mockEvent);
const index = Key.keys.indexOf(keyCode);
if (index > -1) {
Key.keys.splice(index, 1);
}
}
}
function createMockEvent(keyCode) {
return {
keyCode: keyCode,
ctrlKey: false,
shiftKey: false,
altKey: false,
preventDefault: () => {},
repeat: false,
};
}
joystickContainer.addEventListener("touchstart", handleTouchStart);
joystickContainer.addEventListener("touchmove", handleTouchMove);
joystickContainer.addEventListener("touchend", handleTouchEnd);
joystickContainer.addEventListener("touchcancel", handleTouchEnd);
window.addEventListener("blur", function () {
if (currentActiveKey) {
simulateKeyRelease(currentActiveKey);
currentActiveKey = null;
}
resetJoystick();
});
}
createJoystick();
}
// ====================================================================================================================
}
// ====================================================================================================================
function amogusSus() {
console.log("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⣤⣤⣤⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠛⠉⠙⠛⠛⠛⠛⠻⢿⣿⣷⣤⡀⠀⠀⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⠋⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠈⢻⣿⣿⡄⠀⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⣸⣿⡏⠀⠀⠀⣠⣶⣾⣿⣿⣿⠿⠿⠿⢿⣿⣿⣿⣄⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⣿⣿⠁⠀⠀⢰⣿⣿⣯⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣷⡄⠀ ");
console.log("⠀⠀⣀⣤⣴⣶⣶⣿⡟⠀⠀⢸⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣷⠀ ");
console.log("⠀⢰⣿⡟⠋⠉⣹⣿⡇⠀⠀⠘⣿⣿⣿⣿⣷⣦⣤⣤⣤⣶⣶⣶⣶⣿⣿⣿⠀ ");
console.log("⠀⢸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀ ");
console.log("⠀⣸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠉⠻⠿⣿⣿⣿⣿⡿⠿⠿⠛⢻⣿⡇⠀⠀ ");
console.log("⠀⣿⣿⠁⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣧⠀⠀ ");
console.log("⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀ ");
console.log("⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀ ");
console.log("⠀⢿⣿⡆⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀ ");
console.log("⠀⠸⣿⣧⡀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⠀ ");
console.log("⠀⠀⠛⢿⣿⣿⣿⣿⣇⠀⠀⠀⠀⣰⣿⣿⣷⣶⣶⣶⣶⠶⠀⠀⢠⣿⣿⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⣽⣿⡏⠁⠀⠀⠀⢸⣿⡇⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⢹⣿⡆⠀⠀⠀⠀⣸⣿⠇⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⢿⣿⣦⣄⣀⣠⣴⣿⣿⠁⠀⠈⠻⣿⣿⣿⣿⡿⠏⠀⠀⠀⠀ ");
console.log("⠀⠀⠀⠀⠀⠀⠀⠈⠛⠻⠿⠿⠿⠿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀");
}
// ====================================================================================================================
// . . . ТАРГЕТИНГ БЛОГОВОЙ СТРАНИЦЫ . . .
// ====================================================================================================================
if (targetBlogsCreation.test(window.location.href)) {
// ====================================================================================================================
// . . . ВОССТАНОВЛЕНИЕ БЛОГОВОГО ТЕКСТА . . .
// ====================================================================================================================
if (settings.restoreBlogCreation) {
const textarea = document.getElementById("creation-text");
function saveTextToStorage(text) {
uwuStorage.setItem("uwu_blogCreation", text);
}
function restoreTextFromStorage() {
const savedText = uwuStorage.getItem("uwu_blogCreation");
if (savedText && !textarea.value) {
textarea.value = savedText;
}
}
textarea.addEventListener("input", () => {
saveTextToStorage(textarea.value);
});
window.addEventListener("beforeunload", () => {
saveTextToStorage(textarea.value);
});
restoreTextFromStorage();
}
}
// ====================================================================================================================
// . . . КНОПКИ BB-КОДОВ . . .
// ====================================================================================================================
if (settings.moreBBCodes) {
function addBBCodeButtons() {
const bbCodeContainers = document.querySelectorAll(".bbcode");
const commonButtonsHTML =
// html
`
<button class="bbcode" title="Абзац" data-code="p">p</button>
<button class="bbcode" title="Перенос" data-code="br" data-parameter="0">br</button>
<button class="bbcode" title="Таблица" data-code="table">table</button>
<button class="bbcode" title="Строка таблицы" data-code="tr">tr</button>
<button class="bbcode" title="Ячейка таблицы" data-code="td">td</button>
<button class="bbcode" title="Нумерованный список" data-code="ol">ol</button>
<button class="bbcode" title="Маркированный список" data-code="ul">ul</button>
<button class="bbcode" title="Строка списка" data-code="li">li</button>
`;
const overblockButtonHTML = `
<button class="bbcode" title="Раскрывающийся блок" data-code="overblock" data-parameter="1" data-text="Введите название раскрывающегося блока (то же, что и у заголовка, который раскрывает этот блок):">overblock</button>
`;
bbCodeContainers.forEach((bbCode) => {
const container = bbCode.parentElement;
if (!container) return;
if (!container.querySelector('.bbcode[data-code="p"]')) {
container.insertAdjacentHTML("beforeend", commonButtonsHTML);
}
const blockElement = container.querySelector('[data-code="block"]');
if (
blockElement &&
!container.querySelector('.bbcode[data-code="overblock"]')
) {
blockElement.insertAdjacentHTML("afterend", overblockButtonHTML);
}
});
}
setupSingleCallback(".bbcode", addBBCodeButtons);
}
// ====================================================================================================================
// . . . ПРОФИЛЬ ИГРОКА . . .
// ====================================================================================================================
if (targetMainProfile.test(window.location.href)) {
const idVal = document.getElementById("id_val");
if (idVal) {
const currentId = idVal.innerText.trim();
let data = uwuStorage.getItem("uwu_personal") || {};
if (!data.cats) data.cats = {};
if (!data.slots) data.slots = [];
data.lastActiveId = currentId;
if (!data.cats[currentId]) {
data.cats[currentId] = {
id: currentId,
name: "Кот " + currentId,
img: "",
size: "",
costume: "",
};
}
uwuStorage.setItem("uwu_personal", data);
// console.log(
// `UwU | Профиль: ID игрока [${currentId}] сохранён как активный.`
// );
}
// --------------------------------------------------------
if (settings.calculators) {
setupSingleCallback("#info", setupActivityCalc);
setupSingleCallback("#info", moonCalculator);
}
}
// ====================================================================================================================
// . . . ПРОФИЛЯ ДРУГИХ ПОЛЬЗОВАТЕЛЕЙ . . .
// ====================================================================================================================
if (targetProfile.test(window.location.href)) {
// ====================================================================================================================
// . . . БУ И ПРОЧЕЕ . . .
// ====================================================================================================================
if (settings.moreProfileInfo) {
setupSingleCallback("tr:has(img[src='img/icon_kraft.png'])", addKraftLevel);
function addKraftLevel() {
const kraftLevels = {
блоха: 0,
котёночек: 1,
задира: 2,
"гроза детской": 3,
"страх барсуков": 4,
"победитель псов": 5,
"защитник племени": 6,
"великий воин": 7,
"достоин Львиного племени": 8,
идеальная: 9,
};
const kraftRow = document.querySelector(
'tr:has(img[src="img/icon_kraft.png"])'
);
const kraftTextElement = kraftRow.querySelector("b");
const kraftText = kraftTextElement.textContent.trim();
const kraftLevel = kraftLevels[kraftText];
if (kraftLevel !== undefined) {
kraftTextElement.textContent = `${kraftText} (${kraftLevel})`;
}
}
}
if (settings.calculators) {
setupSingleCallback("#info", moonCalculator);
}
}
// ===================================================================================================================
// Калькуляторы возраста/лун и активности частично под авторством "CatWar Mod (Варомод) от Хвойницы"
// ====================================================================================================================
// . . . КАЛЬКУЛЯТОР ВОЗРАСТА / ЛУН . . .
// ====================================================================================================================
function moonCalculator() {
const months = [
"января",
"февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
];
const catTimeStart = 1200000000000;
const infoElement = document.getElementById("info");
if (!infoElement) return;
const style = document.createElement("style");
style.textContent = `
.calculator-error { color: darkred; }
.hidden { display: none; }
.calculator-style { max-width: 400px; margin: 5px; padding: 5px; border-radius: 10px; background: #ffffff08; }
`;
document.head.appendChild(style);
let calculatorAgeElement = document.getElementById("calculator-age");
if (!calculatorAgeElement) {
infoElement.insertAdjacentHTML(
"afterend",
`<div id="calculator-age" class="calculator-style hidden"></div>`
);
calculatorAgeElement = document.getElementById("calculator-age");
}
const infoObserver = new MutationObserver((mutations) => {
mutations.forEach(() => {
if (!infoElement.textContent.match("Дата")) {
calculatorAgeElement.classList.add("hidden");
return;
}
calculatorAgeElement.classList.remove("hidden");
const birthDateString = infoElement.textContent
.match(/\d{4}-\d\d-\d\d \d\d:\d\d/)[0]
.replace(" ", "T");
const nowDateString = formatDate(new Date());
const ageMoons = getMoonsFromElement("age_icon");
const age2Moons = getMoonsFromElement("age2_icon");
const avatarElement = document.querySelector('img[src^="/avatar/"]');
const sex = avatarElement ? avatarElement.style.borderColor : null;
const isRegistrationDate = /регистрац/.test(infoElement.textContent);
const moonsNow = age2Moons
? isRegistrationDate
? ageMoons
: age2Moons
: ageMoons;
const bornWord = getBornWord(sex, isRegistrationDate);
const catTimeString = formatCatTime(
Date.parse(birthDateString),
moonsNow
);
calculatorAgeElement.innerHTML = `
<p><b>Калькулятор возраста</b></p>
<label>Дата и время: <input type="datetime-local" id="calculator-date" min="${birthDateString}" value="${nowDateString}" max="9999-12-31T23:59"></label> <span id="calculator-error-date" class="calculator-error"></span>
<br><label>Возраст: <input type="number" id="calculator-moons" min="0" step="0.1" value="${moonsNow}" style="width: 60px"></label> <span id="moon-word"></span> <span id="calculator-error-moons" class="calculator-error"></span>
<br>${bornWord} ${catTimeString} по кошачьему времени.
<br><br>
`;
updateMoonWord(moonsNow);
const calculatorDateElement = document.getElementById("calculator-date");
const calculatorMoonsElement =
document.getElementById("calculator-moons");
calculatorDateElement.addEventListener("input", function () {
handleDateInput.call(this, birthDateString);
});
calculatorMoonsElement.addEventListener("input", function () {
handleMoonsInput.call(this, birthDateString);
});
});
});
infoObserver.observe(infoElement, { childList: true });
function getMoonsFromElement(iconId) {
const iconElement = document.querySelector(`img[id="${iconId}"]`);
if (!iconElement) return 0;
const ageElement = iconElement
.closest("tr")
.querySelector("td:nth-child(2) b");
return parseFloat(ageElement.textContent);
}
function getBornWord(sex, isRegistrationDate) {
const sexWords = {
pink: ["Зарегистрировалась", "Родилась"],
blue: ["Зарегистрировался", "Родился"],
default: ["Зарегистрировалось", "Родилось"],
};
return isRegistrationDate
? sexWords[sex]
? sexWords[sex][0]
: sexWords.default[0]
: sexWords[sex]
? sexWords[sex][1]
: sexWords.default[1];
}
function formatDate(date) {
const pad = (num) => String(num).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
date.getDate()
)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function formatCatTime(birthTimestamp, moons) {
const daysToAdd = moons * 4;
const targetDate = new Date(
birthTimestamp + daysToAdd * 24 * 60 * 60 * 1000
);
const ms = birthTimestamp - catTimeStart;
let time = Math.round((ms / 1000) * 7);
const secInYear = 12 * 28 * 24 * 60 * 60;
const secInMonth = 28 * 24 * 60 * 60;
const year = Math.floor(time / secInYear);
time -= year * secInYear;
const month = Math.floor(time / secInMonth);
time -= month * secInMonth;
const day = Math.floor(time / (24 * 60 * 60)) + 1;
time -= (day - 1) * 24 * 60 * 60;
const hour = Math.floor(time / (60 * 60));
time -= hour * 60 * 60;
const minute = Math.floor(time / 60);
const pad = (num) => String(num).padStart(2, "0");
return `${day} ${months[month]} ${year} года в ${pad(hour)}:${pad(minute)}`;
}
function handleDateInput(birthDateString) {
const dateString = this.value;
const date = Date.parse(dateString);
const errorDateElement = document.getElementById("calculator-error-date");
errorDateElement.textContent = "";
if (isNaN(date) || date < Date.parse(birthDateString)) {
errorDateElement.textContent = "Ошибка!";
return;
}
const moons = getMoonsFromDate(birthDateString, dateString);
const calcMoonsElement = document.getElementById("calculator-moons");
if (calcMoonsElement) {
calcMoonsElement.value = moons;
updateMoonWord(moons);
const catTimeString = formatCatTime(Date.parse(birthDateString), moons);
document.querySelector(
"br"
).nextSibling.textContent = `${catTimeString} по кошачьему времени.`;
}
updateMoonWord(moons);
}
function handleMoonsInput(birthDateString) {
const moons = Number(this.value);
const errorMoonsElement = document.getElementById("calculator-error-moons");
errorMoonsElement.textContent = "";
if (moons < 0 || isNaN(moons)) {
errorMoonsElement.textContent = "Ошибка!";
return;
}
const calcDateElement = document.getElementById("calculator-date");
if (calcDateElement) {
calcDateElement.value = getDateStringFromMoons(birthDateString, moons);
updateMoonWord(moons);
const catTimeString = formatCatTime(Date.parse(birthDateString), moons);
document.querySelector(
"br"
).nextSibling.textContent = `${catTimeString} по кошачьему времени.`;
}
updateMoonWord(moons);
}
function getMoonsFromDate(birthDateString, dateString) {
const birthday = Date.parse(birthDateString);
const date = Date.parse(dateString);
const moons =
Math.floor(((date - birthday) / (1000 * 60 * 60 * 24 * 4)) * 10) / 10;
return moons;
}
function getDateStringFromMoons(birthDateString, moons) {
const birthday = Date.parse(birthDateString);
const age = Math.round(moons * 4 * 24 * 60 * 60 * 1000);
return formatDate(new Date(birthday + age));
}
function updateMoonWord(moons) {
const integerMoons = Math.floor(moons);
document.getElementById("moon-word").textContent = declOfNum(integerMoons, [
"луна",
"луны",
"лун",
]);
}
function declOfNum(number, titles) {
const cases = [2, 0, 1, 1, 1, 2];
return titles[
number % 100 > 4 && number % 100 < 20
? 2
: cases[number % 10 < 5 ? number % 10 : 5]
];
}
}
// ====================================================================================================================
// . . . КАЛЬКУЛЯТОР АКТИВНОСТИ . . .
// ====================================================================================================================
// TODO - Переписать, сделать рефакторинг и как-то объединить и упростить код с калькулятором выше.
function setupActivityCalc() {
const catId = document.getElementById("id_val").textContent;
const activityStages = [
{ name: "пустое место", fromZero: -5000 },
{ name: "подлежащий удалению", fromZero: -5000 },
{ name: "покинувший игру", fromZero: -2000 },
{ name: "забывший про игру", fromZero: -1000 },
{ name: "забытый кот", fromZero: -750 },
{ name: "ужаснейшая", fromZero: -500 },
{ name: "ужасная", fromZero: -300 },
{ name: "ухудшающаяся", fromZero: -150 },
{ name: "отрицательная", fromZero: -50 },
{ name: "переходная", fromZero: -5 },
{ name: "положительная", fromZero: 5 },
{ name: "улучшающаяся", fromZero: 50 },
{ name: "замечательная", fromZero: 150 },
{ name: "переход 2 мин 15 с", fromZero: 225 },
{ name: "замечательнейшая", fromZero: 300 },
{ name: "переход 2 мин", fromZero: 450 },
{ name: "любимый кот", fromZero: 500 },
{ name: "переход 1 мин 45 с", fromZero: 675 },
{ name: "легенда сайта", fromZero: 750 },
{ name: "переход 1 мин 30 с", fromZero: 900 },
{ name: "ходячий миф", fromZero: 1000 },
{ name: "переход 1 мин 15 с", fromZero: 1125 },
{ name: "переход 1 мин", fromZero: 1350 },
{ name: "переход 45 c", fromZero: 1575 },
{ name: "император Игровой", fromZero: 2000 },
{ name: "частичка Игровой", fromZero: 5000 },
{ name: "хранитель Игровой", fromZero: 20000 },
{ name: "идеальная", fromZero: 75000 },
{ name: "сверхидеальная", fromZero: 150000 },
];
const months = [
"января",
"февраля",
"марта",
"апреля",
"мая",
"июня",
"июля",
"августа",
"сентября",
"октября",
"ноября",
"декабря",
];
const activitySettings = uwuStorage.getItem("uwu_activity") || {};
if (!activitySettings[catId]) {
activitySettings[catId] = { hours: 24, opened: false };
}
if (activitySettings[catId].actgoal) {
activityStages.forEach(function (stage, index) {
if (index && Number(activitySettings[catId].actgoal) === stage.fromZero) {
activitySettings[catId].goal = index;
delete activitySettings[catId].actgoal;
}
});
}
function calculateActivityLength(days) {
const minus = activitySettings[catId].minus || 0;
if (days <= 14) return 150 - minus;
else if (days >= 1575) return 45 - minus;
else return Math.ceil(150 - days / 15) - minus;
}
function calculateRemainingTime(currentActivity, goal, hoursPerDay) {
if (currentActivity >= goal) {
return { actions: 0, time: "0 с", date: "уже достигнута" };
}
const secondsPerDay = convertTime("h s", hoursPerDay);
if (calculateActivityLength(currentActivity) * 4 + 1 > secondsPerDay) {
return { actions: "∞", time: "∞", date: "никогда" };
}
const actionsWithoutDecrease = goal - currentActivity;
let days = 0;
let secondsToday = 0;
while (currentActivity < goal) {
secondsToday = 0;
while (secondsToday < secondsPerDay) {
currentActivity++;
secondsToday += calculateActivityLength(currentActivity);
if (currentActivity >= goal) break;
}
if (currentActivity >= goal) break;
days++;
currentActivity -= 4.8;
}
const actionsDecrease = Math.floor(
days * 4.8 + convertTime("s h", secondsToday) / 5
);
const totalTime = secondsPerDay * days + secondsToday;
const now = new Date();
const tomorrow = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + 1
);
const secondsToTomorrow = convertTime("ms s", tomorrow - now);
if (days === 0 && secondsToday > secondsToTomorrow) days++;
const targetDate = new Date(Date.now() + convertTime("d ms", days));
return {
actions: actionsWithoutDecrease + actionsDecrease,
time: secondsToTime(totalTime),
date:
targetDate.getDate() +
" " +
months[targetDate.getMonth()] +
" " +
targetDate.getFullYear(),
};
}
function updateGoalProgress() {
if (progress.stage === activityStages.length - 1) {
document.getElementById("goal-progress").style.display = "none";
return;
}
const goalIndex = Number(document.getElementById("activity-list").value);
const result = calculateRemainingTime(
progress.doneFromZero,
activityStages[goalIndex].fromZero,
activitySettings[catId].hours
);
document.querySelector("#goal-progress > ul").innerHTML = `
<li>${result.actions} ${declensionOfNumber(result.actions, [
"переход",
"перехода",
"переходов",
])} (${result.time})</li>
<li>будет достигнута ${result.date}</li>
`;
}
const activity = document
.querySelector("#act_name b")
.textContent.split(" (");
const progress = {};
const currentStage = activityStages.find(
(stage) => stage.name === activity[0]
);
if (currentStage) {
progress.doneFromZero =
currentStage.fromZero + Number(activity[1].split("/")[0]);
progress.stage = activityStages.indexOf(currentStage);
}
const activityInfoHTML =
// html
`
<details id="calculator-activity" class="calculator-style">
<summary id="open-calculator"><b>Калькулятор активности</b></summary>
<div id="calculator-content" style="margin-top: 10px;">
<p id="congratulations" style="display:none"></p>
<div id="activity-length"><b>Переход</b>: ${secondsToTime(
calculateActivityLength(progress.doneFromZero)
)}</div>
<div>Мой переход изменён на <input id="minus" type="number" value="${
activitySettings[catId].minus || 0
}" min="-60" max="10" step="1" style="width: 50px;"> <span id="minus-word"></span></nobr></div>
<div>Я качаю активность <input id="hours-per-day" type="number" step="0.25" min="0" max="24" value="${
activitySettings[catId].hours
}" style="width: 60px"> <span id="hour-word"></span> в сутки</div>
<div id="goal-progress">
<b>Цель: <select style="display: inline" id="activity-list"></select></b>:
<ul style="margin: 0.5em"></ul>
</div>
<div id="to-fall-container" style="display: none;">Переход начнёт падать <span id="to-fall"></span></div>
</div>
</details>
`;
document
.getElementById("info")
.insertAdjacentHTML("afterend", activityInfoHTML);
if (activitySettings[catId].opened) {
document.getElementById("calculator-activity").open = true;
}
for (let i = progress.stage + 1; i < activityStages.length; i++) {
const option = document.createElement("option");
option.value = i;
option.textContent = activityStages[i].name;
document.getElementById("activity-list").appendChild(option);
}
function showCongratulations() {
const activityList = document.getElementById("activity-list");
const nextGoalName = activityList.options[activityList.selectedIndex].text;
document.getElementById("congratulations").innerHTML =
/* HTML */
`
Цель
<b>«${activityStages[activitySettings[catId].goal].name}»</b>
достигнута!
<br />
Ваша новая цель: <b>«${nextGoalName}»</b>
<center><img src="/img/stickers/systempaw3/6.png" /></center>
<input id="congratulations-button" type="button" value="Скрыть" />
<br /><input id="never-show-congratulations" type="checkbox" /> Больше
не поздравлять на этом персонаже
`;
document.getElementById("congratulations").style.display = "block";
document
.getElementById("congratulations-button")
.addEventListener("click", function () {
document.getElementById("congratulations").style.display = "none";
activitySettings[catId].goal = Number(
document.getElementById("activity-list").value
);
activitySettings[catId].noGrats = document.getElementById(
"never-show-congratulations"
).checked;
saveData(activitySettings);
});
}
if (
activitySettings[catId].goal > progress.stage ||
activitySettings[catId].noGrats
) {
const goalOption = document.querySelector(
`#activity-list > [value="${activitySettings[catId].goal}"]`
);
if (goalOption) {
goalOption.selected = true;
}
} else if (activitySettings[catId].goal) {
showCongratulations();
}
if (activitySettings[catId].minus) {
document.getElementById("minus").value = activitySettings[catId].minus;
}
const hours = activitySettings[catId].hours;
const minusValue = activitySettings[catId].minus || 0;
updateHourWord(hours);
updateGoalProgress();
updateMinusWord(minusValue);
if (calculateActivityLength(progress.doneFromZero) !== 45) {
document.getElementById("to-fall-container").style.display = "none";
} else {
const timeFall = new Date(
Date.now() + (progress.doneFromZero - 1575) * 5 * 3600000
);
document.getElementById("to-fall").innerHTML =
timeFall.getDate() +
" " +
months[timeFall.getMonth()] +
" " +
timeFall.getFullYear();
document.getElementById("to-fall-container").style.display = "block";
}
document.getElementById("minus").addEventListener("change", function () {
activitySettings[catId].minus = this.value;
saveData(activitySettings);
updateGoalProgress();
document.getElementById(
"activity-length"
).innerHTML = `<b>Переход</b>: ${secondsToTime(
calculateActivityLength(progress.doneFromZero)
)}`;
updateMinusWord(this.value);
});
document
.getElementById("activity-list")
.addEventListener("change", function () {
activitySettings[catId].goal = Number(this.value);
saveData(activitySettings);
updateGoalProgress();
});
document
.getElementById("hours-per-day")
.addEventListener("input", function () {
const hours = Number(this.value);
if (hours < 0 || hours > 24 || !Number.isInteger(hours * 1000)) {
this.value = activitySettings[catId].hours;
return;
}
activitySettings[catId].hours = hours;
saveData(activitySettings);
updateHourWord(hours);
updateGoalProgress();
});
document
.getElementById("open-calculator")
.addEventListener("click", function () {
activitySettings[catId].opened = !document.getElementById(
"calculator-activity"
).open;
saveData(activitySettings);
});
function saveData(data) {
uwuStorage.setItem("uwu_activity", data);
}
function declensionOfNumber(number, titles) {
const cases = [2, 0, 1, 1, 1, 2];
const intNumber = Math.floor(Math.abs(number));
return titles[
intNumber % 100 > 4 && intNumber % 100 < 20
? 2
: cases[intNumber % 10 < 5 ? intNumber % 10 : 5]
];
}
function convertTime(from, value) {
const factors = {
ms: 1,
s: 1000,
m: 60000,
h: 3600000,
d: 86400000,
};
const [fromUnit, toUnit] = from.split(" ");
return (value * factors[fromUnit]) / factors[toUnit];
}
function secondsToTime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
let result = "";
if (days > 0) result += `${days} д `;
if (hours > 0) result += `${hours} ч `;
if (minutes > 0) result += `${minutes} мин `;
if (secs > 0 || result === "") result += `${secs} с`;
return result.trim();
}
function updateHourWord(hours) {
document.getElementById("hour-word").textContent = declensionOfNumber(
hours,
["час", "часа", "часов"]
);
}
function updateMinusWord(minusValue) {
document.getElementById("minus-word").textContent = declensionOfNumber(
minusValue,
["секунду", "секунды", "секунд"]
);
}
}
// ====================================================================================================================
// . . . ПИСЬМА . . .
// ====================================================================================================================
if (targetLs.test(window.location.href)) {
if (settings.lsWrapPreview) {
setupMutationObserver("#main", setupPreviewButton, {
childList: true,
subtree: true,
});
}
}
// ====================================================================================================================
// . . . БЛОГИ . . .
// ====================================================================================================================
if (targetBlog.test(window.location.href)) {
if (settings.commentPreview) {
setupMutationObserver("#site_table", addCommentPreview);
}
if (settings.moreCommentButtons) {
setupMutationObserver("#view_comments", addCommentButtons, {
childList: true,
subtree: true,
});
setupSingleCallback("#view_comments", handleCommentActions);
}
}
// ====================================================================================================================
// . . . ЛЕНТА . . .
// ====================================================================================================================
if (targetSniff.test(window.location.href)) {
if (settings.commentPreview) {
setupMutationObserver("#site_table", addCommentPreview);
}
if (settings.moreCommentButtons) {
setupMutationObserver("#view_comments", addCommentButtons, {
childList: true,
subtree: true,
});
setupSingleCallback("#view_comments", handleCommentActions);
}
}
// ====================================================================================================================
// . . . ПРЕДПРОСМОТР КОММЕНТАРИЯ . . .
// ====================================================================================================================
function addCommentPreview() {
const form = document.querySelector("#send_comment_form");
if (!form || document.getElementById("comment-preview")) return;
const lastParagraph = form.querySelector("p:last-child");
lastParagraph.insertAdjacentHTML(
"afterbegin",
`
<input type="button" id="comment-preview" value="Предпросмотр">
`
);
form.insertAdjacentHTML(
"afterend",
`
<p id="comment-preview-hide" style="display: none; margin: 0.5em 0;"><a href="#">Скрыть предпросмотр</a></p>
<div id="comment-preview-div" style="display: none;"></div>
`
);
const previewButton = document.getElementById("comment-preview");
const hideParagraph = document.getElementById("comment-preview-hide");
const previewDiv = document.getElementById("comment-preview-div");
const ws = io(window.location.origin, {
path: "/ws/blogs/socket.io",
reconnectionDelay: 10000,
reconnectionDelayMax: 20000,
});
ws.on("creation preview", (data) => {
previewDiv.innerHTML = data;
previewDiv.style.display = "block";
hideParagraph.style.display = "block";
});
previewButton.addEventListener("click", function () {
const commentText = document.getElementById("comment").value;
ws.emit("creation preview", commentText);
});
form
.querySelector('[type="submit"]')
.addEventListener("click", hideCommentPreview);
hideParagraph.addEventListener("click", function (e) {
e.preventDefault();
hideCommentPreview();
});
function hideCommentPreview() {
hideParagraph.style.display = "none";
previewDiv.innerHTML = "";
previewDiv.style.display = "none";
}
}
// ====================================================================================================================
// . . . КНОПКИ ОТВЕТИТЬ И ЦИТИРОВАТЬ . . .
// ====================================================================================================================
function addCommentButtons() {
const comments = document.querySelectorAll("#view_comments .view-comment");
comments.forEach((comment) => {
if (!comment.querySelector(".comment-answer-buttons")) {
comment.insertAdjacentHTML(
"beforeend", // html
`
<p class="comment-answer-buttons">
<a class="comment-answer" href="#">Ответить</a>
<span class="comment-cite-wrap"> | <a class="comment-cite" href="#">Цитировать</a></span>
</p>
`
);
}
});
}
function getCommentInfo(comment) {
const commentId = comment.getAttribute("data-id");
const commentNum = comment.querySelector(".num").textContent;
const authorLink = comment.querySelector(".comment-info a.author");
const authorSpan = comment.querySelector(".comment-info span[data-id]");
const authorName = authorLink
? authorLink.textContent
: authorSpan
? authorSpan.textContent
: "...";
const authorProfile = authorLink
? authorLink.getAttribute("href").replace("/cat", "")
: null;
const commentText = comment.querySelector(".comment-text .parsed").innerText;
const commentInfo = comment.querySelector(".comment-info");
const commentTime = commentInfo.innerHTML
.split("</b>")[1]
.split(" @")[0]
.trim();
return {
commentId,
commentNum,
authorName,
authorProfile,
commentText,
commentTime,
};
}
function handleAnswerAction(commentInfo) {
const textarea = document.getElementById("comment");
const currentText = textarea.value;
const newText = commentInfo.authorProfile
? `[link${commentInfo.authorProfile}] (#${commentInfo.commentNum}), `
: `[b][code]${commentInfo.authorName}[/code][/b] (#${commentInfo.commentNum}), `;
textarea.value = currentText + newText;
}
function handleCiteAction(commentInfo) {
const selectedText = window.getSelection().toString().trim();
const quoteText = selectedText ? selectedText : commentInfo.commentText;
const profileLink = commentInfo.authorProfile
? `[link${commentInfo.authorProfile}]`
: commentInfo.authorName;
const quote = `[table][tr][td][size=10][i]Цитата:[/i] [b]#${commentInfo.commentNum}[/b] ${commentInfo.commentTime} @ ${profileLink}[/size][/td][/tr][tr][td][table=0][tr][td] [/td][td]${quoteText}[/td][/tr][/table][/td][/tr][/table]`;
const textarea = document.getElementById("comment");
const currentText = textarea.value;
textarea.value = currentText + quote;
}
function handleCommentActions() {
const viewComments = document.getElementById("view_comments");
viewComments.addEventListener("click", function (event) {
const target = event.target;
const actionMap = {
"comment-answer": handleAnswerAction,
"comment-cite": handleCiteAction,
};
for (const className in actionMap) {
if (target.classList.contains(className)) {
event.preventDefault();
const comment = target.closest(".view-comment");
const commentInfo = getCommentInfo(comment);
actionMap[className](commentInfo);
break;
}
}
});
}
// ====================================================================================================================
// . . . КРАСИВЫЙ ПРЕДПРОСМОТР ПИСЬМА . . .
// ====================================================================================================================
function setupPreviewButton() {
const previewButton = document.getElementById("preview");
if (previewButton) {
previewButton.addEventListener("click", wrapPreviewInTable);
}
}
function wrapPreviewInTable() {
const previewDiv = document.getElementById("preview_div");
if (!previewDiv) return;
const mainElement = document.getElementById("main");
const senderId = mainElement.getAttribute("data-id");
const senderLogin = mainElement.getAttribute("data-login");
const subject = document.getElementById("subject").value;
const currentDate = new Date().toLocaleString("ru-RU", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
const newTable = document.createElement("table");
newTable.border = "1";
newTable.style.width = "90%";
newTable.style.maxWidth = "500px";
newTable.innerHTML =
// html
`
<tbody>
<tr><td id="preview-subject" colspan="2">${subject}</td></tr>
<tr>
<td valign="top" id="msg_info">
Отправитель: <span id="preview-sender"><a href="cat${senderId}">${senderLogin}</a></span>
<br>${currentDate}
<br>Переписка: <u><big><b>+</b></big></u> …
</td>
<td id="preview-text">${previewDiv.outerHTML}</td>
</tr>
</tbody>
`;
const existingTable = document.querySelector("table");
if (existingTable) {
existingTable.parentNode.replaceChild(newTable, existingTable);
} else {
previewDiv.parentNode.insertBefore(newTable, previewDiv);
previewDiv.style.display = "none";
}
}
// ====================================================================================================================
// . . . ШАБЛОНЫ . . .
// ====================================================================================================================
function initializeTemplates() {
if (!settings.showTemplates) return;
const templateContainer =
/* HTML */
`
<div id="uwu-templates">
<h2>ШАБЛОНЫ</h2>
<div id="uwu-templates-list"></div>
<div class="button-container">
<button id="create-template-button" class="uwu-button install-button">
Создать шаблон ✎
</button>
</div>
</div>
`;
const templateItem =
/* HTML */
`
<div class="uwu-template-item">
<div class="template-name-container">
<span class="template-name"></span>
<button
class="rename-button uwu-button install-button"
title="Переименовать шаблон"
>
✎
</button>
</div>
<div class="template-actions-container">
<button
class="update-button uwu-button install-button"
title="Обновить шаблон"
>
↻
</button>
<button
class="remove-button uwu-button install-button"
title="Удалить шаблон"
>
X
</button>
</div>
</div>
`;
const cssStyles =
/* CSS */
`
#uwu-templates {
font-family: Montserrat;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
background-color: #242424;
margin-bottom: 5px;
margin-top: 5px;
color: #d5d5d5;
}
#uwu-templates > h2 {
font-size: 2em;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
letter-spacing: 20px;
}
#uwu-templates-list {
max-height: 220px;
overflow-x: auto;
border-radius: 20px;
background-color: #2e2e2e;
}
.uwu-template-item {
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
#uwu-templates > div.button-container {
display: flex;
justify-content: flex-end;
padding-left: 10px;
padding-right: 4px;
padding-top: 5px;
padding-bottom: 5px;
}
.template-name {
cursor: pointer;
text-decoration: underline;
}
.uwu-button {
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.uwu-button:hover {
background-color: rgba(255, 255, 255, 0.2);
}
`;
document.head.insertAdjacentHTML("beforeend", `<style>${cssStyles}</style>`);
function setupTemplates(
targetElementId,
contentElementId,
subjectElementId = null,
pageType
) {
const targetElement = document.getElementById(targetElementId);
if (!document.getElementById("uwu-templates")) {
if (targetElementId === "mess_form") {
targetElement.insertAdjacentHTML("beforeend", templateContainer);
} else {
targetElement.insertAdjacentHTML("afterbegin", templateContainer);
}
}
const templatesList = document.getElementById("uwu-templates-list");
const createButton = document.getElementById("create-template-button");
createButton.addEventListener("click", (event) => {
event.preventDefault();
createTemplate(contentElementId, subjectElementId, pageType);
});
function createTemplate(contentElementId, subjectElementId, pageType) {
const templateName = prompt("Введите название шаблона:");
if (templateName) {
const template = {
name: templateName,
content:
document.getElementById(contentElementId).innerText ||
document.getElementById(contentElementId).value,
subject: subjectElementId
? document.getElementById(subjectElementId).value || ""
: "",
pageType: pageType,
};
saveTemplate(template);
renderTemplates(pageType);
}
}
function saveTemplate(template) {
if (!uwuStorage.getItem("uwu_templates")) {
uwuStorage.setItem("uwu_templates", []);
}
const templates = uwuStorage.getItem("uwu_templates");
templates.push(template);
uwuStorage.setItem("uwu_templates", templates);
}
function renderTemplates(pageType) {
const templates = uwuStorage.getItem("uwu_templates") || [];
templatesList.innerHTML = "";
templates.forEach((template, index) => {
if (template.pageType === pageType) {
const templateItemHTML = document.createElement("div");
templateItemHTML.innerHTML = templateItem;
const templateItemElement = templateItemHTML.children[0];
const templateName =
templateItemElement.querySelector(".template-name");
templateName.textContent = template.name;
templateName.addEventListener("click", () => {
if (document.getElementById(contentElementId).tagName === "DIV") {
document.getElementById(contentElementId).innerText =
template.content;
} else {
document.getElementById(contentElementId).value =
template.content;
}
if (subjectElementId) {
document.getElementById(subjectElementId).value =
template.netbject || "";
}
});
const renameButton =
templateItemElement.querySelector(".rename-button");
renameButton.addEventListener("click", () => renameTemplate(index));
const updateButton =
templateItemElement.querySelector(".update-button");
updateButton.addEventListener("click", () =>
updateTemplate(index, contentElementId, subjectElementId)
);
const removeButton =
templateItemElement.querySelector(".remove-button");
removeButton.addEventListener("click", () => removeTemplate(index));
templatesList.appendChild(templateItemElement);
}
});
}
function renameTemplate(index) {
const newName = prompt("Введите новое название шаблона:");
if (newName) {
const templates = uwuStorage.getItem("uwu_templates");
templates[index].name = newName;
uwuStorage.setItem("uwu_templates", templates);
renderTemplates(pageType);
}
}
function updateTemplate(index, contentElementId, subjectElementId) {
const templates = uwuStorage.getItem("uwu_templates");
if (document.getElementById(contentElementId).tagName === "DIV") {
templates[index].content =
document.getElementById(contentElementId).innerText;
} else {
templates[index].content =
document.getElementById(contentElementId).value;
}
if (subjectElementId) {
templates[index].netbject =
document.getElementById(subjectElementId).value || "";
}
uwuStorage.setItem("uwu_templates", templates);
renderTemplates(pageType);
}
function removeTemplate(index) {
const confirmation = confirm(
"Вы уверены, что хотите удалить этот шаблон?"
);
if (confirmation) {
const templates = uwuStorage.getItem("uwu_templates");
templates.splice(index, 1);
uwuStorage.setItem("uwu_templates", templates);
renderTemplates(pageType);
}
}
renderTemplates(pageType);
}
function checkUrlAndSetup() {
if (targetLsNew.test(window.location.href) && settings.templatesInLs) {
setupSingleCallback("#write_form", () =>
setupTemplates("write_div", "text", "subject", "ls")
);
} else if (
(targetBlogsCreation.test(window.location.href) ||
targetSniffCreation.test(window.location.href)) &&
settings.templatesInBlogsAndSniffs
) {
setupSingleCallback(".creation_form", () =>
setupTemplates(
"creation_form",
"creation-text",
"creation-title",
"blogsAndSniffs"
)
);
} else if (
targetChats.test(window.location.href) &&
settings.templatesInChats
) {
setupSingleCallback("#mess_form", () =>
setupTemplates("mess_form", "mess", null, "chat")
);
}
}
setupMutationObserver("#main", checkUrlAndSetup, {
childList: true,
attributes: true,
});
setupMutationObserver("#branch", checkUrlAndSetup, {
childList: true,
});
}
initializeTemplates();
// ====================================================================================================================
// . . . СОХРАНЕНИЕ ЛИЧНЫХ СООБЩЕНИЙ . . .
// ====================================================================================================================
if (targetLs.test(window.location.href) && settings.savingLS) {
// console.log("UwU | Модуль сохранения ЛС активен.");
/**
* Отображает сохраненное сообщение в контейнере.
* @param {string} lsId - ID сообщения для отображения.
*/
function displaySavedMessage(lsId) {
const container = document.getElementById("uwu-saved-ls-container");
if (!container) return;
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
const ls = savedLs[lsId];
if (!ls) {
container.innerHTML =
"<h3>Ошибка: Сохранённое сообщение не найдено.</h3><p><a href='#' id='uwu-back-to-saved-list'>Назад к списку</a></p>";
document
.getElementById("uwu-back-to-saved-list")
.addEventListener("click", showSavedMessagesInterface);
return;
}
const typeLabel = ls.type === 0 ? "Отправитель" : "Получатель";
const catLink = `<a href="/cat${ls.catId}" id="msg_login">${ls.catName}</a>`;
const messageHTML = `
<p><a href="#" id="uwu-back-to-saved-list">← Назад к сохранённым</a></p>
<table id="msg_table" border="1">
<tbody>
<tr>
<td colspan="2"><span id="msg_subject">${ls.subject}</span></td>
</tr>
<tr>
<td valign="top" id="msg_info">
${typeLabel}: ${catLink}<br>
${ls.date}<br>
<i>(сохранённая оффлайн-копия)</i>
</td>
<td><div class="parsed">${ls.text}</div></td>
</tr>
</tbody>
</table>
`;
container.innerHTML = messageHTML;
document
.getElementById("uwu-back-to-saved-list")
.addEventListener("click", showSavedMessagesInterface);
}
/**
* Удаляет сохраненное ЛС из localStorage по его ID.
* @param {number} lsId - ID личного сообщения для удаления.
* @param {boolean} silent - Если true, не показывать alert.
*/
function deleteSavedLS(lsId, silent = false) {
try {
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
if (savedLs.hasOwnProperty(lsId)) {
delete savedLs[lsId];
uwuStorage.setItem("uwu_saved_ls", savedLs);
if (!silent) {
alert("Сохранённая переписка удалена.");
}
if (window.location.search.includes(`?id=${lsId}`)) {
addSaveButtonsToMessagePage();
}
updateSavedLsCount();
}
} catch (error) {
console.error("UwU | Ошибка при удалении ЛС:", error);
if (!silent) {
alert("Произошла ошибка при удалении переписки.");
}
}
}
/**
* Сохраняет текущее открытое ЛС в uwuStorage.
*/
function saveCurrentLS() {
try {
const mainDiv = document.getElementById("main");
const lsId = parseInt(window.location.href.split("=")[1], 10);
if (isNaN(lsId)) return;
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
const ls = {};
ls.subject = document.getElementById("msg_subject").textContent;
ls.text = document.querySelector(".parsed").innerHTML;
const msgInfo = document.getElementById("msg_info");
const dateMatch = msgInfo.innerHTML.match(
/\d{1,2} [а-я]+ \d{4} в \d{2}:\d{2}/
);
ls.date = dateMatch ? dateMatch[0] : new Date().toLocaleString();
ls.catId = parseInt(
document
.getElementById("msg_login")
.getAttribute("href")
.match(/\d+/)[0],
10
);
ls.catName = document.getElementById("msg_login").textContent;
ls.myId = mainDiv.dataset.id;
ls.myName = mainDiv.dataset.login;
ls.type = /Отправитель:/.test(msgInfo.innerHTML) ? 0 : 1;
const now = new Date();
ls.savedate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
2,
"0"
)}-${String(now.getDate()).padStart(2, "0")} ${String(
now.getHours()
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(
now.getSeconds()
).padStart(2, "0")}`;
savedLs[lsId] = ls;
uwuStorage.setItem("uwu_saved_ls", savedLs);
alert("Переписка успешно сохранена!");
addSaveButtonsToMessagePage();
updateSavedLsCount();
} catch (error) {
console.error("UwU | Ошибка при сохранении ЛС:", error);
alert("Произошла ошибка при сохранении переписки.");
}
}
/**
* Добавляет кнопки управления сохранением на страницу просмотра ЛС.
*/
function addSaveButtonsToMessagePage() {
const subjectSpan = document.getElementById("msg_subject");
if (!subjectSpan) return;
const subjectTd = subjectSpan.closest("td");
if (!subjectTd) return;
const oldButtons = document.getElementById("uwu-ls-buttons");
if (oldButtons) oldButtons.remove();
const lsId = parseInt(window.location.href.split("=")[1], 10);
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
const isSaved = savedLs.hasOwnProperty(lsId);
const buttonsContainer = document.createElement("span");
buttonsContainer.id = "uwu-ls-buttons";
buttonsContainer.style.float = "right";
buttonsContainer.style.display = "inline-block";
const saveButton = document.createElement("input");
saveButton.type = "button";
saveButton.value = isSaved ? "Обновить" : "Сохранить";
saveButton.className = "uwu-button install-button";
saveButton.style.marginLeft = "5px";
saveButton.onclick = saveCurrentLS;
buttonsContainer.appendChild(saveButton);
if (isSaved) {
const deleteButton = document.createElement("input");
deleteButton.type = "button";
deleteButton.value = "Удалить";
deleteButton.className = "uwu-button remove-button";
deleteButton.style.marginLeft = "5px";
deleteButton.onclick = () => deleteSavedLS(lsId);
buttonsContainer.appendChild(deleteButton);
const savedDate = document.createElement("i");
savedDate.textContent = `Сохранено: ${savedLs[lsId].savedate}`;
savedDate.style.marginRight = "10px";
buttonsContainer.prepend(savedDate);
}
subjectTd.appendChild(buttonsContainer);
}
/**
* Обновляет счетчик сохраненных сообщений во вкладке.
*/
function updateSavedLsCount() {
const counter = document.getElementById("uwu-saved-ls-count");
if (!counter) return;
const savedLs = uwuStorage.getItem("uwu_saved_ls") || {};
counter.textContent = Object.keys(savedLs).length;
}
/**
* Отображает интерфейс с сохраненными сообщениями.
*/
function showSavedMessagesInterface(event) {
if (event) event.preventDefault();
console.log("UwU | Открываю вкладку сохранённых ЛС.");
document.getElementById("main").style.display = "none";
document
.querySelectorAll("#links a")
.forEach((a) => a.classList.remove("active"));
document.getElementById("uwu-saved-ls-tab").classList.add("active");
let container = document.getElementById("uwu-saved-ls-container");
if (!container) {
container = document.createElement("div");
container.id = "uwu-saved-ls-container";
document.getElementById("main").after(container);
}
container.style.display = "block";
renderSavedMessagesList(container);
}
/**
* Скрывает интерфейс сохраненных сообщений и показывает стандартный.
*/
function hideSavedMessagesInterface() {
const container = document.getElementById("uwu-saved-ls-container");
if (container) container.style.display = "none";
document.getElementById("main").style.display = "block";
document.getElementById("uwu-saved-ls-tab")?.classList.remove("active");
}
/**
* Внедряет CSS-стили для интерфейса сохранения ЛС.
*/
function injectLSSyles() {
if (document.getElementById("uwu-ls-styles")) return;
const css =
/* CSS */
`
#uwu-saved-ls-tab {
padding: 2px 8px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.3s ease;
text-decoration: none !important;
}
#uwu-saved-ls-tab:hover, #uwu-saved-ls-tab.active {
background-color: rgba(255, 255, 255, 0.2);
}
#uwu-saved-ls-container .messList {
table-layout: fixed;
width: 100%;
}
#uwu-saved-ls-container .messList a {
color: #0000cd;
}
#uwu-saved-ls-container .messList th:nth-child(1) { width: 50%; }
#uwu-saved-ls-container .messList th:nth-child(2) { width: 25%; }
#uwu-saved-ls-container .messList th:nth-child(3) { width: 20%; }
#uwu-saved-ls-container .messList th:nth-child(4) { width: 5%; }
#uwu-saved-ls-container .delete-saved-ls {
padding: 1px 7px;
}
`;
const styleElement = document.createElement("style");
styleElement.id = "uwu-ls-styles";
styleElement.textContent = css;
document.head.appendChild(styleElement);
}
/**
* Отрисовывает список сохраненных сообщений в указанном контейнере.
* @param {HTMLElement} container - Элемент для отрисовки.
*/
function renderSavedMessagesList(container) {
const savedLsRaw = uwuStorage.getItem("uwu_saved_ls");
const savedLs = savedLsRaw || {};
const keys = Object.keys(savedLs);
const storageSize = savedLsRaw
? (new TextEncoder().encode(savedLsRaw).length / 1024 / 1024).toFixed(2)
: 0;
if (keys.length === 0) {
container.innerHTML = "<h3>У вас нет сохранённых сообщений.</h3>";
return;
}
let inboxHTML = "";
let outboxHTML = "";
keys.sort(
(a, b) => new Date(savedLs[b].savedate) - new Date(savedLs[a].savedate)
);
keys.forEach((key) => {
const ls = savedLs[key];
const rowHTML =
/* HTML */
`
<tr class="msg_read">
<td>
<a href="#" class="uwu-saved-msg-open" data-id="${key}"
>${ls.subject}</a
>
</td>
<td><a href="/cat${ls.catId}">${ls.catName}</a></td>
<td>${ls.savedate}</td>
<td>
<input
type="button"
value="X"
class="uwu-button remove-button delete-saved-ls"
data-id="${key}"
title="Удалить"
/>
</td>
</tr>
`;
if (ls.type === 0) {
inboxHTML += rowHTML;
} else {
outboxHTML += rowHTML;
}
});
container.innerHTML =
/* HTML */
`
<p style="text-align: center; color: #888;">
Использовано примерно ${storageSize} из 5.00 МБ дискового
пространства.
</p>
<h2>Входящие</h2>
<table class="messList">
<tbody>
<tr>
<th>Тема</th>
<th>Отправитель</th>
<th>Дата сохранения</th>
<th></th>
</tr>
${inboxHTML}
</tbody>
</table>
<br />
<h2>Отправленные</h2>
<table class="messList">
<tbody>
<tr>
<th>Тема</th>
<th>Получатель</th>
<th>Дата сохранения</th>
<th></th>
</tr>
${outboxHTML}
</tbody>
</table>
`;
container.querySelectorAll(".delete-saved-ls").forEach((button) => {
button.addEventListener("click", (e) => {
const lsId = e.target.dataset.id;
if (
confirm(
"Вы уверены, что хотите удалить эту переписку из сохранённых?"
)
) {
deleteSavedLS(lsId, true);
renderSavedMessagesList(container);
}
});
});
container.querySelectorAll(".uwu-saved-msg-open").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const lsId = e.target.dataset.id;
displaySavedMessage(lsId);
});
});
}
/**
* Добавляет вкладку "Сохранённые" в меню ЛС.
*/
function addSavedMessagesTab() {
const linksContainer = document.getElementById("links");
if (!linksContainer || document.getElementById("uwu-saved-ls-tab")) return;
linksContainer.insertAdjacentHTML(
"beforeend",
` | <a href="#" id="uwu-saved-ls-tab">Сохранённые (<span id="uwu-saved-ls-count">0</span>)</a>`
);
const savedTab = document.getElementById("uwu-saved-ls-tab");
savedTab.addEventListener("click", showSavedMessagesInterface);
linksContainer.querySelectorAll("a:not(#uwu-saved-ls-tab)").forEach((a) => {
a.addEventListener("click", () => {
if (!a.href.includes("ls?id=")) {
hideSavedMessagesInterface();
}
});
});
updateSavedLsCount();
}
/**
* Функция-обработчик, которая определяет, что делать на странице ЛС.
*/
function initializeLSPageLogic() {
injectLSSyles();
if (
window.location.search.includes("?id=") &&
document.getElementById("msg_table")
) {
console.log("UwU | Обнаружена страница сообщения. Встраиваю кнопки...");
addSaveButtonsToMessagePage();
hideSavedMessagesInterface();
} else {
console.log("UwU | Обнаружена главная страница ЛС. Встраиваю вкладку...");
addSavedMessagesTab();
}
}
setupMutationObserver("#main", initializeLSPageLogic, { childList: true });
}