FEAT: Громкость промороликов

This commit is contained in:
_sega 2026-02-16 10:18:10 +03:00
parent ba782a9a9d
commit c49f7fa850
19 changed files with 1097 additions and 1167 deletions

View File

@ -11349,7 +11349,7 @@ Material:
- _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767}
- _EnvMatrixRotation: {r: 0, g: 0, b: 0, a: 0}
- _FaceColor: {r: 1, g: 1, b: 1, a: 1}
- _GlowColor: {r: 0, g: 1, b: 0.017659903, a: 1}
- _GlowColor: {r: 1, g: 0.8658613, b: 0, a: 1}
- _MaskCoord: {r: 0, g: 0, b: 32767, a: 32767}
- _OutlineColor: {r: 0.554717, g: 0.554717, b: 0.554717, a: 1}
- _ReflectFaceColor: {r: 0, g: 0, b: 0, a: 1}

338
Assets/Resources/script.txt Normal file
View File

@ -0,0 +1,338 @@
(() => {
// =========================
// Utils / status
// =========================
const elGlobalStatus = document.getElementById("globalStatus");
const setStatus = (txt) => {
if (!elGlobalStatus) return;
elGlobalStatus.textContent = txt || "";
if (txt) setTimeout(() => (elGlobalStatus.textContent = ""), 1200);
};
const debounceMap = new Map();
const debounce = (key, fn, ms) => {
if (debounceMap.has(key)) clearTimeout(debounceMap.get(key));
debounceMap.set(key, setTimeout(fn, ms));
};
// Base URL from current page
const HTTP_BASE = `${location.protocol}//${location.host}`;
const WS_URL = (() => {
const proto = location.protocol === "https:" ? "wss" : "ws";
return `${proto}://${location.host}/ws`;
})();
// Helpful logs (можешь потом удалить)
console.log("Page:", location.href);
console.log("HTTP_BASE:", HTTP_BASE);
console.log("WS_URL:", WS_URL);
// =========================
// HTTP helpers
// =========================
async function postJson(path, payload) {
const r = await fetch(`${HTTP_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload || {}),
});
return r;
}
async function sendSetting(id, value) {
try {
const r = await postJson("/api/setting", { id, value });
if (!r.ok) {
setStatus("Ошибка применения");
return;
}
setStatus("Сохранено");
} catch (e) {
setStatus("Сервер недоступен");
}
}
async function sendCoins(amount) {
const coinsStatus = document.getElementById("coinsStatus");
try {
if (coinsStatus) coinsStatus.textContent = "";
const r = await postJson("/api/coins", { amount });
if (!r.ok) {
if (coinsStatus) coinsStatus.textContent = "Ошибка запроса";
return;
}
if (coinsStatus) {
coinsStatus.textContent = "Ок";
setTimeout(() => (coinsStatus.textContent = ""), 1200);
}
} catch (e) {
if (coinsStatus) coinsStatus.textContent = "Сервер недоступен";
}
}
async function sendAction(name, value) {
// value — опционально (для новой схемы: action + payload)
try {
const r = await postJson("/api/action", value !== undefined ? { name, value } : { name });
const t = await r.text();
if (!r.ok) {
setStatus("Ошибка действия");
try {
const j = JSON.parse(t);
if (j && j.message) setStatus(j.message);
} catch (_) {}
return false;
}
setStatus("Ок");
return true;
} catch (e) {
setStatus("Сервер недоступен");
return false;
}
}
// =========================
// Bind UI events (existing page)
// =========================
// Settings controls by data-setting-id
document.querySelectorAll("[data-setting-id]").forEach((el) => {
const id = el.getAttribute("data-setting-id");
if (!id) return;
if (el.type === "checkbox") {
el.addEventListener("change", () => sendSetting(id, el.checked ? "on" : "off"));
return;
}
if (el.type === "range") {
const valueSpanId = el.getAttribute("data-value-span");
const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null;
const apply = () => {
if (valueSpan) valueSpan.textContent = el.value;
debounce("rng_" + id, () => sendSetting(id, el.value), 120);
};
el.addEventListener("input", apply);
el.addEventListener("change", apply);
return;
}
if (el.type === "radio") {
el.addEventListener("change", () => {
if (el.checked) sendSetting(id, el.value);
});
return;
}
// number/text
el.addEventListener("change", () => sendSetting(id, el.value));
});
// Coins: input + button
const coinsAmount = document.getElementById("coinsAmount");
const addCoinsBtn = document.getElementById("addCoinsBtn");
if (addCoinsBtn) {
addCoinsBtn.addEventListener("click", async () => {
const v = parseInt((coinsAmount && coinsAmount.value) || "0", 10);
if (!Number.isFinite(v) || v <= 0) {
const coinsStatus = document.getElementById("coinsStatus");
if (coinsStatus) coinsStatus.textContent = "Введи число > 0";
return;
}
await sendCoins(v);
});
}
// Actions (buttons)
document.querySelectorAll("[data-action-name]").forEach((btn) => {
btn.addEventListener("click", async () => {
const name = btn.getAttribute("data-action-name") || "";
if (!name) return;
// Если кнопка "Сохранить" в карточках цен/валют — берём значение инпута рядом
// (не влияет на обычные action-btn)
let value;
const card = btn.closest(".price-card, .currency-card");
if (card) {
const input = card.querySelector("input");
if (input) value = input.value;
}
try {
btn.disabled = true;
await sendAction(name, value);
} finally {
setTimeout(() => (btn.disabled = false), 200);
}
});
});
// =========================
// WebSocket (preparing for new scheme)
// =========================
let ws = null;
let wsTimer = null;
let wsBackoffMs = 500;
function wsConnect() {
if (ws) {
try { ws.close(); } catch (_) {}
ws = null;
}
if (wsTimer) {
clearTimeout(wsTimer);
wsTimer = null;
}
console.log("WS connecting:", WS_URL);
try {
ws = new WebSocket(WS_URL);
} catch (e) {
console.log("WS create failed:", e);
scheduleReconnect();
return;
}
ws.addEventListener("open", () => {
console.log("WS open");
wsBackoffMs = 500;
// На будущее: можно отправлять hello/subscribe
// ws.send(JSON.stringify({ op: "hello", page: "settings" }));
});
ws.addEventListener("message", (ev) => {
// Сервер дальше будет слать JSON snapshot/patch
try {
const msg = JSON.parse(ev.data);
handleWsMessage(msg);
} catch (e) {
console.log("WS message not json:", ev.data);
}
});
ws.addEventListener("close", (e) => {
console.log("WS close", e.code, e.reason);
scheduleReconnect();
});
ws.addEventListener("error", (e) => {
console.log("WS error", e);
// Обычно после error будет close, но подстрахуемся
scheduleReconnect();
});
}
function scheduleReconnect() {
if (wsTimer) return;
const delay = Math.min(wsBackoffMs, 5000);
wsBackoffMs = Math.min(wsBackoffMs * 1.6, 5000);
wsTimer = setTimeout(() => {
wsTimer = null;
wsConnect();
}, delay);
}
// ===== WS message protocol (заготовка) =====
// Ожидаем:
// { op:"snapshot", data:{ counters:{}, hints:{}, characterPrizes:[] } }
// { op:"patch", data:{ counters:{...} } }
function handleWsMessage(msg) {
if (!msg || !msg.op) return;
if (msg.op === "snapshot" || msg.op === "patch") {
const data = msg.data || {};
if (data.counters) applyCounters(data.counters);
if (data.hints) applyHints(data.hints);
if (data.characterPrizes) applyCharacterPrizes(data.characterPrizes);
if (data.passwordStatus) applyPasswordStatus(data.passwordStatus);
return;
}
if (msg.op === "toast") {
if (msg.text) setStatus(String(msg.text));
return;
}
}
// =========================
// Apply data to DOM (no visual changes)
// =========================
// counters: { key: value }
// Важно: сейчас твои счетчики — это .form-group.counter где внутри label + .status
// Мы ищем по тексту label (как fallback), но лучше позже добавить data-counter-key в html.
function applyCounters(counters) {
// 1) special ids if exist
if (counters.impulsesResetCurrent !== undefined) {
const el = document.getElementById("impulsesResetCurrent");
if (el) el.textContent = String(counters.impulsesResetCurrent);
}
// 2) generic: match label text -> value
document.querySelectorAll(".form-group.counter").forEach((fg) => {
const label = fg.querySelector("label");
const st = fg.querySelector(".status");
if (!label || !st) return;
const key = label.textContent.trim();
if (Object.prototype.hasOwnProperty.call(counters, key)) {
st.textContent = String(counters[key]);
}
});
}
function applyHints(hints) {
// пример: { impulsesResetHint: "..." }
if (hints.impulsesResetHint !== undefined) {
const el = document.getElementById("impulsesResetHint");
if (el) el.textContent = String(hints.impulsesResetHint);
}
}
// characterPrizes: [{ character:"...", prize:"..." }, ...] или массив строк
function applyCharacterPrizes(list) {
const root = document.getElementById("characterPrizes");
const empty = document.getElementById("characterPrizesEmpty");
if (!root) return;
root.innerHTML = "";
if (!list || !list.length) {
if (empty) empty.style.display = "block";
return;
}
if (empty) empty.style.display = "none";
// вывод без смены стиля: просто строки в .status
list.forEach((row) => {
const line = document.createElement("div");
line.className = "status";
if (typeof row === "string") {
line.textContent = row;
} else if (row && typeof row === "object") {
const c = row.character ?? row.name ?? "";
const p = row.prize ?? row.value ?? "";
line.textContent = `${c}: ${p}`.trim();
} else {
line.textContent = String(row);
}
root.appendChild(line);
});
}
function applyPasswordStatus(statusText) {
const el = document.getElementById("settingsPasswordStatus");
if (el && statusText !== undefined) el.textContent = String(statusText);
}
// Start WS
wsConnect();
})();

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b86e707637282a74aa5de600a2b27be6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang='ru'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' href='style.css'>
<title>Настройки Фишки</title>
</head>
<body>
<h1>Настройки фишки</h1>
<!--Баланс-->
<details class="block">
<summary class="block-title">Баланс и выручка</summary>
<div class='panel'>
<div class='form-group counter'>
<label>Текущий баланс</label>
<div class='status' data-counter-key="Balance">650</div>
</div>
<div class='coins-row'>
<input id='coinsAmount' type='number' value='10' min='1' step='1' class='coins-input' />
<button id='addCoinsBtn' type='button' class='coins-btn'>Добавить монеты</button>
</div>
<button type='button' class='action-btn' data-action-name='Сбросить баланс'>Сбросить баланс</button>
<div class='form-group counter'>
<label>Выручка</label>
<div class='status' data-counter-key="RevenueTotal">650</div>
</div>
<div class='form-group counter'>
<label>Выручка (обнуляемый)</label>
<div class='status' data-counter-key="RevenueReset">650</div>
</div>
<button type='button' class='action-btn' data-action-name='Cбросить счетчик выручки'>Cбросить счетчик
выручки</button>
</div>
</details>
<!--Цены-->
<details class="block">
<summary class="block-title">Цены</summary>
<div class='panel'>
<div class="form-group price-card" data-price-action="Цена игры с ботом">
<label>Цена игры с ботом</label>
<div class="coins-row price-row">
<input type="number" class="coins-input price-input" value="34" min="0" step="1">
<button type="button" class="coins-btn price-btn action-btn"
data-action-name="Цена игры с ботом">Сохранить</button>
</div>
</div>
<div class="form-group price-card" data-price-action="Цена игры на двоих">
<label>Цена игры на двоих</label>
<div class="coins-row price-row">
<input type="number" class="coins-input price-input" value="10" min="0" step="1">
<button type="button" class="coins-btn price-btn action-btn"
data-action-name="Цена игры на двоих">Сохранить</button>
</div>
</div>
<div class="form-group price-card" data-price-action="Цена реванша">
<label>Цена реванша</label>
<div class="coins-row price-row">
<input type="number" class="coins-input price-input" value="10" min="0" step="1">
<button type="button" class="coins-btn price-btn action-btn"
data-action-name="Цена реванша">Сохранить</button>
</div>
</div>
<div class='form-group'>
<label for='FreeplayID'>Бесплатная игра</label>
<input type='checkbox' id='FreeplayID' data-setting-id='Freeplay'>
</div>
</div>
</details>
<!--Валюты-->
<details class="block">
<summary class="block-title">Валюты</summary>
<div class='panel'>
<div class="form-group currency-card" data-currency-action="Название валюты">
<label>Название валюты</label>
<div class="coins-row currency-row">
<input type="text" class="coins-input currency-input" value="руб." maxlength="12">
<button type="button" class="coins-btn currency-btn action-btn"
data-action-name="Название валюты">Сохранить</button>
</div>
</div>
<div class="form-group currency-card" data-currency-action="Валюта за импульс">
<label>Валюта за импульс</label>
<div class="coins-row currency-row">
<input type="number" class="coins-input currency-input" value="10" min="0" step="1">
<button type="button" class="coins-btn currency-btn action-btn"
data-action-name="Валюта за импульс">Сохранить</button>
</div>
</div>
</div>
</details>
<!--Звук-->
<details class="block">
<summary class="block-title">Звук</summary>
<div class='panel'>
<div class='form-group'>
<label for='MusicVolumeID'>Громкость музыки: <span id='MusicVolumeID_VAL'>0.44</span></label>
<input id='MusicVolumeID' type='range' min='0' max='1' value='0.44' step='0.01'
data-setting-id='MusicVolume' data-value-span='MusicVolumeID_VAL'>
</div>
<div class='form-group'>
<label for='SoundsVolumeID'>Громкость эффектов и голоса: <span
id='SoundsVolumeID_VAL'>0.8699996</span></label>
<input id='SoundsVolumeID' type='range' min='0' max='1' value='0.8699996' step='0.01'
data-setting-id='SoundsVolume' data-value-span='SoundsVolumeID_VAL'>
</div>
<div class='form-group'>
<label for='PromoVolumeID'>Громкость проморолика(нереализовано): <span
id='PromoVolumeID_VAL'>0.8699996</span></label>
<input id='SoundsVolumePromoID' type='range' min='0' max='1' value='0.8699996' step='0.01'
data-setting-id='PromoVolume' data-value-span='PromoVolumeID_VAL'>
</div>
<div class='form-group'>
<label for='EnableVoicesID'>Голосовые реплики</label>
<input type='checkbox' id='EnableVoicesID' data-setting-id='EnableVoices'>
</div>
<div class='actions-grid'>
<button type='button' class='action-btn' data-action-name='Тест звука'>Тест звука</button>
</div>
</div>
</details>
<!--Призы-->
<details class="block">
<summary class="block-title">Призы и выдача</summary>
<div class='panel'>
<div class='actions-grid'>
<!-- Суперприз: как валюта (инпут + отправить) -->
<div class="form-group currency-card" data-currency-action="Суперприз">
<label>Суперприз</label>
<div class="coins-row currency-row">
<input type="text" class="coins-input currency-input" value="5000р" maxlength="24">
<button type="button" class="coins-btn currency-btn action-btn"
data-action-name="Суперприз">Сохранить</button>
</div>
</div>
<!-- Остаток призов: как цены (число + отправить) -->
<div class="form-group price-card" data-price-action="Внести остаток призов">
<label>Остаток призов</label>
<div class="coins-row price-row">
<input type="number" class="coins-input price-input" value="0" min="0" step="1">
<button type="button" class="coins-btn price-btn action-btn"
data-action-name="Внести остаток призов">Сохранить</button>
</div>
</div>
<!-- Сброс ошибки: оставить как есть -->
<button type='button' class='action-btn' data-action-name='Сбросить ошибку выдачи'>Сбросить ошибку
выдачи</button>
</div>
<div class='form-group'>
<label for='PrizingID'>Выдача призов при игре с ботом</label>
<input type='checkbox' id='PrizingID' data-setting-id='Prizing' checked>
</div>
<button type='button' class='action-btn' data-action-name='Тест хоппера'>Тест хоппера</button>
<button type='button' class='action-btn' data-action-name='Тест QR кодов'>Тест QR кодов(призовых)</button>
<div class='form-group'>
<label for='DisplayNoPrizesID'>Выводить призы закончились</label>
<input type='checkbox' id='DisplayNoPrizesID' data-setting-id='DisplayNoPrizes'>
</div>
<div class='form-group'>
<label for='DisplayPrizingErrorID'>Вывод ошибки выдачи в меню</label>
<input type='checkbox' id='DisplayPrizingErrorID' data-setting-id='DisplayPrizingError' checked>
</div>
<!-- Счетчики (ключи для WS) -->
<div class='form-group counter'><label>Марблс</label>
<div class='status' data-counter-key="Prizes.Marbles.Total">0</div>
</div>
<div class='form-group counter'><label>Марблс (обнуляемый)</label>
<div class='status' data-counter-key="Prizes.Marbles.Reset">0</div>
</div>
<div class='form-group counter'><label>Остаток призов</label>
<div class='status' data-counter-key="Prizes.Remaining">0</div>
</div>
<div class='form-group counter'><label>Суперприз</label>
<div class='status' data-counter-key="Prizes.Superprize.Value">5000р</div>
</div>
<div class='form-group counter'><label>Суперприз (обнуляемый)</label>
<div class='status' data-counter-key="Prizes.Superprize.Reset">0</div>
</div>
<div class='form-group counter'><label>Тикетник</label>
<div class='status' data-counter-key="Prizes.Ticket.Total">0</div>
</div>
<div class='form-group counter'><label>Тикетник (обнуляемый)</label>
<div class='status' data-counter-key="Prizes.Ticket.Reset">0</div>
</div>
<div class='form-group counter'><label>Хоппер</label>
<div class='status' data-counter-key="Prizes.Hopper.Total">0</div>
</div>
<div class='form-group counter'><label>Хоппер (обнуляемый)</label>
<div class='status' data-counter-key="Prizes.Hopper.Reset">0</div>
</div>
<button type='button' class='action-btn' data-action-name='Сбросить выданные призы'>Сбросить выданные
призы</button>
<div class='form-group'>
<label for='PrizingWhenErrorID'>Выдавать приз при ошибке</label>
<input type='checkbox' id='PrizingWhenErrorID' data-setting-id='PrizingWhenError' checked>
</div>
<button type='button' class='action-btn' data-action-name='Настроить призы персонажей'>Настроить призы
персонажей</button>
<!-- сюда скрипт будет подставлять строки -->
<div id="characterPrizes" class="status" aria-live="polite" data-dynamic="characterPrizes">
<div class="status">Загрузка…</div>
</div>
<!-- fallback на случай, если данных нет -->
<div id="characterPrizesEmpty" class="status" style="display:none;">
Нет данных
</div>
</div>
</details>
<!--Сервис-->
<details class="block">
<summary class="block-title">Сервис</summary>
<div class='panel'>
<button type='button' class='action-btn' data-action-name='Тест кнопок'>Тест кнопок</button>
<button type='button' class='action-btn' data-action-name='Тест экрана'>Тест экрана</button>
<!-- Пароль настроек: статус + действия -->
<div class="form-group">
<label>Пароль настроек</label>
<!-- статус (сервер/скрипт будет подставлять текст) -->
<div id="settingsPasswordStatus" class="status" aria-live="polite" data-status-key="SettingsPassword">
Статус: неизвестно
</div>
<div class="actions-grid" style="margin-top:12px;">
<button type="button" class="action-btn" data-action-name="Установить пароль настроек">
Установить пароль настроек
</button>
<button type="button" class="action-btn" data-action-name="Сбросить пароль настроек">
Сбросить пароль настроек
</button>
</div>
</div>
</div>
</details>
<!--Номер-->
<details class="block">
<summary class="block-title">Номер</summary>
<div class='panel'>
<!-- Номер телефона: как "Суперприз" (инпут + отправить) -->
<div class="form-group currency-card" data-currency-action="Номер техподдержки">
<label>Номер телефона</label>
<div class="coins-row currency-row">
<input type="tel" class="coins-input currency-input" value="70000000000" maxlength="18">
<button type="button" class="coins-btn currency-btn action-btn"
data-action-name="Номер техподдержки">Отправить</button>
</div>
</div>
<div class='form-group'>
<label for='ShowNumberID'>Показывать контакты администрации</label>
<input type='checkbox' id='ShowNumberID' data-setting-id='ShowNumber' checked>
</div>
</div>
</details>
<!--Сложность-->
<details class="block">
<summary class="block-title">Сложность</summary>
<div class='panel'>
<div class='form-group'>
<p>Сложность ботов</p>
<div class='radio-row'>
<label for='DifficultyID0'>Легкая</label>
<input type='radio' id='DifficultyID0' name='Difficulty' value='Легкая'
data-setting-id='Difficulty'>
</div>
<div class='radio-row'>
<label for='DifficultyID1'>Средняя</label>
<input type='radio' id='DifficultyID1' name='Difficulty' value='Средняя' checked
data-setting-id='Difficulty'>
</div>
<div class='radio-row'>
<label for='DifficultyID2'>Сложная</label>
<input type='radio' id='DifficultyID2' name='Difficulty' value='Сложная'
data-setting-id='Difficulty'>
</div>
</div>
<div class='form-group'>
<p>Текущая сложность аппарата</p>
<div class='radio-row'>
<label for='BaseBotDifficultyID0'>Обычная</label>
<input type='radio' id='BaseBotDifficultyID0' name='BaseBotDifficulty' value='Обычная'
data-setting-id='BaseBotDifficulty'>
</div>
<div class='radio-row'>
<label for='BaseBotDifficultyID1'>Повышенная</label>
<input type='radio' id='BaseBotDifficultyID1' name='BaseBotDifficulty' value='Повышенная'
data-setting-id='BaseBotDifficulty'>
</div>
<div class='radio-row'>
<label for='BaseBotDifficultyID2'>Фиксированная</label>
<input type='radio' id='BaseBotDifficultyID2' name='BaseBotDifficulty' value='Фиксированная' checked
data-setting-id='BaseBotDifficulty'>
</div>
</div>
<!-- Ввод "Импульсов до сброса сложности": как цены (число + отправить) -->
<div class="form-group price-card" data-price-action="Импульсов до сброса сложности">
<label>Импульсов до сброса сложности</label>
<div class="coins-row price-row">
<input id="impulsesResetValue" type="number" class="coins-input price-input" value="0" min="0"
step="1">
<button type="button" class="coins-btn price-btn action-btn"
data-action-name="Импульсов до сброса сложности">
Отправить
</button>
</div>
</div>
<!-- Счетчик текущих импульсов до сброса -->
<div class="form-group counter">
<label>Текущих импульсов до сброса</label>
<div id="impulsesResetCurrent" class="status" data-counter-key="Difficulty.ImpulsesToReset">—</div>
</div>
<!-- Заготовка пояснения, подтягивается с сервера -->
<div class="form-group">
<label>Пояснение</label>
<div id="impulsesResetHint" class="status" aria-live="polite" data-status-key="DifficultyHint">Загрузка…
</div>
</div>
</div>
</details>
<!--Быстрые проверки-->
<details class="block">
<summary class="block-title">Быстрые проверки</summary>
<div class='panel'>
<div class='form-group'>
<label for='FastCheckID'>Быстрая проверка счетчиков</label>
<input type='checkbox' id='FastCheckID' data-setting-id='FastCheck' checked>
</div>
<div class='form-group'>
<label for='FastCheckPrizesID'>Быстрая проверка призов</label>
<input type='checkbox' id='FastCheckPrizesID' data-setting-id='FastCheckPrizes' checked>
</div>
</div>
</details>
<script src="script.js"></script>
<div id='globalStatus' class='status'></div>
</body>
</html>

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 83beab9d114dcd44f82d12d2076d57a9
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,4 @@
/* =========================
/* =========================
Base reset
========================= */
* {
@ -28,7 +28,8 @@ h1 {
font-size: 3.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 800px; /* ширина под блоки */
max-width: 800px;
/* ширина под блоки */
}
h2 {
@ -66,8 +67,13 @@ h2 {
}
/* убираем стандартный маркер */
.block > summary::-webkit-details-marker { display: none; }
.block > summary::marker { content: ""; }
.block>summary::-webkit-details-marker {
display: none;
}
.block>summary::marker {
content: "";
}
/* заголовок блока */
.block-title {
@ -94,11 +100,11 @@ h2 {
opacity: 0.85;
}
.block[open] > .block-title {
.block[open]>.block-title {
border-bottom: 2px solid rgba(0, 0, 0, 0.06);
}
.block[open] > .block-title::after {
.block[open]>.block-title::after {
transform: rotate(90deg);
}
@ -123,7 +129,8 @@ h2 {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 0; /* теперь управляем gap'ом в .block-body */
margin-bottom: 0;
/* теперь управляем gap'ом в .block-body */
}
/* чуть мягче панели внутри раскрывающегося блока (не ломает общий стиль) */
@ -140,6 +147,7 @@ h2 {
padding: 20px;
transition: all 0.3s ease;
background: #f8f9fa;
}
/* hover реакция */
@ -302,18 +310,18 @@ input[type="radio"]:hover {
}
/* поддержка старой разметки */
.form-group > div {
.form-group>div {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 8px;
transition: background 0.2s ease;
}
.form-group > div:hover {
.form-group>div:hover {
background: rgba(102, 126, 234, 0.05);
}
.form-group > div:last-child,
.form-group>div:last-child,
.radio-row:last-child {
margin-bottom: 0;
}
@ -356,11 +364,14 @@ button {
.coins-row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
gap: 12px;
flex-wrap: nowrap;
}
.coins-input {
width: 160px;
border: 2px solid #e0e0e0;
@ -371,6 +382,18 @@ button {
transition: all 0.2s ease;
}
.coins-row .coins-input {
width: 140px;
/* слева компактно */
flex: 0 0 auto;
}
.coins-row .coins-btn {
flex: 1 1 auto;
/* кнопка занимает остальное */
white-space: nowrap;
}
.coins-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
@ -406,7 +429,8 @@ button {
font-size: 1.05rem;
color: #2d3748;
opacity: 0.9;
min-height: 1.2em; /* чтобы не прыгало */
min-height: 1.2em;
/* чтобы не прыгало */
}
/* глобальный статус снизу */
@ -417,7 +441,7 @@ button {
color: white;
opacity: 0.95;
margin-top: 6px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.25);
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
}
/* =========================
@ -527,6 +551,75 @@ button {
outline: none;
}
/* =========================
Counters: Название:Значение в одну строку
========================= */
.form-group.counter {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.form-group.counter label {
margin-bottom: 0;
/* убираем столбик */
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* двоеточие рисуем автоматически */
.form-group.counter label::after {
content: ":";
margin-left: 6px;
}
/* значение справа */
.form-group.counter .status {
margin: 0;
padding: 0;
min-height: 0;
flex: 0 0 auto;
white-space: nowrap;
font-weight: 700;
opacity: 1;
}
/* перебиваем твой общий .form-group > div (там padding/margin),
чтобы status не превращался в "вторую строку" */
.form-group.counter>div {
margin: 0 !important;
padding: 0 !important;
}
.form-group.counter>div:hover {
background: transparent !important;
}
/* цены: чуть компактнее и ровнее */
.price-card label {
margin-bottom: 10px; /* меньше, чем у обычных form-group */
}
.price-row {
gap: 12px;
}
.price-input {
width: 140px; /* как у монет */
}
.price-btn {
flex: 1 1 auto;
}
.currency-card label { margin-bottom: 10px; }
.currency-row { gap: 12px; }
.currency-input { width: 160px; }
.currency-btn { flex: 1 1 auto; }
/* =========================
Focus accessibility
========================= */
@ -605,8 +698,16 @@ summary:focus {
}
.coins-row {
flex-direction: column;
align-items: stretch;
flex-direction: row;
align-items: center;
}
.coins-row .coins-input {
width: 120px;
}
.coins-row .coins-btn {
width: auto;
}
.actions-grid {
@ -633,4 +734,10 @@ summary:focus {
.login-root h1 {
font-size: 1.8rem;
}
}
.form-group.counter label,
.form-group.counter .status {
font-size: 1rem;
/* как у остальных на мобилке */
}
}

View File

@ -13688,6 +13688,8 @@ MonoBehaviour:
prefab: {fileID: 1804015508813908771, guid: cdc2db675d691d3438e213fc716e5ae6, type: 3}
- id: PasswordOnSettings
prefab: {fileID: 1804015508813908771, guid: cdc2db675d691d3438e213fc716e5ae6, type: 3}
- id: PromoVolume
prefab: {fileID: 7872553190807181184, guid: f2cc4d65460a1864cac8d119b87a76a2, type: 3}
--- !u!114 &581100739
MonoBehaviour:
m_ObjectHideFlags: 0
@ -13773,6 +13775,8 @@ MonoBehaviour:
descriptionPrefab: {fileID: 7265677761729542613, guid: 00a1bdf8cf92f60439bc2e7b62d53e51, type: 3}
- id: PasswordOnSettings
descriptionPrefab: {fileID: 7265677761729542613, guid: 2fd63316851eaa84d9771255e07e69cf, type: 3}
- id: PromoVolume
descriptionPrefab: {fileID: 7265677761729542613, guid: 20804b36a3f36e54588502fc0e16c610, type: 3}
defaultDescription: {fileID: 7265677761729542613, guid: 3da6c849967083641b4128baeb7ab2cd, type: 3}
--- !u!114 &581100740
MonoBehaviour:
@ -14011,6 +14015,7 @@ MonoBehaviour:
adminPasswordFallback: 1234
passwordFileName: admin_password.txt
settingActions: []
wsPushIntervalMs: 500
--- !u!4 &592995719
Transform:
m_ObjectHideFlags: 0

View File

@ -6,9 +6,11 @@ public class PromoState : IState
float videoLength;
public void enter()
{
LightUtility.Promo();
VideoUtility.PlayAds(); VideoUtility.PlayAds();
SoundManager.instance.music.mute = true;
SoundManager.instance.SetPromoVolumeToVideo();
startTime = Time.time;
videoLength = VideoUtility.adsLength;
}

View File

@ -10,10 +10,15 @@ public class RulesState : IState
public void enter()
{
rules = GameManager.instance.rulesController;
SoundManager.instance.playSwitch();
rules.Open();
restTime = timeout;
}
public void exit() => rules.Close();
public void exit()
{
SoundManager.instance.playSwitch();
rules.Close();
}
public void update()
{

View File

@ -17,6 +17,7 @@ public class VictoryVideoState : IState
{
VideoUtility.PlayEnding(); VideoUtility.PlayEnding();
SoundManager.instance.music.mute = true;
SoundManager.instance.SetMusicVolumeToVideo();
startTime = Time.time;
videoLength = VideoUtility.victoryLength;
}

View File

@ -15,7 +15,7 @@ public static class PrizeSerializer
{
if (!File.Exists(FILE_PATH))
{
Debug.Log("Не записаны призы беру дефолт");
//Debug.Log("Не записаны призы беру дефолт");
return GetDefaultPrizes();
}

View File

@ -44,6 +44,7 @@ public class CountersCollector : MonoBehaviour
// Другое
new CounterData("Number", "Номер телефона", "<не указан>"),
new CounterData("SpecialPrize", "Суперприз", "<не указан>")
};

View File

@ -459,4 +459,4 @@ public class HtmlBuilder
}
private static string EscapeAttr(string s) => Escape(s).Replace("'", "&#39;");
}
}

View File

@ -25,7 +25,7 @@ public class WebServer : MonoBehaviour
[SerializeField] private string adminPasswordFallback = "1234"; // если файла нет
[SerializeField] private string passwordFileName = "admin_password.txt"; // лежит в persistentDataPath
private string AdminPassword;
private string AdminPassword;
public List<SettingAction> settingActions;
@ -33,7 +33,7 @@ public class WebServer : MonoBehaviour
async void Start()
{
if(ExternalFilesGetter.NeedToStartServer() == false)
if (ExternalFilesGetter.NeedToStartServer() == false)
{
Debug.Log("Web сервер не запущен (по настройке)");
return;
@ -528,4 +528,3 @@ public class WebServer : MonoBehaviour
public string name;
}
}

View File

@ -0,0 +1,173 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &334621903248152329
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4641018153116856876}
- component: {fileID: 7974557143938056251}
- component: {fileID: 3186854818972227115}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4641018153116856876
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 334621903248152329}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4639762839304224986}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 10, y: -10}
m_SizeDelta: {x: -20, y: -20}
m_Pivot: {x: 0, y: 1}
--- !u!222 &7974557143938056251
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 334621903248152329}
m_CullTransparentMesh: 1
--- !u!114 &3186854818972227115
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 334621903248152329}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "- \u0418\u0437\u043C\u0435\u043D\u044F\u0435\u0442 \u0433\u0440\u043E\u043C\u043A\u043E\u0441\u0442\u044C
\u043F\u0440\u043E\u043C\u043E\u0440\u043E\u043B\u0438\u043A\u0430"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 3f10bbcf486189d4f95b8de6dc2e2177, type: 2}
m_sharedMaterial: {fileID: 4262305343556741190, guid: 3f10bbcf486189d4f95b8de6dc2e2177, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 20
m_fontSizeBase: 20
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 16
m_fontSizeMax: 64
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &7265677761729542613
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4639762839304224986}
m_Layer: 5
m_Name: PromoVolume
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4639762839304224986
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7265677761729542613}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4641018153116856876}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 820, y: 160}
m_Pivot: {x: 0.5, y: 0.5}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 20804b36a3f36e54588502fc0e16c610
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -185,6 +185,16 @@ public class SettingsManager : MonoBehaviour
max: 1f,
step: 0.01f));
settings.Add(new Setting(id: "PromoVolume",
alias: "Громкость промо ролика",
group: "Звук и озвучка",
type: SettingType.Float,
cycling: false,
value: 0.5f,
min: 0f,
max: 1f,
step: 0.01f));
settings.Add(new Setting(id: "Prizing",
alias: "Выдача призов при игре с ботом",
group: "Игровой процесс",

View File

@ -39,6 +39,10 @@ public class SoundManager : MonoBehaviour
private int poolIndex = 0;
private float musicVolume = 0.5f;
private float promoVolume = 0.5f;
public void Initialize()
{
if (instance == null)
@ -99,7 +103,9 @@ public class SoundManager : MonoBehaviour
public void ApplySettings()
{
//print("Применяем настройки звука");
float musicVolume = SettingsManager.GetFloat("MusicVolume", 0.5f);
musicVolume = SettingsManager.GetFloat("MusicVolume", 0.5f);
promoVolume = SettingsManager.GetFloat("PromoVolume", 0.5f);
music.volume = musicVolume;
effectsVolume = SettingsManager.GetFloat("SoundsVolume", 0.5f);
@ -113,10 +119,13 @@ public class SoundManager : MonoBehaviour
audioSource.volume = effectsVolume;
}
VideoAudio.volume = musicVolume;
VideoAudio.volume = promoVolume;
}
public void SetMusicVolumeToVideo() => VideoAudio.volume = musicVolume;
public void SetPromoVolumeToVideo() => VideoAudio.volume = promoVolume;
public void playClip(AudioClip clip)
{
if (clip == null)

File diff suppressed because one or more lines are too long