FEAT: Действия настроек выведены на сайт-панель

This commit is contained in:
_sega 2026-01-29 12:18:15 +03:00
parent 56a9e2464b
commit 00462af4c3
19 changed files with 491 additions and 111 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.08173752, a: 1}
- _GlowColor: {r: 0, g: 0.72760534, b: 1, 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}

View File

@ -1,9 +1,15 @@
* {
/* =========================
Base reset
========================= */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* =========================
Page
========================= */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -22,7 +28,7 @@ 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 {
@ -31,7 +37,9 @@ h2 {
margin-bottom: 14px;
}
/* Основная форма (legacy) */
/* =========================
Legacy: если где-то ещё используется
========================= */
.form-root {
background: white;
padding: 25px;
@ -45,7 +53,65 @@ h2 {
}
/* =========================
NEW: Панели (как карточки)
NEW: раскрывающиеся блоки (details)
========================= */
.block {
width: 100%;
max-width: 800px;
border-radius: 20px;
background: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
overflow: hidden;
margin-bottom: 18px;
}
/* убираем стандартный маркер */
.block > summary::-webkit-details-marker { display: none; }
.block > summary::marker { content: ""; }
/* заголовок блока */
.block-title {
padding: 20px 24px;
cursor: pointer;
user-select: none;
font-weight: 700;
color: #2d3748;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: white;
}
/* стрелка справа */
.block-title::after {
content: "›";
font-size: 2.4rem;
line-height: 1;
transform: rotate(0deg);
transition: transform 0.2s ease;
opacity: 0.85;
}
.block[open] > .block-title {
border-bottom: 2px solid rgba(0, 0, 0, 0.06);
}
.block[open] > .block-title::after {
transform: rotate(90deg);
}
/* тело блока */
.block-body {
padding: 22px 24px 24px;
display: flex;
flex-direction: column;
gap: 18px;
}
/* =========================
Панели (карточки внутри блока)
========================= */
.panel {
background: white;
@ -57,10 +123,17 @@ h2 {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
margin-bottom: 0; /* теперь управляем gap'ом в .block-body */
}
/* Группы элементов формы */
/* чуть мягче панели внутри раскрывающегося блока (не ломает общий стиль) */
.block-body .panel {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
/* =========================
Группы элементов формы
========================= */
.form-group {
border: 2px solid #e0e0e0 !important;
border-radius: 12px !important;
@ -69,7 +142,7 @@ h2 {
background: #f8f9fa;
}
/* hover оставляем (это не анимация появления, это реакция) */
/* hover реакция */
.form-group:hover {
border-color: #667eea !important;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.1);
@ -91,7 +164,9 @@ h2 {
font-size: 2.1rem;
}
/* Стили для range input */
/* =========================
Range input
========================= */
input[type="range"] {
width: 100%;
height: 8px;
@ -120,7 +195,9 @@ input[type="range"]::-webkit-slider-thumb:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
/* Стили для чекбоксов */
/* =========================
Checkbox
========================= */
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
@ -157,7 +234,9 @@ input[type="checkbox"]:hover {
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Стили для радиокнопок */
/* =========================
Radio
========================= */
input[type="radio"] {
-webkit-appearance: none;
appearance: none;
@ -195,8 +274,7 @@ input[type="radio"]:hover {
}
/* =========================
CHANGED: радио-строки
(раньше было .form-group > div, но у тебя теперь .radio-row)
Радио-строки (.radio-row)
========================= */
.radio-row {
display: flex;
@ -223,7 +301,7 @@ input[type="radio"]:hover {
margin-right: 0;
}
/* Оставляем старую поддержку на случай других div внутри form-group */
/* поддержка старой разметки */
.form-group > div {
margin-bottom: 10px;
padding: 8px 12px;
@ -240,7 +318,9 @@ input[type="radio"]:hover {
margin-bottom: 0;
}
/* Кнопка отправки (legacy) */
/* =========================
Legacy submit
========================= */
input[type="submit"] {
background: linear-gradient(to right, #667eea, #764ba2);
color: white;
@ -268,7 +348,7 @@ input[type="submit"]:active {
}
/* =========================
NEW: кнопки и поля (монеты + логин)
Buttons / Coins / Login
========================= */
button {
font-family: inherit;
@ -326,10 +406,10 @@ button {
font-size: 1.05rem;
color: #2d3748;
opacity: 0.9;
min-height: 1.2em; /* чтобы не прыгало, когда текст появляется/исчезает */
min-height: 1.2em; /* чтобы не прыгало */
}
/* глобальный статус снизу — визуально мягче */
/* глобальный статус снизу */
#globalStatus.status {
width: 100%;
max-width: 800px;
@ -341,7 +421,68 @@ button {
}
/* =========================
NEW: логин-страница (в стиле карточек)
NEW: actions (SettingAction)
========================= */
.action-group {
border: 2px solid #e0e0e0;
border-radius: 16px;
padding: 18px;
background: #f8f9fa;
transition: all 0.3s ease;
}
.action-group:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
.action-group-title {
color: #2d3748;
font-size: 1.7rem;
font-weight: 700;
margin-bottom: 14px;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.action-btn {
background: linear-gradient(to right, #667eea, #764ba2);
color: white;
border: none;
padding: 16px 16px;
font-size: 1.1rem;
font-weight: 700;
border-radius: 14px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.22);
text-align: center;
line-height: 1.15;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.35);
}
.action-btn:active {
transform: translateY(-1px);
}
.action-btn:disabled {
opacity: 0.65;
cursor: not-allowed;
transform: none !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.18);
}
/* =========================
Login page card
========================= */
.login-root {
background: white;
@ -356,7 +497,6 @@ button {
}
.login-root h1 {
/* На логине свой h1 внутри белой карточки, поэтому переопределим */
color: #2d3748;
text-shadow: none;
font-size: 2.6rem;
@ -371,7 +511,6 @@ button {
min-height: 1.2em;
}
/* Инпут пароля в стиле coins-input */
.login-root input[type="password"] {
width: 100%;
border: 2px solid #e0e0e0;
@ -388,29 +527,19 @@ button {
outline: none;
}
/* Улучшение читаемости на светлом фоне */
.form-group label[for],
.form-group p {
color: #2d3748;
}
/* Эффект фокуса для доступности */
input:focus {
/* =========================
Focus accessibility
========================= */
input:focus,
button:focus,
summary:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
}
/* =========================
CHANGED: УБРАЛИ анимацию появления
Mobile
========================= */
/* Удалено:
@keyframes fadeIn {...}
.form-group { animation: ... }
.form-group:nth-child(...) ...
input[type="submit"] { animation: ... }
*/
/* Адаптивность для мобильных устройств */
@media (max-width: 768px) {
body {
padding: 15px;
@ -428,6 +557,20 @@ input[type="submit"] { animation: ... }
margin-bottom: 10px;
}
.block-title {
font-size: 1.35rem;
padding: 16px 18px;
}
.block-title::after {
font-size: 1.9rem;
}
.block-body {
padding: 18px;
gap: 16px;
}
.form-root,
.panel {
padding: 20px;
@ -445,12 +588,13 @@ input[type="submit"] { animation: ... }
margin-bottom: 10px;
}
/* кнопки */
.coins-btn,
#loginBtn,
input[type="submit"] {
input[type="submit"],
.action-btn {
padding: 14px 16px;
font-size: 1rem;
border-radius: 12px;
}
.coins-input,
@ -465,6 +609,10 @@ input[type="submit"] { animation: ... }
align-items: stretch;
}
.actions-grid {
grid-template-columns: 1fr;
}
/* тач-таргеты */
input[type="checkbox"],
input[type="radio"],

View File

@ -10,6 +10,8 @@ public class SettingsActions : MonoBehaviour
[SerializeField] AudioClip prizingDisabled;
private GameManager gm => GameManager.instance;
private IState CurrentState => GameManager.instance.stateMachine.currentState;
private void OnEnable()
{
SettingsManager.OnSettingsChanged?.AddListener(RefreshUI);
@ -36,22 +38,23 @@ public class SettingsActions : MonoBehaviour
RefreshUI();
}
//Панель1
public void EnterSpecialPrize() => gm.stateMachine.ChangeState(new EnterSpecialPrizeState());
public void EnterPvePrice() => gm.stateMachine.ChangeState(new EnterPricePVEState());
public void EnterPveRevengePrice() => gm.stateMachine.ChangeState(new EnterPricePVERevengeState());
public void EnterPvpPrice() => gm.stateMachine.ChangeState(new EnterPricePVPState());
public void EnterSupportNumber() => gm.stateMachine.ChangeState(new GetNumberState());
public void EnterPrizesAvailable() => gm.stateMachine.ChangeState(new EnterPrizesAvailableState());
//Смены состояний
public void EnterSpecialPrize() => gm.stateMachine.ChangeState(new EnterSpecialPrizeState(CurrentState));
public void EnterPvePrice() => gm.stateMachine.ChangeState(new EnterPricePVEState(CurrentState));
public void EnterPveRevengePrice() => gm.stateMachine.ChangeState(new EnterPricePVERevengeState(CurrentState));
public void EnterPvpPrice() => gm.stateMachine.ChangeState(new EnterPricePVPState(CurrentState));
public void EnterSupportNumber() => gm.stateMachine.ChangeState(new GetNumberState(CurrentState));
public void EnterPrizesAvailable() => gm.stateMachine.ChangeState(new EnterPrizesAvailableState(CurrentState));
public void ResetPrizeError() => PlayerPrefs.SetInt(KeyVault.PRIZING_ERROR_REGISTRED, 0);
public void EnterPrizesSettings() => gm.stateMachine.ChangeState(new EnterPrizesAvailableState());
public void ConfigurePrizes() => gm.stateMachine.ChangeState(new EnterPrizesForCharactersState());
public void ConfigurePulsesToResetDifficulty() => gm.stateMachine.ChangeState(new EnterPulsesToResetDifficulty());
public void ConfigureValutaPerPulse() => gm.stateMachine.ChangeState(new EnterValutaPerImpulse());
public void EnterValutaName() => gm.stateMachine.ChangeState(new EnterValutaNameState());
public void EnterPrizesSettings() => gm.stateMachine.ChangeState(new EnterPrizesAvailableState(CurrentState));
public void ConfigurePrizes() => gm.stateMachine.ChangeState(new EnterPrizesForCharactersState(CurrentState));
public void ConfigurePulsesToResetDifficulty() => gm.stateMachine.ChangeState(new EnterPulsesToResetDifficulty(CurrentState));
public void ConfigureValutaPerPulse() => gm.stateMachine.ChangeState(new EnterValutaPerImpulse(CurrentState));
public void EnterValutaName() => gm.stateMachine.ChangeState(new EnterValutaNameState(CurrentState));
public void QrTest() => gm.stateMachine.ChangeState(new CheckQrState(CurrentState));
public void EnterIO_test() => gm.stateMachine.ChangeState(new IoTestState(IO_test, CurrentState));
//Панель2
public void EnterIO_test() => gm.stateMachine.ChangeState(new IoTestState(IO_test));
public void HopperTest()
{
if(SettingsManager.GetBool("Prizing", false))
@ -65,7 +68,6 @@ public class SettingsActions : MonoBehaviour
}
public void SoundTest() => SoundManager.instance.playClip(testSound);
public void ScreenTest() => ScreenTester.SetActive(true);
public void QrTest() => gm.stateMachine.ChangeState(new CheckQrState());
public void ResetBalance()
{
ComWrapper.Balance = 0;

View File

@ -5,27 +5,22 @@ using UnityEngine;
public class CheckQrState : IState
{
float timer = 0;
IState prevState;
public CheckQrState(IState prevState)
{
this.prevState = prevState;
}
public void enter() => GameManager.instance.checkQrPanel.SetActive(true);
public void exit() => GameManager.instance.checkQrPanel.SetActive(false);
public void update()
{
/*Это был выход по и1 + и2 с таймером 3 сек
timer -= Time.deltaTime;
if (In.GetButton(ButtonPinMapping.startOnePlayer) && In.GetButton(ButtonPinMapping.startTwoPlayers))
{
timer += Time.deltaTime * 2;
}
timer = Mathf.Clamp(timer, 0, 3);
if (timer >= 2.9f)
{
GameManager.instance.stateMachine.ChangeState(new SettingsState());
}*/
if (In.GetButtonDownAnyPlayer(PlayerAction.down))
{
GameManager.instance.stateMachine.ChangeState(new SettingsState());
//GameManager.instance.stateMachine.ChangeState(new SettingsState());
GameManager.instance.stateMachine.ChangeState(prevState);
}
}
}

View File

@ -6,6 +6,12 @@ public class EnterPricePVERevengeState : IState
{
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterPricePVERevengeState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -33,6 +39,7 @@ public class EnterPricePVERevengeState : IState
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}

View File

@ -2,6 +2,12 @@
{
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterPricePVEState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -28,7 +34,8 @@
numberInput.AcceptAndClose();
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}

View File

@ -6,6 +6,12 @@ public class EnterPricePVPState : IState
{
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterPricePVPState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -32,7 +38,8 @@ public class EnterPricePVPState : IState
numberInput.AcceptAndClose();
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}

View File

@ -7,6 +7,13 @@ public class EnterPrizesAvailableState : IState
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterPrizesAvailableState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
gm = GameManager.instance;
@ -32,7 +39,8 @@ public class EnterPrizesAvailableState : IState
numberInput.AcceptAndClose();
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}
}

View File

@ -6,7 +6,12 @@ public class EnterPrizesForCharactersState : IState
{
GameObject rootPanel;
List<Prize> prizes;
IState prevState;
public EnterPrizesForCharactersState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -14,7 +19,11 @@ public class EnterPrizesForCharactersState : IState
rootPanel.SetActive(true);
}
public void exit() => rootPanel.SetActive(false);
public void exit()
{
GameManager.instance.stateMachine.ChangeState(prevState);
rootPanel.SetActive(false);
}
public void update()
{

View File

@ -6,6 +6,12 @@ public class EnterPulsesToResetDifficulty : IState
{
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterPulsesToResetDifficulty(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -32,7 +38,8 @@ public class EnterPulsesToResetDifficulty : IState
numberInput.AcceptAndClose();
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}
}

View File

@ -6,6 +6,12 @@ public class EnterSpecialPrizeState : IState
{
TextInput textInput;
GameManager gm;
IState prevState;
public EnterSpecialPrizeState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -36,7 +42,8 @@ public class EnterSpecialPrizeState : IState
PlayerPrefs.SetString("SpecialPrize", textInput.AcceptAndClose());
//переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}

View File

@ -6,6 +6,12 @@ public class EnterValutaNameState : IState
{
TextInput textInput;
GameManager gm;
IState prevState;
public EnterValutaNameState(IState prevState)
{
this.prevState = prevState;
}
//private string name;
public void enter()
@ -37,7 +43,8 @@ public class EnterValutaNameState : IState
PlayerPrefs.SetString("Price", textInput.AcceptAndClose());
//переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}
}

View File

@ -6,6 +6,12 @@ public class EnterValutaPerImpulse : IState
{
NumberInput numberInput;
GameManager gm;
IState prevState;
public EnterValutaPerImpulse(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
@ -32,7 +38,8 @@ public class EnterValutaPerImpulse : IState
numberInput.AcceptAndClose();
//обратный переход
gm.stateMachine.ChangeState(new SettingsState());
//gm.stateMachine.ChangeState(new SettingsState());
gm.stateMachine.ChangeState(prevState);
}
}

View File

@ -10,6 +10,7 @@ using UnityEngine.UI;
public class GetNumberState : IState
{
IState prevState;
const string NUMBER_KEY = "Number";
//Иерархия для панели: Панель -> 10 Image -> У каждого tmpro
GameObject entryPanel;
@ -25,6 +26,11 @@ public class GetNumberState : IState
private int currentSlotIndex = 0;
private int length = 11;
public GetNumberState(IState prevState)
{
this.prevState = prevState;
}
public void enter()
{
//включить панель, получить доступ ко всем слотам
@ -72,7 +78,8 @@ public class GetNumberState : IState
//записываем и выходим
if (In.GetButtonDown(ButtonPinMapping.down2))
{
GameManager.instance.stateMachine.ChangeState(new SettingsState());
//GameManager.instance.stateMachine.ChangeState(new SettingsState());
GameManager.instance.stateMachine.ChangeState(prevState);
}
}

View File

@ -2,7 +2,7 @@
public class GameStateMachine : MonoBehaviour
{
IState currentState;
public IState currentState;
public void ChangeState(IState nextState)
{

View File

@ -3,10 +3,11 @@
public class IoTestState : IState
{
GameObject io;
public IoTestState(GameObject ioPanel)
IState prevState;
public IoTestState(GameObject ioPanel, IState prevState)
{
io = ioPanel;
this.prevState = prevState;
}
public void enter() => io.SetActive(true);
public void exit() => io.SetActive(false);

View File

@ -1,12 +1,18 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
public class HtmlBuilder
{
// старый метод оставим, чтобы ничего не поломать
public static string GetSettingsPage(List<Setting> settings)
=> GetSettingsPage(settings, actions: null);
// новый метод
public static string GetSettingsPage(List<Setting> settings, List<SettingAction> actions)
{
return Decorate(GetSettingsString(settings), title: "Настройки Фишки");
return Decorate(GetSettingsString(settings, actions), title: "Настройки Фишки");
}
public static string GetLoginPage()
@ -59,26 +65,38 @@ public class HtmlBuilder
return Decorate(content, title: "Вход");
}
// старый метод оставим
public static string GetSettingsString(List<Setting> settings)
=> GetSettingsString(settings, actions: null);
// новый метод: настройки + действия (раздельные блоки)
public static string GetSettingsString(List<Setting> settings, List<SettingAction> actions)
{
var sb = new StringBuilder();
sb.AppendLine("<h1>Настройки фишки</h1>");
// блок монет
// === БЛОК: НАСТРОЙКИ ===
sb.AppendLine(@"
<div class='panel'>
<h2>Монеты</h2>
<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>
<div id='coinsStatus' class='status'></div>
</div>
<details class='block' open>
<summary class='block-title'>Настройки</summary>
<div class='block-body'>
");
sb.AppendLine("<div class='panel'>");
sb.AppendLine("<h2>Параметры</h2>");
// монеты внутри настроек
sb.AppendLine(@"
<div class='panel'>
<h2>Монеты</h2>
<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>
<div id='coinsStatus' class='status'></div>
</div>
");
sb.AppendLine(" <div class='panel'>");
sb.AppendLine(" <h2>Параметры</h2>");
foreach (Setting setting in settings)
{
@ -93,9 +111,25 @@ public class HtmlBuilder
sb.AppendLine(view);
}
sb.AppendLine("</div>");
sb.AppendLine(" </div>"); // panel
sb.AppendLine(@"
</div>
</details>
");
// JS для realtime отправки
// === БЛОК: ДЕЙСТВИЯ ===
sb.AppendLine(@"
<details class='block'>
<summary class='block-title'>Действия</summary>
<div class='block-body'>
");
sb.AppendLine(GetActionsView(actions));
sb.AppendLine(@"
</div>
</details>
");
// JS: realtime settings + coins + actions
sb.AppendLine(@"
<script>
(function(){
@ -129,7 +163,7 @@ public class HtmlBuilder
}
}
// Все элементы настроек
// === Настройки ===
document.querySelectorAll('[data-setting-id]').forEach(el=>{
const id = el.getAttribute('data-setting-id');
@ -146,7 +180,6 @@ public class HtmlBuilder
const apply = ()=>{
if(valueSpan) valueSpan.textContent = el.value;
// range шлём с небольшой задержкой, чтобы не заспамить
debounce('rng_'+id, ()=>sendSetting(id, el.value), 120);
};
@ -162,18 +195,18 @@ public class HtmlBuilder
return;
}
// number/text (int)
el.addEventListener('change', ()=>{
sendSetting(id, el.value);
});
});
// Монеты
// === Монеты ===
const coinsAmount = document.getElementById('coinsAmount');
const addBtn = document.getElementById('addCoinsBtn');
const coinsStatus = document.getElementById('coinsStatus');
async function addCoins(){
if(!coinsAmount || !addBtn || !coinsStatus) return;
coinsStatus.textContent = '';
const v = parseInt(coinsAmount.value || '0', 10);
if(!Number.isFinite(v) || v <= 0){
@ -197,7 +230,39 @@ public class HtmlBuilder
}
}
addBtn.addEventListener('click', addCoins);
if(addBtn) addBtn.addEventListener('click', addCoins);
// === Действия ===
document.querySelectorAll('[data-action-name]').forEach(btn=>{
btn.addEventListener('click', async ()=>{
const name = btn.getAttribute('data-action-name') || '';
if(!name) return;
try{
btn.disabled = true;
const r = await fetch('/api/action', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ name })
});
const t = await r.text();
if(!r.ok){
status('Ошибка действия');
try{
const j = JSON.parse(t);
if(j && j.message) status(j.message);
}catch(e){}
return;
}
status('Ок');
}catch(e){
status('Сервер недоступен');
}finally{
setTimeout(()=>{ btn.disabled = false; }, 200);
}
});
});
})();
</script>
@ -207,6 +272,41 @@ public class HtmlBuilder
return sb.ToString();
}
private static string GetActionsView(List<SettingAction> actions)
{
var sb = new StringBuilder();
if (actions == null || actions.Count == 0)
{
sb.AppendLine("<p class='muted'>Нет доступных действий</p>");
return sb.ToString();
}
// группировка по Group (если пусто — в "Другое")
var groups = actions
.Where(a => a != null && !string.IsNullOrWhiteSpace(a.Name))
.GroupBy(a => string.IsNullOrWhiteSpace(a.Group) ? "Другое" : a.Group);
foreach (var g in groups)
{
sb.AppendLine($"<div class='action-group'>");
sb.AppendLine($" <h3 class='action-group-title'>{Escape(g.Key)}</h3>");
sb.AppendLine(" <div class='actions-grid'>");
foreach (var a in g)
{
sb.AppendLine(
$" <button type='button' class='action-btn' data-action-name='{EscapeAttr(a.Name)}'>{Escape(a.Name)}</button>"
);
}
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
}
return sb.ToString();
}
private static string GetBoolView(Setting setting)
{
string name = setting.Id;
@ -294,12 +394,10 @@ public class HtmlBuilder
+ "<body>";
res += content;
res += "</body></html>";
return res;
}
// минимальная защита от мусора в html
private static string Escape(string s)
{
if (string.IsNullOrEmpty(s)) return "";

View File

@ -27,11 +27,16 @@ public class WebServer : MonoBehaviour
private string AdminPassword => AuthManager.LoadPasswordOrFallback(adminPasswordFallback, passwordFileName);
public List<SettingAction> settingActions;
async void Start()
{
// SettingsManager.Initialize();//надо будет убирать
LoadCss();
settingActions = GameManager.instance.AdminPanel.GetComponent<SettingsScreen>().settingActions;
await StartWebServerAsync();
}
@ -39,10 +44,6 @@ public class WebServer : MonoBehaviour
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
//тест
listener.Prefixes.Add("http://fishka:8080/");
// 4. ВСЕ сетевые интерфейсы (самый важный!)
listener.Prefixes.Add("http://*:8080/");
listener.Start();
@ -126,6 +127,12 @@ public class WebServer : MonoBehaviour
return;
}
if (method == "POST" && path == "/api/action")
{
await HandlePostActionAsync(context);
return;
}
SendHtmlResponse(context, "<h1>404 Not Found</h1>", 404);
}
catch (Exception ex)
@ -144,8 +151,10 @@ public class WebServer : MonoBehaviour
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var htmlTask = Task.Run(() =>
HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings)
);
{
var actionsSnapshot = settingActions != null ? new List<SettingAction>(settingActions) : new List<SettingAction>();
return HtmlBuilder.GetSettingsPage(SettingsManager.Container.Settings, actionsSnapshot);
});
if (htmlTask.Wait(TimeSpan.FromSeconds(5)))
{
@ -317,6 +326,40 @@ public class WebServer : MonoBehaviour
}
}
async Task HandlePostActionAsync(HttpListenerContext context)
{
try
{
string body = await ReadBodyAsStringAsync(context.Request);
var dto = JsonUtility.FromJson<ActionDto>(body);
string name = dto?.name ?? "";
if (string.IsNullOrWhiteSpace(name))
{
SendJson(context, 400, "{\"ok\":false,\"message\":\"name is empty\"}");
return;
}
bool exists = settingActions != null && settingActions.Exists(a => a != null && a.Name == name);
if (!exists)
{
SendJson(context, 404, "{\"ok\":false,\"message\":\"action not found\"}");
return;
}
// Важно: UnityEvent только на главном потоке
UnityMainThreadDispatcher.Instance.Enqueue(() => ExecuteSettingAction(name));
SendJson(context, 200, "{\"ok\":true}");
}
catch (Exception ex)
{
Debug.LogError($"POST /api/action error: {ex.Message}\n{ex.StackTrace}");
SendJson(context, 500, "{\"ok\":false,\"message\":\"Server error\"}");
}
}
private void OnAddCoins(int amount) => ComWrapper.AddCreditNoCounters(amount);
// =========================
@ -443,6 +486,20 @@ public class WebServer : MonoBehaviour
context.Response.OutputStream.Close();
}
public void ExecuteSettingAction(string name)
{
SettingAction action = settingActions.Find(s => s.Name == name);
if (action != null)
{
action.OnPressed?.Invoke();
}
else
{
Debug.Log($"Действие с именем {name} не найдено.");
}
}
void OnDestroy()
{
isRunning = false;
@ -456,4 +513,12 @@ public class WebServer : MonoBehaviour
listener?.Stop();
listener?.Close();
}
[Serializable]
private class ActionDto
{
public string name;
}
}

View File

@ -7,7 +7,7 @@ using UnityEngine;
public class SettingsScreen : MonoBehaviour
{
[SerializeField] RectTransform pointer;
[SerializeField] List<SettingAction> settingActions;
public List<SettingAction> settingActions;
private List<Setting> settings;
[SerializeField] private List<string> excludedIds;
@ -80,8 +80,6 @@ public class SettingsScreen : MonoBehaviour
private void Controls()
{
int direction = 0;
//if (In.GetButtonDownAnyPlayer(PlayerAction.left)) direction -= 1;
//if (In.GetButtonDownAnyPlayer(PlayerAction.right)) direction += 1;
if (In.GetButtonAnyPlayer(PlayerAction.left)) direction -= 1;
if (In.GetButtonAnyPlayer(PlayerAction.right)) direction += 1;