Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a5cf4dff | ||
|
|
0b33b442e9 | ||
|
|
d12afe62d0 | ||
|
|
30270916a4 | ||
|
|
2f0b22ccd4 | ||
|
|
89cbb81be7 | ||
|
|
f5f8dc03ce | ||
|
|
ec1e94119c | ||
|
|
8d4f7a95df | ||
|
|
5b89dc9d2f | ||
|
|
00bd3c72b6 | ||
|
|
551032d66e | ||
|
|
d31e998f87 | ||
|
|
976b8fc622 | ||
|
|
07a14ecc9b | ||
|
|
9d77971b0f | ||
|
|
d3ecbca731 | ||
|
|
a3984638c3 | ||
|
|
96c32d99c5 | ||
|
|
30e9669361 | ||
|
|
867b1f868c | ||
|
|
74b9b6698f | ||
|
|
05e1f79c25 | ||
|
|
98cd82f045 | ||
|
|
e44a62f9cc | ||
|
|
9f599f18d7 | ||
|
|
90d0eb2546 | ||
|
|
64ac0efb36 | ||
|
|
5fad9dbea4 |
+14
-6
@@ -6,7 +6,7 @@
|
||||
|
||||
# ─── ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ────────────────────────────────────────────────────
|
||||
# Этот параметр читает ЛАУНЧЕР (run.ps1 / run.sh) и решает, что запускать:
|
||||
# development — нативно: uvicorn --reload + vite, БД в ./data/dev/, вход TG+ник
|
||||
# development — нативно: uvicorn --reload + vite, БД в ./data/dev/, есть вход по нику (stub)
|
||||
# production — НЕ запускается лаунчером; деплой на Pi отдельно (docker compose up -d).
|
||||
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
||||
APP_ENV=development
|
||||
@@ -44,8 +44,8 @@ ADMIN_NICKNAME=Администратор
|
||||
ADMIN_BOOTSTRAP_ENABLED=true
|
||||
|
||||
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
||||
# Методы входа задаёт APP_ENV: dev → Telegram + stub (вход по нику), prod → только
|
||||
# Telegram. Для Telegram нужны токен и юзернейм бота (@BotFather). /setdomain у
|
||||
# Вход везде — логин/пароль и Telegram; в development ещё stub (по нику без пароля).
|
||||
# Для Telegram нужны токен и юзернейм бота (@BotFather). /setdomain у
|
||||
# BotFather укажи на ОБА домена, где открывается виджет: forbiddenstars.ru (prod)
|
||||
# и forbidden-stars.ru (dev).
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
@@ -81,7 +81,7 @@ DEV_ACHIEVEMENTS_DIR=./data/dev/achievements
|
||||
PROD_ACHIEVEMENTS_DIR=/data/achievements
|
||||
|
||||
# ─── ОБРАЗЫ ПРОДА (реестр для docker compose pull на Pi) ──────────────────────
|
||||
# Образы собираются под arm64 на ПК (scripts/build-push.sh) и пушатся в Gitea-реестр,
|
||||
# Образы собираются под arm64 на ПК (scripts/build-push.ps1; в Linux — .sh) и пушатся в Gitea-реестр,
|
||||
# а Pi их тянет (docker compose pull). Owner в пути — строчными. Тег можно версионировать.
|
||||
# Перед пушем/пуллом: docker login gitea.arseniev.info
|
||||
IMAGE_REGISTRY=gitea.arseniev.info/notbigghost
|
||||
@@ -89,7 +89,8 @@ IMAGE_TAG=latest
|
||||
|
||||
# ─── РЕСУРСЫ ПРОД-КОНТЕЙНЕРА (docker-compose.yml) ─────────────────────────────
|
||||
# Лимиты под Raspberry Pi. Не заданы → дефолты compose (512m / 1.5 CPU).
|
||||
# Подними, если у Pi больше RAM/ядер.
|
||||
# Подними, если у Pi больше RAM/ядер. Лимит памяти работает, только если на Pi включён
|
||||
# memory cgroup (cgroup_enable=memory в cmdline.txt) — см. deploy/pi/README.md, раздел 1.
|
||||
#APP_MEM_LIMIT=512m
|
||||
#APP_CPUS=1.5
|
||||
|
||||
@@ -123,13 +124,20 @@ BACKUP_VPS_DIR=/srv/fs-backups/restic
|
||||
# в deploy/backup/id_backup (в git НЕ идёт), pubkey — в authorized_keys у fsbackup@VPS.
|
||||
# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy\backup\id_backup")))
|
||||
BACKUP_SSH_KEY_B64=
|
||||
# Отчёты о бэкапах в Telegram и команды /backups, /status (deploy/backup/README.md, раздел 10).
|
||||
# id своего чата: напишите боту /start → `docker compose exec backup fs-backup telegram chats`. Несколько — через
|
||||
# запятую. Пусто = отчёты выключены. Токен по умолчанию — TELEGRAM_BOT_TOKEN приложения.
|
||||
BACKUP_TELEGRAM_CHAT_ID=
|
||||
#BACKUP_TELEGRAM_BOT_TOKEN=
|
||||
# Только для ПК (scripts/fs-backup.ps1 / .sh): как зайти на Pi по SSH и где там лежит
|
||||
# docker-compose.yml прода.
|
||||
BACKUP_PI_SSH=pi@192.168.1.10
|
||||
BACKUP_PI_DIR=~/forbidden-stars
|
||||
|
||||
# ─── ПРОЧЕЕ ───────────────────────────────────────────────────────────────────
|
||||
# Часовой пояс приложения (фикс. смещение в часах; МСК = 3)
|
||||
# Часовой пояс приложения (фикс. смещение в часах, −12..14; МСК = 3). Один на всех: в нём
|
||||
# сервер ставит «дату игры», а фронт показывает время и принимает даты объявлений —
|
||||
# пояс устройства игрока не учитывается.
|
||||
APP_TZ_OFFSET_HOURS=3
|
||||
# CORS нужен только в dev (фронт и API на разных портах); в prod single-origin
|
||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
|
||||
@@ -3,16 +3,3 @@
|
||||
# Скрипты образов (entrypoint.sh, deploy/*/…) контейнер дополнительно чинит sed-ом при сборке.
|
||||
*.sh text eol=lf
|
||||
backend/entrypoint.sh text eol=lf
|
||||
|
||||
# ── export-ignore: НЕ попадает в `git archive` (чистая выгрузка прода) ─────────
|
||||
# В git эти файлы есть и доступны на всех ветках (нужны для разработки),
|
||||
# но в архив деплоя (scripts/export-prod.sh) не идут. На Docker-сборку НЕ влияет —
|
||||
# там чистоту образа обеспечивает .dockerignore.
|
||||
backend/tests/ export-ignore
|
||||
backend/app/auth/dev_stub.py export-ignore
|
||||
backend/app/routers/dev_auth.py export-ignore
|
||||
backend/pyproject.toml export-ignore
|
||||
README.md export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitattributes export-ignore
|
||||
.dockerignore export-ignore
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
backend/ FastAPI: ядро, REST API, БД, миграции Alembic, seed, тесты
|
||||
frontend/ React + Vite SPA
|
||||
deploy/ публикация и эксплуатация: vps/ (Caddy), pi/ (прод), tunnel/ и backup/ (образы)
|
||||
scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-prod.sh
|
||||
scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК)
|
||||
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
|
||||
docker-compose.yml прод на Pi: app + tunnel + backup
|
||||
docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel
|
||||
@@ -214,10 +214,6 @@ docker compose up -d # pull_policy: always — тянет свежие
|
||||
- **Деплой на Pi:** на ПК с ветки `main` — `.\scripts\build-push.ps1` (собирает и пушит
|
||||
образы app + tunnel + backup под arm64), на Pi — `docker compose up -d`. На Windows нужна
|
||||
именно PS-версия скрипта (`build-push.sh` из PowerShell уходит в WSL).
|
||||
- **Чистая выгрузка в папку без git** (опц., к деплою на Pi не относится):
|
||||
`scripts/export-prod.sh <dir> [ref]` — через `git archive` + `export-ignore` из
|
||||
`.gitattributes` (без тестов, stub-входа, `pyproject.toml`, README-файлов и лаунчера).
|
||||
`dev_admin.py` в `export-ignore` пока не внесён (задача #70).
|
||||
|
||||
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
Тонкий слой поверх `admin_service.authenticate_admin`: throttle по IP, по паре «IP + логин»
|
||||
и по самому аккаунту через тот же `LoginThrottle`, что и вход игрока (`core/ratelimit`).
|
||||
Сервис остаётся чистым от инфраструктуры лимитов. Пароль администратора — единственный
|
||||
барьер к полному контролю приложения, поэтому перебор здесь ограничиваем строже игроцкого.
|
||||
Сервис остаётся чистым от инфраструктуры лимитов. Лимиты те же, что у игрока (5 на пару,
|
||||
20 на IP, 50 на аккаунт за 15 минут); отличие — при успешном входе снимаются все счётчики,
|
||||
включая IP.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Dev-провайдер: вход без секрета по нику/идентификатору (только не-production)."""
|
||||
"""Dev-провайдер: вход без секрета по нику/идентификатору (только development)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Общий вход: внешняя личность → пользователь → сессия.
|
||||
|
||||
Прод-безопасный модуль (без импортов dev-провайдера). Используется и Telegram-входом,
|
||||
и dev-входом.
|
||||
Прод-безопасный модуль (без импортов dev-провайдера). establish_session зовут вход через
|
||||
Telegram, /auth/login, /auth/register и dev-вход.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Проверяет подпись данных виджета (HMAC-SHA256 ключом SHA256(BOT_TOKEN)) и свежесть
|
||||
auth_date. Нужны TELEGRAM_BOT_TOKEN (+ TELEGRAM_BOT_USERNAME для виджета на фронте).
|
||||
Доступен и в dev, и в prod (в prod — единственный метод входа).
|
||||
Доступен во всех окружениях — наряду со входом по логину и паролю.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
# ── Главный переключатель окружения: development | production ─────────────
|
||||
# development — нативный dev (uvicorn + vite), БД в ./data/dev/, вход Telegram+ник.
|
||||
# development — нативный dev (uvicorn + vite), БД в ./data/dev/, есть stub-вход по нику.
|
||||
# production — Docker на Pi; контейнер форсит это значение, игнорируя .env.
|
||||
app_env: str = "development"
|
||||
log_level: str = "INFO"
|
||||
@@ -42,8 +42,9 @@ class Settings(BaseSettings):
|
||||
# от этого зависят Secure-cookie и проверка секретов при старте.
|
||||
local_public: str = "local"
|
||||
|
||||
# Часовой пояс приложения (фиксированное смещение, по умолчанию МСК +3).
|
||||
# Хранение всегда в UTC; смещение применяется к «дате игры» и отображению.
|
||||
# Часовой пояс приложения (фиксированное смещение, по умолчанию МСК +3). Хранение всегда
|
||||
# в UTC; смещение применяется к «дате игры» (timeutil.app_today) и к отображению времени
|
||||
# на фронте — оно приходит туда в GET /api/auth/config (#68).
|
||||
app_tz_offset_hours: int = 3
|
||||
|
||||
# ── БД: структура общая, файлы РАЗНЫЕ для dev и prod; выбор по app_env ─────
|
||||
@@ -67,8 +68,8 @@ class Settings(BaseSettings):
|
||||
# cookie_secure НЕ задаётся вручную — выводится из окружения (см. property ниже).
|
||||
cookie_domain: str | None = None
|
||||
|
||||
# Аутентификация. Методы входа определяются окружением (dev: telegram+stub,
|
||||
# prod: только telegram) — отдельного переключателя провайдера нет.
|
||||
# Аутентификация. Методы входа определяются окружением (auth/registry.py): везде
|
||||
# логин/пароль + Telegram, в development ещё stub — отдельного переключателя нет.
|
||||
telegram_bot_token: str | None = None
|
||||
telegram_bot_username: str | None = None
|
||||
public_base_url: str | None = None
|
||||
@@ -139,6 +140,15 @@ class Settings(BaseSettings):
|
||||
)
|
||||
return value
|
||||
|
||||
@field_validator("app_tz_offset_hours")
|
||||
@classmethod
|
||||
def _known_tz_offset(cls, value: int) -> int:
|
||||
"""Реальные пояса — от −12 до +14: опечатка в .env — ошибка старта, а не время,
|
||||
сдвинутое на сутки."""
|
||||
if not -12 <= value <= 14:
|
||||
raise ValueError(f"APP_TZ_OFFSET_HOURS={value} вне диапазона −12..14")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _forbid_default_secrets_when_published(self) -> "Settings":
|
||||
"""Fail-fast: опубликованное приложение не стартует с дефолтными/слабыми
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* рестарт (в т.ч. деплой) сбрасывает окно — злоумышленник получает новую квоту после
|
||||
перезапуска, но окно короткое, а рестарты редки;
|
||||
* при уходе от одного воркера лимит делится между процессами (каждый считает своё) —
|
||||
тогда счётчики нужно вынести во внешний стор (Redis pub/sub, как отмечено в CLAUDE.md
|
||||
тогда счётчики нужно вынести во внешний стор (Redis pub/sub, как отмечено в core/events.py
|
||||
про SSE-шину), общий для всех воркеров.
|
||||
Помимо пары «IP + логин» и лимита по IP есть IP-независимый лимит на аккаунт
|
||||
(`login-user:*` / `admin-login-user:*`), чтобы ротация X-Forwarded-For / многих адресов
|
||||
|
||||
@@ -175,6 +175,6 @@ def is_session_revoked(payload: dict) -> bool:
|
||||
def client_ip(request: Request) -> str | None:
|
||||
"""IP клиента для журнала аудита.
|
||||
|
||||
Одна точка на всё приложение: за VPS-привратником адрес придётся брать из
|
||||
X-Forwarded-For, и менять это в двух десятках роутеров — не вариант."""
|
||||
Одна точка на всё приложение. Реальный адрес за VPS-привратником уже подставляет
|
||||
uvicorn (--proxy-headers + --forwarded-allow-ips в entrypoint.sh) — отсюда он и берётся."""
|
||||
return request.client.host if request.client else None
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Инициализация схемы и справочников (для тестов и локального быстрого старта).
|
||||
|
||||
В production схема создаётся миграциями Alembic; этот модуль удобен для тестов,
|
||||
где БД поднимается из чистого состояния.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.db.session import engine
|
||||
from app.seed.reference_data import seed_reference_data
|
||||
from sqlmodel import Session
|
||||
|
||||
# Импорт моделей обязателен, чтобы они зарегистрировались в SQLModel.metadata.
|
||||
import app.models # noqa: F401
|
||||
|
||||
|
||||
def create_db_and_seed() -> None:
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
seed_reference_data(session)
|
||||
@@ -28,6 +28,7 @@ def auth_config() -> s.AuthConfig:
|
||||
return s.AuthConfig(
|
||||
methods=enabled_methods(),
|
||||
telegram_bot_username=settings.telegram_bot_username,
|
||||
tz_offset_hours=settings.app_tz_offset_hours,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""DEV-ТОЛЬКО роутер: вход по нику (stub) + тестовые пользователи.
|
||||
|
||||
Этот файл и app/auth/dev_stub.py ФИЗИЧЕСКИ исключены из прод-образа (.dockerignore),
|
||||
а подключается роутер лишь когда APP_ENV != production (см. app/main.py). Так код
|
||||
входа по логину остаётся только на деве.
|
||||
а подключается роутер лишь при APP_ENV=development (см. app/main.py). Так вход
|
||||
без пароля остаётся только на деве (по логину и паролю входят везде).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ class AuthConfig(BaseModel):
|
||||
# Доступные методы входа: ["password","telegram"] в проде, плюс "stub" в деве.
|
||||
methods: list[str] = []
|
||||
telegram_bot_username: str | None = None
|
||||
# Пояс приложения (APP_TZ_OFFSET_HOURS): в нём сервер считает «дату игры», а фронт
|
||||
# показывает время всем игрокам — независимо от пояса устройства (#68).
|
||||
tz_offset_hours: int
|
||||
|
||||
|
||||
# Верхняя граница длины пароля на входе API: отсекает мегабайтные тела до bcrypt.
|
||||
|
||||
@@ -140,3 +140,20 @@ def test_unknown_app_env_rejected(app_env):
|
||||
def test_app_env_case_insensitive():
|
||||
s = config.Settings(app_env="Development")
|
||||
assert s.is_development
|
||||
|
||||
|
||||
# ─── Пояс приложения (#68) ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("offset", [-13, 15])
|
||||
def test_tz_offset_out_of_range_rejected(offset):
|
||||
with pytest.raises(ValidationError, match="APP_TZ_OFFSET_HOURS"):
|
||||
config.Settings(app_env="development", app_tz_offset_hours=offset)
|
||||
|
||||
|
||||
def test_auth_config_exposes_app_tz_offset(client, monkeypatch):
|
||||
"""Фронт показывает время в поясе приложения — смещение приходит из настроек."""
|
||||
from app.routers import auth
|
||||
|
||||
monkeypatch.setattr(auth.settings, "app_tz_offset_hours", 5)
|
||||
assert client.get("/api/auth/config").json()["tz_offset_hours"] == 5
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
FROM restic/restic:0.19.1
|
||||
|
||||
# sqlite — консистентная копия и проверка БД; supercronic — cron для контейнера без root;
|
||||
# tini — корректные сигналы. jq, openssh-client, busybox (wget, flock, tar) уже есть в базе.
|
||||
RUN apk add --no-cache sqlite supercronic tini
|
||||
# tini — корректные сигналы; curl — отчёты и бот в Telegram (#83).
|
||||
# jq, openssh-client, busybox (wget, flock, tar) уже есть в базе.
|
||||
RUN apk add --no-cache sqlite supercronic tini curl
|
||||
|
||||
# Тот же uid, что у appuser в образе приложения (10001): файлы после restore получают
|
||||
# правильного владельца, а -wal/-shm SQLite никогда не достаются root.
|
||||
|
||||
+55
-5
@@ -737,16 +737,23 @@
|
||||
```
|
||||
|
||||
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
|
||||
Контейнер `backup` стартует одновременно с `app` и сразу пробует сделать первый бэкап. В его
|
||||
журнале (`docker compose logs backup`) нормально увидеть одно из двух:
|
||||
Контейнер `backup` стартует одновременно с `app`, ждёт, пока приложение ответит (до
|
||||
10 минут), и пробует сделать первый бэкап. В его журнале (`docker compose logs backup`)
|
||||
нормально увидеть одно из трёх:
|
||||
|
||||
- если `backup` успел раньше, чем `app` создал БД:
|
||||
- если приложение так и не поднялось и БД нет:
|
||||
|
||||
```
|
||||
ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось?
|
||||
Первый бэкап не удался — следующая попытка по расписанию.
|
||||
```
|
||||
|
||||
- если БД есть, но без таблиц (миграции ещё не прошли):
|
||||
|
||||
```
|
||||
Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches.
|
||||
```
|
||||
|
||||
- если БД уже была:
|
||||
|
||||
```
|
||||
@@ -763,8 +770,8 @@
|
||||
```
|
||||
|
||||
Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`.
|
||||
Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания
|
||||
БД, в списке может появиться свежий снимок с `?` (задача #74). Такой снимок не выбирайте.
|
||||
Снимки без данных защита больше не создаёт, но снимок с `?` мог остаться от старой версии
|
||||
бэкапов — такой не выбирайте.
|
||||
|
||||
5. Восстановите, подставив ID из `list vps`:
|
||||
|
||||
@@ -831,6 +838,42 @@ ID одного и того же снимка в `local` и `vps` разные
|
||||
**Обновить образ бэкапа** (после изменений в `deploy/backup/`). На ПК —
|
||||
`.\scripts\build-push.ps1`, на Pi — `docker compose up -d backup`.
|
||||
|
||||
### Отчёты в Telegram
|
||||
|
||||
Бот приложения (тот же, что для входа через Telegram) может присылать вам отчёты о бэкапах
|
||||
и отвечать на команды. Пока `BACKUP_TELEGRAM_CHAT_ID` пуст, отчёты выключены.
|
||||
|
||||
**Что приходит само:**
|
||||
- после каждого бэкапа — «✅ Бэкап»: сколько игроков и партий в БД, по каждому репозиторию
|
||||
снимок, число снимков и размер;
|
||||
- если бэкап не удался, в том числе не начавшись (нет пароля, занята другая операция, БД
|
||||
не прошла проверку) — «❌ Бэкап не удался» с причиной и упавшими репозиториями;
|
||||
- после еженедельной проверки — «🔍 Проверка данных: OK» или «❌».
|
||||
|
||||
**Команды боту** (отвечает только чатам из `BACKUP_TELEGRAM_CHAT_ID`):
|
||||
- `/backups` — хранящиеся снимки: по репозиторию число и размер, 10 последних с временем,
|
||||
игроками и партиями, 📌 у именованных;
|
||||
- `/status` — то же, что `fs-backup status`;
|
||||
- `/help` — список команд.
|
||||
|
||||
**Настройка:**
|
||||
1. В Telegram найдите бота приложения и напишите ему `/start`.
|
||||
2. Узнайте id своего чата:
|
||||
```bash
|
||||
docker compose exec backup fs-backup telegram chats
|
||||
# 123456789 @you /start
|
||||
```
|
||||
Когда бот уже работает (id вписан), он сам забирает сообщения. Тогда id нового чата
|
||||
ищите в журнале: `docker compose logs backup | grep "чужого чата"`.
|
||||
3. Впишите id в `.env` — `BACKUP_TELEGRAM_CHAT_ID=123456789`, несколько через запятую —
|
||||
и пересоздайте контейнер: `docker compose up -d backup`.
|
||||
4. Проверьте связь: `docker compose exec backup fs-backup telegram test` — в чат придёт
|
||||
пробное сообщение.
|
||||
|
||||
Токен бота по умолчанию берётся из `TELEGRAM_BOT_TOKEN` приложения. Чтобы слать отчёты от
|
||||
другого бота, задайте `BACKUP_TELEGRAM_BOT_TOKEN`. Если Telegram недоступен, бэкапы работают
|
||||
как обычно — в журнале будет только строка «Telegram: … не отправлено».
|
||||
|
||||
> **Никогда не выполняйте на проде `docker compose down -v`.** Флаг `-v` удаляет тома —
|
||||
> данные приложения **и** локальную копию бэкапов. Обычный `docker compose down` данные
|
||||
> не трогает.
|
||||
@@ -852,6 +895,8 @@ docker compose logs --tail 100 backup
|
||||
| Симптом | Причина | Что сделать |
|
||||
|---|---|---|
|
||||
| `BACKUP_PASSWORD не задан в .env — бэкапы ОТКЛЮЧЕНЫ` | нет пароля в `.env` | добавить `BACKUP_PASSWORD` (шаг 5), затем `docker compose up -d backup` |
|
||||
| отчёты в Telegram не приходят, `telegram test` пишет «отправить не удалось» | неверный chat id или токен, боту не писали `/start` | раздел 10, «Отчёты в Telegram»: написать боту, взять id из журнала, `docker compose up -d backup` |
|
||||
| бот не отвечает на `/backups`, в журнале «сообщение из чужого чата» | ваш id не в `BACKUP_TELEGRAM_CHAT_ID` | вписать id из этой строки журнала, `docker compose up -d backup` |
|
||||
| `неверный BACKUP_PASSWORD для репозитория …` | пароль в `.env` не тот, с которым создан репозиторий | вернуть правильный пароль из менеджера паролей; `docker compose up -d backup` |
|
||||
| `BACKUP_SSH_KEY_B64 не декодируется из base64` / `— не приватный SSH-ключ` | строка ключа обрезана, с пробелами или от `.pub` | заново скопировать base64 **приватного** ключа (шаг 5, пункт 5), одной строкой |
|
||||
| `Репозиторий vps недоступен` и выше `Permission denied (publickey)` | на VPS нет публичного ключа или ключ другой | шаг 3, пункты 4 и 9: проверить `authorized_keys` и вход `sftp` с ПК этим ключом |
|
||||
@@ -920,6 +965,9 @@ docker volume rm <имя тома>
|
||||
| `recover` | разбор прерванного восстановления |
|
||||
| `export <ID\|latest> [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` |
|
||||
| `restic <local\|vps> <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` |
|
||||
| `telegram chats` | id чатов, писавших боту, — для `BACKUP_TELEGRAM_CHAT_ID` |
|
||||
| `telegram test` | пробное сообщение в чаты `BACKUP_TELEGRAM_CHAT_ID` |
|
||||
| `telegram bot` | бот-слушатель команд `/backups`, `/status` (контейнер запускает его сам) |
|
||||
| `help` | краткая справка |
|
||||
| `init`, `info`, `health`, `has-snapshots` | служебные: создать репозитории, данные снимка для скриптов ПК, healthcheck, проверка «есть ли снимки» при старте |
|
||||
|
||||
@@ -955,6 +1003,8 @@ docker volume rm <имя тома>
|
||||
| `BACKUP_VPS_PORT` | Pi | `22` | SSH-порт VPS |
|
||||
| `BACKUP_VPS_DIR` | Pi | `/srv/fs-backups/restic` | папка репозитория на VPS |
|
||||
| `BACKUP_SSH_KEY_B64` | Pi | — | приватный ключ для VPS, base64 |
|
||||
| `BACKUP_TELEGRAM_CHAT_ID` | Pi | — | id чатов для отчётов и команд, через запятую; пусто = без Telegram |
|
||||
| `BACKUP_TELEGRAM_BOT_TOKEN` | Pi | `TELEGRAM_BOT_TOKEN` | токен бота, если отчёты должен слать другой бот |
|
||||
| `BACKUP_MEM_LIMIT` | Pi | `384m` | лимит памяти контейнера |
|
||||
| `BACKUP_PI_SSH` | ПК | — | как зайти на Pi: `pi@<IP>` |
|
||||
| `BACKUP_PI_DIR` | ПК | `~/forbidden-stars` | папка прода на Pi |
|
||||
|
||||
@@ -5,6 +5,14 @@ set -eu
|
||||
|
||||
log() { printf '[backup %s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
|
||||
|
||||
# Telegram-бот (#83): отвечает владельцу на /backups и /status. Стартует раньше проверки
|
||||
# пароля — при выключенных бэкапах скажет, что они выключены. Упал — перезапуск через минуту.
|
||||
if [ -n "${BACKUP_TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${BACKUP_TELEGRAM_CHAT_ID:-}" ]; then
|
||||
( while :; do fs-backup telegram bot || true; sleep 60; done ) &
|
||||
else
|
||||
log "Telegram не настроен (BACKUP_TELEGRAM_CHAT_ID/токен бота) — отчёты в Telegram выключены."
|
||||
fi
|
||||
|
||||
# Без пароля бэкапы невозможны. Не падаем (иначе restart-петля и спам в логах) — ждём,
|
||||
# пока пароль появится в .env; healthcheck при этом показывает unhealthy.
|
||||
if [ -z "${BACKUP_PASSWORD:-}" ]; then
|
||||
@@ -29,7 +37,21 @@ if [ ! -s "$CRONTAB" ]; then
|
||||
fi
|
||||
|
||||
if ! fs-backup has-snapshots; then
|
||||
log "Снимков в локальном репозитории ещё нет — делаю первый бэкап сразу."
|
||||
# Новый сервер: app и backup стартуют разом, и БД может быть ещё без таблиц. /api/health
|
||||
# отвечает только после миграций и bootstrap — ждём его (#74). Не дождались — пробуем всё
|
||||
# равно: guard_empty не сохранит БД без таблиц.
|
||||
_app="${BACKUP_APP_HOST:-app}"
|
||||
_wait="${BACKUP_APP_WAIT_SECONDS:-600}"
|
||||
log "Снимков в локальном репозитории ещё нет — жду приложение (до ${_wait} с) и делаю первый бэкап."
|
||||
_waited=0
|
||||
until wget -q -T 3 -O /dev/null "http://$_app:8000/api/health" 2>/dev/null; do
|
||||
if [ "$_waited" -ge "$_wait" ]; then
|
||||
log "Приложение не ответило за ${_wait} с — пробую бэкап без него."
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
_waited=$((_waited + 10))
|
||||
done
|
||||
fs-backup run --scheduled || log "Первый бэкап не удался — следующая попытка по расписанию."
|
||||
fi
|
||||
|
||||
|
||||
+243
-4
@@ -34,11 +34,15 @@ APP_HOST="${BACKUP_APP_HOST:-app}"
|
||||
export RESTIC_PASSWORD="${BACKUP_PASSWORD:-}"
|
||||
export RESTIC_COMPRESSION="${BACKUP_COMPRESSION:-max}"
|
||||
export RESTIC_CACHE_DIR="${RESTIC_CACHE_DIR:-/backup/cache}"
|
||||
TG_TOKEN="${BACKUP_TELEGRAM_BOT_TOKEN:-}"
|
||||
TG_CHATS="${BACKUP_TELEGRAM_CHAT_ID:-}"
|
||||
TG_API="${BACKUP_TELEGRAM_API:-https://api.telegram.org}"
|
||||
LAST_ERROR="" # текст последнего die — причина сбоя в отчёте Telegram
|
||||
|
||||
# ─── Общие функции ────────────────────────────────────────────────────────────
|
||||
# Весь служебный вывод — в stderr: stdout у export занят tar-потоком.
|
||||
log() { printf '[backup %s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >&2; }
|
||||
die() { log "ОШИБКА: $*"; exit 1; }
|
||||
die() { LAST_ERROR="$*"; log "ОШИБКА: $*"; exit 1; }
|
||||
|
||||
require_password() {
|
||||
[ -n "$RESTIC_PASSWORD" ] || die "BACKUP_PASSWORD не задан в .env (см. deploy/backup/README.md, шаг 1)."
|
||||
@@ -118,6 +122,99 @@ human() { # байты → «12.3 MB»
|
||||
|
||||
fmt_epoch() { date -d "@$1" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "$1"; }
|
||||
|
||||
# ─── Telegram (#83) ───────────────────────────────────────────────────────────
|
||||
# Отчёты о бэкапах и проверках — в чаты BACKUP_TELEGRAM_CHAT_ID (через запятую) от бота
|
||||
# BACKUP_TELEGRAM_BOT_TOKEN. Не настроено — молчим. Telegram недоступен — бэкап от этого не
|
||||
# страдает: неудача отправки только пишется в лог. Токен не попадает ни в лог, ни в текст.
|
||||
tg_enabled() { [ -n "$TG_TOKEN" ] && [ -n "$TG_CHATS" ]; }
|
||||
|
||||
tg_escape() { sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'; }
|
||||
|
||||
tg_chat_ids() { printf '%s' "$TG_CHATS" | tr ',;' ' '; }
|
||||
|
||||
tg_api() { # tg_api <метод> [аргументы curl…] → JSON ответа
|
||||
_method="$1"
|
||||
shift
|
||||
curl -fsS --max-time 70 "$@" "$TG_API/bot$TG_TOKEN/$_method" 2>/dev/null
|
||||
}
|
||||
|
||||
tg_post() { # tg_post <chat_id> <html> — одно сообщение в один чат
|
||||
tg_api sendMessage -o /dev/null \
|
||||
--data-urlencode "chat_id=$1" \
|
||||
--data-urlencode "text=$2" \
|
||||
--data-urlencode "parse_mode=HTML" \
|
||||
--data-urlencode "disable_web_page_preview=true"
|
||||
}
|
||||
|
||||
tg_send() { # tg_send <html> — во все чаты владельца
|
||||
tg_enabled || return 0
|
||||
for _chat in $(tg_chat_ids); do
|
||||
tg_post "$_chat" "$1" || log "Telegram: сообщение в чат $_chat не отправлено."
|
||||
done
|
||||
}
|
||||
|
||||
repo_summary() { # repo_summary <repo> → «снимков 30 · 45.2 MB»
|
||||
if _st="$(r "$1" stats --mode raw-data --json 2>/dev/null)"; then
|
||||
printf '%s' "$_st" | jq -r '"\(.snapshots_count)\t\(.total_size)"' | {
|
||||
IFS="$(printf '\t')" read -r _cnt _size
|
||||
echo "снимков $_cnt · $(human "$_size")"
|
||||
}
|
||||
else
|
||||
echo "размер недоступен"
|
||||
fi
|
||||
}
|
||||
|
||||
# Итог run/verify одним сообщением. Списки — «репо:поле:поле» через пробел; их собирают
|
||||
# cmd_run/cmd_verify, а вызывает обработчик EXIT — так отчёт уходит и при раннем отказе.
|
||||
notify_run() { # notify_run <код выхода>
|
||||
tg_enabled || return 0
|
||||
_when="$(date '+%d.%m %H:%M')"
|
||||
_db="БД: игроков $_players, партий $_matches"
|
||||
if [ "$1" -eq 0 ]; then
|
||||
_text="✅ <b>Бэкап</b> $_when
|
||||
$_db"
|
||||
else
|
||||
_text="❌ <b>Бэкап не удался</b> $_when
|
||||
$(printf '%s' "${LAST_ERROR:-прервался с кодом $1}" | tg_escape)"
|
||||
[ "$_players" = "?" ] || _text="$_text
|
||||
$_db"
|
||||
fi
|
||||
for _item in $_ok_repos; do
|
||||
_text="$_text
|
||||
• ${_item%%:*}: снимок <code>${_item#*:}</code> · $(repo_summary "${_item%%:*}")"
|
||||
done
|
||||
for _repo in $_failed; do
|
||||
_text="$_text
|
||||
• $_repo: ошибка"
|
||||
done
|
||||
[ "$1" -eq 0 ] || _text="$_text
|
||||
Подробности: <code>docker compose logs backup</code>"
|
||||
tg_send "$_text"
|
||||
}
|
||||
|
||||
notify_verify() { # notify_verify <код выхода>
|
||||
tg_enabled || return 0
|
||||
_when="$(date '+%d.%m %H:%M')"
|
||||
if [ "$1" -eq 0 ]; then
|
||||
_text="🔍 <b>Проверка данных: OK</b> $_when"
|
||||
else
|
||||
_text="❌ <b>Проверка данных не прошла</b> $_when
|
||||
$(printf '%s' "${LAST_ERROR:-прервалась с кодом $1}" | tg_escape)"
|
||||
fi
|
||||
for _item in $_verified; do
|
||||
_rest="${_item#*:}"
|
||||
_text="$_text
|
||||
• ${_item%%:*}: данные целы, в последнем снимке игроков ${_rest%%:*}, партий ${_rest#*:}"
|
||||
done
|
||||
for _repo in $_failed; do
|
||||
_text="$_text
|
||||
• $_repo: ошибка"
|
||||
done
|
||||
[ "$1" -eq 0 ] || _text="$_text
|
||||
Подробности: <code>docker compose logs backup</code>"
|
||||
tg_send "$_text"
|
||||
}
|
||||
|
||||
# Проверка SQLite без записи рядом с файлом (immutable: ни -wal, ни -shm не создаются).
|
||||
db_ok() {
|
||||
[ -s "$1" ] || return 1
|
||||
@@ -178,6 +275,17 @@ forget_repo() {
|
||||
# Защита истории от пустых данных: новый Pi до восстановления или случайно очищенная БД не
|
||||
# должны становиться «последним снимком» (restore latest вернул бы пустоту).
|
||||
guard_empty() { # guard_empty <игроков> <партий>
|
||||
# «?» — счётчики не прочитались: в БД нет таблиц (миграции ещё не прошли). Такой снимок
|
||||
# стал бы latest и сломал бы restore latest (#74) — отказ, как и для пустой БД.
|
||||
case "$1$2" in
|
||||
*'?'*)
|
||||
for _r in $(repos); do mark "run-$_r" err "в БД нет таблиц users/matches — бэкап не сделан"; done
|
||||
log "Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches."
|
||||
log "Похоже, приложение ещё не применило миграции. Бэкап НЕ сделан, чтобы снимок без данных"
|
||||
log "не стал последним. Дождитесь запуска приложения; если так и задумано:"
|
||||
die "fs-backup run --allow-empty"
|
||||
;;
|
||||
esac
|
||||
if [ "$1" != 0 ] || [ "$2" != 0 ]; then return 0; fi
|
||||
for _repo in $(repos); do
|
||||
_prev="$(r "$_repo" snapshots latest --host "$SNAP_HOST" --json 2>/dev/null | jq -r \
|
||||
@@ -236,11 +344,18 @@ cmd_run() {
|
||||
esac
|
||||
shift
|
||||
done
|
||||
_players="?"
|
||||
_matches="?"
|
||||
_ok_repos=""
|
||||
_failed=""
|
||||
# Отчёт в Telegram уходит при любом исходе, в том числе при раннем отказе (занят lock,
|
||||
# нет пароля). Копию БД убираем только после take_lock: до него она может быть чужой.
|
||||
trap '_rc=$?; notify_run "$_rc"' EXIT
|
||||
require_password
|
||||
setup_ssh
|
||||
take_lock
|
||||
warn_leftovers
|
||||
trap 'rm -f "$SNAP_ROOT/$DB_NAME"' EXIT
|
||||
trap '_rc=$?; rm -f "$SNAP_ROOT/$DB_NAME"; notify_run "$_rc"' EXIT
|
||||
|
||||
log "Снимок данных: консистентная копия БД…"
|
||||
stage_db
|
||||
@@ -252,7 +367,6 @@ cmd_run() {
|
||||
log "БД в порядке: игроков $_players, партий $_matches."
|
||||
if [ "$_allow_empty" = no ]; then guard_empty "$_players" "$_matches"; fi
|
||||
|
||||
_failed=""
|
||||
for _repo in $(repos); do
|
||||
log "=== Репозиторий $_repo ($(repo_url "$_repo")) ==="
|
||||
if backup_to "$_repo" --tag "$_kind" --tag "players:$_players" --tag "matches:$_matches" ${_tag:+--tag keep --tag "$_tag"} \
|
||||
@@ -260,6 +374,7 @@ cmd_run() {
|
||||
&& r "$_repo" check >&2; then
|
||||
_sid="$(latest_short_id "$_repo")"
|
||||
mark "run-$_repo" ok "снимок $_sid"
|
||||
_ok_repos="$_ok_repos $_repo:$_sid"
|
||||
log "OK: репозиторий $_repo, снимок $_sid."
|
||||
else
|
||||
mark "run-$_repo" err "бэкап/очистка/проверка не удались — см. docker compose logs backup"
|
||||
@@ -365,10 +480,12 @@ cmd_has_snapshots() {
|
||||
|
||||
# ─── verify ───────────────────────────────────────────────────────────────────
|
||||
cmd_verify() {
|
||||
_verified=""
|
||||
_failed=""
|
||||
trap '_rc=$?; notify_verify "$_rc"' EXIT
|
||||
require_password
|
||||
setup_ssh
|
||||
take_lock
|
||||
_failed=""
|
||||
for _repo in $(repos); do
|
||||
log "=== Проверка репозитория $_repo: структура + $VERIFY_SUBSET данных ==="
|
||||
_tmp="$RUNTIME_DIR/verify.db"
|
||||
@@ -378,6 +495,7 @@ cmd_verify() {
|
||||
&& db_ok "$_tmp"; then
|
||||
_counts="$(db_counts "$_tmp")"
|
||||
mark "verify-$_repo" ok "данные целы; последний снимок: игроков ${_counts%%|*}, партий ${_counts##*|}"
|
||||
_verified="$_verified $_repo:${_counts%%|*}:${_counts##*|}"
|
||||
log "OK: $_repo — данные целы, БД последнего снимка открывается (игроков ${_counts%%|*}, партий ${_counts##*|})."
|
||||
else
|
||||
mark "verify-$_repo" err "проверка не прошла — см. docker compose logs backup"
|
||||
@@ -779,6 +897,121 @@ cmd_init() {
|
||||
for _repo in $(repos); do ensure_repo "$_repo" && log "Репозиторий $_repo готов."; done
|
||||
}
|
||||
|
||||
# ─── telegram ─────────────────────────────────────────────────────────────────
|
||||
tg_help_text() {
|
||||
cat <<'EOF'
|
||||
<b>Бэкапы Forbidden Stars</b>
|
||||
/backups — хранящиеся снимки по репозиториям
|
||||
/status — последние бэкапы и проверки, размеры
|
||||
Отчёт о каждом бэкапе и проверке приходит сюда сам.
|
||||
EOF
|
||||
}
|
||||
|
||||
tg_backups_text() { # хранящиеся снимки: по репозиторию итог и 10 последних
|
||||
[ -n "$RESTIC_PASSWORD" ] || { echo "Бэкапы отключены: BACKUP_PASSWORD не задан."; return 0; }
|
||||
setup_ssh
|
||||
for _repo in $(repos); do
|
||||
echo "<b>$_repo</b> · $(repo_summary "$_repo")"
|
||||
if _json="$(r "$_repo" snapshots --json 2>/dev/null)"; then
|
||||
printf '%s' "$_json" | jq -r '
|
||||
def tagval($p): ([.tags[]? | select(startswith($p)) | ltrimstr($p)] | first) // "?";
|
||||
def names: [.tags[]? | select(test("^(players:|matches:|scheduled$|manual$|keep$)") | not)];
|
||||
sort_by(.time) | .[-10:] | reverse | .[] |
|
||||
"<code>\(.time[5:16] | sub("T"; " "))</code> игроков \(tagval("players:")), партий \(tagval("matches:"))"
|
||||
+ (if (names | length) > 0 then " · 📌 " + (names | join(", ")) else "" end)'
|
||||
else
|
||||
echo "репозиторий недоступен или занят — повторите позже"
|
||||
fi
|
||||
echo
|
||||
done
|
||||
}
|
||||
|
||||
tg_allowed() { # tg_allowed <chat_id> — чат из белого списка владельца
|
||||
for _allowed in $(tg_chat_ids); do [ "$_allowed" = "$1" ] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
tg_reply() { # tg_reply <chat_id> <текст команды>
|
||||
# Каждая выборка — в подоболочке $(…): die внутри не роняет цикл бота.
|
||||
case "$2" in
|
||||
/backups*|/list*) _reply="$(tg_backups_text 2>/dev/null)" ;;
|
||||
/status*) _reply="<pre>$(cmd_status 2>&1 | head -n 60 | tg_escape)</pre>" ;;
|
||||
*) _reply="$(tg_help_text)" ;;
|
||||
esac
|
||||
[ -n "$_reply" ] || _reply="Не удалось получить данные — см. docker compose logs backup."
|
||||
tg_post "$1" "$_reply" || log "Telegram: ответ в чат $1 не отправлен."
|
||||
}
|
||||
|
||||
# Бот: long polling getUpdates. Отвечает только чатам из BACKUP_TELEGRAM_CHAT_ID; про чужие
|
||||
# пишет chat id в лог — так владелец узнаёт свой при настройке. Lock не берёт: чтение
|
||||
# restic совместимо с идущим бэкапом.
|
||||
cmd_telegram_bot() {
|
||||
tg_enabled || die "Telegram не настроен: нужны BACKUP_TELEGRAM_CHAT_ID и токен бота."
|
||||
_offset_file="$STATE_DIR/telegram.offset"
|
||||
_updates="$RUNTIME_DIR/telegram-updates"
|
||||
log "Telegram-бот: слушаю команды /backups, /status, /help."
|
||||
while :; do
|
||||
_offset="$(cat "$_offset_file" 2>/dev/null || echo 0)"
|
||||
if ! _resp="$(tg_api getUpdates --data-urlencode "offset=$_offset" \
|
||||
--data-urlencode "timeout=50" --data-urlencode 'allowed_updates=["message"]')"; then
|
||||
sleep 30
|
||||
continue
|
||||
fi
|
||||
# Разделитель — \037 (не пробельный): пустые поля (нет username/текста) не схлопываются.
|
||||
printf '%s' "$_resp" | jq -r '.result[]? |
|
||||
[.update_id, (.message.chat.id // ""), (.message.from.username // ""),
|
||||
((.message.text // "") | gsub("[\n]"; " "))] | map(tostring) | join("")' \
|
||||
> "$_updates" 2>/dev/null || { sleep 30; continue; }
|
||||
while IFS="$(printf '\037')" read -r _uid _chat _from _text; do
|
||||
echo $((_uid + 1)) > "$_offset_file"
|
||||
[ -n "$_chat" ] || continue
|
||||
if tg_allowed "$_chat"; then
|
||||
tg_reply "$_chat" "$_text"
|
||||
else
|
||||
log "Telegram: сообщение из чужого чата $_chat (@${_from:-?}) — без ответа. Если это вы, впишите $_chat в BACKUP_TELEGRAM_CHAT_ID."
|
||||
fi
|
||||
done < "$_updates"
|
||||
done
|
||||
}
|
||||
|
||||
# Кто писал боту: id чатов для BACKUP_TELEGRAM_CHAT_ID. Нужен только токен; апдейты не
|
||||
# подтверждаются (без offset), так что ничего не теряется. Пока бот-слушатель запущен, он
|
||||
# забирает сообщения сам — тогда id смотрите в журнале («сообщение из чужого чата …»).
|
||||
cmd_telegram_chats() {
|
||||
[ -n "$TG_TOKEN" ] || die "нет токена бота: задайте TELEGRAM_BOT_TOKEN или BACKUP_TELEGRAM_BOT_TOKEN."
|
||||
_resp="$(tg_api getUpdates --data-urlencode "timeout=0")" \
|
||||
|| die "Telegram не ответил (неверный токен или нет сети)."
|
||||
_chats="$(printf '%s' "$_resp" | jq -r '[.result[]? | .message | select(. != null)
|
||||
| "\(.chat.id)\t@\(.from.username // "?")\t\(.text // "")"] | unique | .[]')"
|
||||
if [ -z "$_chats" ]; then
|
||||
echo "Сообщений боту нет: напишите ему /start в Telegram и повторите."
|
||||
return 0
|
||||
fi
|
||||
echo "Чаты, писавшие боту (id — в BACKUP_TELEGRAM_CHAT_ID):"
|
||||
printf '%s\n' "$_chats"
|
||||
}
|
||||
|
||||
cmd_telegram() {
|
||||
case "${1:-}" in
|
||||
bot) cmd_telegram_bot ;;
|
||||
chats) cmd_telegram_chats ;;
|
||||
test)
|
||||
tg_enabled || die "Telegram не настроен: нужны BACKUP_TELEGRAM_CHAT_ID и токен бота (BACKUP_TELEGRAM_BOT_TOKEN или TELEGRAM_BOT_TOKEN)."
|
||||
_sent=0
|
||||
for _chat in $(tg_chat_ids); do
|
||||
if tg_post "$_chat" "🔔 Проверка связи: отчёты о бэкапах Forbidden Stars будут приходить сюда."; then
|
||||
_sent=$((_sent + 1))
|
||||
log "Telegram: тестовое сообщение в чат $_chat отправлено."
|
||||
else
|
||||
log "Telegram: в чат $_chat отправить не удалось (неверный токен/chat id или боту не писали /start)."
|
||||
fi
|
||||
done
|
||||
[ "$_sent" -gt 0 ] || die "ни одно тестовое сообщение не дошло."
|
||||
;;
|
||||
*) die "telegram: укажите chats, test или bot" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
fs-backup — бэкапы Forbidden Stars (restic). Запуск на Pi из папки с docker-compose.yml:
|
||||
@@ -806,6 +1039,11 @@ fs-backup — бэкапы Forbidden Stars (restic). Запуск на Pi из
|
||||
export <id|latest> [--repo local|vps] > fs.tar
|
||||
снимок в tar без сжатия (нужен exec -T)
|
||||
|
||||
Telegram (отчёты о каждом бэкапе и проверке, команды /backups и /status):
|
||||
telegram chats id чатов, писавших боту (для BACKUP_TELEGRAM_CHAT_ID)
|
||||
telegram test пробное сообщение в чаты BACKUP_TELEGRAM_CHAT_ID
|
||||
telegram bot бот-слушатель команд (контейнер запускает его сам)
|
||||
|
||||
Прочее:
|
||||
init создать репозитории (делается автоматически)
|
||||
restic <local|vps> <аргументы> произвольная команда restic с настройками контейнера
|
||||
@@ -833,6 +1071,7 @@ case "$_cmd" in
|
||||
restic) cmd_restic "$@" ;;
|
||||
health) cmd_health "$@" ;;
|
||||
has-snapshots) cmd_has_snapshots "$@" ;;
|
||||
telegram) cmd_telegram "$@" ;;
|
||||
help|-h|--help) usage ;;
|
||||
*) usage >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -35,6 +35,32 @@ newgrp docker # применить группу
|
||||
docker version && docker compose version # проверка
|
||||
```
|
||||
|
||||
### Лимиты памяти (memory cgroup)
|
||||
Прошивка Raspberry Pi сама добавляет ядру `cgroup_disable=memory`. Без контроллера `memory`
|
||||
Docker **молча игнорирует** `mem_limit` из `docker-compose.yml` (app 512m, tunnel 64m, backup
|
||||
384m) и на каждый контейнер пишет «Your kernel does not support memory limit capabilities or
|
||||
the cgroup is not mounted. Limitation discarded.». Тогда утечка или тяжёлый бэкап могут
|
||||
занять всю RAM Pi, и OOM-killer прибьёт что попало.
|
||||
|
||||
Проверка — если есть вывод, лимиты не работают:
|
||||
```bash
|
||||
docker info 2>&1 | grep -i "no memory limit"
|
||||
cat /sys/fs/cgroup/cgroup.controllers # в списке должно быть слово memory
|
||||
```
|
||||
Включить — дописать параметр **в ту же единственную строку** `cmdline.txt` (перевод строки
|
||||
в этом файле ломает загрузку) и перезагрузить Pi. Прод на время перезагрузки недоступен,
|
||||
контейнеры поднимутся сами:
|
||||
```bash
|
||||
sudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline.txt.bak
|
||||
grep -q "cgroup_enable=memory" /boot/firmware/cmdline.txt || \
|
||||
sudo sed -i '1 s/$/ cgroup_enable=memory/' /boot/firmware/cmdline.txt
|
||||
cat /boot/firmware/cmdline.txt # одна строка, в конце cgroup_enable=memory
|
||||
sudo reboot
|
||||
```
|
||||
После перезагрузки `cgroup.controllers` содержит `memory`, а `docker info` не пишет
|
||||
`No memory limit support`. Если Pi не загрузился, верните бэкап: вставьте карту в ПК и на
|
||||
разделе `bootfs` замените `cmdline.txt` содержимым `cmdline.txt.bak`.
|
||||
|
||||
## 2. SSH-ключ для туннеля
|
||||
**Что это.** Отдельная пара ключей **только для туннеля** — ею контейнер `tunnel` логинится на
|
||||
`tunnel@VPS`, чтобы открыть `ssh -R`. Это не системный ключ Pi, ты создаёшь его сам. Распределение:
|
||||
@@ -154,3 +180,6 @@ docker compose exec backup fs-backup list # хронология снимк
|
||||
слот 9000 → на VPS `sudo fuser -k 9000/tcp`, затем `docker compose restart tunnel`).
|
||||
- `pull` не проходит → проверь `docker login gitea.arseniev.info` и что реестр по HTTPS с валидным
|
||||
сертификатом (иначе хост в `/etc/docker/daemon.json` → `insecure-registries`, `systemctl restart docker`).
|
||||
- `docker compose up` пишет «Your kernel does not support memory limit capabilities…
|
||||
Limitation discarded.» → лимиты памяти не работают: включите memory cgroup, раздел 1,
|
||||
«Лимиты памяти».
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Caddy на VPS (186.246.51.17) — единственная публичная точка входа.
|
||||
# Два домена, ОБА с твоими сертификатами; проксируют в SSH-туннели:
|
||||
#
|
||||
# forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD
|
||||
# forbiddenstars.ru → 127.0.0.1:9000 ← Pi (туннель-контейнер, ssh + restart) PROD
|
||||
# forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV
|
||||
#
|
||||
# Caddy сам терминирует TLS (он и есть edge: видит реального клиента), а вниз к
|
||||
|
||||
+11
-5
@@ -21,8 +21,8 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
# Образ из реестра (Gitea): собирается под arm64 на ПК (scripts/build-push.sh) и тянется
|
||||
# на Pi через `docker compose pull`. build: оставлен как локальный фолбэк (сборка на Pi).
|
||||
# Образ из реестра (Gitea): собирается под arm64 на ПК (scripts/build-push.ps1, в Linux —
|
||||
# .sh) и тянется на Pi. build: нужен только этой сборке на ПК — на Pi не используется.
|
||||
image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars:${IMAGE_TAG:-latest}
|
||||
pull_policy: always # на Pi всегда тянем образ из реестра (без сборки)
|
||||
restart: unless-stopped
|
||||
@@ -42,7 +42,9 @@ services:
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
mem_limit: ${APP_MEM_LIMIT:-512m} # лимиты под Pi; переопределяются в .env
|
||||
# Лимиты под Pi; переопределяются в .env. mem_limit действует, только если на Pi включён
|
||||
# memory cgroup — иначе Docker его молча игнорирует (deploy/pi/README.md, раздел 1).
|
||||
mem_limit: ${APP_MEM_LIMIT:-512m}
|
||||
cpus: ${APP_CPUS:-1.5}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -75,8 +77,8 @@ services:
|
||||
|
||||
# Бэкапы (restic): снимки по расписанию в локальный репозиторий (том backup-data) и на VPS
|
||||
# (SFTP, если задан BACKUP_VPS_HOST). От app не зависит и app не мешает. Переменные —
|
||||
# только BACKUP_* (секреты приложения сюда не передаются). Всё про настройку и
|
||||
# восстановление — deploy/backup/README.md.
|
||||
# только BACKUP_* (секреты приложения сюда не передаются; исключение — токен бота для
|
||||
# отчётов в Telegram). Всё про настройку и восстановление — deploy/backup/README.md.
|
||||
backup:
|
||||
build: ./deploy/backup
|
||||
image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars-backup:${IMAGE_TAG:-latest}
|
||||
@@ -97,6 +99,10 @@ services:
|
||||
BACKUP_VPS_PORT: ${BACKUP_VPS_PORT:-22}
|
||||
BACKUP_VPS_DIR: ${BACKUP_VPS_DIR:-/srv/fs-backups/restic}
|
||||
BACKUP_SSH_KEY_B64: ${BACKUP_SSH_KEY_B64:-}
|
||||
# Отчёты и бот в Telegram (#83). Единственное исключение из «секреты приложения сюда
|
||||
# не передаются»: по умолчанию — токен бота приложения (тот же ForbidenStarsBot).
|
||||
BACKUP_TELEGRAM_BOT_TOKEN: ${BACKUP_TELEGRAM_BOT_TOKEN:-${TELEGRAM_BOT_TOKEN:-}}
|
||||
BACKUP_TELEGRAM_CHAT_ID: ${BACKUP_TELEGRAM_CHAT_ID:-}
|
||||
TZ: ${BACKUP_TZ:-Europe/Moscow}
|
||||
volumes:
|
||||
- db-data:/fs-db # живая БД (снимается консистентно)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Правила Forbidden Stars в Markdown
|
||||
|
||||
Текст правил, разбитый на логические блоки. Из него строится поиск по правилам (веха v1.6):
|
||||
на блоки навешиваются теги и синонимы (#7), затем их индексирует Meilisearch (#10).
|
||||
|
||||
| Файл | Источник | Страниц | Блоков |
|
||||
|---|---|---|---|
|
||||
| [rules.md](rules.md) | буклет «Правила игры» — `fs_rules_rus_web.pdf` | 16 | 184 |
|
||||
| [reference.md](reference.md) | буклет «Справочник» — `fs_reference_rus_web.pdf` | 20 | 427 |
|
||||
| [cards-faq.md](cards-faq.md) | «Уточнение карт», декабрь 2020 — `Уточнение_карт_Forbidden_Stars_декабрь2020.pdf` | 12 | 93 |
|
||||
|
||||
PDF в репозитории нет: они лежат вложениями в задаче #6. Правила и справочник — сканы без
|
||||
текстового слоя, их текст распознан вручную по страницам. У «Уточнения карт» текстовый слой
|
||||
есть, он взят за основу и сверен со страницами. Что не перенесено (обложки, художественный
|
||||
текст, содержание), сказано в шапке каждого файла.
|
||||
|
||||
## Блок
|
||||
|
||||
Блок — наименьшая единица поиска: абзац под заголовком источника, пункт списка статьи
|
||||
справочника или одна карта в уточнениях. Блок начинается маркером — HTML-комментарием,
|
||||
который при отображении не виден:
|
||||
|
||||
```markdown
|
||||
<!-- block: rules.battle.retreat.attacker; page: 15 -->
|
||||
#### Атакующий отступает
|
||||
|
||||
Когда атакующий отступает, он должен переместить…
|
||||
```
|
||||
|
||||
- **Граница блока — маркер.** Блок тянется до следующего маркера. Заголовок внутри блока
|
||||
относится к нему. Если у абзаца нет своего заголовка, блок начинается прямо с текста.
|
||||
- **Контекст блока** — цепочка заголовков над ним: `#` — файл, `##` — раздел или статья
|
||||
глоссария, `###`/`####`/`#####` — подразделы. Первый блок раздела несёт его заголовок.
|
||||
- **Нумерованные шаги** (подготовка к игре, шаги битвы) — отдельные блоки, заголовок шага
|
||||
сохраняет номер источника: `### 2. Раунд битвы`, `#### а. Выберите Боевую Карту`.
|
||||
- **Список** остаётся одним блоком вместе с вводной фразой, если пункты не имеют смысла
|
||||
по отдельности. В справочнике каждый пункт статьи — самостоятельное правило, поэтому
|
||||
каждый пункт — свой блок.
|
||||
|
||||
### Поля маркера
|
||||
|
||||
| Поле | Обязательно | Значение |
|
||||
|---|---|---|
|
||||
| `block` | да | постоянный id блока, уникальный во всей папке |
|
||||
| `page` | да | страница PDF; `4-5`, если блок переходит на следующую страницу |
|
||||
| `kind` | нет | тип не-правила (см. ниже); без `kind` блок — правило |
|
||||
|
||||
### Id
|
||||
|
||||
`<файл>.<раздел>[.<подраздел>…]` — латиница в нижнем регистре, слова через дефис, смысловые
|
||||
английские слаги: `rules.orders.advance.limits`, `ref.retreats.defender.not-attacker-system`,
|
||||
`faq.eldar.combat.psychic-lance`. Префикс: `rules.`, `ref.`, `faq.`. Вложенность id повторяет
|
||||
вложенность заголовков, но не обязана совпадать с ней буквально.
|
||||
|
||||
**Id постоянны.** К ним привязываются теги, синонимы и ссылки «см. также» (#7), а через
|
||||
них — поисковый индекс. Исправлять текст блока можно свободно. Id переименовывать нельзя:
|
||||
новый блок получает новый id, а id удалённого блока больше не используется.
|
||||
|
||||
### `kind`
|
||||
|
||||
| Значение | Что это |
|
||||
|---|---|
|
||||
| `example` | врезка «Пример…» — иллюстрирует правило, но и сама может его уточнять |
|
||||
| `caption` | подпись к схеме или иллюстрации. Если схема без подписи несёт информацию (например, раскладка тайлов), её описание составлено при переносе и выделено курсивом |
|
||||
| `related` | строка «Связанные темы» из справочника — готовые связи между статьями |
|
||||
|
||||
## Текст
|
||||
|
||||
- Дословно, включая опечатки и авторскую пунктуацию источника. Ссылки «см. … на стр. N» —
|
||||
номера страниц буклета.
|
||||
- Переносы склеены, колонки и врезки выведены в порядке чтения.
|
||||
- Выделение источника сохранено: жирный — `**…**`, курсив — `*…*`.
|
||||
- Художественный текст (лор) не переносится.
|
||||
|
||||
## Значки
|
||||
|
||||
Значки внутри текста передаются словом в квадратных скобках. При отображении их можно
|
||||
заменить картинкой, а для поиска они остаются словами.
|
||||
|
||||
| Запись | Значок |
|
||||
|---|---|
|
||||
| `[атака]`, `[защита]`, `[мораль]` | боевой значок на любом компоненте (мораль — боевой дух, аквила) |
|
||||
| `[куб: атака]`, `[куб: защита]`, `[куб: мораль]` | значок на грани боевого куба |
|
||||
| `[куб]` | куб, который нужно бросить |
|
||||
| `[жетон: атака]`, `[жетон: защита]` | боевой жетон (значок в круге) |
|
||||
| `[ресурс: кузница]`, `[ресурс: запасы]`, `[ресурс: подкрепление]`, `[ресурс: процветание]` | значок ценного ресурса на знамени мира |
|
||||
| `[приказ: развертывание]`, `[приказ: планирование]`, `[приказ: доминирование]`, `[приказ: продвижение]` | жетон приказа |
|
||||
|
||||
## Проверка
|
||||
|
||||
Формальная проверка файлов (выполнялась при переносе, в репозиторий не входит): у каждого
|
||||
маркера корректный формат, id уникален и имеет префикс своего файла, страница — в пределах
|
||||
PDF, у каждого блока есть текст, каждая непропущенная страница дала хотя бы один блок, в
|
||||
тексте только значки из таблицы выше, нет висячих переносов.
|
||||
@@ -1,539 +0,0 @@
|
||||
# Уточнение карт
|
||||
|
||||
> **Источник:** «Уточнения эффектов карт» — `Уточнение_карт_Forbidden_Stars_декабрь2020.pdf`, 12 стр.
|
||||
> Уточнение по эффектам карт для перевода 2020 года. Автор: Дмитрий Wergg Комаров.
|
||||
> Верстка: Андрей Yxo Паровой. Декабрь 2020.
|
||||
> «Все возможные разночтения в эффектах карт решены на усмотрение автора. Пользуясь данной
|
||||
> справкой, вы соглашаетесь с его виденьем по данному вопросу.»
|
||||
>
|
||||
> **Не перенесено:** стр. 1 (обложка), изображения карт и иллюстрации; выходные данные
|
||||
> со стр. 12 — в этой шапке. Формат блоков и id — в [README](README.md).
|
||||
|
||||
## Общие положения
|
||||
|
||||
<!-- block: faq.general.effect-resolution; page: 2 -->
|
||||
### Розыгрыш эффекта
|
||||
|
||||
- Если карта задает выбор, то выбор делается владельцем карты, если не сказано иного. Как в отношении своих компонентов, так и в отношении компонентов соперника.
|
||||
- **Жирным** в тексте карт выделены смысловые акценты, которые легко пропустить.
|
||||
- Владелец карты вправе отказаться разыгрывать эффект карты, кроме тех случаев, где эффект карты содержит слово **обязан**, выделенное жирным. От исполнения таких эффектов ни вы, ни ваш соперник не имеют права отказаться. Такие эффекты встречаются только в колоде Орков.
|
||||
- Эффекты карт изложены в специальных полях. В боевых картах обычно 2 поля - зеленое и коричневое. В остальных картах поле одно. В одном поле может быть сложносоставной эффект. Решение о розыгрыше или отказе от розыгрыша должно приниматься для всего эффекта, описанного в поле.
|
||||
- Если вы приняли решение разыграть эффект карты, то должны выполнить все предписания в поле настолько, насколько это возможно и именно в том порядке, в котором они перечислены.
|
||||
- Нельзя отказаться от части эффекта карты, если вы приняли решение разыграть эффект.
|
||||
- Если эффект подразумевает трату кубиков, то разыграть такой эффект можно ровно 1 раз, если только не оговорено, что потратить можно любое количество кубиков (и получить соответствующее количество активаций эффекта).
|
||||
|
||||
<!-- block: faq.general.may-avoid; page: 2 -->
|
||||
### "Может избежать этого эффекта..."
|
||||
|
||||
- Карты с формулировкой **"Может избежать этого эффекта..."** построены по принципу: происходит событие **А**. Противник может избежать наступления события путем уплаты стоимости, указанной после слов **"Может избежать этого эффекта..."**
|
||||
- Противник вправе отказаться уплачивать стоимость отмены. В этом случае событие **А** происходит.
|
||||
- Если противник **не может** уплатить стоимость отмены в виду отсутствия необходимых ресурсов или в виду эффектов карт, то событие **А** происходит. Требуется именно уплата стоимости отмены в полном объеме, а не принципиальный факт согласия на уплату.
|
||||
- Если противник согласился уплатить стоимость отмены, но не смог этого сделать в виду каких-либо эффектов карт или других причин, то событие **А** происходит.
|
||||
|
||||
<!-- block: faq.general.retreat; page: 2 -->
|
||||
### Отступите / отступает
|
||||
|
||||
- Если эффект карты предписывает отступать, то отступление производится по общим правилам с соответствующими ограничениями (см. правила). Не забывайте, что после отступления отряд становится деморализованным.
|
||||
- Отряд, отступивший в никем не контролируемую область, делает ее дружественной владельцу отряда.
|
||||
- Если за бой было несколько отступлений, то они могут производиться в разные области.
|
||||
- Если сражение началось по эффекту **карты** приказа, а не по самому приказу (карта "десантный модуль") или в результате действия боевых карт ("волновой змей"), то атакующий не сможет отступать (см правила отступления).
|
||||
- Если сражение началось по приказу "продвижение", вызванному эффектом карты (карта орков "вперед!"), то отступление атакующих - легально.
|
||||
|
||||
<!-- block: faq.general.move; page: 2 -->
|
||||
### Переместите
|
||||
|
||||
- Перемещать деморализованные отряды нельзя. Хоть по эффекту приказа, хоть по эффекту карты.
|
||||
|
||||
<!-- block: faq.general.take-and-place; page: 3 -->
|
||||
### Забрать и поместить / разместить
|
||||
|
||||
- Забрать и поместить в другую область можно только недеморализованные отряды.
|
||||
- При размещении игнорируется вместимость мира. Выбор, кого оставить производится после боя (если размещение было вызвано эффектом карты в бою) или после выполнения приказа (если размещение было сопряжено с приказом).
|
||||
- При размещении отряда в бою игнорируется лимит в "5 отрядов максимум в одной области" т.к. этот лимит наложен только на перемещение.
|
||||
- При размещении отряда в бою после броска кубиков новые кубики за вновь прибывший отряд уже не бросаются.
|
||||
|
||||
<!-- block: faq.general.replace; page: 3 -->
|
||||
### Замените
|
||||
|
||||
- Т.к. в бою (и только в бою!) фишки подкрепления являются отрядами, то заменять их на пластиковые фигурки - легально.
|
||||
- Процедура "заменить" не равна "купить". При замещении игнорируется командный уровень.
|
||||
- Можно заменить деморализованный отряд на недеморализованный.
|
||||
|
||||
<!-- block: faq.general.purchase; page: 3 -->
|
||||
### Купите
|
||||
|
||||
- Только процедура покупки, инициированная жетоном приказа "развертывание" требует наличия фабрики для приобретения отрядов.
|
||||
- Если эффект карты позволяет вам покупать отряды, то наличие фабрики не требуются. Однако командный уровень все еще учитывается.
|
||||
|
||||
<!-- block: faq.general.gain; page: 3 -->
|
||||
### Получите
|
||||
|
||||
- "Получить" не равно "Купить". Получение не требует соблюдения командного уровня.
|
||||
|
||||
<!-- block: faq.general.command-level; page: 3 -->
|
||||
### Командный уровень
|
||||
|
||||
- Ваш фактический командный уровень равен числу ваших городов.
|
||||
- Жетоны кузницы дают вам временное понижение командного уровня покупаемого отряда, сам командный уровень отрядов не меняется. Фактически командный уровень отрядов не уменьшается и всегда равен написанному на листе фракции.
|
||||
- Если эффект карты ссылается на командный уровень отряда или улучшения, то имеется в виду именно то, что написано фактически на листе фракции или карте.
|
||||
- Если эффект предписывает вам получить (а так же разместить или заменить) что-либо, то командный уровень игнорируется.
|
||||
|
||||
<!-- block: faq.general.orbital-strike; page: 3 -->
|
||||
### Орбитальный удар
|
||||
|
||||
- Помните про лимит в 8 кубиков.
|
||||
- Если способность предписывает потратить кубики во время выполнения орбитального удара, то это нужно сделать **до** назначения урона.
|
||||
- Если эффект подразумевает трату кубиков, то разыграть такой эффект можно ровно 1 раз (воруй, грабь, десантный модуль, огненный дождь, обстрел, расчетливый удар, полное разрушение, страх с небес).
|
||||
- Несмертельный урон не приводит к деморализации (в отличии от боя).
|
||||
- Вы в праве объявить орбитальный удар по миру без отрядов (и даже без бастиона) для активации способности.
|
||||
- Нельзя объявить орбитальный удар по никем не контролируемому или своему миру.
|
||||
- Нельзя объявить орбитальный удар по области без миров.
|
||||
- Нельзя объявить орбитальный удар по миру в соседней системе.
|
||||
|
||||
## Орки
|
||||
|
||||
<!-- block: faq.orks.faction-ability; page: 4 -->
|
||||
### Фракционная способность
|
||||
|
||||
Вы можете купить только наземный отряд. Для покупки отряда **не** требуется наличие фабрики. Командный уровень покупаемого отряда учитывается. Вы можете использовать жетон запасов (шестеренки), чтобы уменьшить стоимость на 2. Вы можете использовать жетон кузницы (молот), чтобы снизить требование по командному уровню покупаемого отряда.
|
||||
|
||||
### События
|
||||
|
||||
<!-- block: faq.orks.events.more-boyz; page: 4 -->
|
||||
#### Больше бойзов!
|
||||
|
||||
Вы не можете иметь более 3 жетонов 1 типа. Лишние жетоны подкрепления придется сразу сбросить.
|
||||
|
||||
<!-- block: faq.orks.events.into-battle; page: 4 -->
|
||||
#### В бой!
|
||||
|
||||
При размещении отрядов вы можете превысить лимит мира (количество черепов). Решение, какие отряды оставить в этом мире, принимается после размещения.
|
||||
|
||||
<!-- block: faq.orks.events.how-did-we-get-here; page: 4 -->
|
||||
#### Как мы сюда попали?
|
||||
|
||||
забрать и поместить можно только недеморализованный отряд. Разместить таким образом можно только наземный отряд.
|
||||
|
||||
<!-- block: faq.orks.events.looting; page: 4 -->
|
||||
#### Мародерство
|
||||
|
||||
Вы получите материалы, даже если у выбранного противника нет жетонов ценных ресурсов. У вас не может быть больше 14 материалов.
|
||||
|
||||
<!-- block: faq.orks.events.tear-it-down; page: 4 -->
|
||||
#### Снести это!
|
||||
|
||||
Вы можете уничтожить любую постройку (не только бастион).
|
||||
|
||||
<!-- block: faq.orks.events.finish-them; page: 4 -->
|
||||
#### Добить их!
|
||||
|
||||
Для того чтобы воспользоваться эффектом данной карты отряд противника должен стать деморализованным фактически. Если какой-либо эффект предотвратил деморализацию отрядов противника (напр. карта " не ведать страха" фракции Ультрамаринов), то уничтожить такой отряд при помощи этой карты нельзя.
|
||||
|
||||
<!-- block: faq.orks.events.warboss; page: 4 -->
|
||||
#### Варбосс
|
||||
|
||||
Скидка действует как при покупке при помощи приказа "развертывание", так и при покупке по эффекту карт или фракционной способности. Вы не можете иметь более 3 жетонов подкрепления.
|
||||
|
||||
<!-- block: faq.orks.events.forward; page: 4 -->
|
||||
#### Вперед!
|
||||
|
||||
Для того, чтобы воспользоваться этой картой вы должны иметь возможность вскрыть жетон приказа. Если все ваши жетоны приказов заблокированы жетонами соперников или ваши жетоны уже закончились, то вы не можете воспользоваться этой картой. При выполнении эффекта этой карты считайте, что вы вскрыли приказ "продвижение" в целевой системе. Т.к. считается, что сражение, начавшееся после розыгрыша данной карты, произошло по приказу "продвижение", то отступление атакующего - легально (см правила отступления).
|
||||
|
||||
### Приказы
|
||||
|
||||
<!-- block: faq.orks.orders.build-faster; page: 5 -->
|
||||
#### Строй быстрее!
|
||||
|
||||
После постройки фабрики можно будет сразу же приобретать отряды в этой системе.
|
||||
|
||||
<!-- block: faq.orks.orders.plunder; page: 5 -->
|
||||
#### Грабь!
|
||||
|
||||
Вы не можете бросить более 8 кубиков для орбитального удара, все лишние кубики пропадут. Если у противника нет материалов, то вы все равно получите 1 материал. Если у вас уже 14 материалов, то противник все равно потеряет 1 материал (если вы потратите кубик, конечно).
|
||||
|
||||
<!-- block: faq.orks.orders.green-tide; page: 5 -->
|
||||
#### Зеленая волна
|
||||
|
||||
Вы должны выбрать тип разыгрываемого приказа при вскрытии жетона. Считайте, что на данном жетоне изображена иконка выбранного вами приказа. Если вы выбрали приказ, отличный от приказа "планирование", то не размещайте этот жетон приказа поверх колоды событий.
|
||||
|
||||
<!-- block: faq.orks.orders.steal; page: 5 -->
|
||||
#### Воруй!
|
||||
|
||||
Если у противника нет жетонов ценных ресурсов, то вы не получите ничего. Если у вас уже 3 жетона выбранного ресурса, то противник все равно потеряет выбранный жетон (вы при этом четвертый жетон не получите).
|
||||
|
||||
<!-- block: faq.orks.orders.ork-ships; page: 5 -->
|
||||
#### Орочьи корабли
|
||||
|
||||
Вы можете составить путь из настоящих кораблей и дружественных миров в любой пропорции и дополнительно **однократно** провести не более 2 наземных отрядов через пустую область как через дружественную. Переместить по приказу можно больше 2 отрядов, главное, чтобы именно через пустую область без кораблей по эффекту данной карты было перемещено не более 2 отрядов. Нельзя отступать через пустую область при помощи данной карты в защите. Нельзя отступать при помощи данной карты в атаке, если вы использовали ее для перемещения в оспариваемую область. Можно отступить при помощи данной карты в атаке (с соблюдением всех правил отступления), если вы не использовали ее для перемещения в оспариваемую область.
|
||||
|
||||
### Боевые карты
|
||||
|
||||
<!-- block: faq.orks.combat.hard-boyz; page: 5 -->
|
||||
#### Хард бойз
|
||||
|
||||
Оба эффекта карты обязательны к исполнению и от них нельзя отказаться.
|
||||
|
||||
<!-- block: faq.orks.combat.mega-nobz; page: 5 -->
|
||||
#### Мега нобз
|
||||
|
||||
верхний эффект обязателен к исполнению и от него нельзя отказаться.
|
||||
|
||||
<!-- block: faq.orks.combat.weirdboyz; page: 5 -->
|
||||
#### Вирдбойз
|
||||
|
||||
верхний эффект обязателен к исполнению и от него нельзя отказаться. Если будет сыграно две карты **вирдбойз**, то их свойства будут складываться, т.е. вы будете получать удвоенное количество жетонов.
|
||||
|
||||
<!-- block: faq.orks.combat.slugga-boyz; page: 5 -->
|
||||
#### Слагга бойз
|
||||
|
||||
Верхний эффект обязателен к исполнению и от него нельзя отказаться.
|
||||
|
||||
<!-- block: faq.orks.combat.biker-nobz; page: 5 -->
|
||||
#### Байкер нобз
|
||||
|
||||
верхний эффект обязателен к исполнению и от него нельзя отказаться.
|
||||
|
||||
<!-- block: faq.orks.combat.mek-boyz; page: 5 -->
|
||||
#### Мек бойз
|
||||
|
||||
Противник сбрасывает карту с **колоды** (не с руки). Вы получаете именно значки (не жетоны). Это может быть важно для эффектов карт, заставляющих сбрасывать боевые жетоны. Если карта была сброшена таким образом в 3 раунде боя, то аквилы (орлы) со сброшенной карты не участвуют в подсчете общего количества морали, т.к. он производится **после** окончания 3 раунда.
|
||||
|
||||
<!-- block: faq.orks.combat.shoota-boyz; page: 5 -->
|
||||
#### Шута бойз
|
||||
|
||||
Оба эффекта карты обязательны к исполнению и от них нельзя отказаться.
|
||||
|
||||
<!-- block: faq.orks.combat.ripping-gargant; page: 5 -->
|
||||
#### Разрывающий гаргант
|
||||
|
||||
если сброшенная карта давала свойство, действующее в течение этого раунда или всего боя, то оно (свойство) немедленно прекращается.
|
||||
|
||||
<!-- block: faq.orks.combat.party-wagon; page: 5 -->
|
||||
#### Пати вагон
|
||||
|
||||
Т.к. жетон размещается бесплатно, то он берется из резерва (а не из вашего запаса). Ограничение на 3 жетона одного типа при этом соответственно игнорируется. Также игнорируется правило, что у вы не можете разместить больше жетонов подкрепления, чем пластиковых фигурок в бою на вашей стороне.
|
||||
|
||||
<!-- block: faq.orks.combat.gretchin; page: 5 -->
|
||||
#### Гретчины
|
||||
|
||||
Несмотря на слово "должен" вы можете не разыгрывать верхний эффект карты. Но тогда вы не получите боевые жетоны.
|
||||
|
||||
<!-- block: faq.orks.combat.crushing-gargant; page: 5 -->
|
||||
#### Сокрушающий гаргант
|
||||
|
||||
если у противника нет необходимого количества кубиков или он не хочет их тратить, то выбранный отряд уничтожается.
|
||||
|
||||
<!-- block: faq.orks.combat.sea-of-green; page: 5 -->
|
||||
#### Море зеленых
|
||||
|
||||
1. Т.к. жетон размещается бесплатно, то он берется из резерва (а не из вашего запаса). Ограничение на 3 жетона одного типа при этом соответственно игнорируется. Также игнорируется правило, что у вы не можете разместить больше жетонов подкрепления, чем фигурок в бою на вашей стороне.
|
||||
2. Если у противника нет кубиков с аквилами (орлами) или он не хочет их тратить, то он должен будет деморализовать 1 свой отряд (если сможет).
|
||||
|
||||
## Космодесант Хаоса
|
||||
|
||||
<!-- block: faq.chaos.faction-ability; page: 6 -->
|
||||
### Фракционная способность
|
||||
|
||||
Т.к. это размещение, а не перемещение, то целевая планета может не иметь легального пути до места изначального размещения культиста и даже может находиться за варп-штормом. Забрать и разместить таким образом можно только недеморализованный отряд.
|
||||
|
||||
### События
|
||||
|
||||
<!-- block: faq.chaos.events.warp-touched; page: 6 -->
|
||||
#### Задетый варпом
|
||||
|
||||
Т.к. вы получаете, а не покупаете улучшение, то ваш фактический командный уровень (число городов) не имеет значения.
|
||||
|
||||
<!-- block: faq.chaos.events.nurgle-rot; page: 6 -->
|
||||
#### Отрава Нургла
|
||||
|
||||
Для того, чтобы воспользоваться этой картой вы должны иметь возможность вскрыть жетон приказа. Если все ваши жетоны приказов заблокированы жетонами соперников или ваши жетоны уже закончились, то вы не можете воспользоваться этой картой.
|
||||
|
||||
<!-- block: faq.chaos.events.tzeentch-incarnation; page: 6 -->
|
||||
#### Инкарнация Тзинча
|
||||
|
||||
Если эта карта была вытянута в числе нескольких карт в 3 фазе (если на колоде событий лежало более 1 жетона) и была разыграна, то сначала тянуться еще 3 карты и играется одна из них, а только потом все карты замешиваются в колоду событий.
|
||||
|
||||
<!-- block: faq.chaos.events.through-the-warp; page: 6 -->
|
||||
#### Сквозь варп
|
||||
|
||||
Вы получите дополнительный кубик с аквилой (орлом) даже если не пересекали варп-шторм перед началом сражения. Помните про лимит в 8 кубиков. Т.к. вы получите кубик с аквилой еще до броска основных кубиков, то фактически вы не сможете бросить больше 7 кубиков (восьмой кубик будет аквилой). Вы можете отступить через варп-шторм после атаки. Вы не можете использовать эту карту для отступления в защите.
|
||||
|
||||
### Приказы
|
||||
|
||||
<!-- block: faq.chaos.orders.from-the-warp; page: 6 -->
|
||||
#### Из варпа
|
||||
|
||||
Наземные отряды **не** могут пересекать варп-штормы по эффекту данной карты. Вы можете отступить через варп-шторм после атаки. Вы не можете использовать эту карту для отступления в защите.
|
||||
|
||||
<!-- block: faq.chaos.orders.dark-gods-favor; page: 6 -->
|
||||
#### Расположение темных богов
|
||||
|
||||
Дополнительные жетоны нужно взять из числа неиспользуемых в этом раунде (не с поля). В комбинации с картой **инкарнация Тзинча** можно просмотреть 6 карт из колоды событий.
|
||||
|
||||
<!-- block: faq.chaos.orders.dread-ritual; page: 6 -->
|
||||
#### Ужасный ритуал
|
||||
|
||||
Для покупки по эффекту данной карты не требуется завод. Вы можете купить корабль по эффекту данной карты, но разместить его нужно обязательно в дружественной области активной системы. Т.к. вы **покупаете** отряд, то ваш командный уровень все равно учитывается. Вы можете использовать жетон кузницы (молоток), если вам не хватает вашего текущего командного уровня для приобретения отряда. Вы не можете покупать титанов по эффекту данной карты, т.к. наложен запрет на покупку отрядов выше фактического второго уровня.
|
||||
|
||||
<!-- block: faq.chaos.orders.terror-from-the-skies; page: 6 -->
|
||||
#### Страх с небес
|
||||
|
||||
Не забывайте, что во время орбитального удара несмертельный урон не приводит к деморализации. Помните про лимит в 8 кубиков.
|
||||
|
||||
<!-- block: faq.chaos.orders.total-destruction; page: 6 -->
|
||||
#### Полное разрушение
|
||||
|
||||
Противник должен выбрать для уничтожения любую пластиковую фигурку (отряд или строение) **из имеющихся** в мире, подвергшемся орбитальному удару. Противник вправе выбрать небоевое строение (город/фабрика). Потратить кубики и разыграть способность нужно **до** назначения урона. Несмертельный урон не приводит к деморализации отряда. Вы в праве объявить орбитальный удар по враждебному миру без отрядов и/или бастиона для активации этой способности.
|
||||
|
||||
### Боевые карты
|
||||
|
||||
<!-- block: faq.chaos.combat.wrath-of-khorne; page: 7 -->
|
||||
#### Ярость Кхорна
|
||||
|
||||
Если у противника нет кубика защиты (щит) или он не хочет его тратить, то он должен выбрать и деморализовать свой отряд. Если у противника нет недеморализованных отрядов или они не могут стать деморализованными (карта "не ведать страха"), то не произойдет ничего.
|
||||
|
||||
<!-- block: faq.chaos.combat.daemonic-resilience; page: 7 -->
|
||||
#### Демоническая стойкость
|
||||
|
||||
Если у противника нет отрядов для уничтожения или он не хочет этого делать, то вы получите жетоны защиты.
|
||||
|
||||
<!-- block: faq.chaos.combat.mark-of-tzeentch-top; page: 7 -->
|
||||
#### Отметка Тзинча (верх)
|
||||
|
||||
можно заменить деморализованный отряд культистов на нормальный отряд космодесанта хаоса. Можно заменить жетон подкрепления на фигурку космодесанта хаоса, т.к. в бою жетон подкрепления является отрядом (культистов).
|
||||
|
||||
<!-- block: faq.chaos.combat.mark-of-tzeentch-bottom; page: 7 -->
|
||||
#### Отметка Тзинча (низ)
|
||||
|
||||
можно выбрать разные грани кубиков, если потрачены 2 аквилы.
|
||||
|
||||
<!-- block: faq.chaos.combat.mark-of-slaanesh-top; page: 7 -->
|
||||
#### Отметка Слаанеш (верх)
|
||||
|
||||
если у противника нет недеморализованных отрядов или они не могут стать деморализованными, то не произойдет ничего.
|
||||
|
||||
<!-- block: faq.chaos.combat.mark-of-khorne-bottom; page: 7 -->
|
||||
#### Отметка Кхорна (низ)
|
||||
|
||||
если у противника нет деморализованных отрядов, то не произойдет ничего. Если у противника есть деморализованные отряды, но нет кубика защиты (щит) или он не хочет его тратить, то он должен выбрать и уничтожить свой деморализованный отряд.
|
||||
|
||||
<!-- block: faq.chaos.combat.death-and-despair-top; page: 7 -->
|
||||
#### Смерть и отчаяние (верх)
|
||||
|
||||
нельзя выбрать 1 болтер и 1 аквилу. Только либо 2 болтера, либо 2 аквилы. Выбор, какой отряд уничтожать, делается игроком за Хаос.
|
||||
|
||||
<!-- block: faq.chaos.combat.lures-of-chaos-top; page: 7 -->
|
||||
#### Соблазны хаоса (верх)
|
||||
|
||||
противник вправе выбрать деморализовать 1 свой отряд даже если у него нет недеморализованных отрядов или они не могут стать деморализованными (карта "не ведать страха"). Это приведет к тому, что хаос не получит культиста (т.к. выбран другой вариант, пусть и невыполнимый), а противник хаоса не получит кубик (т.к. он не "уплатил" цену этого кубика - фактически не деморализовал отряд).
|
||||
|
||||
<!-- block: faq.chaos.combat.mark-of-nurgle-bottom; page: 7 -->
|
||||
#### Отметка Нургла (низ)
|
||||
|
||||
если у противника нет деморализованных отрядов или он не хочет их уничтожать, то вы получите жетоны защиты.
|
||||
|
||||
<!-- block: faq.chaos.combat.unholy-intent-bottom; page: 7 -->
|
||||
#### Нечистые намерения (низ)
|
||||
|
||||
Если у противника нет недеморализованных отрядов, или они не могут стать деморализованными (карта "не ведать страха"), или он не хочет деморализовать свой отряд, то хаос получит жетоны атаки. Т.е. избежать получения жетонов атаки можно только фактически деморализовав свой отряд.
|
||||
|
||||
<!-- block: faq.chaos.combat.dark-faith; page: 7 -->
|
||||
#### Темная вера
|
||||
|
||||
размещенный таким образом культист может отрезать легальный путь к отступлению противника.
|
||||
|
||||
<!-- block: faq.chaos.combat.chaos-united-top; page: 7 -->
|
||||
#### Хаос объединенный (верх)
|
||||
|
||||
Противник может выбрать деморализацию своих отрядов даже если у него нет недеморализованных отрядов или они не могут стать деморализованными ( карта "не ведать страха"). Такой выбор не приведет к тому, что Хаос получит кубик. Противник обязан сделать выбор не зная, кубик с каким значением возьмет игрок за Хаос в случае отказа.
|
||||
|
||||
<!-- block: faq.chaos.combat.chaos-united-bottom; page: 7 -->
|
||||
#### Хаос объединенный (низ)
|
||||
|
||||
Можно превысить вместимость мира и даже превысить планку в 5 отрядов в бою т.к. это не перемещение, а размещение.
|
||||
|
||||
## Ультрамарины
|
||||
|
||||
<!-- block: faq.ultramarines.faction-ability; page: 8 -->
|
||||
### Фракционная способность
|
||||
|
||||
Можно заменить деморализованный отряд на недеморализованный более высокого уровня. Командный уровень при замене игнорируется. Нельзя использовать жетон запасов (шестеренки) для активации этого свойства, т.к. это не покупка.
|
||||
|
||||
### События
|
||||
|
||||
<!-- block: faq.ultramarines.events.exterminatus; page: 8 -->
|
||||
#### Экстерминатус
|
||||
|
||||
Вы должны заявить эффект данной карты до броска кубиков. Нельзя сначала бросить кубики и посмотреть результат, а затем сыграть эту карту.
|
||||
|
||||
<!-- block: faq.ultramarines.events.steadfast; page: 8 -->
|
||||
#### Непоколебимость
|
||||
|
||||
Для того, чтобы воспользоваться этой картой вы должны иметь возможность вскрыть жетон приказа. Если все ваши жетоны приказов заблокированы жетонами соперников или ваши жетоны уже закончились, то вы не можете воспользоваться этой картой.
|
||||
|
||||
<!-- block: faq.ultramarines.events.pre-battle-ceremony; page: 8 -->
|
||||
#### Церемония перед боем
|
||||
|
||||
Т.к. это покупка, то командный уровень учитывается. Купить можно только улучшение приказа (не боевое улучшение). Скидка дается именно за миры с бастионами, а не сами бастионы.
|
||||
|
||||
<!-- block: faq.ultramarines.events.emperors-protection; page: 8 -->
|
||||
#### Защита Императора
|
||||
|
||||
Т.к. это не строительство, а размещение, то второй вариант розыгрыша карты не может быть оплачен жетоном запасов (шестеренки). За 2 материала вы не покупаете бастион, а активируете способность карты, которая даст вам бастион.
|
||||
|
||||
### Приказы
|
||||
|
||||
<!-- block: faq.ultramarines.orders.crusade; page: 8 -->
|
||||
#### Крестовый поход
|
||||
|
||||
Вы не обязаны использовать полученный жетон в этой битве. Помните про лимит в 3 жетона одного типа. Вы получите жетон подкрепления, если вы нападаете из одного мира в другой в пределах одной системы и переместили все наземные войска в оспариваемую область.
|
||||
|
||||
<!-- block: faq.ultramarines.orders.recruiting-worlds; page: 8 -->
|
||||
#### Миры призыва
|
||||
|
||||
Чтобы не ошибиться с подсчетом суммарного лимита следует сначала сложить все лимиты миров с фабриками и бастионами в системе. Если в 1 мире находятся и фабрика и бастион или 2 бастиона, то лимит этого мира следует считать дважды (как будто это 2 фабрики, см. эффект карты **направлять верующих**). После этого из получившейся суммы следует вычесть количество бастионов в системе.
|
||||
|
||||
<!-- block: faq.ultramarines.orders.guide-the-faithful; page: 8 -->
|
||||
#### Направлять верующих
|
||||
|
||||
Можно создавать миры с одинаковыми постройками. Если будет создан мир с 2 фабриками, то лимит развертывания будет равен удвоенному количеству черепов этого мира.
|
||||
|
||||
<!-- block: faq.ultramarines.orders.drop-pod; page: 8 -->
|
||||
#### Десантный модуль
|
||||
|
||||
Вы вправе объявить орбитальный удар по миру противника без отрядов и/или бастиона для активации свойства. Сочетание этой карты и карты **крестовый поход** даст вам жетон подкрепления после высадки космодесантника, если остальные условия получения жетона будут соблюдены. Если космодесантник в бою, инициированном эффектом данной карты, должен будет отступить, то его придется уничтожить (см. правила отступления).
|
||||
|
||||
### Боевые карты
|
||||
|
||||
<!-- block: faq.ultramarines.combat.know-no-fear; page: 9 -->
|
||||
#### Не ведать страха
|
||||
|
||||
Вы должны выбрать будете ли вы использовать верхнее свойство карты в момент ее вскрытия. Если эффект карты активирован, то вы не сможете уплатить цену эффектов с ключевыми словами "деморализуйте свой отряд". Вы все еще вправе выбирать варианты, подразумевающие деморализацию ваших отрядов, если такой выбор задается. В этом случае вы не сможете деморализовать свой отряд, но это и не приведет к автоматическому выбору второго варианта. Подробнее сочетание этой карты с другими смотрите в соответствующих уточнениях (Хаос объединенный, Соблазны хаоса, Ярость Кхорна, Нечистые намерения, Море зеленых)
|
||||
|
||||
<!-- block: faq.ultramarines.combat.veteran-scouts; page: 9 -->
|
||||
#### Скауты ветераны
|
||||
|
||||
Т.к. проверка окончания боя будет только в конце шага нанесения боевых повреждений, то противнику придется распределять урон в свои отряды даже если все ваши отряды выйдут из боя. Достаточно 1 недеморализованного скаута, чтобы отступить несколькими отрядами (за аналогичное количество кубиков с аквилами). После отступления отряд становится деморализованным. Отступить можно и деморализованным отрядом. Отряд, отступивший в никем не контролируемую область, делает ее дружественной. Отступление по данной карте должно производиться в 1 область. Если будет разыграна вторая карта **скауты ветераны**, то отступление можно будет производить в другую область.
|
||||
|
||||
<!-- block: faq.ultramarines.combat.ambush; page: 9 -->
|
||||
#### Засада
|
||||
|
||||
Трата кубиков с аквилами не предотвращает уничтожение отрядов другим способом (например, от смертельного урона или эффектов карт с ключевым словом "уничтожьте"). Если **засада** разыграна в защите, а игрок за Эльдар ранее в этом раунде разыграл **наставления видящих**, то отряд Эльдар не уничтожается, т.к. эффект засады действует от момента раскрытия карты.
|
||||
|
||||
<!-- block: faq.ultramarines.combat.drop-assault; page: 9 -->
|
||||
#### Десант
|
||||
|
||||
Можно забрать и поместить только недеморализованный отряд
|
||||
|
||||
<!-- block: faq.ultramarines.combat.power-fist; page: 9 -->
|
||||
#### Бронекулак
|
||||
|
||||
Противник тоже наносит урон. Урон между шагами не складывается. Поэтому если с 1 стороны остался только 1 отряд с выносливостью 3 и против него нужно дважды разыграть урон 2, то после первого шага отряд станет деморализованным, а второй шаг не сделает ничего.
|
||||
|
||||
<!-- block: faq.ultramarines.combat.recon; page: 9 -->
|
||||
#### Разведка
|
||||
|
||||
Т.к. проверка окончания боя будет только в конце шага нанесения боевых повреждений, то противнику придется распределять урон в свои отряды даже если все ваши отряды выйдут из боя. По эффекту данной карты отступить могут любые отряды, а не только скаут/ударный крейсер. После отступления отряд становится деморализованным. Отступить можно и деморализованным отрядом. Отряд, отступивший в никем не контролируемую область, делает ее дружественной. Отступление по данной карте должно производиться в 1 область. Если будет разыграна вторая карта **Разведка**, то отступление можно будет производить в другую область.
|
||||
|
||||
## Эльдар
|
||||
|
||||
<!-- block: faq.eldar.faction-ability; page: 10 -->
|
||||
### Фракционная способность
|
||||
|
||||
Можно превысить лимит мира при размещении отряда. Выбор, кого оставить, производится после размещения. Разместить таким образом можно только наземные отряды. Вам не требуется легальный путь, т.к. это не перемещение, а именно размещение. Забрать и поместить в другую область деморализованный отряд нельзя.
|
||||
|
||||
### События
|
||||
|
||||
<!-- block: faq.eldar.events.legacy-of-vaul; page: 10 -->
|
||||
#### Наследие Ваула
|
||||
|
||||
У вас не может быть более 3 жетонов 1 типа. Лишние придется сбросить.
|
||||
|
||||
<!-- block: faq.eldar.events.exodite-colony; page: 10 -->
|
||||
#### Колония экзодитов
|
||||
|
||||
Данное действие не является строительством, это бесплатное размещение города, где цена в 1 материал – это цена активации способности. Нельзя потратить жетон запасов (шестеренки) на активацию этого свойства, т.к. это не строительство.
|
||||
|
||||
<!-- block: faq.eldar.events.terror-raid; page: 10 -->
|
||||
#### Ужасный налет
|
||||
|
||||
Помните про лимит в 8 кубиков. Если вы должны получить девятый вы вместо этого не получаете кубик.
|
||||
|
||||
<!-- block: faq.eldar.events.path-of-the-warrior; page: 10 -->
|
||||
#### Путь воина
|
||||
|
||||
Купить можно только боевое улучшение колоды (не улучшение приказа). Улучшение приказа таким образом купить нельзя. Скидка считается именно по количеству миров (планет) с городами, а не количеству городов. Командный уровень при покупке учитывается.
|
||||
|
||||
<!-- block: faq.eldar.events.warp-gate; page: 10 -->
|
||||
#### Врата варпа
|
||||
|
||||
Для того, чтобы воспользоваться этой картой вы должны иметь возможность вскрыть жетон приказа. Если все ваши жетоны приказов заблокированы жетонами соперников или ваши жетоны уже закончились, то вы не можете воспользоваться этой картой. Забрать и поместить в другую область деморализованный отряд нельзя.
|
||||
|
||||
<!-- block: faq.eldar.events.foresight; page: 10 -->
|
||||
#### Предвидение
|
||||
|
||||
Забрать и поместить в другую область деморализованные отряды нельзя. Проверка окончания боя будет только в конце шага нанесения боевых повреждений, значит противнику придется распределять урон в свои отряды (не смотря на то, что все отряды Эльдар выйдут из боя). Игроку за Эльдар урон распределять не нужно, если все его отряды выйдут из боя. Если у игрока за Эльдар в бою останется бастион и/или деморализованные отряды, то урон от противника достанется им, а бой продолжится.
|
||||
|
||||
### Приказы
|
||||
|
||||
<!-- block: faq.eldar.orders.bombardment; page: 10 -->
|
||||
#### Обстрел
|
||||
|
||||
Деморализованные отряды перемещать нельзя.
|
||||
|
||||
<!-- block: faq.eldar.orders.calculated-strike; page: 10 -->
|
||||
#### Расчетливый удар
|
||||
|
||||
Помните про лимит в 8 кубиков. Если вы должны получить девятый, вы вместо этого не получаете кубик.
|
||||
|
||||
<!-- block: faq.eldar.orders.corsair-raid; page: 10 -->
|
||||
#### Рейд корсаров
|
||||
|
||||
Нельзя выполнить 2 орбитальных удара по эффекту данной карты.
|
||||
|
||||
### Боевые карты
|
||||
|
||||
<!-- block: faq.eldar.combat.hit-and-run; page: 11 -->
|
||||
#### Бей и беги
|
||||
|
||||
Деморализованные отряды перемещать нельзя. Можно переместиться в соседнюю систему, главное, чтобы область назначения была соседней (примыкала ортогонально). Таким перемещением можно отрезать легальный путь для отступления противнику.
|
||||
|
||||
<!-- block: faq.eldar.combat.howling-banshees; page: 11 -->
|
||||
#### Воющие баньши
|
||||
|
||||
Если у противника нет отрядов для деморализации или они не могут стать деморализованными по эффекту карт, то не произойдет ничего.
|
||||
|
||||
<!-- block: faq.eldar.combat.ranger-cover; page: 11 -->
|
||||
#### Прикрытие рейнджеров
|
||||
|
||||
После отступления отряд становится деморализованным. Отступить можно и деморализованным отрядом. Отряд, отступивший в никем не контролируемую область, делает ее дружественной. Отступление по данной карте должно производиться в 1 область. Если будет разыграна вторая карта **прикрытие рейнджеров**, то отступление можно будет производить в другую область.
|
||||
|
||||
<!-- block: faq.eldar.combat.autarch-guidance; page: 11 -->
|
||||
#### Руководство Аутарха
|
||||
|
||||
Помните про лимит в 8 кубиков. Если вы не можете разыграть свойство на получение кубика или восстановление подразделения, то это не запрещает вам разыграть свойство на розыгрыш дополнительной карты.
|
||||
|
||||
<!-- block: faq.eldar.combat.wraithguard-advance; page: 11 -->
|
||||
#### Наступление призрачных стражей
|
||||
|
||||
Если у противника нет кубиков с аквилами (орлами), то он должен деморализовать свой отряд (если есть и если это возможно). Если у противника нет недеморализованных отрядов или его отряды не могут стать деморализованными, то не произойдет ничего (противник не обязан тратить кубик с аквилой).
|
||||
|
||||
<!-- block: faq.eldar.combat.seer-counsel; page: 11 -->
|
||||
#### Наставления видящих
|
||||
|
||||
Противник тоже не получит повреждений. Карта не запрещает деморализовать отряды и напрямую их уничтожать по эффектам других карт. Блокируются только боевые повреждения.
|
||||
|
||||
<!-- block: faq.eldar.combat.wave-serpent-top; page: 11 -->
|
||||
#### Волновой змей (верхнее свойство)
|
||||
|
||||
Если у противника нет кубиков с аквилами (орлами), то Эльдар получат жетоны защиты.
|
||||
|
||||
<!-- block: faq.eldar.combat.wave-serpent-bottom; page: 11 -->
|
||||
#### Волновой змей (нижнее свойство)
|
||||
|
||||
Деморализованные отряды перемещать нельзя. Можно переместиться в соседнюю систему, главное, чтобы область назначения была соседней (примыкала ортогонально). Такое перемещение может отрезать легальный путь к отступлению сопернику. Отряды Эльдар не смогут отступать из боя, вызванного этим эффектом (см правила отступления).
|
||||
|
||||
<!-- block: faq.eldar.combat.psychic-lance; page: 11 -->
|
||||
#### Психокопье
|
||||
|
||||
Формулировка карты переписана (по сравнению с английской версией). Карта задает выбор: ИЛИ владелец психокопья получает 4 жетона атаки (**А**), ИЛИ противник **соглашается** на то, что владелец психокопья выберет открытую боевую карту соперника и сбросит ее (прикажет ее сбросить) (**Б**). Если у противника нет открытых боевых карт, то это никак не влияет на саму принципиальную возможность **согласиться** на эту процедуру. Противник вправе выбрать вариант **Б**, даже если у него нет отрытых боевых карт (например, идет 1 раунд боя и Эльдар атакуют). В таком случае игрок за Эльдар не получит боевые жетоны, а противник Эльдар не потеряет ничего, т.к. ему нечего терять.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,835 +0,0 @@
|
||||
# Правила игры
|
||||
|
||||
> **Источник:** буклет «Правила игры» — `fs_rules_rus_web.pdf`, 16 стр. (скан без текстового
|
||||
> слоя, распознан вручную). Перевод: Копылов Олег.
|
||||
>
|
||||
> **Не перенесено:** стр. 1 (обложка); художественный текст — «Нет мира среди звезд» и описания
|
||||
> фракций (стр. 2), вводный абзац врезки «Что такое варп-штормы?» (стр. 7); иллюстрации. От схем оставлены подписи; описание схемы, составленное
|
||||
> при переносе, выделено курсивом. Формат блоков, id и обозначения иконок — в [README](README.md).
|
||||
|
||||
<!-- block: rules.overview; page: 2 -->
|
||||
## Обзор игры
|
||||
|
||||
*Forbidden Stars* — игра о галактической войне, действие которой происходит во вселенной *Warhammer 40,000.* От двух до четырех игроков командуют Космодесантниками, Орками, Эльдарами или Космодесантниками Хаоса (в базовой игре). Каждая фракция стремится вернуть потерянные реликвии и места силы, которые являются ключевыми для их выживания. Для этого они должны собирать ресурсы, улучшать свои силы и завоевывать миры давно потерянного Скопления Геракон.
|
||||
|
||||
<!-- block: rules.overview.using-this-booklet; page: 2 -->
|
||||
### Использование этого буклета
|
||||
|
||||
Этот буклет «Правила Игры» предназначен для обучения новых игроков игре в Forbidden Stars. Чтобы научить игре быстро и просто, в этом буклете опущены многие исключения из правил и сложные игровые взаимодействия из-за большого количества типов отрядов и боевых карт. Игроки должны использовать прилагающийся Справочник, чтобы разрешить эти ситуации.
|
||||
|
||||
<!-- block: rules.factions; page: 2 -->
|
||||
## Фракции
|
||||
|
||||
В *Forbidden Stars* представлены четыре основные фракции вселенной Warhammer 40,000. Каждая фракция имеет свои уникальные компоненты, которые можно отличить по цвету или по символу фракции. Это позволяет определить, какие компоненты принадлежат каким игрокам.
|
||||
|
||||
- **Орден Космодесантников Ультрамарины**
|
||||
- **Легион-Предатель Космодесантников Хаоса Пожиратели Миров**
|
||||
- **Мир-Корабль Эльдар Йанден**
|
||||
- **Орки Клана Злосолнца**
|
||||
|
||||
<!-- block: rules.components; page: 3 -->
|
||||
## Список компонентов
|
||||
|
||||
- 1 Справочник
|
||||
- 112 Боевых Карт (28 на фракцию)
|
||||
- 32 Карт Событий (8 на фракцию)
|
||||
- 20 Карт Улучшения Приказов (5 на фракцию)
|
||||
- 24 Маркера Целей (6 на фракцию)
|
||||
- 36 Маркеров Контроля Построек (9 на фракцию)
|
||||
- 32 Жетона Приказов (8 на фракцию)
|
||||
- 4 Карты Справок
|
||||
- 1 Счетчик Раунда, 1 Маркер Раунда
|
||||
- 1 Жетон Первого Игрока
|
||||
- 36 Жетонов Ценных Ресурсов (по 12 каждого типа)
|
||||
- 16 Кубов
|
||||
- 12 Боевых Жетонов ([жетон: атака] спереди, [жетон: защита] сзади)
|
||||
- 4 Счетчиков Материалов
|
||||
- 4 Жетона Варп-Штормов
|
||||
- 12 Двусторонних Тайлов Систем
|
||||
- 4 Листа Фракций
|
||||
- 35 Пластиковых Построек (10 Фабрик, 15 Городов,и 10 Бастионов)
|
||||
- 105 Пластиковых Отрядов (27 для Космодесанта, 24 для Эльдаров, 27 для Орков, 27 для Хаоса), и 27 пластиковых подставок (Пластиковые Отряды от КТ визуально отличаются)
|
||||
|
||||
<!-- block: rules.components.material-dials-assembly; page: 3 -->
|
||||
### Сборка счетчиков материалов
|
||||
|
||||
Перед началом игры в *Forbidden Stars* в первый раз, осторожно соберите четыре счетчика материалов, как указано на рисунке ниже. (В редакции от КТ счетчики материалов уже собраны)
|
||||
|
||||
<!-- block: rules.components.ships-assembly; page: 3 -->
|
||||
### Сборка кораблей
|
||||
|
||||
Перед началом игры в *Forbidden Stars*, осторожно вставьте пластиковые подставки в каждую фигурку корабля, как показано на рисунке справа.
|
||||
|
||||
<!-- block: rules.setup; page: 4 -->
|
||||
## Подготовка к игре
|
||||
|
||||
Перед началом игры в *Forbidden Stars*, игроки должны выполнить следующие шаги подготовки к игре:
|
||||
|
||||
<!-- block: rules.setup.choose-factions; page: 4 -->
|
||||
### 1. Выберите Фракции
|
||||
|
||||
Каждый игрок выбирает одну фракцию и берет соответствующие листы фракции, жетоны, карты событий, карты улучшений приказов, боевые карты и отряды этой фракции.
|
||||
|
||||
<!-- block: rules.setup.choose-factions.first-game; page: 4 -->
|
||||
**Если** это ваша первая игра, и игроков меньше четырех, то **исключите** фракции Орков и Эльдар из игры для двух игроков, и фракцию Эльдар из игры для трех игроков.
|
||||
|
||||
<!-- block: rules.setup.starting-components; page: 4 -->
|
||||
### 2. Получите Начальные Компоненты
|
||||
|
||||
Каждый игрок берет все компоненты, перечисленные в графе «*Стартовые Силы*» на обратной стороне листа его фракции. Затем он переворачивает лист своей фракции лицевой стороной вверх и кладет эти компоненты поверх него.
|
||||
|
||||
<!-- block: rules.setup.starting-components.materials; page: 4 -->
|
||||
Наконец, он берет счетчик материалов и устанавливает его на цифру «6». Это значение также указано в поле «*Стартовые Силы*» на обратной стороне листа его фракции.
|
||||
|
||||
<!-- block: rules.setup.first-player; page: 4 -->
|
||||
### 3. Определите Первого Игрока
|
||||
|
||||
Перемешайте по одному маркеру контроля постройки каждого игрока в крышке коробки и вытяните один случайным образом. Этому игроку дается жетон первого игрока.
|
||||
|
||||
<!-- block: rules.setup.game-board; page: 4 -->
|
||||
### 4. Соберите Игровое Поле
|
||||
|
||||
Для первой игры разместите необходимые тайлы систем в центре игровой зоны, как показано на диаграмме «*Расстановка к Первой Игре*» ниже. При игре на менее чем четырех игроков часть игрового поля исключается.
|
||||
|
||||
<!-- block: rules.setup.game-board.place-components; page: 4 -->
|
||||
Затем разместите отряды, постройки, жетоны контроля над постройками, маркеры цели и жетоны варп-штормов на игровом поле, как показано на схеме. Не размещайте жетоны или отряды неиспользуемых фракций. Жетон подкрепления, указанный в «*Стартовых Силах*» орков, не помещается на игровое поле, а остается рядом с его листом фракции, пока не будет использован.
|
||||
|
||||
<!-- block: rules.setup.game-board.first-game-layout; page: 4; kind: caption -->
|
||||
#### Расстановка к первой игре
|
||||
|
||||
*Схема: игровое поле из трех рядов по четыре тайла систем. Верхний ряд — 10A, 1A, 11B, 2B; средний — 5A, 3B, 8A, 6B; нижний — 7A, 12A, 4B, 9A. «2 игрока» — два левых столбца, «3 игрока» — три левых столбца, «4 игрока» — все четыре столбца.*
|
||||
|
||||
<!-- block: rules.setup.game-board.custom-board-note; page: 5 -->
|
||||
**Примечание:** Сыграв одну игру с использованием схемы «*Расстановка к Первой Игре*», игроки могут подготавливаться к игре, используя правила «*Создание Игрового Поля*» на стр. 16. Эти правила позволяют игрокам создавать уникальное игровое поле для каждой игры, выбирая, где размещать тайлы систем, отряды, жетоны целей и варп-штормы.
|
||||
|
||||
<!-- block: rules.setup.decks; page: 5 -->
|
||||
### 5. Подготовьте Колоды Событий, Улучшений и Боевые Колоды
|
||||
|
||||
Каждый игрок перемешивает свои карты событий и кладет их колодой лицевой стороной вниз в своей игровой зоне.
|
||||
|
||||
<!-- block: rules.setup.decks.combat-deck; page: 5 -->
|
||||
Затем каждый игрок находит десять боевых карт с символом своей фракции, напечатанным в верхнем левом углу, и перемешивает их, чтобы сформировать свою боевую колоду.
|
||||
|
||||
<!-- block: rules.setup.decks.faction-symbol-caption; page: 5; kind: caption -->
|
||||
Символ Фракции на Боевой Карте
|
||||
|
||||
<!-- block: rules.setup.decks.upgrade-decks; page: 5 -->
|
||||
Наконец, каждый игрок формирует две колоды улучшений, используя все карты улучшения приказов своей фракции и оставшиеся боевые карты. Эти колоды не нужно тасовать. Игроки кладут их **лицевой стороной вверх** рядом со своим листом фракции, не смешивая их с боевой колодой.
|
||||
|
||||
<!-- block: rules.setup.round-track; page: 5 -->
|
||||
### 6. Подготовьте Счетчик Раундов
|
||||
|
||||
Поместите шкалу раундов рядом с игровым полем и поместите маркер раунда на ячейку «1» на шкале.
|
||||
|
||||
<!-- block: rules.setup.supply; page: 5 -->
|
||||
### 7. Создайте Запас
|
||||
|
||||
Разделите все жетоны подкреплений, жетоны запасов, жетоны кузницы, кубы и пластиковые постройки в кучки и положите их рядом с игровым полем, где все игроки могут их достать.
|
||||
|
||||
<!-- block: rules.setup.ready; page: 5 -->
|
||||
После завершения шагов подготовки к игре компоненты каждого игрока должны находиться в его игровой зоне, как показано ниже. Затем игроки готовы начать первый раунд игры.
|
||||
|
||||
<!-- block: rules.setup.play-area-example; page: 5; kind: caption -->
|
||||
#### Пример игровой зоны
|
||||
|
||||
*Схема: игровая зона игрока — Колода Событий, Жетоны Приказов, Пластиковые Отряды, Счетчик Материалов, Маркеры Контроля Построек, Улучшения Приказов, Боевые Улучшения, Лист Фракции, Боевая Колода.*
|
||||
|
||||
<!-- block: rules.game-round; page: 5 -->
|
||||
## Ход игры
|
||||
|
||||
Игра в *Forbidden Stars* состоит из серии игровых раундов. Каждый игровой раунд состоит из трех фаз, которые играются в следующем порядке:
|
||||
|
||||
1. **Фаза Планирования:** Во время этой фазы игроки по очереди размещают жетоны приказов на игровом поле.
|
||||
2. **Фаза Действий:** Во время этой фазы игроки по очереди разыгрывают жетоны приказов, которые они разместили на игровом поле.
|
||||
3. **Фаза Обновления:** Во время этой фазы игроки собирают цели, материалы, восстанавливают деморализованные отряды, перемещают варп-штормы и разыгрывают карты событий. Затем первый игрок передает жетон первого игрока и продвигает маркер раунда по счетчику раундов.
|
||||
|
||||
<!-- block: rules.game-round.next-round; page: 5 -->
|
||||
После завершения Фазы Обновления игроки начинают новый раунд, начиная с новой Фазы Планирования. Они продолжают разыгрывать игровые раунды до тех пор, пока один из игроков не выиграет игру, собрав достаточное количество маркеров целей (подробно объяснено позже).
|
||||
|
||||
<!-- block: rules.planning; page: 5 -->
|
||||
## Фаза 1: Фаза Планирования
|
||||
|
||||
Во время Фазы Планирования игроки по очереди кладут жетоны приказов лицевой стороной вниз на игровое поле. Игроки раскрывают эти жетоны и применяют их эффекты во время Фазы Действий.
|
||||
|
||||
<!-- block: rules.planning.placing-orders; page: 5 -->
|
||||
Чтобы начать Фазу Планирования, первый игрок кладет один из своих жетонов приказов лицевой стороной вниз на зону для жетонов приказов на **тайле системы**. Затем по часовой стрелке каждый игрок кладет по одному жетону приказа на любой тайл системы. Игроки повторяют этот процесс до тех пор, пока каждый игрок не положит на игровое поле по четыре жетона приказов.
|
||||
|
||||
<!-- block: rules.planning.system-tile-diagram; page: 5; kind: caption -->
|
||||
*Схема тайла системы:* Области Миров; Зона для Жетонов Приказов; Области Пустот.
|
||||
|
||||
<!-- block: rules.planning.order-stacks; page: 5 -->
|
||||
Игроки кладут жетоны приказов **лицевой стороной вниз**, чтобы тип приказа был скрыт от других игроков. Если в системе уже есть один или несколько жетонов приказов, игрок кладет свой жетон приказа поверх стека существующих жетонов приказов. Жетон приказа на вершине стека всегда является жетоном приказа, который был помещен в эту систему последним.
|
||||
|
||||
<!-- block: rules.planning.order-stacks.captions; page: 5; kind: caption -->
|
||||
Жетоны приказов размещаются лицевой стороной вниз на зону для жетонов приказов. Любое количество жетонов приказов могут быть размещены друг на друга формируя стек.
|
||||
|
||||
<!-- block: rules.planning.adjacency; page: 6 -->
|
||||
**Важно:** Игрок не может положить жетон приказа в систему, если у него нет отрядов или построек либо в этой системе, либо в соседних с ней. Каждая система соседствует с системами, с которыми её тайл имеет общее ребро. Если у тайлов двух систем общий только угол, а не ребро, то они не являются соседними.
|
||||
|
||||
<!-- block: rules.planning.order-types; page: 6 -->
|
||||
### Типы жетонов приказов
|
||||
|
||||
Игроки используют жетоны приказов, чтобы выполнять основные действия в *Forbidden Stars*, такие как перемещение отрядов, атака отрядами и производство новых отрядов и построек.
|
||||
|
||||
<!-- block: rules.planning.order-types.list; page: 6 -->
|
||||
Существует четыре типа жетонов приказов. Когда игрок раскрывает жетон приказа, он разыгрывает эффект, соответствующий типу раскрытого жетона.
|
||||
|
||||
- **Развертывание:** Игрок может потратить свои материалы на покупку новых отрядов и построек в системе.
|
||||
- **Планирование:** Игрок может купить улучшение приказа, боевое улучшение или оба. Затем он кладет жетон приказа на верх своей колоды событий, что позволяет ему взять карту события во время Фазы Обновления.
|
||||
- **Доминирование:** Игрок получает ценные ресурсы из каждого дружественного мира в системе. Он также может использовать специальную способность на листе своей фракции.
|
||||
- **Продвижение:** Игрок может переместить свои отряды в систему и начать одну битву.
|
||||
|
||||
Детали для выполнения каждого типа приказов подробно объяснены позже.
|
||||
|
||||
<!-- block: rules.action; page: 6 -->
|
||||
## Фаза 2: Фаза Действий
|
||||
|
||||
Во время Фазы Действий игроки по очереди разыгрывают жетоны приказов, размещенные во время Фазы Планирования. Начиная с первого игрока и далее по часовой стрелке, каждый игрок выбирает один из своих жетонов приказов **сверху любого стека**. Он раскрывает выбранный жетон приказа, разыгрывает его эффект и убирает его с игрового поля. Фаза Действий заканчивается, когда игроки разыграли все жетоны приказов на игровом поле.
|
||||
|
||||
<!-- block: rules.action.resolve-or-event; page: 6 -->
|
||||
Когда активный игрок раскрывает жетон приказа, он решает либо разыграть эффект жетона, либо положить жетон лицевой стороной вверх на верх своей колоды событий. Каждый жетон на колоде событий игрока позволяет ему взять одну карту события во время Фазы Обновления.
|
||||
|
||||
<!-- block: rules.action.no-available-order; page: 6 -->
|
||||
**Примечание:** Если у игрока нет ни одного жетона приказа на вершине какого-либо стека, он обязан пропустить свой ход. Он обязан выполнить приказ во время своего следующего хода, если в этот момент на вершине какого-либо стека будет его приказ.
|
||||
|
||||
<!-- block: rules.units-and-structures; page: 6 -->
|
||||
## Что такое отряды и постройки?
|
||||
|
||||
Наземные отряды и корабли — это типы **отрядов**, представленные пластиковыми фигурками. Игроки перемещают отряды, атакуют ими и размещают их, чтобы контролировать области на игровом поле, что в конечном счете позволяет им собирать свои маркеры целей. **Постройки** предоставляют игрокам боевые усиления (бастионы), позволяют игрокам покупать отряды (фабрики) и повышают командный уровень игрока (города), который необходим для приобретения карт улучшений и более мощных отрядов. Таким образом, для достижения победы игроку необходим тщательный баланс как построек, так и отрядов.
|
||||
|
||||
<!-- block: rules.refresh; page: 6 -->
|
||||
## Фаза 3: Фаза Обновления
|
||||
|
||||
Во время Фазы Обновления каждый игрок собирает материалы и маркеры целей из **дружественных** миров — миров, в которых есть только его отряды и/или постройки. Также во время этой фазы каждый игрок восстанавливает свои деморализованные отряды, чтобы они были готовы к следующему игровому раунду.
|
||||
|
||||
<!-- block: rules.refresh.steps; page: 6 -->
|
||||
Во время Фазы Обновления каждый игрок выполняет следующие шаги по порядку:
|
||||
|
||||
1. Соберите Цели
|
||||
2. Соберите Материалы
|
||||
3. Восстановите Деморализованные Отряды
|
||||
4. Возьмите События и Переместите Варп-Штормы
|
||||
5. Конец Раунда
|
||||
|
||||
После завершения этой фазы игроки начинают новый игровой раунд.
|
||||
|
||||
<!-- block: rules.refresh.objectives; page: 6 -->
|
||||
### Соберите Цели
|
||||
|
||||
Каждый игрок собирает любые **свои** маркеры **целей**, которые находятся в дружественных мирах. Он помещает маркеры в поле «Маркеры целей» на листе своей фракции, чтобы все игроки могли легко видеть, сколько маркеров он собрал.
|
||||
|
||||
<!-- block: rules.refresh.objectives.caption; page: 6; kind: caption -->
|
||||
Игрок за Эльдар собрал маркер цели с дружественного мира.
|
||||
|
||||
<!-- block: rules.refresh.objectives.victory; page: 6 -->
|
||||
Игрок выигрывает игру, если он собирает количество маркеров цели, равное количеству игроков в игре (см. «Победа в игре» ниже).
|
||||
|
||||
<!-- block: rules.refresh.materials; page: 6 -->
|
||||
### Соберите Материалы
|
||||
|
||||
У большинства миров есть **производственное значение**, которое обозначается числом на зеленом значке материалов.
|
||||
|
||||
<!-- block: rules.refresh.materials.caption; page: 6; kind: caption -->
|
||||
Производственное значение этого мира равно двум
|
||||
|
||||
<!-- block: rules.refresh.materials.gain; page: 7 -->
|
||||
Во время Фазы Обновления каждый игрок получает количество материалов, равное сумме производственных значений всех его дружественных миров. Когда игрок получает материалы, он поворачивает счетчик материалов на соответствующую величину в большую сторону.
|
||||
|
||||
<!-- block: rules.refresh.materials.dial-caption; page: 7; kind: caption -->
|
||||
Счетчик материалов установлен на значении «4».
|
||||
|
||||
<!-- block: rules.refresh.rally; page: 7 -->
|
||||
### Восстановите Деморализованные Отряды
|
||||
|
||||
Каждый игрок **восстанавливает** все свои **деморализованные** отряды. Отряды могут стать деморализованными во время битвы и кладутся на бок, чтобы это обозначить. Деморализованный отряд не может перемещаться или быть использованными для срабатывания способностей боевых карт. Он также не может привносить свои кубы или значение своего боевого духа в битву или орбитальный удар. Чтобы показать восстановление отряда, игрок возвращает его в исходное недеморализованное состояние: ставит фигурку ровно.
|
||||
|
||||
<!-- block: rules.refresh.rally.caption; page: 7; kind: caption -->
|
||||
Недеморализованный Отряд; Деморализованный Отряд
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms; page: 7 -->
|
||||
### Возьмите События и Переместите Варп-Штормы
|
||||
|
||||
Каждый игрок берет из своей колоды событий количество карт, равное количеству жетонов приказов **на ней**, возвращая жетоны приказов в свой запас неиспользованных жетонов приказов. Жетоны приказов кладутся на верх этой колоды после выполнения Приказа Планирования (подробно объяснено позже).
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms.choose-event; page: 7 -->
|
||||
Затем, начиная с первого игрока и далее по часовой стрелке, каждый игрок выбирает одну карту события из тех, что он только что вытянул. Он перемещает один варп-шторм по игровому полю, следуя значку движения варп-шторма, изображенному на выбранной им карте (см. «Перемещение варп-шторма» справа). Затем он **может** разыграть способность карты события.
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms.event-types; page: 7 -->
|
||||
Карты событий бывают двух видов: **тактика** и **план**.
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms.event-types.caption; page: 7; kind: caption -->
|
||||
Карта Событий «Тактика»; Карта Событий «План»
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms.tactic-and-scheme; page: 7 -->
|
||||
Разыграв карту события-тактики, игрок замешивает ее обратно в свою колоду событий. Карты событий-планов размещаются лицом вверх рядом с листом фракции игрока, и их эффекты можно использовать в будущем, как описано на карте.
|
||||
|
||||
<!-- block: rules.refresh.events-and-warp-storms.reshuffle; page: 7 -->
|
||||
После того, как игрок разыграет свою карту события, он замешивает все невыбранные карты событий из своей руки обратно в свою колоду.
|
||||
|
||||
<!-- block: rules.refresh.warp-storms; page: 7 -->
|
||||
#### Что такое варп-штормы?
|
||||
|
||||
В *Forbidden Stars* жетоны варп-штормов размещаются на игровом поле вдоль ребер некоторых тайлов систем. Отряды не могут двигаться сквозь варп-штормы. Варп-штормы перемещаются в конце каждого игрового раунда, превращая игровое поле в постоянно меняющуюся и непредсказуемую среду.
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement; page: 7 -->
|
||||
#### Перемещение Варп-Штормов
|
||||
|
||||
Чтобы переместить варп-шторм, игрок выбирает один варп-шторм, **который не перемещали в этой фазе**. Затем он перемещает жетон в одном из двух направлений, указанных значком движения варп-шторма в правой части его карты события.
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement.icon-caption; page: 7; kind: caption -->
|
||||
Иконка Движения Варп-Шторма
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement.caption; page: 7; kind: caption -->
|
||||
Существует восемь направлений, в которых может двигаться варп-шторм.
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement.no-stacking; page: 7 -->
|
||||
Игроки **не могут** переместить жетон варп-шторма на другой варп-шторм.
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement.must-move; page: 7 -->
|
||||
Когда идёт выбор, какой варп-шторм двигать и направление в котором его двигать, игрок всегда должен выбирать тот результат, при котором произойдёт движение варп-шторма, если это возможно.
|
||||
|
||||
<!-- block: rules.refresh.warp-storm-movement.no-events; page: 7 -->
|
||||
**Важно:** Если игрок не брал карты событий, он все равно должен переместить варп-шторм. Для этого он раскрывает верхнюю карту своей колоды событий и перемещает один варп-шторм, используя значок движения варп-шторма на этой карте, как описано выше. Затем он замешивает карту события обратно в свою колоду, не разыгрывая ее способность.
|
||||
|
||||
<!-- block: rules.refresh.end-of-round; page: 8 -->
|
||||
### Конец Раунда
|
||||
|
||||
Первый игрок отдает жетон первого игрока игроку слева от себя. Затем он передвигает маркер раунда на одно деление по счетчику раундов. Если маркер раунда сдвинется с деления «8» счетчика, игра заканчивается, и игрок с наибольшим количеством маркеров целей выигрывает игру.
|
||||
|
||||
<!-- block: rules.victory; page: 8 -->
|
||||
## Победа в игре
|
||||
|
||||
Игрок выигрывает игру, когда он собрал количество своих маркеров целей, **равное количеству игроков в игре**. Например, во время игры для двух игроков игрок выигрывает игру, когда он собирает два своих маркера целей.
|
||||
|
||||
<!-- block: rules.victory.after-round-eight; page: 8 -->
|
||||
Если ни один игрок не выиграл игру к концу восьмого игрового раунда, игра заканчивается, и игрок, собравший больше всего маркеров целей, побеждает в игре.
|
||||
|
||||
<!-- block: rules.victory.objective-markers; page: 8 -->
|
||||
### Что такое маркеры целей?
|
||||
|
||||
Маркеры целей представляют собой важные объекты, людей или места, являющиеся ключевыми для выживания фракции. На обратной стороне каждого маркера цели показано, что тематически представляет этот маркер цели. Хотя у каждого маркера есть уникальное изображение, оно несет лишь тематический характер и **не влияет на игровой процесс**. На обратной стороне листа фракции каждого игрока есть краткое описание историй, связанных с его целями.
|
||||
|
||||
<!-- block: rules.victory.objective-markers.description-caption; page: 8; kind: caption -->
|
||||
Описание Маркера Цели: «Лорд Системы: Лорд Халовар присягнул на верность Империуму. Теперь он просит помощи в избавлении от тьмы.»
|
||||
|
||||
<!-- block: rules.victory.objective-markers.face-up; page: 8 -->
|
||||
Маркеры целей **всегда остаются лежать лицевой стороной вверх**, чтобы игроки могли видеть символ фракции. Это важно, потому что игрок не может собирать маркеры целей, принадлежащие другим игрокам. Однако игроки могут размещать отряды в мирах, содержащих вражеские маркеры целей, чтобы помешать своим противникам добиться победы.
|
||||
|
||||
<!-- block: rules.victory.objective-markers.face-caption; page: 8; kind: caption -->
|
||||
Лицевая Сторона Маркера Цели
|
||||
|
||||
<!-- block: rules.orders; page: 8 -->
|
||||
## Приказы подробно
|
||||
|
||||
Каждая фракция имеет по два жетона каждого из четырех типов приказов. Когда игрок выполняет приказ во время Фазы Действий, тайл системы, на котором был размещен приказ, становится **активной системой**. В этом разделе описывается, как игроки выполняют приказы каждого из четырех типов.
|
||||
|
||||
<!-- block: rules.orders.deploy; page: 8 -->
|
||||
### Приказ Развертывания
|
||||
|
||||
Игроки используют Приказ Развертывания, чтобы размещать новые отряды и постройки на игровом поле. Чтобы выполнить Приказ Развертывания, активный игрок совершает следующие два шага **в указанном порядке**:
|
||||
|
||||
1. **Покупка Отрядов**: Если у игрока есть фабрика в активной системе, он может купить отряды и разместить их в любых дружественных или неконтролируемых (не содержащих никаких отрядов или построек) областях в активной системе.
|
||||
2. **Покупка Построек**: Игрок может купить **одну** постройку и разместить ее в любом дружественном мире в активной системе, где еще нет постройки.
|
||||
|
||||
<!-- block: rules.orders.deploy.token-caption; page: 8; kind: caption -->
|
||||
Жетон Приказа Развертывания
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units; page: 8 -->
|
||||
#### Покупка Отрядов
|
||||
|
||||
Игрок покупает отряд, тратя количество материала, равное стоимости в материалах этого отряда, указанной на его листе фракции. Чтобы потратить материал, игрок поворачивает счетчик материалов на соответствующую величину в меньшую сторону. Затем он размещает отряд в любую **дружественную или неконтролируемую** область в активной системе.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units.forge-tokens; page: 8 -->
|
||||
Некоторые отряды стоят жетон кузницы в дополнение к их стоимости в материалах. Чтобы потратить жетон кузницы, игрок берет жетон кузницы из своей игровой зоны и возвращает его в запас.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units.cost-caption; page: 8; kind: caption -->
|
||||
*Схема строки отряда на листе фракции:* Материальная Стоимость Отряда; Требование Жетона Кузницы.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units.placement; page: 8 -->
|
||||
Игроки могут размещать наземные отряды только в мирах, а корабли — в пустотах. В дополнение к этим основным ограничениям игрок обязан соблюдать ограничения на командный уровень.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units.deployment-limit; page: 8 -->
|
||||
Максимальное количество отрядов, которое игрок может приобрести в рамках одного Приказа Развертывания, определяется его **лимитом развертывания**. Его лимит развертывания равен вместимости мира активной системы, где расположена фабрика. Вместимость равна количеству черепов на знамени этого мира.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-units.capacity-caption; page: 8; kind: caption -->
|
||||
Вместимость отрядов этого мира равна двум.
|
||||
|
||||
<!-- block: rules.orders.deploy.command-level; page: 9 -->
|
||||
#### Командный уровень
|
||||
|
||||
У каждого игрока есть **командный уровень**, который указывает, какие отряды и улучшения он может купить. Командный уровень игрока равен количеству городов, которые он контролирует.
|
||||
|
||||
<!-- block: rules.orders.deploy.command-level.requirements; page: 9 -->
|
||||
У каждого отряда и карты улучшения приказа или боевой карты есть требование к командному уровню. Игрок может приобретать отряды и улучшения только с требованиями, равными его командному уровню или меньшими, чем его командный уровень.
|
||||
|
||||
<!-- block: rules.orders.deploy.command-level.caption; page: 9; kind: caption -->
|
||||
*Схема строки отряда на листе фракции:* Требуемый Командный Уровень Отряда.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-structures; page: 9 -->
|
||||
#### Покупка Построек
|
||||
|
||||
Игрок покупает постройку (город, бастион или фабрику), потратив количество материалов, равное стоимости в материалах этой постройки, указанной на его листе фракции. Затем он размещает пластиковую постройку на любой дружественный мир в активной системе поверх одного из своих **маркеров контроля постройки**. Маркер под каждой постройкой указывает, кому принадлежит эта постройка.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-structures.one-per-world; page: 9 -->
|
||||
**Важно:** Игрок не может разместить постройку в мире, в котором уже есть постройка.
|
||||
|
||||
<!-- block: rules.orders.deploy.purchase-structures.caption; page: 9; kind: caption -->
|
||||
Эта постройка контролируется игроком фракции Эльдар.
|
||||
|
||||
<!-- block: rules.orders.dominate; page: 9 -->
|
||||
### Приказ Доминирования
|
||||
|
||||
Игроки используют Приказы Доминирования, чтобы получать ценные ресурсы с миров и использовать свои особые способности фракции. Чтобы выполнить Приказ Доминирования, активный игрок выполняет следующие шаги по порядку:
|
||||
|
||||
<!-- block: rules.orders.dominate.token-caption; page: 9; kind: caption -->
|
||||
Жетон Приказа Доминирования
|
||||
|
||||
<!-- block: rules.orders.dominate.gain-resources; page: 9 -->
|
||||
#### 1. Получение ценных ресурсов
|
||||
|
||||
Игрок получает ценные ресурсы, предоставленные каждым дружественным миром в активной системе. Ценные ресурсы отображаются в правом углу знамени мира.
|
||||
|
||||
<!-- block: rules.orders.dominate.gain-resources.take-tokens; page: 9 -->
|
||||
Игрок берет соответствующие жетоны из запаса и кладет их в свою игровую зону. Если мир предоставляет несколько ценных ресурсов, игрок берет их все.
|
||||
|
||||
<!-- block: rules.orders.dominate.faction-ability; page: 9 -->
|
||||
#### 2. Использование специальной способности фракции
|
||||
|
||||
Игрок может использовать специальную способность, указанную на листе его фракции.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources; page: 9 -->
|
||||
#### Ценные ресурсы
|
||||
|
||||
Игроки могут получить ценные ресурсы с дружественных миров, выполнив Приказ Доминирования. Каждый ценный ресурс имеет уникальный эффект, как описано ниже:
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.forge; page: 9 -->
|
||||
##### Жетоны Кузницы
|
||||
|
||||
Некоторые отряды требуют, чтобы игрок потратил жетон кузницы, чтобы купить их. Это отображается на листе фракции ниже стоимости соответствующего отряда в материалах.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.forge.command-level; page: 9 -->
|
||||
В качестве альтернативы игрок может потратить жетон кузницы при покупке отряда, чтобы снизить требование к его командному уровню на **один**. Это позволяет ему купить отряд, который при нормальных условиях не может быть куплен (см. «Командный уровень» слева).
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.forge.caption; page: 9; kind: caption -->
|
||||
Этот мир предоставляет жетон кузницы.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.supply; page: 9 -->
|
||||
##### Жетоны Запасов
|
||||
|
||||
При покупке отряда или постройки игрок может потратить жетон запасов, чтобы снизить стоимость в материалах этого отряда или постройки на два.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.supply.caption; page: 9; kind: caption -->
|
||||
Этот мир предоставляет жетон запасов.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.reinforcement; page: 9 -->
|
||||
##### Жетоны Подкрепления
|
||||
|
||||
Во время шага вызова подкреплений во время битвы атакующий, а затем защищающийся могут разместить свои жетоны подкреплений в оспариваемую область.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.reinforcement.unit; page: 9 -->
|
||||
Жетон подкрепления остается на время битвы и считается наземным отрядом или кораблем соответствующей фракции нулевого командного уровня. Находясь в области, он соблюдает все правила и ограничения, применимые к отрядам.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.reinforcement.example; page: 9 -->
|
||||
Например, каждый жетон подкрепления, который игрок Ультрамаринов размещает в мире, рассматривается как отряд Разведчиков.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.reinforcement.caption; page: 9; kind: caption -->
|
||||
Этот мир предоставляет жетон подкрепления.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.prosperity; page: 9 -->
|
||||
##### Процветание
|
||||
|
||||
Когда игрок получает этот ценный ресурс, он вместо этого получает один жетон ресурса по своему выбору.
|
||||
|
||||
<!-- block: rules.orders.dominate.resources.prosperity.caption; page: 9; kind: caption -->
|
||||
Этот мир предоставляет один жетон ценного ресурса любого типа
|
||||
|
||||
<!-- block: rules.orders.strategize; page: 10 -->
|
||||
### Приказ Планирования
|
||||
|
||||
Чтобы выполнить Приказ Планирования у игрока должен быть **отряд или постройка в активной системе**. Он может просмотреть все карты в своих колодах улучшений. Затем он может купить одно улучшение приказа и/или одно боевое улучшение, если он соответствует ограничениям командного уровня каждой карты (см. «Командный уровень» на стр. 9). После выполнения этого приказа он кладет жетон приказа **на верх своей колоды событий**, что позволит ему взять карту события во время Фазы Обновления.
|
||||
|
||||
<!-- block: rules.orders.strategize.token-caption; page: 10; kind: caption -->
|
||||
Жетон Приказа Планирования
|
||||
|
||||
<!-- block: rules.orders.strategize.upgrade-cost; page: 10 -->
|
||||
Чтобы купить улучшение, игрок должен потратить количество материалов, равное стоимости в материалах, указанной в левом верхнем углу карты улучшения приказа или боевой колоды. Чтобы потратить материалы, игрок поворачивает счетчик материалов на соответствующую величину в меньшую сторону.
|
||||
|
||||
<!-- block: rules.orders.strategize.upgrade-cost.caption; page: 10; kind: caption -->
|
||||
*Схема карты улучшения:* Требуемый Командный Уровень Улучшения; Материальная Стоимость Улучшения.
|
||||
|
||||
<!-- block: rules.orders.strategize.upgrade-types; page: 10 -->
|
||||
Существует два типа улучшений: **улучшения приказов** и **боевые улучшения**, а именно:
|
||||
|
||||
- **Улучшения Приказов:** Улучшения приказов повышают функциональность жетонов приказов. После покупки улучшения приказа игрок помещает его в свою игровую зону рядом со своим листом фракции. Каждое улучшение приказа соответствует одному из четырех приказов (Развертывание, Планирование, Доминирование, Продвижение) и дает игроку преимущество при выполнении соответствующего приказа.
|
||||
- **Боевые Улучшения:** Боевые улучшения — это более мощные боевые карты, которые игрок добавляет в свою боевую колоду.
|
||||
|
||||
<!-- block: rules.orders.strategize.upgrade-types.caption; page: 10; kind: caption -->
|
||||
Карта Улучшения Приказов («Крестовый поход»); Карта Боевого Улучшения («Держать строй»)
|
||||
|
||||
<!-- block: rules.orders.strategize.combat-upgrades; page: 10 -->
|
||||
#### Покупка боевых улучшений
|
||||
|
||||
Игрок покупает карты боевых улучшений парами. Когда игрок покупает одну карту боевого улучшения, он получает **обе копии карты**.
|
||||
|
||||
<!-- block: rules.orders.strategize.combat-upgrades.swap; page: 10 -->
|
||||
Когда игрок покупает пару боевых карт, он должен убрать две копии любой другой карты из своей боевой колоды и поместить их обе в свою колоду боевых улучшений. Затем он добавляет обе копии купленного улучшения в свою боевую колоду и перемешивает колоду.
|
||||
|
||||
<!-- block: rules.orders.strategize.combat-upgrades.deck-size; page: 10 -->
|
||||
Боевая колода всегда содержит две копии пяти карт, что в сумме дает десять карт.
|
||||
|
||||
<!-- block: rules.orders.advance; page: 10 -->
|
||||
### Приказ Продвижения
|
||||
|
||||
Игроки используют Приказы Продвижения для перемещения отрядов и начала битв. Чтобы выполнить Приказ Продвижения, игрок перемещает отряды **в активную систему**. После перемещения отрядов игрок разрешает битву, если есть **оспариваемая область** — область, в которой есть как дружественные, так и вражеские отряды или постройки.
|
||||
|
||||
<!-- block: rules.orders.advance.token-caption; page: 10; kind: caption -->
|
||||
Жетон Приказа Продвижения
|
||||
|
||||
<!-- block: rules.orders.advance.steps; page: 10 -->
|
||||
Чтобы выполнить Приказ Продвижения, активный игрок совершает следующие шаги по порядку:
|
||||
|
||||
1. **Перемещение Кораблей:** Активный игрок может переместить свои корабли в активной системе и из одной соседней системы в любые пустоты в активной системе.
|
||||
2. **Перемещение Наземных Отрядов:** Активный игрок может переместить свои наземные отряды в активной системе и из одной соседней системы в любые миры в активной системе. Если он перемещал корабли из соседней системы, он не может перемещать наземные отряды из другой соседней системы.
|
||||
3. **Разрешение Битвы:** Активный игрок разрешает битву, если есть оспариваемая область (см. «Битва» на стр. 12). Если оспариваемой области нет, он может вместо этого провести орбитальный удар (см. «Орбитальный удар» на стр. 11).
|
||||
|
||||
<!-- block: rules.orders.advance.limits; page: 10 -->
|
||||
**Важно:** Отряды **не могут двигаться через варп-штормы**, и игрок может создать **максимум одну** оспариваемую область при выполнении Приказа Продвижения. Игрок может переместить любое количество отрядов Приказом Продвижения, но **максимум пять отрядов** могут закончить свое перемещение в каждой области.
|
||||
|
||||
<!-- block: rules.orders.advance.ship-movement; page: 10 -->
|
||||
#### Перемещение Кораблей
|
||||
|
||||
При перемещении корабля игрок может переместить корабль из пустоты, которую он в данный момент занимает, в любую пустоту в активной системе, даже если эти две области не являются соседними.
|
||||
|
||||
<!-- block: rules.orders.advance.ground-movement; page: 10 -->
|
||||
#### Перемещение Наземных Отрядов
|
||||
|
||||
При перемещении наземного отряда игрок может переместить отряд из мира, в котором он находится в данный момент, в любой мир в активной системе, соединенной **путем**. Путь представляет собой ряд соседних (не по диагонали) дружественных областей. Путь может состоять из миров, пустот или их сочетаний (см. «Пример Приказа Продвижения» на стр. 11).
|
||||
|
||||
<!-- block: rules.orders.advance.ground-movement.adjacent-worlds; page: 10 -->
|
||||
Наземные отряды могут перемещаться из одного мира в соседний без использования корабля. Тематически они используют невоенные транспортные корабли или другие простые формы передвижения, не представленные в игре. Количество отрядов, которые игрок может перемещать через дружественную область, не ограничено.
|
||||
|
||||
<!-- block: rules.orders.advance.capacity; page: 10 -->
|
||||
#### Вместимость Отрядов
|
||||
|
||||
Каждая область имеет **вместимость**. Вместимость пустоты равна трем, а вместимость каждого мира равна количеству значков черепа, представленных на знамени мира. Вместимость области указывает **максимальное количество отрядов**, которые могут существовать в области после полного выполнения приказа.
|
||||
|
||||
<!-- block: rules.orders.advance.capacity.caption; page: 10; kind: caption -->
|
||||
Вместимость Отрядов Мира
|
||||
|
||||
<!-- block: rules.orders.advance.example; page: 11; kind: example -->
|
||||
#### Пример Приказа Продвижения
|
||||
|
||||
1. Настала очередь игрока за Ультрамаринов разыгрывать приказ, поэтому он раскрывает один из своих Приказов Продвижения переворачивая его лицевой стороной вверх.
|
||||
2. Сначала он должен переместить корабли. У него нет кораблей в активной системе, поэтому он перемещает Ударный Крейсер из соседней системы в пустоту в активной системе.
|
||||
3. Затем игрок решает переместить некоторые из своих наземных отрядов из соседней системы в активную систему. Теперь, когда у него есть корабль в пустоте, он может перемещать через нее наземные отряды. Он перемещает своих Космодесантников через пустоту в мир в правом верхнем углу активной системы.
|
||||
4. Затем игрок перемещает своего Разведчика через пустоту в мир в левом нижнем углу активной области.
|
||||
5. Наконец, игрок решает переместить своего Разведчика, который уже был в активной системе, в соседний мир в правом нижнем углу.
|
||||
|
||||
<!-- block: rules.orbital-strike; page: 11 -->
|
||||
## Орбитальный удар
|
||||
|
||||
После выполнения Приказа Продвижения, если нет оспариваемых областей (и, следовательно, битва не начиналась), активный игрок может выполнить орбитальный удар.
|
||||
|
||||
<!-- block: rules.orbital-strike.procedure; page: 11 -->
|
||||
Чтобы выполнить орбитальный удар, игрок выбирает вражеский мир в активной системе. Затем он выбирает одну пустоту в активной системе, соседнюю этому миру. Он бросает количество кубиков, равное общей боевой мощи всех его кораблей в этой пустоте.
|
||||
|
||||
<!-- block: rules.orbital-strike.damage; page: 11 -->
|
||||
Вражеские отряды в выбранном мире получают урон, равный количеству выпавших значков атаки ([куб: атака]). Боевая мощь и получение урона подробно объясняются позже.
|
||||
|
||||
<!-- block: rules.orbital-strike.bastion; page: 11 -->
|
||||
Игрок не может выполнить орбитальный удар по миру, содержащему бастион. Бастионы описаны позже.
|
||||
|
||||
<!-- block: rules.battle; page: 12 -->
|
||||
## Битва
|
||||
|
||||
После того, как игрок перемещает отряды с помощью Приказа Продвижения, он должен разыграть битву, если у него есть отряды в области, содержащей как дружественные, так и вражеские отряды или постройки (т. е. оспариваемой области). Во время битвы игрок, выполняющий Приказ Продвижения, является **атакующим**, а другой игрок - **защищающимся**.
|
||||
|
||||
<!-- block: rules.battle.overview; page: 12 -->
|
||||
В битвах в *Forbidden Stars* используется комбинация кубов и карт. Игроки начинают битву, бросая кубы в зависимости от боевой мощи своих отрядов. Затем они разыгрывают боевые карты, чтобы активировать специальные способности и получить боевые знаки, кубы и боевые жетоны. Игрок с наибольшим значением боевого духа ([мораль]) в конце битвы становится победителем, а его противник должен отступить.
|
||||
|
||||
Битва состоит из следующих шагов:
|
||||
|
||||
<!-- block: rules.battle.preparation; page: 12 -->
|
||||
### 1. Подготовка
|
||||
|
||||
Игроки готовятся к битве, выполняя следующие подшаги:
|
||||
|
||||
<!-- block: rules.battle.preparation.roll-dice; page: 12 -->
|
||||
#### а. Бросьте Кубы
|
||||
|
||||
И нападающий, и защищающийся одновременно бросают количество кубов, равное суммарной **боевой мощи** всех его недеморализованных отрядов в бою. Боевая мощь отрядов игрока указана на его листе фракции. Например, если игрок за Ультрамаринов имеет в бою два Ленд Рейдера, он бросает шесть кубов.
|
||||
|
||||
<!-- block: rules.battle.preparation.roll-dice.caption; page: 12; kind: caption -->
|
||||
Боевая Мощь Отряда На Листе Фракции
|
||||
|
||||
<!-- block: rules.battle.preparation.roll-dice.group; page: 12 -->
|
||||
Каждый игрок распределяет свои брошенные кубы по группам с одинаковыми значками, чтобы и он, и его противник могли легко оценить результаты.
|
||||
|
||||
<!-- block: rules.battle.preparation.draw-cards; page: 12 -->
|
||||
#### б. Возьмите Боевые Карты
|
||||
|
||||
Каждый игрок берет пять боевых карт с верха своей боевой колоды.
|
||||
|
||||
<!-- block: rules.battle.preparation.reinforcements; page: 12 -->
|
||||
#### в. Шаг Вызова Подкреплений
|
||||
|
||||
Сначала у атакующего, а затем у защищающегося есть возможность разместить несколько своих жетонов подкреплений рядом со своими отрядами в оспариваемой области. Количество жетонов подкреплений, которое может разместить игрок, равно количеству его отрядов в битве (см. «Ценные ресурсы» на стр. 9).
|
||||
|
||||
<!-- block: rules.battle.round; page: 12 -->
|
||||
### 2. Раунд битвы
|
||||
|
||||
Игроки играют до трех раундов битвы, разыгрывая боевые карты со своих рук. Во время **каждого раунда битвы** игроки выполняют следующие подшаги:
|
||||
|
||||
<!-- block: rules.battle.round.choose-card; page: 12 -->
|
||||
#### а. Выберите Боевую Карту
|
||||
|
||||
Атакующий и защищающийся одновременно выбирают по одной боевой карте с руки и кладут выбранную карту лицевой стороной вниз в игровую зону.
|
||||
|
||||
<!-- block: rules.battle.round.resolve-card; page: 12 -->
|
||||
#### б. Разыграйте Боевую Карту
|
||||
|
||||
Атакующий раскрывает и разыгрывает свою боевую карту. Затем защитник раскрывает и разыгрывает свою боевую карту, как описано на стр. 13.
|
||||
|
||||
<!-- block: rules.battle.round.damage; page: 12 -->
|
||||
#### в. Шаг Нанесения Повреждений
|
||||
|
||||
Начиная с атакующего, оба игрока должны получить урон, равный общему количеству значков атаки его противника ([атака]) с кубов, боевых жетонов и открытых боевых карт. Каждый значок защиты ([защита]), который есть у игрока (с кубов, боевых жетонов и его открытых боевых карт), уменьшает количество получаемого урона на один. Правила получения урона объясняются на стр. 13.
|
||||
|
||||
<!-- block: rules.battle.round.damage.caption; page: 12; kind: caption -->
|
||||
Игрок Ультрамаринов в общей сложности имеет три знака атаки ([атака]) и четыре защиты ([защита]).
|
||||
|
||||
<!-- block: rules.battle.round.damage.sole-survivor; page: 12 -->
|
||||
После того, как оба игрока получили урон, если только у одного игрока остались отряды и/или бастионы в области, он побеждает в битве и переходит к подэтапу «Захватите постройки», как описано ниже.
|
||||
|
||||
<!-- block: rules.battle.resolution; page: 12 -->
|
||||
### 3. Разрешение
|
||||
|
||||
После завершения третьего раунда битвы игроки заканчивают битву, выполнив следующие подшаги:
|
||||
|
||||
<!-- block: rules.battle.resolution.determine-winner; page: 12 -->
|
||||
#### а. Определите Победителя
|
||||
|
||||
Игрок с наибольшим суммарным значением боевого духа побеждает в бою (защитник побеждает при ничье), а вражеские отряды должны отступить (объясняется позже).
|
||||
|
||||
<!-- block: rules.battle.resolution.determine-winner.total-morale; page: 12 -->
|
||||
Игрок определяет свой суммарный боевой дух, складывая количество значков своего боевого духа ([мораль]) на своих кубах и боевых картах, лежащих лицом вверх, а также значения боевого духа и на листе своей фракции, которые соответствуют каждому из его бастионов и недеморализованных отрядов в битве.
|
||||
|
||||
<!-- block: rules.battle.resolution.determine-winner.caption; page: 12; kind: caption -->
|
||||
Боевой Дух Отряда На Листе Фракции
|
||||
|
||||
<!-- block: rules.battle.resolution.capture-structures; page: 12 -->
|
||||
#### б. Захватите Постройки
|
||||
|
||||
Если атакующий побеждает в битве, он получает контроль над всеми постройками в мире, убирая маркеры контроля построек противника и заменяя их своими собственными.
|
||||
|
||||
<!-- block: rules.battle.resolution.cleanup; page: 12 -->
|
||||
#### в. Очистка
|
||||
|
||||
Каждый игрок сбрасывает все жетоны подкреплений, которые он использовал, в запас и замешивает все свои боевые карты обратно в свою боевую колоду.
|
||||
|
||||
<!-- block: rules.battle.resolving-cards; page: 13 -->
|
||||
### Розыгрыш Боевых Карт
|
||||
|
||||
Все боевые карты имеют одну или две ячейки боевых способностей. Разыгрывая боевую карту, игрок сначала применяет все общие способности (зеленая ячейка), а затем применяет все способности отрядов (коричневая ячейка). Он применяет каждую способность в том порядке, в котором она указана, сверху вниз.
|
||||
|
||||
<!-- block: rules.battle.resolving-cards.unit-abilities; page: 13 -->
|
||||
Способностям отрядов всегда предшествует хотя бы одно имя отряда. Чтобы использовать способность отрядов, игрок должен иметь хотя бы один из перечисленных отрядов в бою, и этот отряд должен быть недеморализованным.
|
||||
|
||||
<!-- block: rules.battle.resolving-cards.caption; page: 13; kind: caption -->
|
||||
*Схема боевой карты:* Общая Способность — «Получите 2 [жетон: защита].»; Способность Отрядов — «Бастион/Космодесантник/Ударный Крейсер: Переверните до 2 ваших кубов на [куб: защита].»
|
||||
|
||||
Чтобы воспользоваться способностью отрядов, у игрока должен быть хотя бы один Бастион или недеморализованный Космодесантник или недеморализованный Ударный Крейсер в битве.
|
||||
|
||||
<!-- block: rules.battle.card-icons; page: 13 -->
|
||||
### Значки Боевых Карт
|
||||
|
||||
Большинство боевых карт содержат один или несколько боевых значков ([атака], [защита], [мораль]) в левой части карты. Эти значки действуют так же, как значки на кубах, и сохраняются до конца битвы.
|
||||
|
||||
<!-- block: rules.battle.card-icons.caption; page: 13; kind: caption -->
|
||||
Значки на Боевой Карте («Не показывать страха»)
|
||||
|
||||
<!-- block: rules.battle.dice; page: 13 -->
|
||||
### Боевые Кубы
|
||||
|
||||
Когда игрок получает [куб: атака], [куб: защита] или [куб: мораль] от способности, он берет один кубик из запаса неиспользованных кубов и кладет его рядом с другими кубами указанным значком вверх. Когда игрок теряет куб, он берет один из своих кубов с соответствующим значком и возвращает его в запас.
|
||||
|
||||
<!-- block: rules.battle.dice.roll-extra; page: 13 -->
|
||||
Если игровой эффект позволяет игроку получить [куб], он берет куб из запаса, бросает его и кладет его вместе с другими своими кубами.
|
||||
|
||||
<!-- block: rules.battle.dice.limit; page: 13 -->
|
||||
**Важно:** Атакующий и защищающийся ограничены **восьмью кубами** во время битвы.
|
||||
|
||||
<!-- block: rules.battle.tokens; page: 13 -->
|
||||
### Боевые Жетоны
|
||||
|
||||
Некоторые способности боевых карт предоставляют игрокам боевые жетоны, представленные значками боевых жетонов ([жетон: атака] и [жетон: защита]). Когда игрок получает [жетон: атака] или [жетон: защита], он берет боевой жетон из резерва и кладет его соответствующей стороной вверх рядом со своими кубами. При расчете атаки или защиты во время битвы боевые жетоны игрока добавляются к его окончательному значению. Все боевые жетоны являются **временными** и возвращаются в запас в конце раунда битвы, в котором они были получены.
|
||||
|
||||
<!-- block: rules.battle.tokens.caption; page: 13; kind: caption -->
|
||||
Жетоны Сражения
|
||||
|
||||
<!-- block: rules.battle.taking-damage; page: 13 -->
|
||||
### Получение Урона
|
||||
|
||||
Когда игрок получает урон, он обязан выбрать один дружественный отряд в оспариваемой области, который получит урон. Этот отряд должен быть недеморализованным, кроме ситуаций, когда все отряды деморализованы. Если урон равен **показателю здоровья** выбранного отряда или превышает его, этот отряд **уничтожается** и удаляется с игрового поля.
|
||||
|
||||
<!-- block: rules.battle.taking-damage.caption; page: 13; kind: caption -->
|
||||
Показатель Здоровья Отряда на Листе Фракции
|
||||
|
||||
<!-- block: rules.battle.taking-damage.excess; page: 13 -->
|
||||
Если урон ниже показателя здоровья выбранного отряда, этот отряд становится деморализованным. Если урон **превышает** показатель здоровья выбранного отряда, любой урон, превышающий показатель здоровья отряда, должен быть нанесен другому дружественному отряду. Игрок повторяет этот процесс до тех пор, пока либо не останется урона, либо не останется больше отрядов.
|
||||
|
||||
<!-- block: rules.battle.taking-damage.orbital-strike; page: 13 -->
|
||||
**Важно:** При получении урона во время орбитального удара любые неуничтоженные отряды **не становятся деморализованными**.
|
||||
|
||||
<!-- block: rules.battle.taking-damage.example; page: 13; kind: example -->
|
||||
*Пример:* У игрока за Ультрамаринов в битве два Разведчика. В течение первого раунда битвы он должен получить три урона. Он выбирает одного из своих Разведчиков, который первым получит урон. Он сравнивает свои два здоровья с тремя единицами урона; поскольку показатель здоровья Разведчика равно или меньше урона, он уничтожается. Затем игрок назначает один оставшийся урон другому своему Разведчику. Этот отряд не уничтожается, а становится деморализованным, а урон отбрасывается, потому что показатель здоровья отряда превышает урон.
|
||||
|
||||
<!-- block: rules.battle.routed-units; page: 13 -->
|
||||
### Деморализованные Отряды
|
||||
|
||||
Отряды могут стать **деморализованными** из-за способностей боевых карт, отступления или получения урона, который меньше их показателей здоровья. При деморализации отряда пластиковая модель кладется на бок. Если отряд представлен жетоном подкрепления, этот жетон переворачивается на деморализованную сторону.
|
||||
|
||||
<!-- block: rules.battle.routed-units.effects; page: 13 -->
|
||||
Деморализованные отряды не добавляют значение своего боевого духа ([мораль]) при определении победителя боя и не могут удовлетворять требованиям отрядов на боевой карте. Кроме того, игрок **не может назначать урон** деморализованному отряду, если у него есть один или несколько недеморализованных отрядов или бастионов в области. Во время Фазы Обновления все деморализованные отряды восстанавливаются: пластиковые модели поднимаются вертикально.
|
||||
|
||||
<!-- block: rules.battle.bastions; page: 13 -->
|
||||
### Бастионы
|
||||
|
||||
Бастионы не являются отрядами, но они добавляют кубы и свои значения боевого духа во время битвы. Кроме того, бастионы могут быть выбраны для получения урона. Если бастион получает урон, равный показателю его здоровья, он удаляется с игрового поля и возвращается в запас. Бастионы не могут стать деморализованными.
|
||||
|
||||
<!-- block: rules.battle.example-part-1; page: 14; kind: example -->
|
||||
### Пример битвы часть I
|
||||
|
||||
1. Игрок за Ультрамаринов (синий) выполняет Приказ Продвижения. После перемещения своих отрядов у него есть два отряда в области с двумя эльдарскими отрядами. Игроки за Ультрамаринов и Эльдар теперь должны разрешить битву.
|
||||
2. Игроки сверяются с листами своих фракций, чтобы определить, сколько кубов дают их отряды. Оба игрока одновременно бросают свои кубы.
|
||||
|
||||
Затем каждый игрок берет пять карт из своей боевой колоды и держит эти карты скрытыми друг от друга.
|
||||
|
||||
**Первый раунд битвы:** Каждый игрок выбирает боевую карту из своей руки и кладет ее лицом вниз перед собой. Затем игроки выполняют следующие шаги:
|
||||
|
||||
3. Игрок за Ультрамаринов является атакующим, поэтому он первым раскрывает и разыгрывает свою карту. Сначала он применяет общую способность в зеленой ячейке, что дает ему два боевых жетона защиты.
|
||||
4. Затем он применяет способность отрядов в коричневой ячейке. У него есть отряд Космодесантников, необходимый для применения этой способности, поэтому он решает перевернуть один из своих [куб: атака] кубов в [куб: защита] куб.
|
||||
5. Игрок за Эльдар раскрывает и разыгрывает свою карту. Сначала он применяет общую способность в зеленой ячейке, что дает ему один куб. Он бросает куб и кладет его рядом с остальными кубами.
|
||||
6. Затем игрок за Эльдар применяет способность отрядов в коричневой ячейке. У него есть Аспектный Воин, и он решает потратить [куб: мораль] куб, чтобы вынудить своего противника выбрать и деморализовать один свой отряд. Игрок за Ультрамаринов решает деморализовать одного из своих Разведчиков.
|
||||
|
||||
В качестве последнего шага этого раунда битвы игроки должны получить урон. Каждый игрок подсчитывает количество имеющихся у него боевых значков на своих кубах, своих боевых картах и своих боевых жетонах.
|
||||
|
||||
Ни один из игроков не имеет больше значков атаки ([атака]), чем значков защиты его противника ([защита]), поэтому ни один из игроков не получает урон.
|
||||
|
||||
Игроки сбрасывают все боевые жетоны, затем проводят следующий раунд битвы(см. «Пример битвы, часть II» на стр. 15).
|
||||
|
||||
<!-- block: rules.battle.example-part-2; page: 15; kind: example -->
|
||||
### Пример битвы часть II
|
||||
|
||||
**Второй Раунд Битвы:** Каждый игрок выбирает боевую карту из своей руки и кладет ее лицом вниз перед собой. Затем игроки выполняют следующие шаги:
|
||||
|
||||
7. Игрок за Ультрамаринов раскрывает и разыгрывает свою карту. Сначала он применяет общую способность в зеленой ячейке, что дает ему один куб. Он бросает куб и кладет его рядом с остальными своими кубами.
|
||||
8. Затем он применяет способность отрядов в коричневой ячейке. У него есть отряд Космодесантников, необходимый для применения этой способности, поэтому он выбирает восстановить своего Разведчика.
|
||||
9. Затем игрок за Эльдар раскрывает и разыгрывает свою карту. Сначала он применяет общую способность в зеленой ячейке, что дает ему два боевых жетона атаки.
|
||||
10. Теперь игрок за Эльдар может применить способность в коричневой ячейке, но он решает это не делать.
|
||||
11. Каждый игрок подсчитывает количество значков атаки ([атака]) на его кубах, боевых картах и боевых жетонах.
|
||||
|
||||
Игрок за Эльдар — единственный игрок, у которого больше значков атаки ([атака]), чем значков защиты ([защита]) его противника. У него шесть значков атаки, а у его противника — два значка защиты, поэтому его противник получает четыре урона.
|
||||
12. Игрок за Ультрамаринов решает сначала распределить урон на Разведчика. У него всего два здоровья и он уничтожен. Остальные два урона распределяются на другой его отряд, у которого три здоровья. Этого урона недостаточно для его уничтожения, поэтому отряд становится деморализованным.
|
||||
|
||||
Игроки сбрасывают все боевые жетоны. Поскольку у обоих игроков остались отряды, они разыгрывают еще один раунд битвы. Если у обоих игроков есть хотя бы по одному выжившему отряду, игрок с наибольшим суммарным значением боевого духа в конце этого раунда битвы побеждает в битве, а его противник должен отступить всеми своими отрядами.
|
||||
|
||||
<!-- block: rules.battle.retreat; page: 15 -->
|
||||
### Отступление
|
||||
|
||||
Отряды должны отступить, когда они проигрывают битву. Кроме того, некоторые способности боевых карт могут заставить отряды отступить. После отступления отряды становятся деморализованными.
|
||||
Атакующие и защищающиеся отряды следуют разным правилам отступления, а именно:
|
||||
|
||||
<!-- block: rules.battle.retreat.attacker; page: 15 -->
|
||||
#### Атакующий отступает
|
||||
|
||||
Когда атакующий отступает, он должен переместить все свои оставшиеся отряды из оспариваемой области в другую область — корабли должны отступить в пустоту, а наземные отряды должны отступить в мир по легальному пути перемещения.
|
||||
|
||||
<!-- block: rules.battle.retreat.attacker.same-area; page: 15 -->
|
||||
Все отступающие отряды обязаны переместиться в одну и ту же область, и это должна быть область, из которой изначально переместился хотя бы один из отрядов в битве.
|
||||
|
||||
<!-- block: rules.battle.retreat.defender; page: 15 -->
|
||||
#### Защищающийся отступает
|
||||
|
||||
Когда защищающийся отступает, он должен переместить все свои оставшиеся отряды из оспариваемой области в дружественную или неконтролируемую область в активной системе или соседней системе — корабли должны отступить в пустоту, а наземные отряды должны отступить в мир по легальному пути перемещения. Все отступающие отряды должны переместиться в одну и ту же область. Если защищающийся может отступить как в дружественную, так и в неконтролируемую область, он обязан отступить в дружественную область.
|
||||
|
||||
<!-- block: rules.board-building.stop; page: 16 -->
|
||||
## Стоп!
|
||||
|
||||
Теперь вы знаете все правила, необходимые для вашей первой игры. Поиграв со схемой *«Расстановка к Первой Игре»*, вы готовы изучить дополнительные правила на этой странице.
|
||||
|
||||
<!-- block: rules.board-building; page: 16 -->
|
||||
## Создание игрового поля
|
||||
|
||||
После игры с использованием игрового поля, представленного на схеме *«Расстановка к Первой Игре»*, игроки готовы играть, создав уникальное игровое поле во время подготовки к игре самостоятельно.
|
||||
|
||||
<!-- block: rules.board-building.steps; page: 16 -->
|
||||
Сборка игрового поля — это стратегическое упражнение, в котором игроки выбирают, где размещать тайлы системы и где размещать свои первоначальные силы. На шаге подготовки к игре «Сборка игрового поля» игроки выполняют следующие действия:
|
||||
|
||||
<!-- block: rules.board-building.distribute-tiles; page: 16 -->
|
||||
### 1. Распределите Тайлы Систем
|
||||
|
||||
Каждый игрок получает тайл системы, на котором есть значок его фракции. Затем первый игрок берет все тайлы систем, на которых нет значков фракций, перемешивает их под столом и раздает по два каждому игроку.
|
||||
|
||||
<!-- block: rules.board-building.distribute-objectives; page: 16 -->
|
||||
### 2. Распределите маркеры целей
|
||||
|
||||
Каждый игрок отдает по два своих маркера целей каждому другому игроку и возвращает оставшиеся маркеры в коробку с игрой.
|
||||
|
||||
<!-- block: rules.board-building.assemble; page: 16 -->
|
||||
### 3. Соберите игровое поле
|
||||
|
||||
Начиная с первого игрока и далее по **часовой стрелке**, каждый игрок размещает один тайл системы в игровой зоне следующим образом:
|
||||
|
||||
<!-- block: rules.board-building.assemble.place-tile; page: 16 -->
|
||||
#### а. Разместите Тайл Системы
|
||||
|
||||
Игрок выбирает один из своих тайлов системы и кладет его рядом хотя бы с одним другим тайлом системы (см. «Пример размещения тайла» справа).
|
||||
|
||||
<!-- block: rules.board-building.assemble.place-forces; page: 16 -->
|
||||
#### б. Разместите Отряды и Постройки
|
||||
|
||||
Игрок может разместить любое количество компонентов со своего листа фракции (в ячейке «Стартовые силы») на тайл, который он только что разместил.
|
||||
|
||||
<!-- block: rules.board-building.assemble.place-objectives; page: 16 -->
|
||||
#### в. Разместите Маркеры Целей
|
||||
|
||||
Игрок **обязан** разместить один из маркеров целей противника **на каждую ячейку для маркеров целей** на только что размещенном тайле системы, соблюдая следующие ограничения:
|
||||
|
||||
- Игрок не может размещать два маркера одной и той же фракции на одном тайле системы.
|
||||
- Игрок не может разместить маркер цели, принадлежащий одной фракции, если у него больше маркеров цели, принадлежащих другой фракции.
|
||||
- После того, как он разместил все тайлы, полученные на шаге 2, он больше не размещает маркеров целей.
|
||||
|
||||
<!-- block: rules.board-building.assemble.objective-zone-caption; page: 16; kind: caption -->
|
||||
Зона Размещения Маркера Целей
|
||||
|
||||
<!-- block: rules.board-building.assemble.repeat; page: 16 -->
|
||||
Игроки повторяют этот процесс до тех пор, пока каждый игрок не поместит все свои тайлы системы и все свои компоненты, перечисленные в поле «Стартовые силы» на листе своей фракции.
|
||||
|
||||
<!-- block: rules.board-building.warp-storms; page: 16 -->
|
||||
### 4. Разместите Варп-Штормы
|
||||
|
||||
Начиная с игрока, **разместившего последнюю систему**, и далее **против часовой стрелки**, каждый игрок размещает один варп-шторм вдоль любого ребра любого тайла системы; это может быть внешнее ребро - край игрового поля.
|
||||
|
||||
<!-- block: rules.board-building.tile-placement; page: 16 -->
|
||||
### Размещение Тайлов Системы
|
||||
|
||||
При размещении тайла системы игрок может разместить либо тайл своей фракции, либо любой другой тайл. Он может положить тайл любой стороной вверх и в любой ориентации; однако после того, как первый игрок поместит свой первый тайл системы, все остальные тайлы должны быть размещены **по соседству** с другим тайлом.
|
||||
|
||||
<!-- block: rules.board-building.tile-placement.max-size; page: 16 -->
|
||||
Игрок **не может размещать тайл** таким образом, чтобы он нарушал максимальный размер игрового поля. Этот размер варьируется в зависимости от количества игроков:
|
||||
|
||||
- **Два Игрока:** Три тайла на два тайла
|
||||
- **Три Игрока:** Три тайла на три тайла
|
||||
- **Четыре Игрока:** Три тайла на четыре тайла
|
||||
|
||||
<!-- block: rules.board-building.tile-placement.orientation; page: 16 -->
|
||||
Ориентация игрового поля не определяется до тех пор, пока максимальное количество тайлов не будет размещено в одном ряду или столбце. Например, игра для четырех игроков может быть либо шириной в четыре тайла, либо высотой в четыре тайла. Это определяется, как только четвертый тайл размещается в строке или столбце.
|
||||
|
||||
<!-- block: rules.board-building.tile-placement.example; page: 16; kind: example -->
|
||||
#### Пример размещения тайла
|
||||
|
||||
Настала очередь игрока за Эльдар размещать тайл во время игры вчетвером. Он решает разместить тайл своей фракции.
|
||||
|
||||
Он может разместить тайл своей фракции в любом из восьми возможных мест, обведенных зеленым.
|
||||
Он не может разместить свой тайл ни на одной из трех ячеек на концах, потому что это нарушит максимальный размер игрового поля, отведенный для игры вчетвером, который составляет три тайла на четыре тайла.
|
||||
@@ -1,15 +1,33 @@
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { ReactNode } from "react";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { queryClient } from "./app/queryClient";
|
||||
import { router } from "./app/router";
|
||||
import { Spinner } from "./components/Spinner";
|
||||
import { ToastProvider } from "./context/ToastContext";
|
||||
import { setAppTzOffsetHours } from "./domain/format";
|
||||
import { useAuthConfig } from "./hooks/auth";
|
||||
|
||||
/**
|
||||
* Пояс приложения (APP_TZ_OFFSET_HOURS) приходит с сервера и нужен до первой отрисовки:
|
||||
* функции format.ts читают его синхронно (#68). Пока конфиг грузится — спиннер; если он
|
||||
* недоступен (нет сети), рендерим с запасным поясом, чтобы не запереть приложение.
|
||||
*/
|
||||
function AppTimeZone({ children }: { children: ReactNode }) {
|
||||
const { data, isPending } = useAuthConfig();
|
||||
if (data) setAppTzOffsetHours(data.tz_offset_hours);
|
||||
if (isPending) return <Spinner />;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<AppTimeZone>
|
||||
<RouterProvider router={router} />
|
||||
</AppTimeZone>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
Vendored
+2
@@ -1610,6 +1610,8 @@ export interface components {
|
||||
methods: string[];
|
||||
/** Telegram Bot Username */
|
||||
telegram_bot_username?: string | null;
|
||||
/** Tz Offset Hours */
|
||||
tz_offset_hours: number;
|
||||
};
|
||||
/** Body_add_attachment_api_matches__match_id__attachments_post */
|
||||
Body_add_attachment_api_matches__match_id__attachments_post: {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
// Отображаем время в фиксированном поясе +3 (МСК) независимо от пояса браузера.
|
||||
const APP_TZ_OFFSET_MIN = 3 * 60;
|
||||
// Время показываем в поясе приложения — одном для всех, независимо от пояса браузера.
|
||||
// Смещение задаёт APP_TZ_OFFSET_HOURS на бэкенде (в нём же считается «дата игры»);
|
||||
// App.tsx получает его из /api/auth/config до первой отрисовки страниц (#68).
|
||||
// 3 ч (МСК) — запасное значение, если конфиг недоступен.
|
||||
let appTzOffsetMin = 3 * 60;
|
||||
|
||||
export function setAppTzOffsetHours(hours: number): void {
|
||||
appTzOffsetMin = hours * 60;
|
||||
}
|
||||
|
||||
const p2 = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
// iso — корректный момент (бэкенд отдаёт UTC со смещением). Сдвигаем в +3
|
||||
// и форматируем по UTC-частям, чтобы получить «настенное» время МСК.
|
||||
// iso — корректный момент (бэкенд отдаёт UTC со смещением). Сдвигаем в пояс приложения
|
||||
// и форматируем по UTC-частям, чтобы получить «настенное» время этого пояса.
|
||||
function shifted(iso: string): Date {
|
||||
return new Date(new Date(iso).getTime() + APP_TZ_OFFSET_MIN * 60_000);
|
||||
return new Date(new Date(iso).getTime() + appTzOffsetMin * 60_000);
|
||||
}
|
||||
|
||||
// Склонение существительного при числе: plural(3, "игрок", "игрока", "игроков").
|
||||
@@ -50,18 +57,19 @@ export function formatDate(iso: string | null | undefined): string {
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : iso;
|
||||
}
|
||||
|
||||
// Значение <input type="datetime-local"> — «настенное» время в том же поясе +3, что и всё
|
||||
// отображение: админ вводит время показа объявления по МСК, где бы ни был его браузер.
|
||||
// Значение <input type="datetime-local"> — «настенное» время в том же поясе приложения, что
|
||||
// и всё отображение: админ вводит время показа объявления в нём, где бы ни был браузер.
|
||||
export function toAppLocalInput(iso: string): string {
|
||||
const d = shifted(iso);
|
||||
return `${d.getUTCFullYear()}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())}T${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}`;
|
||||
}
|
||||
|
||||
// Обратно: «настенное» время +3 из datetime-local → момент ISO (UTC). null — поле пустое.
|
||||
// Обратно: «настенное» время пояса приложения из datetime-local → момент ISO (UTC).
|
||||
// null — поле пустое.
|
||||
export function fromAppLocalInput(value: string): string | null {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value);
|
||||
if (!m) return null;
|
||||
const ms = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - APP_TZ_OFFSET_MIN * 60_000;
|
||||
const ms = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - appTzOffsetMin * 60_000;
|
||||
return new Date(ms).toISOString();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[33mcommit 65bebf8b85fcbf921cbbc4f2599953a9fa3ca9c6[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mdev[m[33m)[m
|
||||
Author: NotBigGhost <ivan@arseniev.info>
|
||||
Date: Wed Jun 17 04:45:33 2026 +0300
|
||||
|
||||
Добавление ssh-ключа к контейнеру tunnel
|
||||
|
||||
[33mcommit 7e8b748caf5d9f4ab3d2d7f76d6a4b850b9b1366[m
|
||||
Author: NotBigGhost <ivan@arseniev.info>
|
||||
Date: Wed Jun 17 04:27:11 2026 +0300
|
||||
|
||||
Правки локального размещения дева, перенос тунеля в контейнер
|
||||
|
||||
[33mcommit b04fbb2e171a7fe3376f3633dfee9e29e4e4bb90[m
|
||||
Author: NotBigGhost <ivan@arseniev.info>
|
||||
Date: Tue Jun 16 19:15:54 2026 +0300
|
||||
|
||||
Завершена настройка домена для дева, теста и прода. Соединение через ssh-туннель
|
||||
|
||||
[33mcommit 56b5d09a4dc5e6f5d06e32c2635c84a66597e71f[m[33m ([m[1;31morigin/main[m[33m, [m[1;32mmain[m[33m)[m
|
||||
Author: NotBigGhost <ivan@arseniev.info>
|
||||
Date: Tue Jun 16 17:41:06 2026 +0300
|
||||
|
||||
v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика
|
||||
|
||||
[33mcommit 6ab74f01aaf1ac042a269c7660b3e564b93350c7[m
|
||||
Author: NotBigGhost <ivan@arseniev.info>
|
||||
Date: Tue Jun 16 16:53:41 2026 +0300
|
||||
|
||||
first commit
|
||||
@@ -135,7 +135,7 @@ switch ($appEnv) {
|
||||
}
|
||||
"production" {
|
||||
Write-Host "production is not started by the launcher - prod is separate." -ForegroundColor Yellow
|
||||
Write-Host "Deploy on Pi (from main branch): docker compose up -d --build"
|
||||
Write-Host "Deploy: PC (main branch) scripts\build-push.ps1, then on Pi: docker compose up -d"
|
||||
exit 1
|
||||
}
|
||||
default {
|
||||
|
||||
@@ -85,7 +85,7 @@ case "$app_env" in
|
||||
;;
|
||||
production)
|
||||
echo "production лаунчером не запускается — прод обособлен."
|
||||
echo "Деплой на Pi (из ветки main): docker compose up -d --build"
|
||||
echo "Деплой: на ПК (ветка main) scripts/build-push.sh, затем на Pi: docker compose up -d"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Выгрузка ПРОДА: в целевую папку попадают только файлы, нужные для запуска
|
||||
# прод-контейнера (без тестов, dev-входа и dev-лаунчера).
|
||||
#
|
||||
# Использование: scripts/export-prod.sh <целевая-папка> [git-ref]
|
||||
# git-ref по умолчанию HEAD; для прод-ветки: scripts/export-prod.sh /srv/fs prod
|
||||
set -euo pipefail
|
||||
|
||||
DEST="${1:?Укажите целевую папку: scripts/export-prod.sh <dir> [ref]}"
|
||||
REF="${2:-HEAD}"
|
||||
|
||||
mkdir -p "$DEST"
|
||||
# git archive уважает export-ignore из .gitattributes (тесты, dev-вход и т.п. отсеяны)
|
||||
git archive --format=tar "$REF" | tar -x -C "$DEST"
|
||||
|
||||
# dev-специфика в проде не нужна (лаунчер)
|
||||
rm -f "$DEST/run.ps1" "$DEST/run.sh"
|
||||
|
||||
echo "[export-prod] Прод выгружен в: $DEST"
|
||||
echo " дальше: cp .env.example .env && docker compose up -d --build"
|
||||
Reference in New Issue
Block a user