fix
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
.venv
|
||||||
|
app.db
|
||||||
BIN
__pycache__/app.cpython-310.pyc
Normal file
BIN
__pycache__/app.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-310.pyc
Normal file
BIN
__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
35
config.py
Normal file
35
config.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
||||||
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||||
|
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
|
# Настройки игры
|
||||||
|
STARTING_CAPITAL = 100000
|
||||||
|
MAX_PLAYERS_PER_ROOM = 10
|
||||||
|
DEFAULT_GAME_MONTHS = 12
|
||||||
|
|
||||||
|
# Тайминги (в секундах)
|
||||||
|
ACTION_PHASE_DURATION = 120
|
||||||
|
MARKET_PHASE_DURATION = 30
|
||||||
|
EVENT_PHASE_DURATION = 30
|
||||||
|
RESULTS_PHASE_DURATION = 45
|
||||||
|
|
||||||
|
# Пути
|
||||||
|
UPLOAD_FOLDER = os.path.join(basedir, 'static/uploads')
|
||||||
|
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_app(app):
|
||||||
|
# Создаем папки если их нет
|
||||||
|
if not os.path.exists(Config.UPLOAD_FOLDER):
|
||||||
|
os.makedirs(Config.UPLOAD_FOLDER)
|
||||||
|
|
||||||
|
|
||||||
|
config = Config()
|
||||||
23
logo.py
Normal file
23
logo.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# create_logo.py
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Создаем папку если её нет
|
||||||
|
os.makedirs('static/images', exist_ok=True)
|
||||||
|
|
||||||
|
# Создаем изображение
|
||||||
|
img = Image.new('RGB', (200, 60), color='#0088cc')
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Рисуем текст (нужен шрифт, или используем стандартный)
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype("arial.ttf", 20)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Текст логотипа
|
||||||
|
draw.text((10, 20), "💰 Капитал & Рынок", fill="white", font=font)
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
img.save('static/images/logo.png')
|
||||||
|
print("Логотип создан: static/images/logo.png")
|
||||||
16
main.py
Normal file
16
main.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# This is a sample Python script.
|
||||||
|
|
||||||
|
# Press Shift+F10 to execute it or replace it with your code.
|
||||||
|
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
||||||
|
|
||||||
|
|
||||||
|
def print_hi(name):
|
||||||
|
# Use a breakpoint in the code line below to debug your script.
|
||||||
|
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
||||||
|
|
||||||
|
|
||||||
|
# Press the green button in the gutter to run the script.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print_hi('PyCharm')
|
||||||
|
|
||||||
|
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
||||||
890
static/css/style.css
Normal file
890
static/css/style.css
Normal file
@@ -0,0 +1,890 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #0088cc; /* Telegram blue */
|
||||||
|
--secondary-color: #f0f2f5;
|
||||||
|
--accent-color: #34b7f1;
|
||||||
|
--danger-color: #e53935;
|
||||||
|
--success-color: #4caf50;
|
||||||
|
--warning-color: #ff9800;
|
||||||
|
--text-color: #333;
|
||||||
|
--light-text: #707579;
|
||||||
|
--border-radius: 10px;
|
||||||
|
--box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
--transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контейнер */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 15px;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Шапка */
|
||||||
|
.header {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 30px;
|
||||||
|
max-width: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Карточки */
|
||||||
|
.card {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2, .card h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-bottom: 2px solid var(--secondary-color);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Списки */
|
||||||
|
.player-list, .room-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-item, .room-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: var(--transition);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-item:hover, .room-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-item:last-child, .room-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info, .room-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-avatar, .room-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-capital {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--success-color);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-ability {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
max-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопки */
|
||||||
|
.button {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: var(--transition);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #0077b3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary {
|
||||||
|
background-color: white;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.danger:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.success {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.success:hover {
|
||||||
|
background-color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.warning {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.warning:hover {
|
||||||
|
background-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Табы */
|
||||||
|
.tab-container {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: white;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
border-bottom: 3px solid var(--primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Активы */
|
||||||
|
.asset-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--success-color);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-price.negative {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-price.neutral {
|
||||||
|
color: var(--light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-change {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-change.positive {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-change.negative {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Прогресс-бар */
|
||||||
|
.progress-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 12px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таймер */
|
||||||
|
.timer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: var(--primary-color);
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer.warning {
|
||||||
|
color: var(--warning-color);
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer.danger {
|
||||||
|
color: var(--danger-color);
|
||||||
|
animation: pulse 0.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Новости и события */
|
||||||
|
.news-item {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0 -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-impact {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impact-positive {
|
||||||
|
color: var(--success-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impact-negative {
|
||||||
|
color: var(--danger-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impact-neutral {
|
||||||
|
color: var(--warning-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Способности */
|
||||||
|
.ability-item {
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-item:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-item.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-left-color: var(--light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-item.disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ability-cooldown {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-top: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Формы авторизации */
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--light-text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input,
|
||||||
|
.input-group select,
|
||||||
|
.input-group textarea {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input:focus,
|
||||||
|
.input-group select:focus,
|
||||||
|
.input-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #0077b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Комнаты */
|
||||||
|
.room-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.waiting {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.playing {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.full {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.finished {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-text);
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:hover {
|
||||||
|
background-color: #0077b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Всплывающие сообщения */
|
||||||
|
.flash-messages {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
background-color: white;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.success {
|
||||||
|
border-left-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.error {
|
||||||
|
border-left-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.warning {
|
||||||
|
border-left-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скроллбар */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #0077b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивные стили */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 25px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-avatar, .room-avatar {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-price {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 350px) {
|
||||||
|
.header-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дополнительные утилиты */
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 5px; }
|
||||||
|
.mt-2 { margin-top: 10px; }
|
||||||
|
.mt-3 { margin-top: 15px; }
|
||||||
|
.mt-4 { margin-top: 20px; }
|
||||||
|
.mt-5 { margin-top: 25px; }
|
||||||
|
|
||||||
|
.mb-1 { margin-bottom: 5px; }
|
||||||
|
.mb-2 { margin-bottom: 10px; }
|
||||||
|
.mb-3 { margin-bottom: 15px; }
|
||||||
|
.mb-4 { margin-bottom: 20px; }
|
||||||
|
.mb-5 { margin-bottom: 25px; }
|
||||||
|
|
||||||
|
.p-1 { padding: 5px; }
|
||||||
|
.p-2 { padding: 10px; }
|
||||||
|
.p-3 { padding: 15px; }
|
||||||
|
.p-4 { padding: 20px; }
|
||||||
|
.p-5 { padding: 25px; }
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-1 { gap: 5px; }
|
||||||
|
.gap-2 { gap: 10px; }
|
||||||
|
.gap-3 { gap: 15px; }
|
||||||
|
.gap-4 { gap: 20px; }
|
||||||
|
.gap-5 { gap: 25px; }
|
||||||
|
|
||||||
|
/* Стили для уведомлений flash */
|
||||||
|
.flash-messages {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10000;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
background-color: white;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.success {
|
||||||
|
border-left-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.error {
|
||||||
|
border-left-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.info {
|
||||||
|
border-left-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.warning {
|
||||||
|
border-left-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--light-text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) translateX(-50%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконки */
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-home { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>'); }
|
||||||
|
.icon-settings { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>'); }
|
||||||
|
.icon-chat { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>'); }
|
||||||
|
.icon-stats { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 11V3H8v6H2v12h20V11h-6zm-6-6h4v14h-4V5zm-6 6h4v8H4v-8zm16 8h-4v-6h4v6z"/></svg>'); }
|
||||||
|
.icon-help { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>'); }
|
||||||
BIN
static/images/logo.png
Normal file
BIN
static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
129
static/js/game.js
Normal file
129
static/js/game.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Логика игрового процесса
|
||||||
|
|
||||||
|
class Game {
|
||||||
|
constructor() {
|
||||||
|
this.month = 1;
|
||||||
|
this.phase = 'action'; // action, market, event, results
|
||||||
|
this.playerCapital = 100000;
|
||||||
|
this.assets = [];
|
||||||
|
this.abilities = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadGameState();
|
||||||
|
this.startPhaseTimer();
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
startPhaseTimer() {
|
||||||
|
const phaseDurations = {
|
||||||
|
action: 120, // 2 минуты
|
||||||
|
market: 30, // 30 секунд
|
||||||
|
event: 30, // 30 секунд
|
||||||
|
results: 45 // 45 секунд
|
||||||
|
};
|
||||||
|
|
||||||
|
this.timer = new GameTimer('phase-timer', phaseDurations[this.phase]);
|
||||||
|
this.timer.onComplete = () => this.nextPhase();
|
||||||
|
this.timer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPhase() {
|
||||||
|
const phases = ['action', 'market', 'event', 'results'];
|
||||||
|
const currentIndex = phases.indexOf(this.phase);
|
||||||
|
const nextIndex = (currentIndex + 1) % phases.length;
|
||||||
|
|
||||||
|
this.phase = phases[nextIndex];
|
||||||
|
|
||||||
|
if (this.phase === 'results') {
|
||||||
|
this.endMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePhaseDisplay();
|
||||||
|
this.startPhaseTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
endMonth() {
|
||||||
|
this.month++;
|
||||||
|
this.calculateMarketChanges();
|
||||||
|
this.applyRandomEvents();
|
||||||
|
this.updateLeaderboard();
|
||||||
|
|
||||||
|
if (this.month > 12) {
|
||||||
|
this.endGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
document.getElementById('game-month').textContent = `Месяц ${this.month}`;
|
||||||
|
document.getElementById('player-capital').textContent = formatCurrency(this.playerCapital);
|
||||||
|
|
||||||
|
// Обновление прогресс-бара
|
||||||
|
const progress = (this.playerCapital / 500000) * 100; // Пример: цель 500к
|
||||||
|
document.getElementById('capital-progress').style.width = Math.min(progress, 100) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePhaseDisplay() {
|
||||||
|
const phaseNames = {
|
||||||
|
action: 'Фаза действий',
|
||||||
|
market: 'Реакция рынка',
|
||||||
|
event: 'Случайные события',
|
||||||
|
results: 'Итоги месяца'
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('phase-timer').textContent = phaseNames[this.phase];
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMarketChanges() {
|
||||||
|
// Здесь будет сложная логика из концепции игры
|
||||||
|
console.log('Расчет изменений рынка...');
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRandomEvents() {
|
||||||
|
// Применение случайных и политических событий
|
||||||
|
console.log('Применение событий...');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLeaderboard() {
|
||||||
|
// Обновление таблицы лидеров
|
||||||
|
console.log('Обновление лидерборда...');
|
||||||
|
}
|
||||||
|
|
||||||
|
endGame() {
|
||||||
|
alert('Игра завершена! Победитель: ...');
|
||||||
|
window.location.href = 'rooms.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGameState() {
|
||||||
|
// Загрузка состояния игры из localStorage или сервера
|
||||||
|
const saved = localStorage.getItem('gameState');
|
||||||
|
if (saved) {
|
||||||
|
const state = JSON.parse(saved);
|
||||||
|
Object.assign(this, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveGameState() {
|
||||||
|
localStorage.setItem('gameState', JSON.stringify({
|
||||||
|
month: this.month,
|
||||||
|
phase: this.phase,
|
||||||
|
playerCapital: this.playerCapital,
|
||||||
|
assets: this.assets,
|
||||||
|
abilities: this.abilities
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация игры
|
||||||
|
function initGame() {
|
||||||
|
window.game = new Game();
|
||||||
|
game.init();
|
||||||
|
|
||||||
|
// Автосохранение каждые 30 секунд
|
||||||
|
setInterval(() => game.saveGameState(), 30000);
|
||||||
|
|
||||||
|
// Обработка завершения хода
|
||||||
|
document.getElementById('end-turn')?.addEventListener('click', () => {
|
||||||
|
game.nextPhase();
|
||||||
|
});
|
||||||
|
}
|
||||||
87
static/js/main.js
Normal file
87
static/js/main.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Общие функции для всех страниц
|
||||||
|
|
||||||
|
// Инициализация табов
|
||||||
|
function initTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.tab');
|
||||||
|
if (tabs.length > 0) {
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
const tabId = this.getAttribute('data-tab');
|
||||||
|
if (tabId) {
|
||||||
|
switchTab(tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabId) {
|
||||||
|
// Удаляем активный класс у всех вкладок
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
// Добавляем активный класс выбранной вкладке
|
||||||
|
const activeTab = document.querySelector(`.tab[data-tab="${tabId}"]`);
|
||||||
|
const activeContent = document.getElementById(`${tabId}-tab`);
|
||||||
|
|
||||||
|
if (activeTab) activeTab.classList.add('active');
|
||||||
|
if (activeContent) activeContent.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Таймер обратного отсчета
|
||||||
|
class GameTimer {
|
||||||
|
constructor(elementId, duration) {
|
||||||
|
this.element = document.getElementById(elementId);
|
||||||
|
this.duration = duration;
|
||||||
|
this.timeLeft = duration;
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.updateDisplay();
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.timeLeft--;
|
||||||
|
this.updateDisplay();
|
||||||
|
|
||||||
|
if (this.timeLeft <= 0) {
|
||||||
|
this.stop();
|
||||||
|
if (this.onComplete) this.onComplete();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
if (this.element) {
|
||||||
|
const minutes = Math.floor(this.timeLeft / 60);
|
||||||
|
const seconds = this.timeLeft % 60;
|
||||||
|
this.element.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форматирование чисел (валюты)
|
||||||
|
function formatCurrency(amount) {
|
||||||
|
return new Intl.NumberFormat('ru-RU', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'RUB',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при загрузке страницы
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initTabs();
|
||||||
|
|
||||||
|
// Инициализация текущего пользователя
|
||||||
|
const currentUser = localStorage.getItem('currentUser');
|
||||||
|
if (currentUser && document.getElementById('current-user')) {
|
||||||
|
document.getElementById('current-user').textContent = currentUser;
|
||||||
|
}
|
||||||
|
});
|
||||||
52
templates/403.html
Normal file
52
templates/403.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Доступ запрещён - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block screen_content %}
|
||||||
|
<div class="header">
|
||||||
|
<a href="{{ url_for('rooms') }}" class="back-button">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div style="font-size: 5rem; color: var(--warning-color); margin: 20px 0;">403</div>
|
||||||
|
<h2>Доступ запрещён</h2>
|
||||||
|
<p>У вас недостаточно прав для доступа к этой странице.</p>
|
||||||
|
|
||||||
|
<div style="margin: 30px 0;">
|
||||||
|
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">🔒💰</div>
|
||||||
|
<p style="color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
Эта комната может быть приватной или игра уже началась.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 mt-4">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('rooms') }}" class="button">
|
||||||
|
Вернуться к списку комнат
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login') }}" class="button">
|
||||||
|
Войти в систему
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('index') }}" class="button secondary">
|
||||||
|
На главную
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Возможные причины:</h3>
|
||||||
|
<ul style="padding-left: 20px; margin-top: 10px;">
|
||||||
|
<li style="margin-bottom: 8px;">Вы не авторизованы в системе</li>
|
||||||
|
<li style="margin-bottom: 8px;">У вас нет прав для доступа к этой комнате</li>
|
||||||
|
<li style="margin-bottom: 8px;">Игра уже началась и присоединение невозможно</li>
|
||||||
|
<li>Комната является приватной</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
46
templates/404.html
Normal file
46
templates/404.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Страница не найдена - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block screen_content %}
|
||||||
|
<div class="header">
|
||||||
|
<a href="{{ url_for('rooms') }}" class="back-button">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div style="font-size: 5rem; color: var(--primary-color); margin: 20px 0;">404</div>
|
||||||
|
<h2>Страница не найдена</h2>
|
||||||
|
<p>К сожалению, запрашиваемая страница не существует или была перемещена.</p>
|
||||||
|
|
||||||
|
<div style="margin: 30px 0;">
|
||||||
|
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">💼📉</div>
|
||||||
|
<p style="color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
Возможно, комната была закрыта или игра завершена.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 mt-4">
|
||||||
|
<a href="{{ url_for('rooms') }}" class="button">
|
||||||
|
Вернуться к списку комнат
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('index') }}" class="button secondary">
|
||||||
|
На главную
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Что можно сделать?</h3>
|
||||||
|
<ul style="padding-left: 20px; margin-top: 10px;">
|
||||||
|
<li style="margin-bottom: 8px;">Проверьте правильность URL-адреса</li>
|
||||||
|
<li style="margin-bottom: 8px;">Создайте новую игровую комнату</li>
|
||||||
|
<li style="margin-bottom: 8px;">Присоединитесь к другой комнате</li>
|
||||||
|
<li>Обратитесь к администратору, если это ошибка</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
74
templates/500.html
Normal file
74
templates/500.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Ошибка сервера - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block screen_content %}
|
||||||
|
<div class="header">
|
||||||
|
<a href="{{ url_for('rooms') }}" class="back-button">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div style="font-size: 5rem; color: var(--danger-color); margin: 20px 0;">500</div>
|
||||||
|
<h2>Ошибка сервера</h2>
|
||||||
|
<p>Произошла внутренняя ошибка сервера. Мы уже работаем над её устранением.</p>
|
||||||
|
|
||||||
|
<div style="margin: 30px 0;">
|
||||||
|
<div style="font-size: 8rem; color: #f0f0f0; margin-bottom: 20px;">💥📊</div>
|
||||||
|
<p style="color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
Рынок временно не работает. Пожалуйста, попробуйте позже.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 mt-4">
|
||||||
|
<button onclick="window.location.reload()" class="button">
|
||||||
|
Попробовать снова
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('rooms') }}" class="button secondary">
|
||||||
|
Вернуться к списку комнат
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('index') }}" class="button">
|
||||||
|
На главную
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Техническая информация</h3>
|
||||||
|
<p style="margin-top: 10px; font-size: 0.9rem; color: var(--light-text);">
|
||||||
|
Если ошибка повторяется, пожалуйста, свяжитесь с администратором:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="background-color: #f8f9fa; padding: 15px; border-radius: var(--border-radius); margin-top: 15px; font-family: monospace; font-size: 0.85rem;">
|
||||||
|
<div>Время ошибки: <span id="error-time">{{ now.strftime('%Y-%m-%d %H:%M:%S') if now else '' }}</span></div>
|
||||||
|
<div>Путь: <span id="error-path">{{ request.path if request else '' }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button onclick="copyErrorInfo()" class="button secondary">
|
||||||
|
Скопировать информацию об ошибке
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function copyErrorInfo() {
|
||||||
|
const errorInfo = `Ошибка 500\nПуть: ${document.getElementById('error-path').textContent}\nВремя: ${document.getElementById('error-time').textContent}\nUser Agent: ${navigator.userAgent}`;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(errorInfo).then(() => {
|
||||||
|
alert('Информация об ошибке скопирована в буфер обмена');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автоматическая перезагрузка через 30 секунд
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Пытаемся перезагрузить страницу через 30 секунд после ошибки...');
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
69
templates/base.html
Normal file
69
templates/base.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Капитал & Рынок{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Статические файлы -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='images/logo.png') }}">
|
||||||
|
|
||||||
|
<!-- WebSocket -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Уведомления -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash-message {{ category }}">
|
||||||
|
{{ message }}
|
||||||
|
<button class="flash-close" onclick="this.parentElement.remove()">×</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Контент страницы -->
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Общие скрипты -->
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Инициализация WebSocket -->
|
||||||
|
<script>
|
||||||
|
// Подключение к WebSocket
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
// Базовая обработка подключения
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Connected to server');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Disconnected from server');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Глобальные обработчики событий
|
||||||
|
socket.on('chat_message', function(data) {
|
||||||
|
// Обработка сообщений чата
|
||||||
|
console.log('New message:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('player_joined', function(data) {
|
||||||
|
console.log('Player joined:', data.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('player_left', function(data) {
|
||||||
|
console.log('Player left:', data.username);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
templates/game.html
Normal file
40
templates/game.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Капитал & Рынок - Игра</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="screen active">
|
||||||
|
<div class="header">
|
||||||
|
<a href="lobby.html" class="back-button" onclick="return confirmExit()">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="logo.png" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
<span id="game-month">Месяц 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Остальной код игрового экрана (табы, карточки и т.д.) -->
|
||||||
|
<!-- Используем тот же HTML из предыдущей версии, но разбиваем на отдельные страницы -->
|
||||||
|
<!-- Полный код смотри в предыдущем сообщении -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/game.js"></script>
|
||||||
|
<script>
|
||||||
|
// Подтверждение выхода из игры
|
||||||
|
function confirmExit() {
|
||||||
|
return confirm('Вы уверены, что хотите выйти из игры? Текущий прогресс может быть потерян.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация игры
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initGame();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
210
templates/index.html
Normal file
210
templates/index.html
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Капитал & Рынок - Экономическая стратегия{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="screen active">
|
||||||
|
<!-- Шапка -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo-container">
|
||||||
|
{% if url_for('static', filename='images/logo.png') %}
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo"
|
||||||
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
|
||||||
|
<span class="logo-text" style="display: none; color: white; font-weight: bold; font-size: 1.2rem;">💰 Капитал & Рынок</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.2rem;">💰 Капитал & Рынок</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- Приветственная карточка -->
|
||||||
|
<div class="card text-center">
|
||||||
|
<h1 style="margin-bottom: 15px;">Капитал & Рынок</h1>
|
||||||
|
<p style="margin: 15px 0; font-size: 1.1rem; color: var(--light-text);">
|
||||||
|
Стань успешным инвестором в динамичной экономической стратегии!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="margin: 25px 0;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 20px;">💰📈🏦</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3" style="max-width: 300px; margin: 0 auto;">
|
||||||
|
<a href="{{ url_for('login') }}" class="button" style="font-size: 1.1rem; padding: 15px;">
|
||||||
|
Войти в игру
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}" class="button success" style="font-size: 1.1rem; padding: 15px;">
|
||||||
|
Создать аккаунт
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('quick_login', username='Гость') }}" class="button secondary" style="padding: 12px;">
|
||||||
|
Быстрый старт (гостевая игра)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Особенности игры -->
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="text-align: center; margin-bottom: 20px;">Особенности игры</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr; gap: 15px;">
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; flex-shrink: 0;">🎮</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Динамичный геймплей</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
|
||||||
|
Каждый месяц - новые решения, события и вызовы. Адаптируйтесь к меняющемуся рынку!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; flex-shrink: 0;">👥</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Мультиплеер до 10 игроков</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
|
||||||
|
Соревнуйтесь с друзьями или случайными соперниками. Создавайте альянсы и заключайте сделки!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; flex-shrink: 0;">💡</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">13 уникальных способностей</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
|
||||||
|
Каждый игрок получает особую способность: от Кризисного инвестора до Теневого бухгалтера.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; padding: 15px; background-color: #f8f9fa; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; flex-shrink: 0;">📊</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Реалистичная экономика</h4>
|
||||||
|
<p style="font-size: 0.9rem; color: var(--light-text); margin: 0;">
|
||||||
|
Рынок реагирует на действия всех игроков. Ваши решения влияют на цены активов!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Как начать играть -->
|
||||||
|
<div class="card">
|
||||||
|
<h3 style="text-align: center; margin-bottom: 20px;">Как начать играть?</h3>
|
||||||
|
<div style="counter-reset: step-counter;">
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
|
||||||
|
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Создайте аккаунт</h4>
|
||||||
|
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
|
||||||
|
Зарегистрируйтесь или войдите как гость. Это займет менее минуты!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
|
||||||
|
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Присоединитесь к комнате</h4>
|
||||||
|
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
|
||||||
|
Выберите существующую комнату или создайте свою. Настройте правила игры.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee;">
|
||||||
|
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Выберите способность</h4>
|
||||||
|
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
|
||||||
|
Получите случайную уникальную способность и стартовый капитал 100,000 ₽.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: flex-start; gap: 15px;">
|
||||||
|
<div style="background-color: var(--primary-color); color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-weight: bold;">
|
||||||
|
4
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 5px;">Станьте самым богатым!</h4>
|
||||||
|
<p style="font-size: 0.95rem; color: var(--light-text); margin: 0;">
|
||||||
|
Инвестируйте в акции, недвижимость, бизнес. Обыграйте конкурентов за 12 месяцев!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статистика (заглушка) -->
|
||||||
|
<div class="card text-center">
|
||||||
|
<h3 style="margin-bottom: 15px;">Игровая статистика</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 20px;">
|
||||||
|
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">1,234</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Активных игроков</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">567</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Игровых комнат</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">89</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Турниров</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px; background-color: #f0f8ff; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">12</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Уникальных способностей</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Футер -->
|
||||||
|
<div style="text-align: center; padding: 20px 0; color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
<p>© 2024 Капитал & Рынок. Экономическая стратегия в реальном времени.</p>
|
||||||
|
<p style="margin-top: 10px;">
|
||||||
|
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Правила</a> |
|
||||||
|
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Контакты</a> |
|
||||||
|
<a href="#" style="color: var(--primary-color); text-decoration: none; margin: 0 10px;">Поддержка</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Простая анимация для привлечения внимания
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const emojis = document.querySelector('.text-center .flex');
|
||||||
|
if (emojis) {
|
||||||
|
emojis.style.opacity = '0';
|
||||||
|
emojis.style.transform = 'translateY(20px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
emojis.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||||
|
emojis.style.opacity = '1';
|
||||||
|
emojis.style.transform = 'translateY(0)';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подсветка кнопок при наведении
|
||||||
|
const buttons = document.querySelectorAll('.button');
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.addEventListener('mouseenter', function() {
|
||||||
|
this.style.transform = 'translateY(-2px)';
|
||||||
|
});
|
||||||
|
button.addEventListener('mouseleave', function() {
|
||||||
|
this.style.transform = 'translateY(0)';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
1085
templates/lobby.html
Normal file
1085
templates/lobby.html
Normal file
File diff suppressed because it is too large
Load Diff
114
templates/login.html
Normal file
114
templates/login.html
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Вход - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block screen_content %}
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Вход в игру</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('login') }}" class="auth-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="username">Имя пользователя</label>
|
||||||
|
<input type="text" id="username" name="username"
|
||||||
|
placeholder="Введите ваш никнейм"
|
||||||
|
value="{{ request.form.username if request.form }}"
|
||||||
|
required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" id="password" name="password"
|
||||||
|
placeholder="Введите пароль" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group" style="flex-direction: row; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="remember" name="remember" checked>
|
||||||
|
<label for="remember" style="margin: 0;">Запомнить меня</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="button">Войти</button>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ url_for('register') }}">Создать аккаунт</a>
|
||||||
|
<a href="#" onclick="showQuickLogin()">Быстрый вход (тест)</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Тестовые аккаунты</h3>
|
||||||
|
<p style="margin-bottom: 15px; color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
Для быстрого тестирования игры:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<a href="{{ url_for('quick_login', username='Игрок1') }}" class="button secondary">
|
||||||
|
Войти как Игрок1
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('quick_login', username='Игрок2') }}" class="button secondary">
|
||||||
|
Войти как Игрок2
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('quick_login', username='Инвестор') }}" class="button secondary">
|
||||||
|
Войти как Инвестор
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно быстрого входа -->
|
||||||
|
<div id="quick-login-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<h3>Быстрый вход</h3>
|
||||||
|
<p>Введите имя для быстрого входа (будет создан аккаунт если его нет):</p>
|
||||||
|
|
||||||
|
<div class="input-group mt-3">
|
||||||
|
<input type="text" id="quick-username" placeholder="Имя пользователя" style="width: 100%;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<button onclick="doQuickLogin()" class="button success">Войти</button>
|
||||||
|
<button onclick="hideQuickLogin()" class="button secondary">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function showQuickLogin() {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById('quick-login-modal').classList.add('active');
|
||||||
|
document.getElementById('quick-username').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideQuickLogin() {
|
||||||
|
document.getElementById('quick-login-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function doQuickLogin() {
|
||||||
|
const username = document.getElementById('quick-username').value.trim();
|
||||||
|
if (username) {
|
||||||
|
window.location.href = `/quick_login/${encodeURIComponent(username)}`;
|
||||||
|
} else {
|
||||||
|
alert('Введите имя пользователя');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Автозаполнение формы для теста
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Если это демо-версия, можно автозаполнить форму
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.get('demo') === '1') {
|
||||||
|
document.getElementById('username').value = 'test_user';
|
||||||
|
document.getElementById('password').value = 'test123';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
178
templates/register.html
Normal file
178
templates/register.html
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Регистрация - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block screen_content %}
|
||||||
|
<div class="header">
|
||||||
|
<a href="{{ url_for('login') }}" class="back-button">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Капитал & Рынок" class="logo">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Создание аккаунта</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="username">Имя пользователя *</label>
|
||||||
|
<input type="text" id="username" name="username"
|
||||||
|
placeholder="Придумайте никнейм (мин. 3 символа)"
|
||||||
|
value="{{ request.form.username if request.form }}"
|
||||||
|
required autofocus>
|
||||||
|
<small style="color: var(--light-text); font-size: 0.85rem;">
|
||||||
|
Будет отображаться другим игрокам
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="email">Email *</label>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
placeholder="Ваш email"
|
||||||
|
value="{{ request.form.email if request.form }}"
|
||||||
|
required>
|
||||||
|
<small style="color: var(--light-text); font-size: 0.85rem;">
|
||||||
|
Только для восстановления пароля
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="password">Пароль *</label>
|
||||||
|
<input type="password" id="password" name="password"
|
||||||
|
placeholder="Придумайте пароль (мин. 4 символа)" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="password2">Повторите пароль *</label>
|
||||||
|
<input type="password" id="password2" name="password2"
|
||||||
|
placeholder="Повторите пароль" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mt-2">
|
||||||
|
<div style="background-color: #f5f5f5; padding: 10px; border-radius: var(--border-radius);">
|
||||||
|
<label style="display: flex; align-items: flex-start; gap: 10px; cursor: pointer;">
|
||||||
|
<input type="checkbox" name="terms" required style="margin-top: 3px;">
|
||||||
|
<span style="font-size: 0.9rem;">
|
||||||
|
Я согласен с <a href="#" onclick="showTerms()">правилами игры</a> и
|
||||||
|
<a href="#" onclick="showPrivacy()">политикой конфиденциальности</a>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="button success mt-3">Зарегистрироваться</button>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ url_for('login') }}">Уже есть аккаунт? Войти</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Почему стоит зарегистрироваться?</h3>
|
||||||
|
<ul style="padding-left: 20px; margin-top: 10px;">
|
||||||
|
<li style="margin-bottom: 8px;">🎮 Сохраняйте прогресс в играх</li>
|
||||||
|
<li style="margin-bottom: 8px;">📊 Отслеживайте статистику и рейтинг</li>
|
||||||
|
<li style="margin-bottom: 8px;">👥 Создавайте приватные комнаты</li>
|
||||||
|
<li style="margin-bottom: 8px;">🏆 Участвуйте в турнирах и соревнованиях</li>
|
||||||
|
<li>💬 Общайтесь с другими игроками</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальные окна с правилами -->
|
||||||
|
<div id="terms-modal" class="modal-backdrop">
|
||||||
|
<div class="modal" style="max-width: 600px;">
|
||||||
|
<h3>Правила игры "Капитал & Рынок"</h3>
|
||||||
|
<div style="max-height: 400px; overflow-y: auto; padding-right: 10px;">
|
||||||
|
<h4>1. Основные правила</h4>
|
||||||
|
<p>Игра проходит в реальном времени, каждый месяц длится 2-5 минут.</p>
|
||||||
|
|
||||||
|
<h4>2. Поведение игроков</h4>
|
||||||
|
<p>Запрещены оскорбления, нецензурная лексика, мошенничество.</p>
|
||||||
|
|
||||||
|
<h4>3. Использование способностей</h4>
|
||||||
|
<p>Способности можно использовать согласно их описанию и ограничениям.</p>
|
||||||
|
|
||||||
|
<h4>4. Определение победителя</h4>
|
||||||
|
<p>Победителем становится игрок с наибольшим капиталом после 12 месяцев.</p>
|
||||||
|
|
||||||
|
<h4>5. Дисквалификация</h4>
|
||||||
|
<p>Администратор может дисквалифицировать игрока за нарушение правил.</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="hideTerms()" class="button mt-3">Понятно</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="privacy-modal" class="modal-backdrop">
|
||||||
|
<div class="modal" style="max-width: 600px;">
|
||||||
|
<h3>Политика конфиденциальности</h3>
|
||||||
|
<div style="max-height: 400px; overflow-y: auto; padding-right: 10px;">
|
||||||
|
<h4>1. Собираемые данные</h4>
|
||||||
|
<p>Мы собираем только необходимые данные: имя пользователя, email (опционально), статистику игр.</p>
|
||||||
|
|
||||||
|
<h4>2. Использование данных</h4>
|
||||||
|
<p>Данные используются только для работы игры: идентификации, статистики, рейтингов.</p>
|
||||||
|
|
||||||
|
<h4>3. Безопасность</h4>
|
||||||
|
<p>Пароли хранятся в зашифрованном виде. Мы не передаем ваши данные третьим лицам.</p>
|
||||||
|
|
||||||
|
<h4>4. Cookies</h4>
|
||||||
|
<p>Используем только технические cookies для работы сессий.</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="hidePrivacy()" class="button mt-3">Понятно</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function showTerms() {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById('terms-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTerms() {
|
||||||
|
document.getElementById('terms-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrivacy() {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById('privacy-modal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePrivacy() {
|
||||||
|
document.getElementById('privacy-modal').classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация формы
|
||||||
|
document.querySelector('form').addEventListener('submit', function(e) {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const password2 = document.getElementById('password2').value;
|
||||||
|
const terms = document.querySelector('input[name="terms"]');
|
||||||
|
|
||||||
|
if (password !== password2) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Пароли не совпадают!');
|
||||||
|
document.getElementById('password2').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 4) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Пароль должен быть не менее 4 символов!');
|
||||||
|
document.getElementById('password').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terms.checked) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Необходимо согласиться с правилами игры!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
568
templates/rooms.html
Normal file
568
templates/rooms.html
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Комнаты - Капитал & Рынок{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="screen active">
|
||||||
|
<!-- Шапка -->
|
||||||
|
<div class="header">
|
||||||
|
<a href="{{ url_for('index') }}" class="back-button">←</a>
|
||||||
|
<div class="logo-container">
|
||||||
|
<span class="logo-text" style="color: white; font-weight: bold; font-size: 1.2rem;">
|
||||||
|
💰 Комнаты
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-left: auto;">
|
||||||
|
<span style="font-size: 0.9rem;">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{{ current_user.username }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- Поиск и создание комнаты -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="room-search" placeholder="Поиск комнат по названию..."
|
||||||
|
onkeyup="searchRooms()">
|
||||||
|
<button onclick="searchRooms()">🔍</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" onclick="createRoom()" style="margin-bottom: 20px;">
|
||||||
|
+ Создать новую комнату
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Доступные комнаты -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>📢 Доступные комнаты</h3>
|
||||||
|
<div style="margin: 10px 0; color: var(--light-text); font-size: 0.9rem;">
|
||||||
|
{% if rooms %}
|
||||||
|
Найдено {{ rooms|length }} комнат
|
||||||
|
{% else %}
|
||||||
|
Нет доступных комнат
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="room-list" id="room-list">
|
||||||
|
{% if rooms %}
|
||||||
|
{% for room in rooms %}
|
||||||
|
<li class="room-item" onclick="joinRoom('{{ room.code }}')">
|
||||||
|
<div class="room-info">
|
||||||
|
<div class="room-avatar"
|
||||||
|
style="background-color: {% if room.status == 'waiting' %}#4caf50{% elif room.status == 'playing' %}#ff9800{% else %}#9e9e9e{% endif %};">
|
||||||
|
{{ room.player_count }}
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong>{{ room.name }}</strong>
|
||||||
|
<span style="font-size: 0.8rem; color: var(--light-text);">
|
||||||
|
Месяц {{ room.current_month }}/{{ room.total_months }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-meta">
|
||||||
|
Создатель: {{ room.creator.username if room.creator else 'Система' }} •
|
||||||
|
Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }}
|
||||||
|
</div>
|
||||||
|
{% if room.settings %}
|
||||||
|
<div style="margin-top: 3px;">
|
||||||
|
{% set settings = room.settings|from_json %}
|
||||||
|
{% if settings.allow_loans %}
|
||||||
|
<span style="background-color: #e3f2fd; color: #1976d2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem; margin-right: 5px;">Кредиты</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if settings.allow_black_market %}
|
||||||
|
<span style="background-color: #f3e5f5; color: #7b1fa2; padding: 2px 6px; border-radius: 10px; font-size: 0.75rem;">Чёрный рынок</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="room-status {{ room.status }}">
|
||||||
|
{% if room.status == 'waiting' %}
|
||||||
|
Ожидание
|
||||||
|
{% elif room.status == 'playing' %}
|
||||||
|
Игра идет
|
||||||
|
{% elif room.status == 'full' %}
|
||||||
|
Заполнена
|
||||||
|
{% else %}
|
||||||
|
{{ room.status }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li style="text-align: center; padding: 20px; color: var(--light-text);">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 10px;">🏢</div>
|
||||||
|
<p>Пока нет доступных комнат</p>
|
||||||
|
<p style="font-size: 0.9rem;">Создайте первую комнату!</p>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ваши комнаты -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>⭐ Ваши комнаты</h3>
|
||||||
|
{% if user_rooms %}
|
||||||
|
<ul class="room-list" id="my-room-list">
|
||||||
|
{% for room in user_rooms %}
|
||||||
|
<li class="room-item" onclick="joinRoom('{{ room.code }}')">
|
||||||
|
<div class="room-info">
|
||||||
|
<div class="room-avatar" style="background-color: var(--primary-color);">
|
||||||
|
{{ room.player_count }}
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong>{{ room.name }}</strong>
|
||||||
|
{% if room.creator_id == current_user.id %}
|
||||||
|
<span style="background-color: #fff3e0; color: #ef6c00; padding: 2px 8px; border-radius: 10px; font-size: 0.75rem;">Админ</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="room-meta">
|
||||||
|
Месяц {{ room.current_month }}/{{ room.total_months }} •
|
||||||
|
Игроков: {{ room.player_count }}/{{ config.MAX_PLAYERS_PER_ROOM }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="room-status {{ room.status }}">
|
||||||
|
{% if room.status == 'waiting' %}
|
||||||
|
Ожидание
|
||||||
|
{% elif room.status == 'playing' %}
|
||||||
|
В игре
|
||||||
|
{% else %}
|
||||||
|
{{ room.status }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align: center; padding: 20px; color: var(--light-text);">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 10px;">👤</div>
|
||||||
|
<p>Вы пока не участвуете в комнатах</p>
|
||||||
|
<p style="font-size: 0.9rem;">Присоединитесь к существующей или создайте свою</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Быстрые действия -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>⚡ Быстрые действия</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;">
|
||||||
|
<button class="button secondary" onclick="createQuickRoom('Быстрая игра')">
|
||||||
|
Быстрая игра
|
||||||
|
</button>
|
||||||
|
<button class="button secondary" onclick="createQuickRoom('Для новичков')">
|
||||||
|
Для новичков
|
||||||
|
</button>
|
||||||
|
<button class="button secondary" onclick="createQuickRoom('Турнир')">
|
||||||
|
Турнир
|
||||||
|
</button>
|
||||||
|
<button class="button secondary" onclick="createQuickRoom('С друзьями')">
|
||||||
|
С друзьями
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статистика -->
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="card">
|
||||||
|
<h3>📊 Ваша статистика</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px;">
|
||||||
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{
|
||||||
|
current_user.total_games }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Всего игр</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">{{
|
||||||
|
current_user.games_won }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Побед</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
|
||||||
|
{% if current_user.total_games > 0 %}
|
||||||
|
{{ "%.1f"|format(current_user.games_won / current_user.total_games * 100) }}%
|
||||||
|
{% else %}
|
||||||
|
0%
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Процент побед</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 15px; background-color: #f5f5f5; border-radius: var(--border-radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-color);">
|
||||||
|
{{ current_user.total_earnings|format_currency }}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.9rem; color: var(--light-text);">Заработано</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно создания комнаты -->
|
||||||
|
<div id="create-room-modal" class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<h3>Создание комнаты</h3>
|
||||||
|
|
||||||
|
<form id="create-room-form" onsubmit="return submitRoomForm(event)">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="room-name">Название комнаты *</label>
|
||||||
|
<input type="text" id="room-name" name="name"
|
||||||
|
placeholder="Например: Быки и медведи" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="total-months">Длительность игры</label>
|
||||||
|
<select id="total-months" name="total_months">
|
||||||
|
<option value="6">6 месяцев (быстрая игра)</option>
|
||||||
|
<option value="12" selected>12 месяцев (стандартная)</option>
|
||||||
|
<option value="18">18 месяцев (продвинутая)</option>
|
||||||
|
<option value="24">24 месяца (экспертная)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="start-capital">Стартовый капитал</label>
|
||||||
|
<input type="number" id="start-capital" name="start_capital"
|
||||||
|
value="100000" min="50000" max="1000000" step="10000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="allow-loans" name="allow_loans" checked>
|
||||||
|
<span>Разрешить кредиты</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="allow-black-market" name="allow_black_market">
|
||||||
|
<span>Разрешить чёрный рынок</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="private-room" name="private_room">
|
||||||
|
<span>Приватная комната (по приглашению)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<button type="submit" class="button success">Создать комнату</button>
|
||||||
|
<button type="button" onclick="hideCreateRoomModal()" class="button secondary">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Глобальные переменные
|
||||||
|
let allRooms = [];
|
||||||
|
|
||||||
|
// Инициализация при загрузке
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Rooms page loaded');
|
||||||
|
|
||||||
|
// Сохраняем все комнаты для поиска
|
||||||
|
const roomItems = document.querySelectorAll('.room-item');
|
||||||
|
allRooms = Array.from(roomItems).map(item => ({
|
||||||
|
name: item.querySelector('strong')?.textContent.toLowerCase() || '',
|
||||||
|
element: item
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Подключение к комнате через WebSocket
|
||||||
|
if (typeof socket !== 'undefined') {
|
||||||
|
console.log('Socket connected, joining global room');
|
||||||
|
socket.emit('join_global_room');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Поиск комнат
|
||||||
|
function searchRooms() {
|
||||||
|
const searchTerm = document.getElementById('room-search').value.toLowerCase();
|
||||||
|
const roomList = document.getElementById('room-list');
|
||||||
|
|
||||||
|
if (!roomList) return;
|
||||||
|
|
||||||
|
const rooms = roomList.querySelectorAll('.room-item');
|
||||||
|
rooms.forEach(room => {
|
||||||
|
const roomName = room.querySelector('strong')?.textContent.toLowerCase() || '';
|
||||||
|
if (roomName.includes(searchTerm) || searchTerm === '') {
|
||||||
|
room.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
room.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание комнаты
|
||||||
|
function createRoom() {
|
||||||
|
console.log('Opening create room modal');
|
||||||
|
const modal = document.getElementById('create-room-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('active');
|
||||||
|
document.getElementById('room-name').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Быстрое создание комнаты
|
||||||
|
function createQuickRoom(name) {
|
||||||
|
console.log('Creating quick room:', name);
|
||||||
|
|
||||||
|
// Создаем форму данных
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', name);
|
||||||
|
formData.append('total_months', '12');
|
||||||
|
formData.append('start_capital', '100000');
|
||||||
|
formData.append('allow_loans', 'on');
|
||||||
|
formData.append('allow_black_market', 'off');
|
||||||
|
formData.append('private_room', 'off');
|
||||||
|
|
||||||
|
// Отправляем запрос
|
||||||
|
createRoomRequest(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправка формы создания комнаты
|
||||||
|
function submitRoomForm(event) {
|
||||||
|
console.log('Submitting room form');
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = document.getElementById('create-room-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
createRoomRequest(formData);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для отправки запроса на создание комнаты
|
||||||
|
function createRoomRequest(formData) {
|
||||||
|
console.log('Sending create room request');
|
||||||
|
|
||||||
|
fetch('/room/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response headers:', response.headers.get('content-type'));
|
||||||
|
|
||||||
|
// Проверяем тип ответа
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
return response.text().then(text => {
|
||||||
|
console.error('Expected JSON, got:', text.substring(0, 200));
|
||||||
|
throw new Error('Server returned HTML instead of JSON');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Create room response:', data);
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert('Ошибка: ' + data.error);
|
||||||
|
} else if (data.redirect) {
|
||||||
|
// Редирект в лобби комнаты
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
} else if (data.success) {
|
||||||
|
// Редирект по коду комнаты
|
||||||
|
window.location.href = `/room/${data.room_code}`;
|
||||||
|
} else {
|
||||||
|
alert('Неизвестный ответ от сервера');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating room:', error);
|
||||||
|
alert('Ошибка при создании комнаты. Проверьте консоль для деталей.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрыть модальное окно
|
||||||
|
function hideCreateRoomModal() {
|
||||||
|
const modal = document.getElementById('create-room-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Присоединение к комнате
|
||||||
|
function joinRoom(roomCode) {
|
||||||
|
console.log('Joining room:', roomCode);
|
||||||
|
window.location.href = `/room/${roomCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление статуса онлайн
|
||||||
|
function updateOnlineStatus() {
|
||||||
|
if (typeof socket !== 'undefined' && socket.connected) {
|
||||||
|
socket.emit('user_online', {
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Периодическое обновление статуса
|
||||||
|
setInterval(updateOnlineStatus, 30000);
|
||||||
|
updateOnlineStatus();
|
||||||
|
|
||||||
|
// Обработка клавиши Escape для закрытия модального окна
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hideCreateRoomModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрытие модального окна при клике на фон
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const modal = document.getElementById('create-room-modal');
|
||||||
|
if (modal && modal.classList.contains('active') && event.target === modal) {
|
||||||
|
hideCreateRoomModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Дополнительные стили для rooms.html */
|
||||||
|
.room-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.waiting {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.playing {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ef6c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status.full {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
padding: 0 20px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button:hover {
|
||||||
|
background-color: #0077b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.room-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-status {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user