This commit is contained in:
2026-01-27 01:18:34 +03:00
parent c59b51c69e
commit 1c231fe28d
4 changed files with 510 additions and 28 deletions

View File

@ -1776,7 +1776,8 @@
connect(url);
socket.once('connect', () => {
console.log('Подключено, создаём комнату');
socket.emit('createRoom', name);
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
});
socket.once('connect_error', (e) => {
console.error('Ошибка подключения:', e);
@ -1785,7 +1786,8 @@
} else {
console.log('Уже подключен, создаём комнату');
// Уже подключен, сразу отправляем
socket.emit('createRoom', name);
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
}
});
@ -1804,13 +1806,15 @@
if (!socket || !socket.connected) {
connect(url);
socket.once('connect', () => {
socket.emit('createRoom', name);
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
});
socket.once('connect_error', (e) => {
showError('Не удалось подключиться к серверу. Проверьте, что сервер запущен.');
});
} else {
socket.emit('createRoom', name);
const aiMode = $('ai-mode')?.checked || false;
socket.emit('createRoom', { name, aiMode });
}
};
} else {
@ -2257,8 +2261,17 @@
}
function addCardToForge(cardId, source) {
if (!cardId) return;
if (forgeSelected.includes(cardId)) return; // Уже добавлена
if (forgeSelected.length >= 3) return; // Максимум 3 карты
if (forgeSelected.length >= 3) {
// Показываем сообщение, что максимум 3 карты
const selectedEl = $('forge-selected');
if (selectedEl) {
selectedEl.style.animation = 'shake 0.5s';
setTimeout(() => selectedEl.style.animation = '', 500);
}
return; // Максимум 3 карты
}
const meta = cardDb[cardId];
if (!meta || meta.type !== 'minion') return; // Только миньоны
@ -2275,6 +2288,9 @@
renderForgeDeck(gameState);
}
}
// Настраиваем drag-and-drop заново после добавления
setTimeout(() => setupForgeDragAndDrop(gameState), 100);
}
function setupForgeDragAndDrop(state) {
@ -2334,6 +2350,131 @@
selectedEl.addEventListener('dragleave', handleDragLeave);
selectedEl.addEventListener('drop', handleDrop);
// Поддержка touch для мобильных устройств
if (isTouchDevice()) {
let touchStartCard = null;
let touchStartSource = null;
let touchStartElement = null;
// Обработка touch для карт в руке - упрощённая версия: просто тап добавляет карту
$all('#your-hand .card-wrap').forEach(cardWrap => {
const cardId = cardWrap.dataset.cardId;
const meta = cardDb[cardId];
if (cardId && meta && meta.type === 'minion') {
// Удаляем старые обработчики
cardWrap.ontouchstart = null;
cardWrap.ontouchend = null;
cardWrap.ontouchstart = function(e) {
if (sidebar && !sidebar.classList.contains('hidden')) {
touchStartCard = cardId;
touchStartSource = 'hand';
touchStartElement = cardWrap;
cardWrap._touchStartTime = Date.now();
cardWrap.classList.add('touch-active');
e.preventDefault();
}
};
cardWrap.ontouchend = function(e) {
if (touchStartCard === cardId && sidebar && !sidebar.classList.contains('hidden')) {
const touch = e.changedTouches[0];
const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now());
// Если это быстрый тап (менее 300мс), просто добавляем карту
if (timeDiff < 300) {
addCardToForge(cardId, 'hand');
} else {
// Иначе проверяем, куда перетащили
const dropEl = document.elementFromPoint(touch.clientX, touch.clientY);
if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) ||
dropEl.closest('#forge-selected'))) {
addCardToForge(cardId, 'hand');
}
}
cardWrap.classList.remove('touch-active');
touchStartCard = null;
touchStartSource = null;
touchStartElement = null;
e.preventDefault();
}
};
cardWrap.ontouchcancel = function() {
if (touchStartCard === cardId) {
cardWrap.classList.remove('touch-active');
touchStartCard = null;
touchStartSource = null;
touchStartElement = null;
}
};
}
});
// Обработка touch для карт в колоде - упрощённая версия
$all('.forge-deck-card').forEach(cardWrap => {
const cardId = cardWrap.dataset.cardId;
if (cardId) {
// Удаляем старые обработчики
cardWrap.ontouchstart = null;
cardWrap.ontouchend = null;
cardWrap.ontouchstart = function(e) {
touchStartCard = cardId;
touchStartSource = 'deck';
touchStartElement = cardWrap;
cardWrap._touchStartTime = Date.now();
cardWrap.classList.add('touch-active');
e.preventDefault();
};
cardWrap.ontouchend = function(e) {
if (touchStartCard === cardId) {
const touch = e.changedTouches[0];
const timeDiff = Date.now() - (touchStartElement?._touchStartTime || Date.now());
// Если это быстрый тап (менее 300мс), просто добавляем карту
if (timeDiff < 300) {
addCardToForge(cardId, 'deck');
} else {
// Иначе проверяем, куда перетащили
const dropEl = document.elementFromPoint(touch.clientX, touch.clientY);
if (dropEl && (dropEl === selectedEl || selectedEl.contains(dropEl) ||
dropEl.closest('#forge-selected'))) {
addCardToForge(cardId, 'deck');
}
}
cardWrap.classList.remove('touch-active');
touchStartCard = null;
touchStartSource = null;
touchStartElement = null;
e.preventDefault();
}
};
cardWrap.ontouchcancel = function() {
if (touchStartCard === cardId) {
cardWrap.classList.remove('touch-active');
touchStartCard = null;
touchStartSource = null;
touchStartElement = null;
}
};
}
});
// Также добавляем обработчик на саму область выбранных карт для визуальной обратной связи
selectedEl.ontouchstart = function(e) {
selectedEl.classList.add('drag-over');
};
selectedEl.ontouchend = function(e) {
selectedEl.classList.remove('drag-over');
};
}
// Делаем карты в руке перетаскиваемыми в кузницу
$all('#your-hand .card-wrap').forEach(cardWrap => {
const cardId = cardWrap.dataset.cardId;
@ -2414,13 +2555,16 @@
</div>`;
}).filter(Boolean).join('');
// Клик по карте в колоде тоже добавляет её
$all('.forge-deck-card').forEach(card => {
card.onclick = function() {
const cardId = card.dataset.cardId;
addCardToForge(cardId, 'deck');
};
});
// Клик по карте в колоде тоже добавляет её (для десктопа)
// На мобильных это обрабатывается через touch события
if (!isTouchDevice()) {
$all('.forge-deck-card').forEach(card => {
card.onclick = function() {
const cardId = card.dataset.cardId;
addCardToForge(cardId, 'deck');
};
});
}
renderForgeSelected();
updateForgeCraftButton();

View File

@ -27,7 +27,7 @@
<span class="logo-sw">STAR WARS</span>
<span class="logo-hs">HEARTHSTONE</span>
</h1>
<p class="subtitle">PvP до 4 игроков · Работает через Radmin VPN</p>
<p class="subtitle">PvP до 4 игроков · Игра с ИИ · Работает через Radmin VPN</p>
<button type="button" id="btn-instructions-lobby" class="btn-instructions-lobby" title="Как играть">?</button>
<button type="button" id="btn-cards-gallery" class="btn-instructions-lobby" title="Галерея карт" style="right: 3.5rem;">📚</button>
<button type="button" id="btn-settings-lobby" class="btn-settings-lobby" title="Настройки"></button>
@ -40,6 +40,10 @@
<div id="host-panel" class="panel active">
<label>Ваше имя</label>
<input type="text" id="host-name" placeholder="Игрок 1" maxlength="20" />
<label style="margin-top: 1rem; display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="ai-mode" style="width: 18px; height: 18px; cursor: pointer;" />
<span>Играть против ИИ</span>
</label>
<button type="button" id="btn-host" class="btn btn-primary">Создать комнату</button>
</div>

View File

@ -34,7 +34,8 @@
html, body {
height: 100%;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
font-family: var(--font-body);
background: var(--space);
color: #e2e8f0;
@ -42,12 +43,16 @@ html, body {
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-overflow-scrolling: touch;
}
#app {
position: relative;
min-height: 100vh;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Static starfield - no animation */
@ -1587,6 +1592,12 @@ html, body {
opacity: 1;
pointer-events: auto;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
@media (max-width: 768px) {
.forge-sidebar {
width: 100vw;
@ -2075,50 +2086,72 @@ html, body {
html, body {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
.screen {
padding: 1rem;
padding: 0.5rem;
min-height: 100vh;
overflow-x: hidden;
}
.lobby-card {
padding: 1.5rem;
max-width: 100%;
margin: 0 auto;
}
.game-header {
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
position: sticky;
top: 0;
z-index: 100;
background: var(--space);
border-bottom: 1px solid rgba(0,180,255,0.2);
}
.header-left, .header-center, .header-right {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
.header-center {
order: -1;
flex-wrap: wrap;
gap: 0.5rem;
}
.btn {
min-height: 44px;
padding: 0.85rem 1.5rem;
font-size: 1rem;
touch-action: manipulation;
}
.btn-end-turn {
min-height: 48px;
padding: 0.7rem 1.6rem;
font-size: 1.05rem;
flex: 1;
min-width: 120px;
}
.btn-instructions, .btn-settings {
width: 40px;
height: 40px;
width: 44px;
height: 44px;
font-size: 1.2rem;
min-width: 44px;
min-height: 44px;
}
.card-wrap {
width: 140px;
height: 196px;
touch-action: manipulation;
}
.board .card-wrap {
@ -2131,22 +2164,51 @@ html, body {
height: 140px;
}
.opponents {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.opponent-block {
min-width: auto;
width: 100%;
padding: 0.5rem;
}
.hand {
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
padding: 0.5rem;
gap: 0.3rem;
}
.hand .card-wrap {
margin-left: -40px;
margin-left: -30px;
scroll-snap-align: start;
flex-shrink: 0;
}
.hand .card-wrap:first-child {
margin-left: 0;
}
.hand .card-wrap:last-child {
margin-right: 0.5rem;
}
.hand-container {
min-height: 180px;
min-height: 200px;
padding: 0.5rem;
position: relative;
}
.deck-fan {
width: 90px;
height: 126px;
flex-shrink: 0;
}
.deck-stack {
@ -2162,21 +2224,31 @@ html, body {
.board {
min-height: 150px;
padding: 0.5rem;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.your-board {
min-height: 110px;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.opponent-block {
min-width: 140px;
padding: 0.5rem;
.your-board .card-wrap {
scroll-snap-align: start;
flex-shrink: 0;
}
.attack-mode {
bottom: 200px;
padding: 0.7rem 1.2rem;
font-size: 0.9rem;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 2rem);
max-width: 400px;
}
.attack-mode p {
@ -2187,6 +2259,8 @@ html, body {
max-width: 95vw;
padding: 1.5rem;
margin: 1rem;
max-height: 90vh;
overflow-y: auto;
}
.cards-gallery-grid {
@ -2199,14 +2273,38 @@ html, body {
height: 140px !important;
}
.forge-sidebar {
width: 100vw;
max-width: 100vw;
right: 0;
left: 0;
border-radius: 0;
max-height: 100vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.forge-deck-list {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 0.5rem;
max-height: 300px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.forge-deck-card {
width: 80px !important;
height: 112px !important;
touch-action: manipulation;
}
.forge-selected {
min-height: 120px;
touch-action: manipulation;
}
.forge-selected-card {
touch-action: manipulation;
}
.mana-display, .health-display {
@ -2225,6 +2323,8 @@ html, body {
.game-log {
font-size: 0.75rem;
max-height: 60px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.card-name {
@ -2233,6 +2333,7 @@ html, body {
.card-text {
font-size: 0.68rem;
line-height: 1.3;
}
.card-cost, .card-atk-hp {
@ -2240,9 +2341,11 @@ html, body {
}
.card-btn-info {
width: 24px;
height: 24px;
font-size: 0.8rem;
width: 28px;
height: 28px;
font-size: 0.9rem;
min-width: 28px;
min-height: 28px;
}
/* Touch feedback */
@ -2256,11 +2359,21 @@ html, body {
}
/* Prevent text selection on touch */
.card-wrap, .btn, .tab {
.card-wrap, .btn, .tab, .deck-fan {
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
/* Улучшенная поддержка touch для интерактивных элементов */
.btn:active {
transform: scale(0.95);
transition: transform 0.1s;
}
.card-wrap:active:not(.disabled) {
transform: scale(0.98);
}
}
/* Small mobile devices */