13 Commits
Author SHA1 Message Date
Nikita 25c29e0d43 final final 2026-10-26 23:51:36 +03:00
Nikita aaf4ace87c final 2026-10-26 23:51:15 +03:00
Nikita 77e62189cf feat(v4): overtime, tutorial hour 0, soundtrack mixer, minimalist menu, immersion UI 2026-10-26 22:18:23 +03:00
Nikita 1c35a9e06c feat(v4): UDP third-try/no-slot, file lifespan and random icons, shop icons, grid/menu fixes 2026-10-26 21:25:41 +03:00
Nikita f0ba5fd2b9 feat(v4): per-hour request mix, UDP, quality gate, folder pricing, briefing, attack alert, UI polish 2026-10-26 20:39:45 +03:00
Nikita a2e7456362 chore(dev): seed demo file for visual QA 2026-10-26 19:11:52 +03:00
Nikita 417a352823 fix(v4): clear stale cells and dedupe file_ids on move 2026-10-26 19:05:42 +03:00
Nikita 6cb2639055 fix(v4): restore file shapes/visibility, detach refresh, reject purge 2026-10-26 18:57:34 +03:00
Nikita 44510e2926 fix(v4): folder focus after drop, reject cleanup, request-count heat, motion polish 2026-10-26 17:33:24 +03:00
Nikita ed1ecb7d1c feat(v4): persistent files, rate pacing, heat UI and motion polish 2026-10-26 16:14:41 +03:00
threenebula23 2191545629 feat(v3): TCP connection, named-file requests, 3-tier send games, rebalanced shop, Golos UI and pixel icons
CI / tests (push) Successful in 37s
2026-10-26 02:04:47 +03:00
threenebula23 9cb983a9b5 merge: Nikita v2 baseline (shop, polyomino grid, sorting table, 11 minigames) 2026-10-25 23:14:40 +03:00
Nikita 39846421bc feat(game): PEAK HOUR v2 - shop, polyomino grid, sorting table, 11 minigames
- 8 lots / 3 offers / rerolls / locks; start 50 USD, SLA 60/60, 1 slot
- 5x4..8x6 polyomino grid, reservations, 4x6 sorting table, save v2 + v1 migration
- 7 send minigames + 4 attack minigames without timers, dense B4 selection
- attacks as minigames, shield, SLA regen/defeat, 20 attacks per run
- balance simulator 240 seeds: all hours within +/-20%, PASS
- UI v2, old attack cards/connection strip removed
- tests: 26/26 PASS, docs v2.0
2026-10-25 21:38:18 +03:00
271 changed files with 46073 additions and 9120 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@
# Сборки и артефакты экспорта
build/
export/
dist/
# Временные файлы
*.tmp
Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+197 -45
View File
@@ -1,4 +1,4 @@
# AI_REPORT — PEAK HOUR 97
# AI_REPORT — PEAK HOUR 97 (v3.0)
## 1. Инструменты и модели
@@ -13,66 +13,218 @@ test-writer, explorer, feis-regulator, oracle):
| **MiMo-V2.6-Flash** | аудио: SFX, ambient, фон |
| дефолтные модели субагентов | поиск, тесты, ревью |
MR-ревью — агент **oracle** (`opencode-go/deepseek-v4.1-flash`): dry-run
MR-ревью — агент **oracle** (opencode-go/deepseek-v4.1-flash): dry-run
74–90 с, комментарий в MR, без мержа.
## 2. Где применялся ИИ
## 2. Что изменилось в v3.0
- **Код:** `GameController`, `HourDirector`, `AttackDirector`, `Shop`,
`SaveManager`, UI, мини-игры.
- **UI v2:** Kaleidos, маскот **БАЙТ**, туториал.
- **Ambient:** 4 WAV (`audio/amb`, PSU/fan/room/seek), генератор
`generate_ambient.py`, autoload после первого жеста, mute/пауза/громкость.
- **VHS:** процедурный `vhs_overlay.gd` (`RefCounted`) в `MainMenu._draw`;
скан-линии/зерно/виньетка, без shader/screen texture, ввод не блокирует,
текст резкий; только меню, чекбокс в настройках, HEADLESS без рендера.
- **Баланс/мета:** SLA, 10 апгрейдов, таймеры, канал, мета 3/5/9.
- **Аудит:** calm-фон, Esc итогов, уничтожение, тосты/fullscreen.
- **Тесты/доки/звук:** unit/fuzz/интеграция/полный забег; `docs/`;
18 SFX + `hum.wav` на Python.
- **Запросы:** 1–4 **конкретных именованных файла** вместо анонимных слотов;
имена на карточке 2×2, прикрепление по точному `file_id`, «ПОТЕРЯН» для
пропавшего файла, жёсткая резервация. Новые `request_mix` H1–H9 и
`request_cap`; H1-затравка и анти-голодание.
- **TCP:** возвращена полоска соединения (3 попадания, 440/340/280 px/с,
формула зоны, промах −4 SLA / 8 с, Esc 0 SLA / 4 с). TCP платит ×1.15,
UDP — ×0.85 и передаёт ×0.8.
- **Мини-игры:** 6 отправки + 4 атаки; игра с соединением проводов удалена;
у отправки 3 ступени (старые T2–T4), у атак 4. Выбор — по сумме
коэффициентов и таблице `rank_games` на 27 достижимых сумм.
- **Магазин:** 8 лотов, дерево **8 085 $**, SLA до **220**, ускорение ×0.90,
щит с 70 % награды, папки до 9×6, новая папка 180 $ (в v3.1 — 9 лотов и
8 825 $, см. §10).
- **UI:** Golos Text для тела текста, Press Start 2P только для логотипа и
цифр; шкала 11/13/17/26; пиксельные иконки `art/icons/`; карточка запроса
400×128 с сеткой файлов и прогнозом; пустое состояние отправки; «СЛОТЫ».
- **Туториал:** 5 шагов с событийными триггерами, пульсирующие рамки,
«ДАЛЕЕ», сохранение `tutorial_done`, повтор из главного меню.
- **Документация:** GDD/DECISIONS/README/ITCH_PAGE/UI_REFRESH/ASSETS
переписаны под фактические числа v3; старые решения помечены, а не удалены.
## 3. Приём: «враждебный критик»
## 3. Где применялся ИИ
- **Код:** HourDirector, FileRegistry, Economy, Shop, AttackDirector,
SaveManager, UI, соединение TCP и мини-игры.
- **UI v3:** Golos Text, пиксельные иконки, карточка запроса, туториал с
рамками, VHS главного меню.
- **Ambient:** 4 WAV (audio/amb), генератор generate_ambient.py, autoload
после первого жеста, mute/пауза/громкость.
- **Баланс:** цели дохода, дерево магазина 8 085 $ (в v3.1 — 8 825 $),
симулятор на 240 сидах.
- **Аудит:** B4 (дыры в выборе мини-игры) закрыт ещё в v2; в v3 выбор
перестроен на 27 сумм и 3 ступени. M8 (масштаб денег атак), M16
(soft-lock «нет денег/нет места») сохранены.
- **Тесты/доки/звук:** unit/fuzz/интеграция/полный забег; docs/;
18 SFX + hum.wav на Python.
## 4. Приём: «враждебный критик»
Промпт просил быть враждебным критиком: «найди блокеры, из-за которых
прототип нельзя собрать; не хвали». Нашлось **6 блокеров**. Один из них:
выбор мини-игры по самому сложному типу ломал смешанные запросы. Заменили на
средний коэффициент:
прототип нельзя собрать; не хвали». Главный дефект v2 — **B4**: выбор
мини-игры по `S−1` давал дыры и недостижимые строки. В v3 выбор перестроен:
```
difficulty = sum(coeff(required)) / n
coeff = {text: 1.0, sound: 2.0, photo: 3.0, video: 4.0}
```
ACHIEVABLE_SUMS = [1..22, 24, 25, 26, 28, 32] # 27 сумм для 1–4 файлов
rank = index of S in ACHIEVABLE_SUMS
tier = T1 при rank 0–7, T2 при 8–13, T3 при 14–26
id = RANK_GAMES[rank] # фиксированная таблица
Это изменило §8.4 и §10 GDD и решение D17; пороги `<1.5 / <2.5 / <3.5`
зафиксированы в `data/balance.json` и проверяются в `test_minigame.gd`.
Все 6 игр достижимы в каждой из 3 ступеней; S=32 → video/T3. Дополнительно
закрыты: H1-голодание запросов (анти-голодание + seed-файл), потерянные
required-файлы (подстановка того же типа), неоднозначность TCP-протокола
(блокировка после соединения). M16 (бесплатное уничтожение неприкреплённого
файла) сохранён, но файл, закреплённый за запросом, удалить нельзя.
## 4. Что сделано без ИИ
## 5. Что сделано без ИИ
Идея, логлайн, исходная спецификация, ключевые решения (1997 год, 9 часов,
4 атаки, экономика, тон 90-х) и финальная приёмка — человеком, автором
проекта. ИИ не придумывал игру: он помогал формулировать, писать код,
считать баланс и искать противоречия.
## 5. Ассеты и лицензии
## 6. Ассеты и лицензии
- **Handjet** и **Press Start 2P** — SIL OFL 1.1, тексты в
`art/fonts/OFL-Handjet.txt` и `OFL-PressStart2P.txt`.
- Pixelify Sans в теме не используется (нет глифов `О`/`П`); файл и OFL
оставлены в `art/fonts/`.
- UI, иконки и `icon.svg` — команда проекта, MIT.
- SFX, ambient и `hum.wav` — скрипты проекта, MIT/CC0.
- Код — MIT (`LICENSE`); OFL-шрифты совместимы с MIT при сохранении
лицензий. Подробности — в `ASSETS.md`.
- **Golos Text** — SIL OFL 1.1 (`art/fonts/OFL-GolosText.txt`), основной
шрифт UI с v3.
- **Press Start 2P** — SIL OFL 1.1 (`art/fonts/OFL-PressStart2P.txt`), только
логотип и цифры.
- **Handjet** — SIL OFL 1.1, с v3 в теме не используется и помечен как
legacy.
- Pixelify Sans в теме не используется (нет глифов О/П); файл и OFL
оставлены в art/fonts/.
- Пиксельные иконки `art/icons/*.png` сгенерированы `generate_icons.py`
(Pillow), команда проекта, **MIT**; исходный тайл-лист был недоступен,
поэтому набор перегенерирован с нуля.
- UI, маскот **БАЙТ** и icon.svg — команда проекта, MIT.
- SFX, ambient и hum.wav — скрипты проекта, MIT/CC0.
- Код — MIT (LICENSE); OFL-шрифты совместимы с MIT при сохранении лицензий.
Подробности — в ASSETS.md.
## 6. Проверки и ограничения
## 7. Проверки v3.1
- **22/22 PASS** (21 тест + parse). Core 543, hour 133, shop 116, minigame
51, slots 34, audio 37; fuzz 3249, integration 231, attacks 173,
controller 143.
- `test_full_playthrough` — 294 на сидах `1/42/777`: 3/3 victory, 9/9,
SLA 200; main 350, toast 14, fullscreen 31, ui 135, ambient 74.
- Web-экспорт — exit 0; Chromium **1920×1080**: меню → «Новый забег» →
подготовка, JS-ошибок нет.
- **Симулятор баланса:** `python tests/sim_balance.py`, 240 сидов, 9 часов,
`dt=0.25`, `q=0.85`, TCP. Отчёт `tests/SIM_BALANCE_REPORT.md`: средний итог
**7 205 $** в полосе **6 982–8 500 $**, расход магазина **3 964 $**
(**44,9 %** дерева **8 825 $**), **0 поражений из 240**, SLA в конце H9 —
**64,9**, запросов выполнено — **62,0**. Все 9 часов в ±20 %, exit code **0**,
вердикт `PASS`. H2, H4 и H5 идут близко к −20 % (H2 −16,4 %, H4 −15,3 %,
H5 −15,1 %), H1 — +14,1 % при цели **100 $**.
- **Тесты:** актуальный список — **полный набор + parse-check** в
`scripts/run_all_tests.sh` / `.ps1` (см. также `tests/`; счётчик наборов не
хардкодится). `test_economy` и `test_save` входят в runner; core-наборы
обновлены и зелёные; UI-наборы (`test_main_*`, `ui_smoke`) прогоняются
отдельно.
- **Web-пресет** `export_presets.cfg` существует: `thread_support=false`,
экспорт в `build/web/index.html`, просмотр только по HTTP.
Ограничения: туториал — маскот и подсказки; web-save — IndexedDB; плейтест,
громкость и масштаб — за человеком; remote push ещё не выполнен.
### Финальная проверка v3
> Замер предыдущей волны. После изменений баланса, каталога файлов и UI
> core-наборы перепрогнаны (зелёные), UI-наборы и E2E/веб-смоук требуют
> повторного прогона.
- **Headless:** полный набор + parse-check; список и статус — в
`scripts/run_all_tests.sh` / `.ps1`. Число наборов здесь не фиксируется.
- **Баланс:** симулятор — `PASS`, 240 сидов, все 9 часов в ±20 %, средний
итог **7 205 $**, магазин — **3 964 $ (44,9 %)** дерева **8 825 $**,
поражений **0/240** (`tests/SIM_BALANCE_REPORT.md`).
- **E2E на реальной сцене Main (1920×1080):** замер предыдущей волны,
**190/190**; после изменений темпа и «ЖАРЫ» требует повторного прогона:
меню → подготовка → час, приём пакета, прикрепление конкретного файла,
модалка TCP и промах/кулдаун, мини-игра отправки → завершение (файлы
**вернулись в папку**, деньги начислены), UDP-обход, конец часа при
открытом соединении, H1-голодание, очистка ghost-привязок при save/load,
защита ввода в модалках, шаг туториала «отправка»; повторный «НОВЫЙ ЗАБЕГ»
тоже получает seed-файл в подготовке.
- **Web (Godot 4.7.2, Web/no-threads; Chromium + SwiftShader):** сборка
загружается, WebGL2 OK, 0 ошибок/предупреждений в консоли; меню →
«Новый забег» → «НАЧАТЬ ЧАС» работает; drag пакета в папку и drag нужного
файла в чип запроса работают; модалка соединения открывается (палочка
промахивается сама, если не кликнуть за ~1,1 с, — так задумано).
- **Полоска TCP** — рефлекторная мини-игра: 3 попадания, зона 30 % на H1;
уход палочки за край = автопромах; промах = −4 SLA и кулдаун 8 с
(`Esc` = 4 с, без штрафа).
## 8. Известные отклонения
- `ПРОПУСТИТЬ` даёт `q=0`; `quality = 0.5`, то есть половина
quality-множителя, а не нулевая выплата. Осознанно.
- `speed` выплаты фиксируется на старте отправки, пока таймер был заморожен
мини-игрой; быстрый игрок видит ≈1.0.
- Лимит папок 6, новая папка 180 $.
- Бесплатное уничтожение любого неприкреплённого файла в подготовке
(review M16); файл, закреплённый за запросом, не удаляется.
- Деньги атак масштабируются по часу `max(0.75, hour/3)` (review M8).
- H2 теоретически может выдать T2-запрос из трёх фото (S12); на практике
кривая H2 остаётся почти полностью T1.
- Расход магазина **44,9 %** — ниже проектной полосы 55–65 %; это известное
отклонение, не влияющее на exit code.
## 9. Ограничения
Туториал автоматически запускается только в первом часу первого забега;
повтор — из главного меню. Web-save — IndexedDB; плейтест, громкость и
масштаб — за человеком; remote push ещё не выполнен. Мета-ключ `search`
остаётся в данных, но UI-поиск не подключён. Скриншоты в `docs/screenshots/`
стоит пересобрать под v3 перед публикацией страницы.
## 10. Изменение v3.1: TCP-модель, UDP-лот и шрифты
Короткая запись о текущей волне — без переписывания истории выше.
- **TCP-соединение — отдельная сущность `TcpConnection`.** Оно живёт в
`HourDirector.active_connections`, занимает out-слот и хранит только клиента,
IP и `request_id`. До соединения файлы в запрос не прикрепляются (drag
отклоняется, backend режет). Кнопка `ПОДКЛЮЧИТЬСЯ` справа снизу запускает
полоску; успех больше не открывает мини-игру отправки.
- **Слот после отправки.** Успешная TCP-отправка удаляет запрос, но файлы
**не расходует**: они возвращаются в папку и доступны новым запросам. Слот
остаётся занятым (idle) до ручного `ОТКЛЮЧИТЬ`. Отключение
мгновенное; во время передачи оно прерывает её без выплаты и без расхода
файлов. `reject`/`timeout`/конец часа освобождают слоты. Слоты общие,
`1 + slot_level` (макс. 5); TCP может занять единственный слот и
заблокировать приём — это осознанно.
- **UDP-риск.** Новый лот `lot_udp` («UDP-повторитель», 4 уровня, цены
60/120/210/350) поднимает шанс с **40 %** до **80 %** шагом **+10 %**.
Ролл — в конце передачи: успех даёт обычную выплату UDP ×0.85; провал —
**−3 SLA**, без денег, файлы остаются, запрос жив, повтор через **4 с**,
слот освобождается, streak сбрасывается. Всего лотов **9**, дерево
**8 825 $**.
- **Шрифты.** Тело — **Kechusilka Regular** (нормализован под Golos,
x-height 0.530 em) с fallback **Golos Text**; крупный текст — **DisplayOTF**
(wordmark 46, «ПОБЕДА/ПОРАЖЕНИЕ» 34, титлбар 14, «РЕЗУЛЬТАТ» 18); цифры и
счётчики — **Press Start 2P**. Шкала 11/13/17/26.
- **Документация:** обновлены `docs/GDD.md`, `docs/DECISIONS.md` (D53–D61),
`docs/UI_REFRESH.md`, `docs/IMPLEMENTATION.md`, `docs/TEST_PLAN.md`,
`docs/ITCH_PAGE.md` и `README.md`. `ASSETS.md` не менялся — шрифтовые
лицензии уже зафиксированы там.
- **Проверки:** полный набор + parse-check; core-наборы обновлены и зелёные;
UI-наборы (`test_main_*`, `ui_smoke`) ещё идут. `tests/SIM_BALANCE_REPORT.md`
отражает текущий `data/balance.json`: **7 205 $**, магазин **44,9 %**,
**0/240** поражений, exit **0**.
## 11. v3.1: темп, каталог файлов и «ЖАРА»
Текущая волна; история выше не переписана.
- **Длительность:** часы **120 / 150 / 180 / 210 / 240 / 270 / 300 / 330 /
360 с** (`120 + 30×(h−1)`), забег **36 минут** + подготовка. H1
`income_target` — **100 $**. Окно спавна — **75 %** часа.
- **Файлы:** успешная отправка не удаляет файл; он остаётся в папке и снова
доступен пулу запросов. Общего лимита на забег нет; `hours[].files`
(**6,6,7,8,9,9,10,10,10**) — скорость, не жёсткий cap. Интервал —
`spawn_window / files`, переноса между часами нет (carry = 0); папки
докупаются, к финалу ~**5–6**.
- **Запросы:** `request_cap` удалён. Темп — `send_interval`
(**20,24,18,24,24,24,24,24,24**), лимит — только `active_requests` +
cutoff последних **10 с**. Анти-голодание — только первый запрос часа;
требования выбираются из файлов в папках и не связаны с приходом.
- **Атаки:** `start = clamp(0.25×duration, 20, 60)`,
`end = min(0.85×duration, duration−20)`; H2 **37,5–127,5 с**,
H9 **60–306 с**.
- **Визуал:** `TemperatureGauge` вместо 03:42 в меню-баре, `StressOverlay`
(heat-модель, alpha-only шейдер без screen texture), пульс SLA, тряска
панелей ≤2 px, `reduce_motion`, `UiMotion`, Lane B (drag/карточки/слоты),
headless-safe.
- **Баланс:** `tests/SIM_BALANCE_REPORT.md` — **7 205 $**, полоса
**6 982–8 500 $**, магазин **3 964 $ (44,9 %)**, **0/240** поражений,
exit **0**; H2, H4 и H5 близко к −20 % (H2 −16,4 %, H4 −15,3 %, H5 −15,1 %).
- **Тесты:** полный набор + parse-check; `test_economy` и `test_save` в
runner; точный счёт не фиксируется.
+190 -26
View File
@@ -5,23 +5,123 @@
## Шрифты
### Handjet — основной UI-шрифт
### Golos Text — fallback для редких символов (v3)
| Поле | Значение |
| --- | --- |
| Файл | `art/fonts/GolosText-VF.ttf` (variable; локальное имя для `GolosText[wght].ttf`) |
| Оси | `wght` 400–900; в теме — `wght 600` |
| Автор / проект | The Golos Text Project Authors — https://github.com/googlefonts/golos-text |
| Источник | https://github.com/google/fonts/tree/main/ofl/golostext |
| Лицензия | SIL Open Font License 1.1 — `art/fonts/OFL-GolosText.txt` |
| SHA-256 | `17bb58fb69aec2dfb047a2ebf52534023e9b688c97a6b7ac795b0a72912c2063` |
| Кириллица | полная (О/П/Ё/№), гуманистический рисунок, хорошо читается в 11–17 px |
| Использование | fallback для Kechusilka и DisplayOTF (`Theme90.FONT_FALLBACK`, `Font.fallbacks`): `№ – … · × © ± ≈ −` и стрелки. Прямо в разметке не используется |
Типографическая шкала: **11 / 13 / 17 / 26 px** (`FS_MICRO`, `FS_BODY`,
`FS_HEAD`, `FS_LOGO`); значения проверяет `ui_smoke.gd`, поэтому менять их
можно только вместе с проверкой. Крупнее 26 px размер передаётся на месте
вызова (24/28/34/46). Роли: тело текста — Kechusilka, крупный текст —
DisplayOTF (`display()`), цифры и счётчики — Press Start 2P (`logo()`),
страховка для редких символов — Golos Text.
### Kechusilka — основной шрифт тела (нормализованный)
| Поле | Значение |
| --- | --- |
| Файл | `art/fonts/Kechusilka-Regular.otf` — нормализованный (UPM 2048, 173 глифа) |
| Оригинал | `art/fonts/source/Kechusilka-Regular-original.otf` — только для сверки, к игре не подключён |
| Источник | архив `kechusilka-regular.zip` (загрузка пользователя), внутри `kechusilka-Regular_1.otf` и два PNG-превью |
| Версия | Version 1.000; 16.09.2026; FontCreator 15 |
| SHA-256 (нормализованный) | `e296eca059c05886da25808170accf6004619850dcac14200046199be04e10c8` |
| SHA-256 (оригинал) | `3f666b9b04dcbbfcfe3a31256d99e807553c9dd2efa3f360cc14422599917d36` |
| Лицензия | **подтверждена владельцем проекта** (license confirmed by project owner). Файла лицензии в архиве нет, строк copyright/license в шрифте тоже нет — поэтому текст лицензии здесь не приводится и её название не угадывается |
| Кириллица | полная: все 66 букв (включая Ё/ё), все цифры |
| Нет глифов | `№` U+2116, `–` U+2013, `…` U+2026, `·` `×` `← → ↑ ↓` `≤ ≥` `§` `°` `€` `•` `©` `±` `≈` `−` — закрываются fallback-шрифтом |
| Fallback | `GolosText-VF.ttf` через `Font.fallbacks` в `Theme90.ui()` |
| Использование | `FONT_UI`, `Theme90.ui()`; `default_font` темы `art/fonts/ui_theme.tres` |
**Нормализация.** У исходника x-height `0.312 em` против `0.530 em` у Golos
Text: в одном кегле текст был бы на 40 % мельче, и шкала 11/13/17/26 поехала
бы во всех карточках. Шрифт пересобран скриптом
`art/fonts/normalize_kechusilka.py`:
- коэффициент `scale = 0.530 × 2048 / 640 = 1.696`;
- масштабируются контуры (`TransformPen`), `hmtx` (advance + lsb),
`hhea`/`OS/2`/`head` и 150 значений `GPOS` — иначе кернинг стал бы на 40 %
слабее;
- результат: x-height `0.5298 em`, cap `0.7422 em`, линия `15.16 px` против
`15.60 px` у Golos при 13 px; ширина русских строк отличается от Golos
в пределах ±6 %, поэтому вёрстку пересчитывать не пришлось;
- хинтов в исходнике нет (0 stem-операторов), потерь при пересборке нет;
- сборка воспроизводима: повторный запуск даёт тот же файл и тот же SHA-256.
Оригинал не удалён — он лежит в `art/fonts/source/` и в теме не используется:
это точка сверки, если понадобится поменять коэффициент.
**Вес штриха.** Kechusilka светлее прежнего Golos Text `wght 600`: толщина
стойки `0.10 em` против `0.145 em`, то есть 1.35 px против 1.88 px при 13 px
(замер по растеризации `l`/`I`). На 11–13 px это читается, но на 9 px стойка
уходит в 0.94 px и текст «сереет». Поэтому подписи мельче 11 px лучше поднять
до `FS_MICRO`. Если после первого запуска в Godot захочется веса ближе к
прежнему, это делается одной строкой — `FontVariation.variation_embolden` в
`Theme90.ui()`; значение подбирать на месте, начиная с 0.1, потому что
масштаб эффекта зависит от версии TextServer.
### Kechusilka: чем закрыты отсутствующие символы
В шрифте нет 19 символов, которые встречаются в тексте проекта. Цепочка
`Kechusilka → Golos Text` закрывает все, кроме трёх: `Σ` U+03A3, `↔` U+2194
(только комментарии и тесты, на экран не попадают) и `▯` U+25AF — его нет ни в
одном шрифте проекта, включая прежний Golos, поэтому он и раньше рисовался
встроенным резервом Godot. Регрессии нет.
Проверено разбором `cmap` всех шрифтов проекта и всех строковых литералов
`.gd`/`.json`/`.tscn`, плюс рендер-симуляция подстановки: на 11–13 px стык
шрифтов не виден, потому что у Kechusilka и Golos совпадает x-height.
### DisplayOTF — крупный текст и логотип
| Поле | Значение |
| --- | --- |
| Файл | `art/fonts/DisplayOTF.otf` (статический, вариаций нет) |
| Автор | Denis A Serikov, 2003–2006 — `art/fonts/COPYRIGHT-DisplayOTF.txt` |
| Источник | архив `displayotf.zip` (загрузка пользователя; каталог `www.otlab.ru`), публичной ссылки нет |
| Лицензия | Freeware — текст соглашения `art/fonts/LICENSE-DisplayOTF.txt` (в архиве `license_131.txt` в CP1251, перекодирован в UTF-8 без правок текста) |
| SHA-256 | `c6578605ddc6992008044a1c7266cdd46dcf846a5d5f5dffbd391023e6ba0552` |
| Кириллица | полная: все 66 букв (включая Ё/ё), цифры, `№ % « » — – … ! ?` |
| Кернинг | таблиц `GPOS`/`kern` нет; есть `liga`, `salt`, `aalt` |
| Цифры | пропорциональные (`1` = 395/1000 em, остальные 553/1000) |
| Embedding | `fsType 0` — встраивание в игру разрешено |
| Использование | крупный текст: `theme90.gd → display()`; wordmark `PEAK HOUR 97`, «ПОБЕДА»/«ПОРАЖЕНИЕ», «РЕЗУЛЬТАТ», заголовки окон, вариация темы `LogoLabel` |
| Не использовать | тело текста и живые счётчики: мельче 16 px насечки сливаются, а пропорциональные цифры «дрожат» при смене значения |
Шрифт держит ту же «машинную» интонацию, что Press Start 2P, но с более
узким и характерным рисунком, поэтому занимает роль крупного текста, а Press
Start 2P остаётся на цифрах с моноширинной сеткой.
Условия лицензии, которые нужно соблюдать: файл распространяется «как есть» —
не переконвертировать в TTF, не сабсетить и не менять состав знаков без
разрешения автора; продавать шрифт отдельно нельзя.
### Handjet — legacy, в теме не используется
| Поле | Значение |
| --- | --- |
| Файл | `art/fonts/Handjet-VF.ttf` (variable; локальное имя для `Handjet[ELGR,ELSH,wght].ttf`) |
| Оси | `wght` 100–900, `ELGR` 1–2, `ELSH` 0–16; в теме — `wght 700, ELGR 2, ELSH 2` |
| Оси | `wght` 100–900, `ELGR` 1–2, `ELSH` 0–16 |
| Автор / проект | The Handjet Project Authors / Rosetta Type — https://github.com/google/fonts/tree/main/ofl/handjet |
| Прямая ссылка | https://raw.githubusercontent.com/google/fonts/main/ofl/handjet/Handjet%5BELGR%2CELSH%2Cwght%5D.ttf |
| Лицензия | SIL Open Font License 1.1 — `art/fonts/OFL-Handjet.txt` |
| SHA-256 | `9262749e8bb0b73ebcae0e20428689c3c59576eebeb6c4e1020300d2d41bdf4d` |
| Кириллица | полная (проверено по cmap: А–Я, а–я, Ё/ё, цифры; 1339 кодпоинтов) |
| Использование | `default_font` темы `art/fonts/ui_theme.tres`; все UI-тексты |
| Кириллица | полная (1339 кодпоинтов) |
| Использование | **нигде**: после перехода на Golos Text (D51) файл оставлен как исторический артефакт |
Handjet узкий (средняя ширина ≈0.36 em), поэтому в карточки 132 px влезают
имена файлов. Допустимые кегли: **10 / 12 / 16 / 24**.
TTF, `.import` и OFL можно удалить без влияния на игру; тема на Handjet не
ссылается. Узкий рисунок Handjet больше не нужен: Golos Text держит плотный
русский текст в карточках 400 px.
### Press Start 2P — логотип и крупные цифры
### Press Start 2P — цифры и числовые акценты
| Поле | Значение |
| --- | --- |
@@ -32,10 +132,12 @@ Handjet узкий (средняя ширина ≈0.36 em), поэтому в
| Лицензия | SIL Open Font License 1.1 — `art/fonts/OFL-PressStart2P.txt` |
| SHA-256 | `034c77f1f05ec89421e4a63f0e3a4ca1ecf852cc6d2bf611f126f275728e017d` |
| Кириллица | полная (656 кодпоинтов, включая Ё/ё и служебные `:/-`) |
| Использование | только логотип, заголовки и числовые строки (`theme90.gd → logo()`) |
| Использование | `theme90.gd → logo()`: цифры и счётчики (`num_line()`), часы, номера слотов, знак «?». Крупные слова и заголовки — уже на `display()` |
Шрифт слишком широкий для карточек (17 символов не влезают в 132 px), поэтому
основным текстом он не используется.
Шрифт слишком широкий для тела текста и карточек, поэтому основным текстом
он не используется (D51). Цифры у него ровно 1 em, поэтому счётчики и часы
не меняют ширину при смене значения — это главная причина держать его на
числах, а крупный текст отдавать DisplayOTF.
### Pixelify Sans — в теме не используется
@@ -54,22 +156,71 @@ Handjet узкий (средняя ширина ≈0.36 em), поэтому в
### Совместимость лицензий
Все три шрифта — **SIL OFL 1.1**. OFL разрешает встраивать шрифты в игру
(включая коммерческую), но требует сохранять текст лицензии и не продавать
шрифт отдельно. Для производных шрифтов действует Reserved Font Name
(у Press Start 2P — «Press Start 2P»). Это совместимо с MIT-лицензией кода:
Golos Text, Handjet, Press Start 2P и Pixelify Sans — **SIL OFL 1.1**.
OFL разрешает встраивать шрифты в игру (включая коммерческую), но требует
сохранять текст лицензии и не продавать шрифт отдельно. Для производных
шрифтов действует Reserved Font Name. Это совместимо с MIT-лицензией кода:
MIT не накладывает ограничений на шрифты, а OFL-тексты лежат рядом с файлами.
DisplayOTF — **freeware**, не OFL. Играть и встраивать в игру можно, текст
соглашения лежит рядом (`LICENSE-DisplayOTF.txt`); продавать сам шрифт
отдельно и менять состав знаков нельзя. Это не мешает MIT-коду игры, но при
смене лицензии проекта или платной сборке условия нужно перечитать.
Kechusilka — по шрифту лицензия **подтверждена владельцем проекта** (license
confirmed by project owner). Текста лицензии в архиве и в файле шрифта нет,
поэтому её название здесь не указано и условия не пересказываются: если
проект будет публиковаться или продаваться, текст нужно запросить у
владельца. Нормализация (масштаб контуров) — техническое изменение файла,
о котором владелец уведомлён.
## Графика
| Файл | Автор | Лицензия | Примечание |
| --- | --- | --- | --- |
| UI, иконки, фаски | команда проекта | MIT | рисуются кодом в `scripts/ui/theme90.gd`; внешних текстур нет |
| `art/icons/file_icons.png`, `art/icons/*.png` | исходный тайлсет — автор не указан; импорт — команда проекта | **неизвестна** (уточнить у владельца) | 128×128 атлас 8×8 (64 id) + шесть 32×32 fallback; собраны `art/icons/import_tileset.py` из `art/icons/source/tileset1.png` |
| UI, панели, маскот | команда проекта | MIT | рисуются кодом в `scripts/ui/theme90.gd`; внешних UI-текстур нет |
| `icon.svg` | команда проекта | MIT | иконка проекта, формы без текста (ThorVG) |
| `docs/screenshots/*.png` | команда проекта | MIT | кадры игры 1280×720, сняты из работающей сборки |
`art/icons/` и `art/ui/` пусты (`.gitkeep`): спрайтов, тайлсетов и сторонней
графики в проекте нет.
### Пиксельные иконки файлов
`art/icons/file_icons.png` — атлас 128×128 RGBA, 8×8 ячеек по 16 px, 64
стабильных `icon_id` (0..63). `art/icons/` также содержит `file_text.png`,
`file_sound.png`, `file_photo.png`, `file_video.png`, `folder.png` и
`packet.png` (все 32×32 RGBA) — fallback по типу файла для старых сейвов с
`icon_id = -1`. Иконки показываются в сетке папки, drag-preview, чипах запроса
и карточках пакетов; в `Theme90.draw_file_icon()` они рисуются с `nearest`,
поэтому остаются резкими в 16/24/32 px.
- Источник: `art/icons/source/tileset1.png` — 128×128 RGBA, сетка 8×8 по
16 px, 64 иконки, прозрачный фон, альфа только 0/255. Файл хранится в
репозитории (каталог исключён из импорта Godot через `.gdignore`).
- Импорт: `art/icons/import_tileset.py` (Python 3 + Pillow) — явная таблица
`ATLAS_SLOTS` (id атласа → тайл источника) и `SINGLE_ICONS` (шесть
fallback-иконок). Атлас собирается 1:1 без масштабирования, отдельные
иконки — nearest 16→32 px; полупрозрачных пикселей скрипт не создаёт.
- Превью для сверки: `art/icons/source/tileset1_preview.png` (все 64 тайла с
номерами) и `art/icons/source/file_icons_preview.png` (атлас 1x/1.5x/2x/4x
— ячейки 16/24/32/64 px, fallback в 16/24/32 px и ×4).
- `art/icons/generate_icons.py` — **legacy**-генератор прежнего векторного
набора; после импорта его запускать не нужно (перезапишет шесть fallback).
- `art/icons/shop/*` — отдельный пак апгрейдов, этим импортом не затрагивается.
**Лицензия:** исходный `tileset1.png` пришёл без файла лицензии, автор и
условия использования **неизвестны**. Прежний набор был сгенерирован в команде
(MIT), но на импортированные PNG это не распространяется. До выяснения условий
у владельца исходника не публиковать и не продавать сборку с этими иконками и
не указывать для них MIT.
Пересборка:
```bash
python3 art/icons/import_tileset.py
# без превью: python3 art/icons/import_tileset.py --no-preview
```
`art/ui/` по-прежнему пуст (`.gitkeep`): отдельных UI-спрайтов нет.
## Аудио
@@ -108,25 +259,38 @@ python3 audio/generate_sfx.py
```bash
cd peak-hour/art/fonts
curl -L -o Handjet-VF.ttf \
"https://raw.githubusercontent.com/google/fonts/main/ofl/handjet/Handjet%5BELGR%2CELSH%2Cwght%5D.ttf"
# Golos Text: скачайте GolosText[wght].ttf из google/fonts/ofl/golostext
# и сохраните локально как GolosText-VF.ttf
curl -L -o PressStart2P-Regular.ttf \
"https://raw.githubusercontent.com/google/fonts/main/ofl/pressstart2p/PressStart2P-Regular.ttf"
sha256sum Handjet-VF.ttf PressStart2P-Regular.ttf
sha256sum GolosText-VF.ttf PressStart2P-Regular.ttf
# Kechusilka пересобирается из оригинала (нужен fontTools):
python3 normalize_kechusilka.py
```
Ожидаемые хеши — в таблицах выше. Тексты OFL уже лежат в `art/fonts/`; при
обновлении шрифта обновите и их. Pixelify Sans скачивается по ссылке из его
раздела, но для сборки не требуется.
обновлении шрифта обновите и их. Handjet и Pixelify Sans для сборки не
требуются.
DisplayOTF уже лежит в `art/fonts/` вместе с лицензией — публичной ссылки на
него нет, файл нужно сохранять как есть (не переконвертировать в TTF и не
сабсетить).
Kechusilka тоже в репозитории: `Kechusilka-Regular.otf` — результат
нормализации, оригинал лежит в `art/fonts/source/`. Правьте не результат, а
скрипт: он пересобирает файл из оригинала и даёт тот же SHA-256. Если
пересобрали — обновите хеш в таблице выше.
### Графика
Генерировать нечего: UI и иконки рисуются кодом, `icon.svg` — обычный SVG.
После клонирования достаточно импортировать проект в Godot 4.7.2.
Иконки пересобираются `python3 art/icons/import_tileset.py` (нужен Pillow) из
`art/icons/source/tileset1.png`. `icon.svg` — обычный SVG. После клонирования
достаточно импортировать проект в Godot 4.7.2.
## Чего в проекте нет
- Сторонних спрайтов, тайлсетов, музыкальных треков и сэмплов.
- Сторонних спрайтов, кроме исходного `art/icons/source/tileset1.png`
(происхождение и лицензия уточняются), музыкальных треков и сэмплов.
- Ассетов из коммерческих игр; Press Start 2P лишь стилизован под аркадные
bitmap-шрифты и распространяется по OFL.
- Секретов, ключей и токенов в ассетах и данных.
+168 -69
View File
@@ -2,56 +2,102 @@
> Час пик. Одна смена. Ни одного свободного места.
Ночной диспетчер узла файлового обмена, 1997 год. Принимайте пакеты,
раскладывайте файлы, собирайте запросы клиентов, держите канал и SLA — в
интерфейсе, вдохновлённом Kaleidos: тёплая бумага, тонкие контуры и
бирюзовый акцент.
1997 год. Ночной диспетчер узла файлового обмена: принимайте пакеты,
раскладывайте файлы-полимино по папкам, собирайте контейнеры запросов,
проходите мини-игры и держите SLA. UI — «Kaleidos»: тёплая бумага, тонкие
контуры и бирюзовый акцент.
**Жанр:** менеджмент / тайм-менеджмент, retro UI.
**Платформа:** Godot **4.7.2** → **Web (HTML5, без threads)**, RU-only.
**Окно:** 1920×1080, логический viewport 1280×720, fractional scale.
**Забег:** 9 игровых часов по 5 минут (≈45 минут) + подготовка между часами.
**Забег:** 9 игровых часов — **120 / 150 / 180 / 210 / 240 / 270 / 300 / 330 /
360 с** (`120 + 30×(h−1)`, всего **36 минут**) + подготовка между часами.
## Статус v1.2
## Статус v3.0
Играбельный веб-прототип полного цикла: **меню → подготовка → час → итоги
часа → … → итоги забега**. Девять часов проходят на сидах `1`, `42`, `777`
без потери часов; SLA после магазина — 200.
часа → … → итоги забега**. Актуальная спецификация — [GDD v3.0](docs/GDD.md),
решения — [DECISIONS](docs/DECISIONS.md) (актуальны D45–D61, D01–D44 — история).
Реализовано:
- 4 типа файлов (text/sound/photo/video), буфер приёма, папки и секции.
- **10 апгрейдов** магазина, включая `SLA +24`; магазин открывается с часа 2
и скрыт во время часа.
- TCP-слоты и полоска соединения: три попадания; промах закрывает панель
без автоповтора.
- 4 атаки, но **час 1 проходит без атак**; junk/ddos — с часа 2,
phishing — с 3, ransomware — с 4.
- **SLA 128 / 200** на старте; потеря пакета: text 2 / sound 4 / photo 8 /
video 16.
- Мета-награды **постепенно по часам**: поиск после 3-го часа, щит после
5-го, `complete_run` после 9-го; итоги показывают «ОТКРЫТО: …», прогресс
живёт между забегами. `minigame_advanced` — резерв под будущий контент.
- Поиск по имени в текущей папке после открытия; заголовок «N ИЗ M ОБЪЕКТОВ».
- Автообновление папки/дерева, уникальные имена папок (`Имя`, `Имя 2`),
удаление пустых папок, attached-галочки, скролл приёма до 8 пакетов.
- «Уничтожить содержимое» папки удаляет файлы с подтверждением и −5 SLA;
пустая папка удаляется без штрафа.
- «Спокойный фон» — плоская бумага без dither; Esc на итогах забега ведёт
в главное меню.
- UI v2 «Kaleidos», маскот **БАЙТ** и короткий туториал первого часа.
- Ambient машинного зала: 4 WAV, автозапуск после первого жеста, mute/пауза/
громкость; генератор `audio/generate_ambient.py`.
- Процедурный VHS только в главном меню (настройки → «ЭКРАН»): скан-линии,
зерно, виньетка и полоса; рисуется кодом, не блокирует ввод, текст резкий.
Игровой UI остаётся чистым, без CRT на весь интерфейс.
- Handjet + Press Start 2P, 18 SFX + фоновый `hum`.
- **9 часов**, длительность **120 → 360 с** (+30 с за час), старт **50 $**,
**SLA 60/60** (кап +40 за уровень `lot_sla` до **220**), 1 слот (макс. 5),
канал 10 (макс. 46), буфер 6 (макс. 12), таймер запроса 30 с (макс. 65),
таймер пакета 20 с.
- **4 типа файлов-полимино:** текст 1, звук 2, фото 3, видео 4 клетки;
поворот `R`/ПКМ во время drag; сетка папки **5×4 → 6×4 → 7×5 → 8×5 →
8×6 → 9×6** (до 54 клеток). Новая папка — **180 $**, всего до 6.
- **Стол сортировки 4×6** в подготовке; файлы со стола нельзя прикрепить,
на старте часа они автоматически возвращаются в папки (если места нет —
час не начинается).
- **Запросы на 1–4 конкретных именованных файла:** имена видны на карточке
сеткой 2×2, прикрепление проверяется по точному `file_id`, потерянный файл
помечается «ПОТЕРЯН», обязательные файлы жёстко зарезервированы. H1
стартует с seed-файла и сразу с запроса на него. Отдельного `request_cap`
нет: одновременно живут не больше `active_requests` запросов, новые не
появляются в последние **10 с** часа; анти-голодание срабатывает только для
первого запроса часа, дальше темп задаёт `send_interval`.
- **Файлы — каталог, а не расходник:** успешная отправка только снимает
привязку к запросу, файл остаётся в папке и может быть затребован снова.
Приход задаётся только скоростью: окно спавна **75 %** часа, интервал —
`spawn_window / hours[].files` с джиттером ±20 %, без переноса и общего
лимита файлов (carry = 0).
- **9 лотов магазина**; ровно 3 предложения, рероллы 40/80/120 (макс. 3 за
подготовку), замок отдельного предложения. Магазин — с H2 и только в
подготовке. Полное дерево — **8 825 $** (было 8 085 $ с 8 лотами), новый
лот `lot_udp` — «UDP-повторитель».
- **6 мини-игр отправки** (`dots`, `sound`, `text`, `video`, `photo`, `maze`;
**3 ступени**) и **4 мини-игры атак** (`junk`, `ddos`, `phishing`,
`ransomware`; 4 ступени); без таймеров, с шапкой «название + цель», полосой
прогресса и экраном результата: имя игры, `q%`, полоса качества, вердикт,
детали и `ПРОДОЛЖИТЬ`/`Esc`.
- **TCP как отдельное соединение:** до `ПОДКЛЮЧИТЬСЯ` файлы в запрос не
прикрепляются. Успех (3 попадания) занимает out-слот, в слоте — имя клиента
и случайный IP; мини-игра отправки сама не открывается. `ОТПРАВИТЬ`
запускает её, только когда прикреплены все файлы. После успешной отправки
запрос исчезает, файлы возвращаются в папку и остаются доступны новым
запросам, выплата начисляется, но TCP-слот остаётся занятым (idle) до
ручного `ОТКЛЮЧИТЬ` на слоте. TCP платит **×1.15**.
- **UDP без соединения:** время передачи **×0.8**, выплата **×0.85**, но
успех — ролл в конце передачи: база **40 %**, +**10 %** за уровень
`lot_udp`, максимум **80 %** (цены 60/120/210/350). Провал: **−3 SLA**, без
выплаты и денежного штрафа, файлы не расходуются, запрос остаётся, повтор
через **4 с**, слот освобождается. Файлы не расходуются ни при успешной
отправке, ни при отмене/таймауте/отключении.
- **Слоты общие для приёма и отправки:** `1 + slot_level`, максимум 5.
TCP-соединение может занять единственный слот и заблокировать приём — это
осознанное требование. Открепление файла — двойной клик по чипу.
- **Шрифты:** тело — **Kechusilka Regular** (нормализован под Golos, fallback
Golos Text), крупный текст — **DisplayOTF**, цифры/счётчики — **Press Start
2P**; шкала 11/13/17/26.
- **«ЖАРА»:** в меню-баре вместо часов 03:42 — `TemperatureGauge`
(216×18, колба, деления, °C); поверх игры — `StressOverlay` с heat-моделью
и alpha-only шейдером без чтения экрана. Пульс SLA/CRIT, тряска панелей
≤2 px, «Снизить эффекты» (`reduce_motion`) отключает движение; в headless
слой скрыт. Анимации карточек, слотов и drag — через `UiMotion` (Lane B).
- **20 атак за забег** (`0,1,1,2,2,3,3,4,4`), 4 вида, 4 ступени; атака — это
мини-игра, которая ставит час на паузу. Щит «Заземление» гасит атаку
автоматически (**70 %** награды, без штрафа).
- **Пошаговый туториал v3:** 5 шагов (пакет → прикрепление → TCP-соединение →
мини-игра → SLA/атаки), пульсирующие рамки у цели и кнопка «ДАЛЕЕ».
Прохождение хранится в `Game.stats["tutorial_done"]`; кнопка «ОБУЧЕНИЕ» в
главном меню запускает повтор.
- **Save VERSION 2**: позиции только в `folder.files`/`table`, метаданные в
`files`; v1-сейвы мигрируют без потери файлов и без уменьшения сеток;
офферы/рероллы/замки, стол и `tutorial_done` сохраняются.
- Мета-награды после 3/5/9 часов, маскот **БАЙТ**, UI v3 «Kaleidos», ambient
машинного зала, VHS только в главном меню.
Ограничения:
- Туториал — маскот и несколько подсказок, без пошагового сценария.
- Сохранение в Web идёт через IndexedDB (`user://save.json`); проверяйте
продолжение после перезагрузки вкладки.
- Средний расход магазина в симуляторе — **44,9 %** дерева **8 825 $**;
итог **7 205 $** в полосе **6 982–8 500 $**, поражений **0/240**.
H2, H4 и H5 идут близко к нижней границе −20 % (−16,4 %, −15,3 %, −15,1 %),
поэтому дальше ужесточать темп нельзя. Подробности —
`tests/SIM_BALANCE_REPORT.md`.
- Remote push ещё не выполнен; сборка для itch.io готовится локально.
## Управление
@@ -60,20 +106,29 @@
| --- | --- |
| Выбор, меню, перетаскивание | ЛКМ |
| Контекстное меню | ПКМ |
| Пакет из «Приёма» → папка; файл → карточка запроса | drag&drop |
| Поиск по имени в текущей папке | после открытия «Поиска» |
| Мини-игра соединения: три попадания | ЛКМ или `Space` |
| Мини-игра подготовки: отправить | `Enter` или кнопка |
| Поворот фигуры во время drag | `R` или ПКМ |
| Пакет из «Приёма» → папка | drag&drop |
| Файл из папки → карточка запроса | drag&drop (TCP — только после `ПОДКЛЮЧИТЬСЯ`) |
| Файл ↔ стол сортировки | drag&drop |
| TCP-соединение | кнопка `ПОДКЛЮЧИТЬСЯ` справа снизу, 3 попадания в зелёную зону |
| Отправка запроса | кнопка `ОТПРАВИТЬ` слева снизу |
| Отклонить запрос | кнопка `ОТКЛОНИТЬ` слева снизу |
| Закрыть TCP-соединение | `ОТКЛЮЧИТЬ` на слоте (мгновенно) |
| Открепить файл от запроса | двойной клик по чипу |
| TCP/UDP | переключатель на карточке запроса до соединения |
| Мини-игра: действие | ЛКМ / `Space` / стрелки (по игре) |
| `dots`: линия | нажать на «С» и вести курсор по клеткам |
| Пропустить мини-игру | `ПРОПУСТИТЬ`/`СДАТЬСЯ` + второй клик |
| Результат мини-игры | `ПРОДОЛЖИТЬ` или `Esc` |
| Магазин: закрыть | `ЗАКРЫТЬ` или `Esc` |
| Пауза | `Esc` |
| Панель соединения: отмена (слот свободен, запрос остаётся) | `Esc` |
## Запуск
Нужен Godot 4.7.2. В этом окружении:
Нужен Godot **4.7.2**. Команды выполняются из корня репозитория; ниже
`GODOT` — путь к бинарю Godot.
```bash
GODOT=/home/eq/techlb/tools/godot/Godot_v4.7.2-stable_linux.x86_64
# один раз после клона: импорт шрифтов и темы
"$GODOT" --headless --path . --import
@@ -84,6 +139,13 @@ GODOT=/home/eq/techlb/tools/godot/Godot_v4.7.2-stable_linux.x86_64
"$GODOT" -e --path .
```
В Windows удобно задать путь один раз:
```powershell
$env:GODOT_BIN = "D:\Apps\godot\Godot_v4.7.2-stable_win64_console.exe"
& $env:GODOT_BIN --path .
```
## Сборка Web
```bash
@@ -97,20 +159,37 @@ mkdir -p build/web
просмотр — только по HTTP:
```bash
python3 -m http.server 8000 --directory build/web
python -m http.server 8000 --directory build/web
# http://localhost:8000
```
Нужны export templates 4.7.2 (`web_nothreads_release.zip`).
Нужны export templates 4.7.2 (`web_nothreads_release.zip`). Каталог шаблонов
должен называться `4.7.2.stable`.
## Тесты
Полный прогон (21 набор + parse-check):
Актуальный список — **полный набор + parse-check** в
`scripts/run_all_tests.sh` / `scripts/run_all_tests.ps1` (см. также `tests/`;
число наборов и проверок может меняться между волнами, поэтому здесь не
хардкодится). Core-наборы (`test_hour`, `test_economy`, `test_save`,
`test_slots`, `test_controller`, `test_integration`, `test_fuzz`,
`test_full_playthrough`, `test_shop`, `test_shop_modal`) обновлены и зелёные;
UI-наборы (`test_main_*`, `ui_smoke`) прогоняются отдельно.
```powershell
# Windows
powershell -ExecutionPolicy Bypass -File scripts/run_all_tests.ps1
```
```bash
# Linux / macOS
bash scripts/run_all_tests.sh
```
Windows-раннер сам ищет Godot (сначала `GODOT_BIN`, затем стандартный путь,
затем `godot4`/`godot` из `PATH`); путь можно передать явно:
`-GodotBin <path>`.
Точечно:
```bash
@@ -120,32 +199,49 @@ bash scripts/run_all_tests.sh
"$GODOT" --headless --path . --script scripts/ui/ui_smoke.gd
```
Фактический итог: **22/22 PASS** (21 набор + parse-check); ключевые
счётчики — `test_main_prep` 92/0, `ui_smoke` 135/0, `test_ambient` 74/0.
Подробности — в
[AI_REPORT](AI_REPORT.md).
Баланс (240 сидов, отчёт `tests/SIM_BALANCE_REPORT.md`):
```bash
python tests/sim_balance.py
```
Отчёт `tests/SIM_BALANCE_REPORT.md` (240 сидов, 9 часов, `q=0.85`, TCP):
средний итог **7 205 $** (payout 6 344 + `recv_fee` 177 + атаки 684) в полосе
**6 982–8 500 $**, расход магазина **3 964 $** (**44,9 %** дерева **8 825 $**),
**0 поражений из 240**, SLA в конце H9 — **64,9**, запросов выполнено — **62,0**,
вердикт `PASS`, exit code **0**. H1 `income_target` — **100 $** (2-минутный
час), H2, H4 и H5 близко к −20 %.
Подробности — в [AI_REPORT](AI_REPORT.md).
E2E-прогоны на реальной сцене `Main` и веб-смоук проводились в предыдущей
волне; после изменений TCP/UDP-модели и UI их нужно перепроверить перед
публикацией. Финальный счёт UI-наборов здесь не хардкодится.
## Структура
```
peak-hour/
Game-JAM/
├─ project.godot # 1920×1080 / 1280×720, GL Compatibility
├─ export_presets.cfg # Web (no threads)
├─ scenes/ # Main + dev-превью
├─ scenes/ # Main + UI/мини-игры
├─ scripts/
│ ├─ main.gd # UI, ввод, модалки, drag&drop, туториал
│ ├─ game_controller.gd # цикл menu → prep → hour → results
│ ├─ run_all_tests.sh # полный headless-прогон
│ ├─ run_all_tests.ps1/.sh # полный headless-прогон
│ ├─ autoload/ # Game, Data, Audio, Rng, Ambient
│ ├─ core/ # HourDirector, FileRegistry, SlotManager, Economy
│ ├─ entities/ # файлы, пакеты, запросы, папки, слоты
│ ├─ systems/ # атаки, магазин, сохранения
│ ├─ ui/ # Theme90 v2, модалки, маскот, VHS-оверлей
│ └─ minigames/ # ConnectionGame, MiniGame
│ ├─ entities/ # файлы, папки, секции, пакеты, запросы, слоты
│ ├─ systems/ # Shop, AttackDirector, SaveManager
│ ├─ ui/ # Theme90, FileGrid, SortingTable, ShopModal, VHS,
│ │ # TemperatureGauge, StressOverlay, UiMotion
│ └─ minigames/ # MiniGame, factory, connection_game, send/, attack/
├─ data/ # balance.json, strings_ru.json, names_ru.json
├─ art/fonts/ # Handjet, Press Start 2P + OFL
├─ art/fonts/ # Kechusilka, DisplayOTF, Golos fallback, Press Start 2P
├─ art/icons/ # пиксельные иконки + generate_icons.py
├─ audio/ # sfx/, music/, amb/, generate_*py
├─ tests/ # headless-наборы
├─ docs/ # GDD, DECISIONS, UI_REFRESH, screenshots
├─ shaders/ # vhs.gdshader (только главное меню)
├─ tests/ # headless-наборы, sim_balance.py
├─ docs/ # GDD, DECISIONS, UI_REFRESH, скриншоты
└─ build/ # артефакты экспорта (gitignored)
```
@@ -153,23 +249,26 @@ peak-hour/
- Godot **4.7.2** (GL Compatibility).
- Для Web: export templates 4.7.2, `web_nothreads_release.zip`.
- Python 3 — только для пересборки SFX.
- Python 3 — для баланса и пересборки SFX/ambient.
- Никаких секретов и внешних сервисов.
## Документация
- [GDD](docs/GDD.md) — правила и числа v1.2.
- [DECISIONS](docs/DECISIONS.md) — решения D01–D25.
- [GDD](docs/GDD.md) — правила и числа v3.0.
- [DECISIONS](docs/DECISIONS.md) — решения D01–D61 (актуальны D45–D61).
- [AI_REPORT](AI_REPORT.md) — модели, приёмы и проверки.
- [ASSETS](ASSETS.md) — шрифты, звук, лицензии и воспроизведение.
- [UI_REFRESH](docs/UI_REFRESH.md) — UI v2 «Kaleidos».
- [ASSETS](ASSETS.md) — шрифты, иконки, звук, лицензии и воспроизведение.
- [UI_REFRESH](docs/UI_REFRESH.md) — UI v3 «Kaleidos».
- [IMPLEMENTATION](docs/IMPLEMENTATION.md) и [TEST_PLAN](docs/TEST_PLAN.md) —
контракты и матрица тестов.
исторические контракты v1 и матрица тестов (v3-дельты — в начале
IMPLEMENTATION).
- [ITCH_PAGE](docs/ITCH_PAGE.md) — текст страницы itch.io.
- [screenshots](docs/screenshots/) — кадры меню, подготовки, часа и атак.
## Лицензии
Код — **MIT** ([LICENSE](LICENSE)). Шрифты — **SIL OFL 1.1**
(`art/fonts/OFL-*.txt`). UI и иконка — MIT. SFX и `hum.wav` — MIT/CC0,
сгенерированы `audio/generate_sfx.py`. Подробности — в [ASSETS.md](ASSETS.md).
Код — **MIT** ([LICENSE](LICENSE)). Шрифты: Golos Text и Press Start 2P —
**SIL OFL 1.1**, Kechusilka — лицензия подтверждена владельцем проекта,
DisplayOTF — **freeware/personal** (Denis A Serikov). UI, пиксельные иконки
(`art/icons/`) и иконка — MIT. SFX и `hum.wav` — MIT/CC0, сгенерированы
`audio/generate_sfx.py`. Подробности и тексты лицензий — в [ASSETS.md](ASSETS.md).
+1
View File
@@ -0,0 +1 @@
Copyright (c) Denis A Serikov, 2003-2006. All rights reserved. denis_box@mtu-net.ru
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://d26do8xk5bs24"
path="res://.godot/imported/DisplayOTF.otf-521c0bcaa41177fabd074bc7be7bb37f.fontdata"
[deps]
source_file="res://art/fonts/DisplayOTF.otf"
dest_files=["res://.godot/imported/DisplayOTF.otf-521c0bcaa41177fabd074bc7be7bb37f.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cwms0uuyam0fk"
path="res://.godot/imported/GolosText-VF.ttf-af72b844d15035464cce46e3ecf9f3ea.fontdata"
[deps]
source_file="res://art/fonts/GolosText-VF.ttf"
dest_files=["res://.godot/imported/GolosText-VF.ttf-af72b844d15035464cce46e3ecf9f3ea.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
Binary file not shown.
+36
View File
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://blvusebgit70d"
path="res://.godot/imported/Kechusilka-Regular.otf-b002f5d4ceda13c25fba42b2e2fc1ede.fontdata"
[deps]
source_file="res://art/fonts/Kechusilka-Regular.otf"
dest_files=["res://.godot/imported/Kechusilka-Regular.otf-b002f5d4ceda13c25fba42b2e2fc1ede.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+34
View File
@@ -0,0 +1,34 @@
-------------------------------------------------------------------------------------------------------------------------------------
Пожалуйста, перед использованием, прочтите данное соглашение!
-------------------------------------------------------------------------------------------------------------------------------------
====================================================================================
Лицензионное соглашение:
Используя этот шрифт, вы автоматически принимаете данное соглашение.
Шрифт является бесплатным (freeware) и предоставляется по принципу "как есть",
Следовательно, никаких гарантий со стороны производителя не прилагается
и не предусматривается. Вы используете этот шрифт на свой страх и риск.
Автор, не будет отвечать ни за какие потери или искажения данных, любую упущенную
выгоду в процессе использования, или не правильного использования этого шрифта.
Любая продажа шрифта или передача с целью получения дохода, запрещена.
Автор оставляет за собой право собственности на данный продукт и возможность
внесения любых изменений без предварительного уведомления и предупреждения.
Декомпиляция шрифта, изменение формата, и внесение изменений в состав
знаков - только с разрешения автора.
В случае возникновения вопросов, свяжитесь со мной по E-mail:denis_box@mtu-net.ru
====================================================================================
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Следует помнить, что все логотипы и торговые марки и зарегистрированные торговые
марки принадлежат их законным владельцам.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
====================================================================================
Этот шрифт был скачен с сайта www.otlab.ru
+93
View File
@@ -0,0 +1,93 @@
Copyright 2019 The Golos Text Project Authors (https://github.com/googlefonts/golos-text)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+129
View File
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
"""Нормализация Kechusilka Regular под метрики Golos Text.
Зачем: у исходного шрифта x-height 0.312 em против 0.530 em у Golos Text,
то есть в одном кегле текст выглядел примерно на 40 % мельче. Из-за этого
шкала Theme90 (11/13/17/26) и вся вёрстка при смене шрифта «поехали» бы.
Что делает: масштабирует всё, что задано в единицах шрифта, на
scale = 0.530 * 2048 / 640 = 1.696
— контуры, advance widths, side bearings, метрики hhea/OS-2/head и значения
GPOS (кернинг). UPM остаётся 2048, поэтому файл выглядит обычным.
Хинтов в исходнике нет (0 stem-операторов на 173 глифа), так что пересборка
чарстрингов через T2CharStringPen ничего не теряет.
Запуск:
python art/fonts/normalize_kechusilka.py
Нужен fontTools. Оригинал не перезаписывается.
"""
import os
import shutil
from fontTools.ttLib import TTFont
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.pens.transformPen import TransformPen
from fontTools.misc.transform import Transform
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "source", "Kechusilka-Regular-original.otf")
DST = os.path.join(HERE, "Kechusilka-Regular.otf")
TARGET_X_HEIGHT = 0.530 # как у Golos Text
SCALE = TARGET_X_HEIGHT * 2048.0 / 640.0
def scale_value_record(vr, s, counter):
if vr is None:
return
for attr in ("XPlacement", "YPlacement", "XAdvance", "YAdvance"):
v = getattr(vr, attr, None)
if v is not None:
setattr(vr, attr, s(v))
counter[0] += 1
def main():
if not os.path.exists(SRC):
raise SystemExit("нет исходника: %s" % SRC)
print("scale = %.6f" % SCALE)
font = TTFont(SRC, recalcTimestamp=False)
if font["head"].unitsPerEm != 2048:
raise SystemExit("ожидался UPM 2048, получен %d" % font["head"].unitsPerEm)
def s(v):
return int(round(v * SCALE))
# 1. контуры + ширины
cff = font["CFF "].cff.topDictIndex[0]
charstrings = cff.CharStrings
glyph_set = font.getGlyphSet()
private = cff.Private
global_subrs = font["CFF "].cff.GlobalSubrs
hmtx = font["hmtx"]
new_hmtx = {}
for name in font.getGlyphOrder():
old_adv, old_lsb = hmtx[name]
pen = T2CharStringPen(s(old_adv), glyph_set)
glyph_set[name].draw(TransformPen(pen, Transform(SCALE, 0, 0, SCALE, 0, 0)))
cs = pen.getCharString(private=private, globalSubrs=global_subrs)
cs.private = private
charstrings[name] = cs
new_hmtx[name] = (s(old_adv), s(old_lsb))
font["hmtx"].metrics = new_hmtx
# 2. вертикальные метрики
hhea = font["hhea"]
hhea.ascent = s(hhea.ascent)
hhea.descent = s(hhea.descent)
hhea.lineGap = s(hhea.lineGap)
os2 = font["OS/2"]
for attr in ("sTypoAscender", "sTypoDescender", "sTypoLineGap",
"usWinAscent", "usWinDescent", "sxHeight", "sCapHeight",
"ySubscriptXSize", "ySubscriptYSize", "ySubscriptXOffset",
"ySubscriptYOffset", "ySuperscriptXSize", "ySuperscriptYSize",
"ySuperscriptXOffset", "ySuperscriptYOffset",
"yStrikeoutSize", "yStrikeoutPosition"):
if hasattr(os2, attr):
setattr(os2, attr, s(getattr(os2, attr)))
head = font["head"]
head.xMin = s(head.xMin)
head.yMin = s(head.yMin)
head.xMax = s(head.xMax)
head.yMax = s(head.yMax)
# 3. кернинг (GPOS): значения в единицах шрифта, иначе станут слабее на 40 %
counter = [0]
if "GPOS" in font and font["GPOS"].table.LookupList:
for lookup in font["GPOS"].table.LookupList.Lookup:
for st in lookup.SubTable:
fmt = getattr(st, "Format", None)
if lookup.LookupType == 2:
if fmt == 1:
for ps in st.PairSet:
for rec in ps.PairValueRecord:
scale_value_record(rec.Value1, s, counter)
scale_value_record(rec.Value2, s, counter)
elif fmt == 2:
for c1 in st.Class1Record:
for c2 in c1.Class2Record:
scale_value_record(c2.Value1, s, counter)
scale_value_record(c2.Value2, s, counter)
elif lookup.LookupType == 1:
if fmt == 1:
scale_value_record(st.Value, s, counter)
elif fmt == 2:
for rec in st.Value:
scale_value_record(rec, s, counter)
font.save(DST)
print("GPOS полей масштабировано: %d" % counter[0])
print("сохранено: %s (%d байт)" % (DST, os.path.getsize(DST)))
if __name__ == "__main__":
main()
Binary file not shown.
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://vsxr4ck4tuq8"
path="res://.godot/imported/Kechusilka-Regular-original.otf-d2be4abf143de1d0b6c7f67898af1427.fontdata"
[deps]
source_file="res://art/fonts/source/Kechusilka-Regular-original.otf"
dest_files=["res://.godot/imported/Kechusilka-Regular-original.otf-d2be4abf143de1d0b6c7f67898af1427.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
+176 -174
View File
@@ -1,17 +1,15 @@
[gd_resource type="Theme" format=3]
[ext_resource type="FontFile" path="res://art/fonts/Handjet-VF.ttf" id="1_hjthv"]
[ext_resource type="FontFile" path="res://art/fonts/Kechusilka-Regular.otf" id="1_cg7jb"]
[ext_resource type="FontFile" path="res://art/fonts/GolosText-VF.ttf" id="1_fum4e"]
[ext_resource type="FontFile" path="res://art/fonts/PressStart2P-Regular.ttf" id="2_fum4e"]
[ext_resource type="FontFile" path="res://art/fonts/DisplayOTF.otf" id="3_dc2yr"]
[sub_resource type="FontVariation" id="FontVariation_fum4e"]
base_font = ExtResource("1_hjthv")
variation_opentype = {
1162626898: 2.0,
1162629960: 2.0,
2003265652: 700.0
}
[sub_resource type="FontVariation" id="FontVariation_6m2gx"]
fallbacks = Array[Font]([ExtResource("1_fum4e")])
base_font = ExtResource("1_cg7jb")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lr7a5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x3s7b"]
bg_color = Color(0.913725, 0.901961, 0.866667, 1)
border_width_left = 1
border_width_top = 1
@@ -24,7 +22,7 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cg7jb"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukyte"]
bg_color = Color(0, 0, 0, 0)
border_width_left = 1
border_width_top = 1
@@ -37,7 +35,7 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dc2yr"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wjr02"]
bg_color = Color(1, 1, 1, 1)
border_width_left = 1
border_width_top = 1
@@ -53,7 +51,7 @@ shadow_color = Color(0, 0, 0, 0.094)
shadow_size = 3
shadow_offset = Vector2(0, 2)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6m2gx"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4munx"]
bg_color = Color(0.968627, 0.960784, 0.933333, 1)
border_width_left = 1
border_width_top = 1
@@ -69,7 +67,7 @@ shadow_color = Color(0, 0, 0, 0.094)
shadow_size = 3
shadow_offset = Vector2(0, 2)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x3s7b"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oc7r5"]
bg_color = Color(0.847059, 0.827451, 0.788235, 1)
border_width_left = 1
border_width_top = 1
@@ -82,33 +80,9 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="Image" id="Image_ukyte"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
"width": 14
}
[sub_resource type="ImageTexture" id="ImageTexture_wjr02"]
image = SubResource("Image_ukyte")
[sub_resource type="Image" id="Image_4munx"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
"width": 14
}
[sub_resource type="ImageTexture" id="ImageTexture_oc7r5"]
image = SubResource("Image_4munx")
[sub_resource type="Image" id="Image_y0w1v"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
@@ -120,7 +94,7 @@ image = SubResource("Image_y0w1v")
[sub_resource type="Image" id="Image_dywbi"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
@@ -130,38 +104,38 @@ data = {
[sub_resource type="ImageTexture" id="ImageTexture_47b07"]
image = SubResource("Image_dywbi")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_danrt"]
[sub_resource type="Image" id="Image_danrt"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
"width": 14
}
[sub_resource type="ImageTexture" id="ImageTexture_1j4f6"]
image = SubResource("Image_danrt")
[sub_resource type="Image" id="Image_djhhc"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 65, 61, 52, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 14,
"mipmaps": false,
"width": 14
}
[sub_resource type="ImageTexture" id="ImageTexture_a0hrt"]
image = SubResource("Image_djhhc")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jvde3"]
bg_color = Color(0, 0, 0, 0)
border_color = Color(0, 0, 0, 0)
corner_detail = 4
[sub_resource type="Image" id="Image_1j4f6"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_djhhc"]
image = SubResource("Image_1j4f6")
[sub_resource type="Image" id="Image_a0hrt"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_jvde3"]
image = SubResource("Image_a0hrt")
[sub_resource type="Image" id="Image_372il"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
@@ -173,7 +147,7 @@ image = SubResource("Image_372il")
[sub_resource type="Image" id="Image_11awt"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
@@ -183,7 +157,31 @@ data = {
[sub_resource type="ImageTexture" id="ImageTexture_loh4e"]
image = SubResource("Image_11awt")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a1mnj"]
[sub_resource type="Image" id="Image_a1mnj"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_4uvxq"]
image = SubResource("Image_a1mnj")
[sub_resource type="Image" id="Image_petse"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 9,
"mipmaps": false,
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_j2rjp"]
image = SubResource("Image_petse")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7om8y"]
bg_color = Color(0.603922, 0.584314, 0.541176, 0.3)
border_color = Color(0, 0, 0, 0)
corner_radius_top_left = 2
@@ -192,7 +190,7 @@ corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4uvxq"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o55pb"]
bg_color = Color(0.184314, 0.498039, 0.560784, 0.55)
border_color = Color(0, 0, 0, 0)
corner_radius_top_left = 2
@@ -201,7 +199,7 @@ corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_petse"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wl44i"]
bg_color = Color(0.847059, 0.827451, 0.788235, 1)
border_width_left = 1
border_width_top = 1
@@ -214,33 +212,9 @@ corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
corner_detail = 4
[sub_resource type="Image" id="Image_j2rjp"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_7om8y"]
image = SubResource("Image_j2rjp")
[sub_resource type="Image" id="Image_o55pb"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_wl44i"]
image = SubResource("Image_o55pb")
[sub_resource type="Image" id="Image_mni32"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 131, 198, 212, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
@@ -250,7 +224,31 @@ data = {
[sub_resource type="ImageTexture" id="ImageTexture_8vjq1"]
image = SubResource("Image_mni32")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w06qd"]
[sub_resource type="Image" id="Image_w06qd"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 232, 230, 221, 255, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 65, 61, 52, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_barbq"]
image = SubResource("Image_w06qd")
[sub_resource type="Image" id="Image_dvi42"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 47, 126, 142, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
"height": 12,
"mipmaps": false,
"width": 12
}
[sub_resource type="ImageTexture" id="ImageTexture_foj1q"]
image = SubResource("Image_dvi42")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jyq6k"]
bg_color = Color(0.513726, 0.780392, 0.831373, 1)
border_color = Color(0, 0, 0, 0)
corner_radius_top_left = 2
@@ -259,7 +257,7 @@ corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_barbq"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cjati"]
bg_color = Color(0.184314, 0.498039, 0.560784, 1)
border_color = Color(0, 0, 0, 0)
corner_radius_top_left = 2
@@ -268,7 +266,7 @@ corner_radius_bottom_right = 2
corner_radius_bottom_left = 2
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dvi42"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m7381"]
bg_color = Color(1, 1, 1, 1)
border_width_left = 1
border_width_top = 1
@@ -281,7 +279,7 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_foj1q"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_of6nr"]
content_margin_left = 2.0
content_margin_top = 2.0
content_margin_right = 2.0
@@ -298,7 +296,11 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jyq6k"]
[sub_resource type="FontVariation" id="FontVariation_s7l1c"]
fallbacks = Array[Font]([ExtResource("1_fum4e")])
base_font = ExtResource("3_dc2yr")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7j4rs"]
bg_color = Color(0.968627, 0.960784, 0.933333, 1)
border_width_left = 1
border_width_top = 1
@@ -314,7 +316,7 @@ shadow_color = Color(0, 0, 0, 0.094)
shadow_size = 3
shadow_offset = Vector2(0, 2)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cjati"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_05v8i"]
content_margin_left = 2.0
content_margin_top = 2.0
content_margin_right = 2.0
@@ -327,7 +329,7 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m7381"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7ph3p"]
content_margin_left = 5.0
content_margin_top = 5.0
content_margin_right = 5.0
@@ -347,7 +349,7 @@ shadow_color = Color(0, 0, 0, 0.16)
shadow_size = 6
shadow_offset = Vector2(0, 3)
[sub_resource type="Image" id="Image_of6nr"]
[sub_resource type="Image" id="Image_2b66g"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
@@ -356,10 +358,10 @@ data = {
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_s7l1c"]
image = SubResource("Image_of6nr")
[sub_resource type="ImageTexture" id="ImageTexture_1tdcs"]
image = SubResource("Image_2b66g")
[sub_resource type="Image" id="Image_7j4rs"]
[sub_resource type="Image" id="Image_hv5yg"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
@@ -368,10 +370,10 @@ data = {
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_05v8i"]
image = SubResource("Image_7j4rs")
[sub_resource type="ImageTexture" id="ImageTexture_yjsxn"]
image = SubResource("Image_hv5yg")
[sub_resource type="Image" id="Image_7ph3p"]
[sub_resource type="Image" id="Image_s5gth"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 154, 149, 137, 255, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 149, 137, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
@@ -380,10 +382,10 @@ data = {
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_2b66g"]
image = SubResource("Image_7ph3p")
[sub_resource type="ImageTexture" id="ImageTexture_bkd55"]
image = SubResource("Image_s5gth")
[sub_resource type="Image" id="Image_1tdcs"]
[sub_resource type="Image" id="Image_xoku7"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 43, 91, 103, 255, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 91, 103, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "RGBA8",
@@ -392,74 +394,74 @@ data = {
"width": 9
}
[sub_resource type="ImageTexture" id="ImageTexture_hv5yg"]
image = SubResource("Image_1tdcs")
[sub_resource type="ImageTexture" id="ImageTexture_d1bwe"]
image = SubResource("Image_xoku7")
[resource]
default_font = SubResource("FontVariation_fum4e")
default_font_size = 12
default_font = SubResource("FontVariation_6m2gx")
default_font_size = 13
Button/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
Button/colors/font_disabled_color = Color(0.603922, 0.584314, 0.541176, 1)
Button/colors/font_hover_color = Color(0.172549, 0.360784, 0.407843, 1)
Button/colors/font_pressed_color = Color(0.121569, 0.27451, 0.313726, 1)
Button/font_sizes/font_size = 12
Button/fonts/font = SubResource("FontVariation_fum4e")
Button/styles/disabled = SubResource("StyleBoxFlat_lr7a5")
Button/styles/focus = SubResource("StyleBoxFlat_cg7jb")
Button/styles/hover = SubResource("StyleBoxFlat_dc2yr")
Button/styles/normal = SubResource("StyleBoxFlat_6m2gx")
Button/styles/pressed = SubResource("StyleBoxFlat_x3s7b")
Button/font_sizes/font_size = 13
Button/fonts/font = SubResource("FontVariation_6m2gx")
Button/styles/disabled = SubResource("StyleBoxFlat_x3s7b")
Button/styles/focus = SubResource("StyleBoxFlat_ukyte")
Button/styles/hover = SubResource("StyleBoxFlat_wjr02")
Button/styles/normal = SubResource("StyleBoxFlat_4munx")
Button/styles/pressed = SubResource("StyleBoxFlat_oc7r5")
CheckBox/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
CheckBox/colors/font_disabled_color = Color(0.603922, 0.584314, 0.541176, 1)
CheckBox/colors/font_hover_color = Color(0.172549, 0.360784, 0.407843, 1)
CheckBox/colors/font_pressed_color = Color(0.121569, 0.27451, 0.313726, 1)
CheckBox/icons/checked = SubResource("ImageTexture_wjr02")
CheckBox/icons/radio_checked = SubResource("ImageTexture_oc7r5")
CheckBox/icons/radio_unchecked = SubResource("ImageTexture_8m7xh")
CheckBox/icons/unchecked = SubResource("ImageTexture_47b07")
CheckBox/styles/focus = SubResource("StyleBoxFlat_danrt")
CheckBox/styles/hover = SubResource("StyleBoxFlat_danrt")
CheckBox/styles/normal = SubResource("StyleBoxFlat_danrt")
CheckBox/styles/pressed = SubResource("StyleBoxFlat_danrt")
HScrollBar/icons/decrement = SubResource("ImageTexture_djhhc")
HScrollBar/icons/decrement_highlight = SubResource("ImageTexture_jvde3")
HScrollBar/icons/increment = SubResource("ImageTexture_i4ifm")
HScrollBar/icons/increment_highlight = SubResource("ImageTexture_loh4e")
HScrollBar/styles/grabber = SubResource("StyleBoxFlat_a1mnj")
HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_4uvxq")
HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_4uvxq")
HScrollBar/styles/scroll = SubResource("StyleBoxFlat_petse")
HScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_petse")
CheckBox/icons/checked = SubResource("ImageTexture_8m7xh")
CheckBox/icons/radio_checked = SubResource("ImageTexture_47b07")
CheckBox/icons/radio_unchecked = SubResource("ImageTexture_1j4f6")
CheckBox/icons/unchecked = SubResource("ImageTexture_a0hrt")
CheckBox/styles/focus = SubResource("StyleBoxFlat_jvde3")
CheckBox/styles/hover = SubResource("StyleBoxFlat_jvde3")
CheckBox/styles/normal = SubResource("StyleBoxFlat_jvde3")
CheckBox/styles/pressed = SubResource("StyleBoxFlat_jvde3")
HScrollBar/icons/decrement = SubResource("ImageTexture_i4ifm")
HScrollBar/icons/decrement_highlight = SubResource("ImageTexture_loh4e")
HScrollBar/icons/increment = SubResource("ImageTexture_4uvxq")
HScrollBar/icons/increment_highlight = SubResource("ImageTexture_j2rjp")
HScrollBar/styles/grabber = SubResource("StyleBoxFlat_7om8y")
HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_o55pb")
HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_o55pb")
HScrollBar/styles/scroll = SubResource("StyleBoxFlat_wl44i")
HScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_wl44i")
HSlider/constants/grabber_offset = 0
HSlider/icons/grabber = SubResource("ImageTexture_7om8y")
HSlider/icons/grabber_disabled = SubResource("ImageTexture_wl44i")
HSlider/icons/grabber_highlight = SubResource("ImageTexture_8vjq1")
HSlider/styles/grabber_area = SubResource("StyleBoxFlat_w06qd")
HSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_barbq")
HSlider/styles/slider = SubResource("StyleBoxFlat_petse")
HSlider/icons/grabber = SubResource("ImageTexture_8vjq1")
HSlider/icons/grabber_disabled = SubResource("ImageTexture_barbq")
HSlider/icons/grabber_highlight = SubResource("ImageTexture_foj1q")
HSlider/styles/grabber_area = SubResource("StyleBoxFlat_jyq6k")
HSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_cjati")
HSlider/styles/slider = SubResource("StyleBoxFlat_wl44i")
HeadLabel/base_type = &"Label"
HeadLabel/font_sizes/font_size = 16
HeadLabel/fonts/font = SubResource("FontVariation_fum4e")
HeadLabel/font_sizes/font_size = 17
HeadLabel/fonts/font = SubResource("FontVariation_6m2gx")
Label/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
Label/font_sizes/font_size = 12
Label/fonts/font = SubResource("FontVariation_fum4e")
Label/font_sizes/font_size = 13
Label/fonts/font = SubResource("FontVariation_6m2gx")
LineEdit/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
LineEdit/colors/font_placeholder_color = Color(0.603922, 0.584314, 0.541176, 1)
LineEdit/font_sizes/font_size = 12
LineEdit/fonts/font = SubResource("FontVariation_fum4e")
LineEdit/styles/focus = SubResource("StyleBoxFlat_dvi42")
LineEdit/styles/normal = SubResource("StyleBoxFlat_foj1q")
LineEdit/font_sizes/font_size = 13
LineEdit/fonts/font = SubResource("FontVariation_6m2gx")
LineEdit/styles/focus = SubResource("StyleBoxFlat_m7381")
LineEdit/styles/normal = SubResource("StyleBoxFlat_of6nr")
LogoLabel/base_type = &"Label"
LogoLabel/colors/font_color = Color(0.172549, 0.360784, 0.407843, 1)
LogoLabel/font_sizes/font_size = 24
LogoLabel/fonts/font = ExtResource("2_fum4e")
LogoLabel/font_sizes/font_size = 26
LogoLabel/fonts/font = SubResource("FontVariation_s7l1c")
MicroLabel/base_type = &"Label"
MicroLabel/colors/font_color = Color(0.172549, 0.164706, 0.14902, 1)
MicroLabel/font_sizes/font_size = 10
MicroLabel/font_sizes/font_size = 11
NumLabel/base_type = &"Label"
NumLabel/font_sizes/font_size = 12
NumLabel/font_sizes/font_size = 13
NumLabel/fonts/font = ExtResource("2_fum4e")
Panel/styles/panel = SubResource("StyleBoxFlat_jyq6k")
Panel/styles/panel = SubResource("StyleBoxFlat_7j4rs")
PopupMenu/colors/font_accelerator_color = Color(0.603922, 0.584314, 0.541176, 1)
PopupMenu/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
PopupMenu/colors/font_disabled_color = Color(0.603922, 0.584314, 0.541176, 1)
@@ -471,20 +473,20 @@ PopupMenu/constants/item_end_padding = 12
PopupMenu/constants/item_start_padding = 8
PopupMenu/constants/separator_outline_size = 0
PopupMenu/constants/v_separation = 3
PopupMenu/font_sizes/font_size = 12
PopupMenu/fonts/font = SubResource("FontVariation_fum4e")
PopupMenu/styles/hover = SubResource("StyleBoxFlat_cjati")
PopupMenu/styles/panel = SubResource("StyleBoxFlat_m7381")
PopupMenu/font_sizes/font_size = 13
PopupMenu/fonts/font = SubResource("FontVariation_6m2gx")
PopupMenu/styles/hover = SubResource("StyleBoxFlat_05v8i")
PopupMenu/styles/panel = SubResource("StyleBoxFlat_7ph3p")
TooltipLabel/colors/font_color = Color(0.121569, 0.117647, 0.105882, 1)
TooltipLabel/font_sizes/font_size = 10
TooltipLabel/fonts/font = SubResource("FontVariation_fum4e")
TooltipPanel/styles/panel = SubResource("StyleBoxFlat_m7381")
VScrollBar/icons/decrement = SubResource("ImageTexture_s7l1c")
VScrollBar/icons/decrement_highlight = SubResource("ImageTexture_05v8i")
VScrollBar/icons/increment = SubResource("ImageTexture_2b66g")
VScrollBar/icons/increment_highlight = SubResource("ImageTexture_hv5yg")
VScrollBar/styles/grabber = SubResource("StyleBoxFlat_a1mnj")
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_4uvxq")
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_4uvxq")
VScrollBar/styles/scroll = SubResource("StyleBoxFlat_petse")
VScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_petse")
TooltipLabel/font_sizes/font_size = 11
TooltipLabel/fonts/font = SubResource("FontVariation_6m2gx")
TooltipPanel/styles/panel = SubResource("StyleBoxFlat_7ph3p")
VScrollBar/icons/decrement = SubResource("ImageTexture_1tdcs")
VScrollBar/icons/decrement_highlight = SubResource("ImageTexture_yjsxn")
VScrollBar/icons/increment = SubResource("ImageTexture_bkd55")
VScrollBar/icons/increment_highlight = SubResource("ImageTexture_d1bwe")
VScrollBar/styles/grabber = SubResource("StyleBoxFlat_7om8y")
VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_o55pb")
VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_o55pb")
VScrollBar/styles/scroll = SubResource("StyleBoxFlat_wl44i")
VScrollBar/styles/scroll_focus = SubResource("StyleBoxFlat_wl44i")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://das8asb4b05gm"
path="res://.godot/imported/file_icons.png-aadd037746f865f0fe6757930f0f608c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/file_icons.png"
dest_files=["res://.godot/imported/file_icons.png-aadd037746f865f0fe6757930f0f608c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djb7w10tao8ns"
path="res://.godot/imported/file_photo.png-b23698027d48c718a6fc500f302db87b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/file_photo.png"
dest_files=["res://.godot/imported/file_photo.png-b23698027d48c718a6fc500f302db87b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dw5oie5dxfky4"
path="res://.godot/imported/file_sound.png-ef9804b30db6fa708a74022e1489cfcd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/file_sound.png"
dest_files=["res://.godot/imported/file_sound.png-ef9804b30db6fa708a74022e1489cfcd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://gtofenp8bd1n"
path="res://.godot/imported/file_text.png-e2cb12a8276ae34819d095735a1e4828.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/file_text.png"
dest_files=["res://.godot/imported/file_text.png-e2cb12a8276ae34819d095735a1e4828.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c8yfny20jcs15"
path="res://.godot/imported/file_video.png-ffcbd0629f5e28094e97a5a051839521.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/file_video.png"
dest_files=["res://.godot/imported/file_video.png-ffcbd0629f5e28094e97a5a051839521.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cb7ge0n5gg8mu"
path="res://.godot/imported/folder.png-2d34cb1a6aa9dbae664638a5df628136.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/folder.png"
dest_files=["res://.godot/imported/folder.png-2d34cb1a6aa9dbae664638a5df628136.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""LEGACY: старый генератор иконок «в стиле Kaleidos» (32×32 RGBA PNG).
Актуальный пайплайн — `art/icons/import_tileset.py`: он собирает
`file_icons.png` (128×128, 8×8) и шесть отдельных иконок из исходного
тайлсета `art/icons/source/tileset1.png`. Этот скрипт оставлен как
legacy/альтернативный стиль; не запускайте его после импорта, иначе он
перезапишет шесть PNG старыми векторными иконками.
Стиль: плоские заливки из палитры Kaleidos, контур 1 px #2C2A26,
без сглаживания (nearest). Иконки читаются в размерах 16/24/32 px.
Запуск:
python3 art/icons/generate_icons.py
python3 art/icons/generate_icons.py --preview /tmp/opencode/icons_preview.png
"""
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
SIZE = 32
# --- палитра Kaleidos ------------------------------------------------------
OUTLINE = (44, 42, 38, 255) # #2C2A26
NAVY = (44, 92, 104, 255) # #2C5C68
TEAL = (47, 127, 143, 255) # #2F7F8F
TEAL_LIGHT = (131, 199, 212, 255) # #83C7D4
TEAL_PALE = (230, 245, 247, 255) # #E6F5F7
WARN = (197, 138, 46, 255) # #C58A2E
SUCCESS = (63, 143, 94, 255) # #3F8F5E
ERROR = (194, 91, 74, 255) # #C25B4A
PAPER = (247, 245, 238, 255) # #F7F5EE
PAPER_LO = (216, 211, 201, 255) # #D8D3C9
WHITE = (255, 255, 255, 255)
def canvas() -> Image.Image:
return Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
def shape(d: ImageDraw.ImageDraw, pts: list[tuple[int, int]], fill, outline=OUTLINE) -> None:
"""Заливка многоугольника + контур 1 px по его периметру."""
d.polygon(pts, fill=fill)
if outline is not None:
d.line(pts + [pts[0]], fill=outline, width=1, joint="curve")
def box(d: ImageDraw.ImageDraw, xy: tuple[int, int, int, int], fill, outline=OUTLINE) -> None:
"""
Прямоугольник с координатами включительно (как у ImageDraw.rectangle).
"""
d.rectangle(xy, fill=fill)
if outline is not None:
d.rectangle(xy, outline=outline)
def file_text() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
# страница с отрезанным верхним правым углом
page = [(7, 4), (19, 4), (25, 10), (25, 27), (7, 27)]
shape(d, page, PAPER)
# загнутый уголок
shape(d, [(19, 4), (25, 10), (19, 10)], PAPER_LO)
# три строки текста
d.rectangle((10, 13, 22, 14), fill=NAVY)
d.rectangle((10, 17, 22, 18), fill=NAVY)
d.rectangle((10, 21, 18, 22), fill=NAVY)
return img
def file_sound() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
# янтарный бейдж со срезанными углами
badge = [(6, 3), (26, 3), (29, 6), (29, 26), (26, 29), (6, 29), (3, 26), (3, 6)]
shape(d, badge, WARN)
# восьмая нота: головка, штиль, флажок
head = [(9, 21), (11, 19), (15, 19), (17, 21), (17, 24), (15, 26), (11, 26), (9, 24)]
shape(d, head, OUTLINE)
d.rectangle((16, 8, 17, 21), fill=OUTLINE)
shape(d, [(17, 8), (23, 11), (23, 14), (20, 14), (20, 13), (17, 12)], OUTLINE)
return img
def file_photo() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
# тёмная рамка
box(d, (3, 6, 28, 26), NAVY)
# светлое поле кадра
box(d, (6, 9, 25, 23), TEAL_PALE)
# дальняя и ближняя горы
shape(d, [(6, 23), (13, 12), (19, 23)], TEAL_LIGHT, outline=None)
shape(d, [(11, 23), (18, 14), (25, 23)], SUCCESS, outline=None)
# солнце
d.rectangle((20, 11, 22, 13), fill=WARN)
return img
def file_video() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
# тело плёнки
box(d, (3, 8, 28, 24), NAVY)
# перфорация сверху и снизу
for x in (5, 9, 21, 25):
d.rectangle((x, 10, x + 1, 11), fill=PAPER)
d.rectangle((x, 21, x + 1, 22), fill=PAPER)
# play
shape(d, [(13, 11), (22, 16), (13, 21)], PAPER)
return img
def folder() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
body = [(3, 27), (3, 9), (13, 9), (16, 12), (29, 12), (29, 27)]
shape(d, body, WARN)
# светлая кромка передней стенки
d.line([(5, 25), (27, 25)], fill=(226, 175, 91, 255), width=1)
return img
def packet() -> Image.Image:
img = canvas()
d = ImageDraw.Draw(img)
# конверт
box(d, (3, 10, 28, 25), PAPER)
# клапан
shape(d, [(4, 11), (16, 19), (27, 11)], TEAL_PALE, outline=None)
d.line([(4, 11), (16, 19)], fill=NAVY, width=1)
d.line([(16, 19), (27, 11)], fill=NAVY, width=1)
# нижние сгибы
d.line([(4, 24), (12, 18)], fill=PAPER_LO, width=1)
d.line([(27, 24), (19, 18)], fill=PAPER_LO, width=1)
# входящий пакет: teal-бейдж со стрелкой вниз
shape(d, [(21, 19), (29, 19), (29, 27), (21, 27)], TEAL)
d.rectangle((24, 21, 25, 23), fill=WHITE)
d.polygon([(22, 23), (27, 23), (24, 26)], fill=WHITE)
return img
ICONS = {
"file_text": file_text,
"file_sound": file_sound,
"file_photo": file_photo,
"file_video": file_video,
"folder": folder,
"packet": packet,
}
# Проверка: только полностью прозрачные или полностью непрозрачные пиксели.
# Это гарантирует отсутствие сглаживания по краям.
def assert_hard_edges(img: Image.Image, name: str) -> None:
alpha = img.getchannel("A")
values = set(alpha.getdata())
assert values <= {0, 255}, f"{name}: найден полупрозрачный пиксель {values}"
def build_preview(path: Path) -> None:
"""Контактный лист: все иконки в 16/24/32 px + увеличение 32 px ×4."""
scale = 4
cell_w = 200
cell_h = 236
cols = 3
sheet = Image.new("RGBA", (cols * cell_w, 2 * cell_h), (233, 230, 221, 255))
d = ImageDraw.Draw(sheet)
try:
font = ImageFont.truetype("/usr/share/fonts/dejavu/DejaVuSans.ttf", 15)
small = ImageFont.truetype("/usr/share/fonts/dejavu/DejaVuSans.ttf", 11)
except OSError: # pragma: no cover - depends on system fonts
font = ImageFont.load_default()
small = font
for index, (name, factory) in enumerate(ICONS.items()):
col = index % cols
row = index // cols
x0 = col * cell_w
y0 = row * cell_h
d.rectangle((x0 + 4, y0 + 4, x0 + cell_w - 6, y0 + cell_h - 6),
fill=(247, 245, 238, 255), outline=(154, 149, 137, 255))
d.text((x0 + 12, y0 + 10), name, fill=(31, 30, 27, 255), font=font)
icon = factory()
# три реальных размера в ряд, каждый на белой «карточке»
cx = x0 + 12
for px in (16, 24, 32):
d.rectangle((cx - 2, y0 + 44, cx + px + 1, y0 + 47 + px),
fill=(255, 255, 255, 255), outline=(216, 211, 201, 255))
sheet.alpha_composite(icon.resize((px, px), Image.Resampling.NEAREST),
(cx, y0 + 46))
d.text((cx, y0 + 52 + px), f"{px}", fill=(90, 86, 78, 255), font=small)
cx += 42
# увеличение 32 px ×4 — чтобы видеть пиксели и контур
big = icon.resize((SIZE * scale, SIZE * scale), Image.Resampling.NEAREST)
sheet.alpha_composite(big, (x0 + 34, y0 + 92))
path.parent.mkdir(parents=True, exist_ok=True)
sheet.save(path)
print(f"preview saved: {path}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--preview", type=Path, default=None,
help="куда сохранить контактный лист (PNG)")
args = parser.parse_args()
out_dir = Path(__file__).resolve().parent
for name, factory in ICONS.items():
img = factory()
assert_hard_edges(img, name)
target = out_dir / f"{name}.png"
img.save(target)
print(f"saved {target.relative_to(out_dir.parent.parent)} {img.size}")
if args.preview is not None:
build_preview(args.preview)
if __name__ == "__main__":
main()
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""Import the project icon pack from ``art/icons/source/tileset1.png``.
Source
------
``source/tileset1.png`` is the user-provided ``tileset1 (1).png`` (128x128
RGBA, transparent background, hard alpha 0/255). It is an 8x8 grid of
16x16 tiles, 64 icons total, and is kept in the repository so the build is
reproducible.
Licence of the source pack is **unknown / not supplied with the file**.
Confirm it with the project owner before publishing or selling the game.
The previous icon set was generated in-house (MIT); this import does not
change the licence of the source artwork itself.
What it writes
--------------
* ``art/icons/file_icons.png`` -- 128x128 atlas, 8x8 cells of 16 px, ids
0..63 in row-major order (identical geometry to ``FileIcons``).
* ``file_text.png`` / ``file_sound.png`` / ``file_photo.png`` /
``file_video.png`` / ``folder.png`` / ``packet.png`` -- 32x32 fallback
icons, each a nearest-neighbour 2x upscale of one source tile.
* ``source/tileset1_preview.png`` -- numbered contact sheet of the source.
* ``source/file_icons_preview.png`` -- atlas at 1x/1.5x/2x/4x (cells
16/24/32/64 px) and the six fallbacks at 16/24/32 px plus 4x.
Only nearest-neighbour resampling is used. No anti-aliased or
semi-transparent pixels are produced by this script.
Usage
-----
python3 art/icons/import_tileset.py
python3 art/icons/import_tileset.py --no-preview
"""
from __future__ import annotations
import argparse
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
BASE = Path(__file__).resolve().parent
SOURCE = BASE / "source" / "tileset1.png"
ATLAS = BASE / "file_icons.png"
TILE = 16
COLS = 8
ROWS = 8
COUNT = COLS * ROWS
SINGLE_SIZE = 32
# Atlas id (0..63) -> source tile id (0..63). The source is already a
# perfect 8x8/16 px sheet, so the safest mapping keeps every tile in place.
# Change this tuple only together with FileIcons.COUNT/COLS/ROWS and
# FileRegistry.FILE_ICON_COUNT.
ATLAS_SLOTS: tuple[int, ...] = tuple(range(COUNT))
# Human-readable names, in source tile order (row-major). Used by the
# preview and by the mapping report; not used at runtime.
TILE_NAMES: tuple[str, ...] = (
# row 0 -- documents and a bolt
"page_white", "page_blue", "page_green", "page_orange",
"page_red", "page_purple", "page_lined", "bolt",
# row 1 -- fruit
"strawberry", "watermelon", "cherries", "banana",
"grapes", "orange_slice", "pineapple", "pear",
# row 2 -- valuables, heart, ghosts
"gem_blue", "gem_silver", "heart", "clover",
"ghost_cyan", "ghost_purple", "ghost_white", "ghost_yellow",
# row 3 -- media, star, folders
"note", "star", "folder_cream", "folder_pink",
"folder_green_face", "folder_blue_star", "folder_tan",
"folder_green_confetti",
# row 4 -- FX, gift, key, bomb, envelopes
"sparkle", "gift", "key", "bomb",
"envelope_pink", "envelope_green", "envelope_peach", "envelope_cyan",
# row 5 -- crown, snowman, tree, gear, hat, check, whale, smiley
"crown", "snowman", "tree", "gear",
"wizard_hat", "check", "whale", "smiley",
# row 6 -- potions, shield, animals
"potion_red", "potion_blue", "potion_green", "potion_yellow",
"shield_plus", "cat_black", "fox_orange", "rabbit_pink",
# row 7 -- orb, bulb, devices, UI buttons
"crystal_ball", "lightbulb", "monitor", "headphones",
"sync", "frame_pink", "no_entry", "close",
)
# Type fallback icons drawn by Theme90 when a save has icon_id = -1.
# Mapping rationale:
# text -> lined page (reads as a document at 16 px)
# sound -> music note (direct match)
# photo -> pink picture frame (no camera/photo tile in this pack)
# video -> monitor/screen (no film/play tile in this pack)
# folder -> cream folder (direct match)
# packet -> green envelope (direct match, incoming mail)
SINGLE_ICONS: dict[str, int] = {
"file_text": 6,
"file_sound": 24,
"file_photo": 61,
"file_video": 58,
"folder": 26,
"packet": 37,
}
def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
for path in (
"C:/Windows/Fonts/arialbd.ttf",
"C:/Windows/Fonts/segoeui.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
):
try:
return ImageFont.truetype(path, size)
except OSError:
continue
return ImageFont.load_default()
def assert_hard_alpha(img: Image.Image, label: str) -> None:
"""Fail if the image has anti-aliased/partial alpha pixels."""
hist = img.getchannel("A").histogram()
partial = sum(hist[1:255])
if partial != 0:
raise SystemExit(f"{label}: {partial} semi-transparent pixels")
def load_source(path: Path) -> Image.Image:
if not path.is_file():
raise SystemExit(f"source not found: {path}")
img = Image.open(path).convert("RGBA")
expected = (COLS * TILE, ROWS * TILE)
if img.size != expected:
raise SystemExit(
f"{path}: expected {expected[0]}x{expected[1]} "
f"(8x8 x 16 px), got {img.size[0]}x{img.size[1]}"
)
assert_hard_alpha(img, str(path))
empty = [i for i in range(COUNT) if not _tile(img, i).getchannel("A").getbbox()]
if empty:
raise SystemExit(f"{path}: empty tiles at ids {empty}")
return img
def _tile(img: Image.Image, tile_id: int) -> Image.Image:
if not 0 <= tile_id < COUNT:
raise ValueError(f"tile id out of range: {tile_id}")
col = tile_id % COLS
row = tile_id // COLS
return img.crop((col * TILE, row * TILE, (col + 1) * TILE, (row + 1) * TILE))
def build_atlas(source: Image.Image) -> Image.Image:
atlas = Image.new("RGBA", (COLS * TILE, ROWS * TILE), (0, 0, 0, 0))
for slot, tile_id in enumerate(ATLAS_SLOTS):
col = slot % COLS
row = slot // COLS
atlas.alpha_composite(_tile(source, tile_id), (col * TILE, row * TILE))
return atlas
def build_single(source: Image.Image, tile_id: int) -> Image.Image:
return _tile(source, tile_id).resize(
(SINGLE_SIZE, SINGLE_SIZE), Image.Resampling.NEAREST
)
def _checker(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int],
cell: int = 8) -> None:
x0, y0, x1, y1 = box
for y in range(y0, y1, cell):
for x in range(x0, x1, cell):
if ((x - x0) // cell + (y - y0) // cell) % 2 == 0:
draw.rectangle((x, y, min(x + cell, x1) - 1,
min(y + cell, y1) - 1), fill=(58, 58, 66, 255))
else:
draw.rectangle((x, y, min(x + cell, x1) - 1,
min(y + cell, y1) - 1), fill=(44, 44, 50, 255))
def build_contact_sheet(source: Image.Image, path: Path) -> None:
"""Numbered 12x source cells -- used to review the 64-tile mapping."""
scale = 12
big = TILE * scale
pad = 26
label_h = 42
font = _font(16)
small = _font(11)
width = COLS * (big + pad) + pad
height = ROWS * (big + label_h + pad) + pad + 34
out = Image.new("RGBA", (width, height), (32, 32, 38, 255))
draw = ImageDraw.Draw(out)
draw.text((pad, 8),
"tileset1.png - 128x128 RGBA, 8x8 tiles @16 px, nearest x12",
fill=(255, 255, 255, 255), font=font)
for tile_id in range(COUNT):
col = tile_id % COLS
row = tile_id // COLS
x = pad + col * (big + pad)
y = 34 + pad + row * (big + label_h + pad)
_checker(draw, (x, y, x + big, y + big), 16)
out.alpha_composite(_tile(source, tile_id).resize(
(big, big), Image.Resampling.NEAREST), (x, y))
draw.rectangle((x - 1, y - 1, x + big, y + big),
outline=(120, 120, 128, 255))
draw.text((x, y - 21), f"{tile_id:02d} ({col},{row})",
fill=(255, 230, 120, 255), font=font)
draw.text((x, y + big + 4), TILE_NAMES[tile_id],
fill=(190, 196, 204, 255), font=small)
path.parent.mkdir(parents=True, exist_ok=True)
out.convert("RGB").save(path)
print(f"preview: {path}")
def build_atlas_preview(atlas: Image.Image, singles: dict[str, Image.Image],
path: Path) -> None:
"""Atlas at 16/24/32/64 px cells + fallbacks at 16/24/32 px and 4x."""
margin = 24
# 1x/1.5x/2x/4x -> 16/24/32/64 px per atlas cell.
atlas_scales = ((1, 16), (1.5, 24), (2, 32), (4, 64))
atlas_width = sum(int(atlas.width * s) for s, _cell in atlas_scales) + \
margin * (len(atlas_scales) - 1)
card_w, card_h = 218, 238
cards_width = 3 * card_w
width = max(atlas_width, cards_width) + margin * 2
atlas_h = int(atlas.height * atlas_scales[-1][0])
icons_top = margin + 30 + atlas_h + 52
height = icons_top + 2 * card_h + 18
out = Image.new("RGBA", (width, height), (30, 30, 35, 255))
draw = ImageDraw.Draw(out)
title = _font(20)
font = _font(14)
small = _font(11)
draw.text((margin, 10),
"file_icons.png (new) - 128x128, 8x8 ids 0..63",
fill=(255, 255, 255, 255), font=title)
x = margin
for scale, cell_px in atlas_scales:
size = int(atlas.width * scale)
y = margin + 30
_checker(draw, (x, y, x + size, y + size), 8)
out.alpha_composite(atlas.resize((size, size),
Image.Resampling.NEAREST), (x, y))
draw.rectangle((x - 1, y - 1, x + size, y + size),
outline=(120, 120, 128, 255))
draw.text((x, y + size + 4),
f"{scale:g}x ({size}px, cell {cell_px}px)",
fill=(255, 230, 120, 255), font=font)
x += size + margin
draw.line((margin, icons_top - 26, width - margin, icons_top - 26),
fill=(90, 94, 102, 255), width=1)
draw.text((margin, icons_top - 22),
"Theme90 fallbacks - 16 / 24 / 32 px and 32 px x4 (nearest)",
fill=(255, 255, 255, 255), font=title)
for index, (name, icon) in enumerate(singles.items()):
col = index % 3
row = index // 3
x0 = margin + col * card_w
y0 = icons_top + row * card_h
draw.rectangle((x0, y0, x0 + card_w - 8, y0 + card_h - 10),
fill=(40, 40, 47, 255), outline=(86, 90, 98, 255))
draw.text((x0 + 8, y0 + 6), f"{name}.png", fill=(255, 255, 255, 255),
font=font)
cx = x0 + 10
for px in (16, 24, 32):
_checker(draw, (cx, y0 + 32, cx + px, y0 + 32 + px), 8)
out.alpha_composite(icon.resize((px, px),
Image.Resampling.NEAREST),
(cx, y0 + 32))
draw.rectangle((cx - 1, y0 + 31, cx + px, y0 + 33 + px),
outline=(120, 120, 128, 255))
draw.text((cx, y0 + 36 + px), f"{px}", fill=(170, 176, 184, 255),
font=small)
cx += px + 16
bx, by = x0 + 10, y0 + 86
_checker(draw, (bx, by, bx + 128, by + 128), 8)
out.alpha_composite(icon.resize((128, 128),
Image.Resampling.NEAREST), (bx, by))
draw.rectangle((bx - 1, by - 1, bx + 128, by + 128),
outline=(120, 120, 128, 255))
draw.text((bx + 134, by + 4), "32 px", fill=(255, 230, 120, 255),
font=font)
draw.text((bx + 134, by + 24), "x4", fill=(190, 196, 204, 255),
font=small)
path.parent.mkdir(parents=True, exist_ok=True)
out.convert("RGB").save(path)
print(f"preview: {path}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=SOURCE,
help=f"16 px tileset (default: {SOURCE})")
parser.add_argument("--no-preview", action="store_true",
help="skip the two preview PNGs")
args = parser.parse_args()
source_path = args.source.resolve()
source = load_source(source_path)
if len(ATLAS_SLOTS) != COUNT:
raise SystemExit(f"ATLAS_SLOTS has {len(ATLAS_SLOTS)} entries, "
f"expected {COUNT}")
if len(TILE_NAMES) != COUNT:
raise SystemExit(f"TILE_NAMES has {len(TILE_NAMES)} entries, "
f"expected {COUNT}")
atlas = build_atlas(source)
if atlas.size != (COLS * TILE, ROWS * TILE):
raise SystemExit(f"atlas size {atlas.size} is not "
f"{COLS * TILE}x{ROWS * TILE}")
assert_hard_alpha(atlas, "file_icons.png")
atlas.save(ATLAS)
print(f"atlas: {ATLAS} ({atlas.width}x{atlas.height}, "
f"{COLS}x{ROWS} @ {TILE}px)")
singles: dict[str, Image.Image] = {}
for name, tile_id in SINGLE_ICONS.items():
icon = build_single(source, tile_id)
assert_hard_alpha(icon, name)
target = BASE / f"{name}.png"
icon.save(target)
singles[name] = icon
print(f"icon: {target} ({icon.width}x{icon.height}) "
f"<- tile {tile_id:02d} {TILE_NAMES[tile_id]}")
if not args.no_preview:
build_contact_sheet(source, source_path.with_name(
"tileset1_preview.png"))
build_atlas_preview(atlas, singles,
BASE / "source" / "file_icons_preview.png")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dj6scq4olnonj"
path="res://.godot/imported/packet.png-2a847fa0ce4ca153b815c85ae5cddeab.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/packet.png"
dest_files=["res://.godot/imported/packet.png-2a847fa0ce4ca153b815c85ae5cddeab.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyryfdgvj0s6m"
path="res://.godot/imported/_source_upgrades.png-5c2186a86af26623e1e1ba5a3e0ad2eb.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/_source_upgrades.png"
dest_files=["res://.godot/imported/_source_upgrades.png-5c2186a86af26623e1e1ba5a3e0ad2eb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://05ucaefb4807"
path="res://.godot/imported/lot_channel.png-86f52ea0f61c04f6a60ada643ca85ccc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_channel.png"
dest_files=["res://.godot/imported/lot_channel.png-86f52ea0f61c04f6a60ada643ca85ccc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://brc1psekc17bc"
path="res://.godot/imported/lot_folder.png-dd754300f7e865cf73009868f7d66e6b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_folder.png"
dest_files=["res://.godot/imported/lot_folder.png-dd754300f7e865cf73009868f7d66e6b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://0x7o4yo8dvps"
path="res://.godot/imported/lot_money.png-9b87cd53690d3369a84cabb7576571b2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_money.png"
dest_files=["res://.godot/imported/lot_money.png-9b87cd53690d3369a84cabb7576571b2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dapkbkpx18mgy"
path="res://.godot/imported/lot_shield.png-98b0b28b4edcfac112e9f4e5d43c5364.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_shield.png"
dest_files=["res://.godot/imported/lot_shield.png-98b0b28b4edcfac112e9f4e5d43c5364.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1xbqwgle6adp"
path="res://.godot/imported/lot_sla.png-9730f109736cf5b78256df880bad17bc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_sla.png"
dest_files=["res://.godot/imported/lot_sla.png-9730f109736cf5b78256df880bad17bc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drgl82ovu7vry"
path="res://.godot/imported/lot_slots.png-8d0c9f2a6fd82fdcf134c18f77d41a10.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_slots.png"
dest_files=["res://.godot/imported/lot_slots.png-8d0c9f2a6fd82fdcf134c18f77d41a10.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://stbfx7k5pt2s"
path="res://.godot/imported/lot_speed.png-17cc7beb1294023e976429ca8555aaaa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_speed.png"
dest_files=["res://.godot/imported/lot_speed.png-17cc7beb1294023e976429ca8555aaaa.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cixxo6hxeisdl"
path="res://.godot/imported/lot_timer.png-9bb944960c84e22cd0cc36cadc18aa86.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_timer.png"
dest_files=["res://.godot/imported/lot_timer.png-9bb944960c84e22cd0cc36cadc18aa86.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dc1sclulaata4"
path="res://.godot/imported/lot_udp.png-111b745d551b21f3321882852d252022.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/icons/shop/lot_udp.png"
dest_files=["res://.godot/imported/lot_udp.png-111b745d551b21f3321882852d252022.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+2
View File
@@ -0,0 +1,2 @@
# Source artwork is not imported by Godot: only the derived PNGs in
# art/icons/ (atlas + six fallbacks) are loaded at runtime.
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1vbo2277t4ix"
path="res://.godot/imported/tutor_cat.png-ad3ee184ce969bcd494931d604daab36.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/mascot/tutor_cat.png"
dest_files=["res://.godot/imported/tutor_cat.png-ad3ee184ce969bcd494931d604daab36.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+455 -215
View File
@@ -1,17 +1,23 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Генератор звуков PEAK HOUR 97 (короткие 90s-style SFX + фоновый гул).
"""Генератор звуков PEAK HOUR 97 (тёплые 90s SFX + фоновый гул).
Все ассеты синтезируются программно, без внешних файлов и без сторонних
библиотек (только стандартная библиотека Python 3) — значит, нет проблем
с лицензиями и генерация воспроизводима на любой машине.
Палитра: ночной узел файлового обмена, тёплая бумага, кассетный лоу-фай,
реле и коммутатор. Мягкая сатурация, короткий комнатный хвост, механические
транзиенты. Тональные сигналы — только прима/квинта/октава, без терций,
чтобы не спорить с тональностью музыки. У подтверждений один «фамильный»
FM-колокольчик, у отказов — один приглушённый деревянный язычок.
Запуск:
python3 audio/generate_sfx.py
Результат:
audio/sfx/*.wav — 18 одноразовых эффектов, 30..800 мс
audio/music/hum.wav — гул серверной, 3 с, loop-friendly
audio/sfx/*.wav — 21 одноразовый эффект, 30..800 мс
audio/music/hum.wav — гул серверной, 3 с, loop-friendly (не изменён)
Формат WAV: 22050 Гц, 16 бит, моно, пик нормируется ровно к −6 dBFS
(без клиппинга: 10^(-6/20) ≈ 0.501 < 1.0).
@@ -36,27 +42,40 @@ MUSIC_DIR = os.path.join(HERE, "music")
# Длительности в секундах (SFX обязаны попадать в 30..800 мс).
DUR = {
"click": 0.045,
"click": 0.048,
"drag": 0.380,
"drop": 0.140,
"drop": 0.150,
"drop_error": 0.240,
"recv": 0.330,
"request_ready": 0.450,
"request_ready": 0.430,
"minigame_win": 0.500,
"minigame_fail": 0.500,
"minigame_fail": 0.520,
"modem": 0.800,
"connect_fail": 0.420,
"tick": 0.040,
"tick": 0.038,
"packet_loss": 0.190,
"attack": 0.650,
"attack_blocked": 0.320,
"money_up": 0.430,
"money_down": 0.320,
"money_down": 0.340,
"hour_end": 0.720,
"run_end": 0.800,
"ok": 0.200,
"bad": 0.240,
"stamp": 0.280,
}
HUM_DUR = 3.0
# Ноты: только прима / квинта / октава.
C3 = 130.81
G3 = 196.00
C4 = 261.63
G4 = 392.00
C5 = 523.25
G5 = 783.99
C6 = 1046.50
G6 = 1567.98
# ---------------------------------------------------------------- базовые ОСЦИЛЛЯТОРЫ
@@ -145,7 +164,7 @@ def rich(dur: float, freq: float, weights, bend=None, amp: float = 1.0) -> list:
# ---------------------------------------------------------------- ФИЛЬТРЫ (RBJ biquad)
def biquad(x: list, kind: float, fc: float, q: float = 0.707) -> list:
def biquad(x: list, kind: str, fc: float, q: float = 0.707) -> list:
"""Прямо-циклический биквад: 'lp' | 'hp' | 'bp'."""
w0 = TWO_PI * fc / SR
c = math.cos(w0)
@@ -176,7 +195,7 @@ def periodic_filter(x: list, kind: str, fc: float, q: float = 0.707) -> list:
Прогоняем биквад по 4 копиям подряд и берём последнюю — состояние фильтра
к её началу уже сошлось к периодическому режиму, поэтому первые и последние
отсчёты совпадают по статистике и шум зациклевается без щелчка.
отсчёты совпадают по статистике и шум зацикливается без щелчка.
"""
reps = 4
y = biquad(list(x) * reps, kind, fc, q)
@@ -285,169 +304,303 @@ def write_wav(path: str, x: list) -> None:
def finish(x: list, fin: float = 0.001, fout: float = 0.010) -> list:
x = remove_dc(x)
x = fade(x, fin, fout)
# DC убираем после fade: короткий низкий удар успевает дать ненулевое
# среднее, если срезать его на нецелом числе периодов.
x = remove_dc(x)
return normalize(x)
def limit(x: list, dur: float) -> list:
"""Обрезать хвост по заявленной длительности (для комнатного хвоста)."""
n = nsamp(dur)
return x[:n] if len(x) > n else x
# ---------------------------------------------------------------- ПАЛИТРА: КАССЕТА / КОМНАТА / ТЕМБРЫ
def soft_sat(x: list, drive: float = 1.6) -> list:
"""Мягкая аналоговая сатурация: tanh без жёсткого клиппинга."""
g = math.tanh(drive)
if g <= 1e-9:
return x
return [math.tanh(drive * v) / g for v in x]
def tape(x: list, wow: float = 0.0, flutter: float = 0.0,
lp: float = 6000.0, hp: float = 55.0) -> list:
"""Кассетная окраска: лёгкий wow/flutter, срез верха и низа."""
y = biquad(biquad(x, "hp", hp, 0.7), "lp", lp, 0.7)
if wow <= 0.0 and flutter <= 0.0:
return y
n = len(y)
out = [0.0] * n
for i in range(n):
t = i / SR
pos = (i + wow * math.sin(TWO_PI * 0.55 * t)
+ flutter * math.sin(TWO_PI * 6.3 * t))
if pos < 0.0:
pos = 0.0
j = int(pos)
if j >= n - 1:
out[i] = y[n - 1]
else:
fr = pos - j
out[i] = y[j] * (1.0 - fr) + y[j + 1] * fr
return out
def room(x: list, mix: float = 0.14, decay: float = 0.12,
damp: float = 0.35, fb: float = 0.70) -> list:
"""Короткий комнатный хвост: 4 демпфированные гребёнки (6.8..15 мс)."""
n = len(x) + nsamp(decay)
dry = [0.0] * n
dry[:len(x)] = x
acc = [0.0] * n
for d in (149, 197, 263, 331):
line = [0.0] * d
idx = 0
lp = 0.0
for i in range(n):
v = line[idx]
lp += damp * (v - lp)
line[idx] = dry[i] + fb * lp
acc[i] += v
idx += 1
if idx >= d:
idx = 0
inv = 1.0 / 4.0
return [dry[i] * (1.0 - mix) + acc[i] * inv * mix for i in range(n)]
def contact(rng: random.Random, dur: float = 0.004, lo: float = 500.0,
hi: float = 2600.0, tau: float = 0.0014) -> list:
"""Механический транзиент: очень короткий полосовой шум."""
n = biquad(biquad(noise(dur, rng), "hp", lo, 0.7), "lp", hi, 0.8)
return apply_env(n, env_exp(dur, tau))
def warm_bell(dur: float, f: float, tau: float = 0.22, amp: float = 1.0,
index: float = 1.1) -> list:
"""Фамильный тембр подтверждений: мягкий FM-колокол.
Модулятор на октаву выше — боковые полосы остаются гармоническими,
поэтому сигнал нейтрален к тональности музыки.
"""
n = nsamp(dur)
out = [0.0] * n
ph = 0.0
for i in range(n):
t = i / SR
idx = index * math.exp(-t / max(1e-4, tau * 0.55))
ph += TWO_PI * f / SR
if ph >= TWO_PI:
ph -= TWO_PI
out[i] = amp * math.sin(ph + idx * math.sin(TWO_PI * f * 2.0 * t))
out = [v * math.exp(-(i / SR) / tau) for i, v in enumerate(out)]
return fade(out, 0.003, min(0.05, dur * 0.5))
def muted_reed(dur: float, f: float, tau: float = 0.16, amp: float = 1.0,
lp: float = 1500.0) -> list:
"""Фамильный тембр отказов: приглушённый деревянный язычок.
Только нечётные гармоники (1/3/5) и низкий срез — без «аркадного» визга.
"""
n = nsamp(dur)
out = [0.0] * n
for mult, w, td in ((1.0, 1.0, tau), (3.0, 0.28, tau * 0.55),
(5.0, 0.10, tau * 0.35)):
part = tone(dur, f * mult, amp=w)
part = apply_env(part, env_exp(dur, td))
out = [a + b for a, b in zip(out, part)]
out = biquad(out, "lp", lp, 0.8)
out = soft_sat([v * 0.9 for v in out], 1.4)
return fade(out, 0.004, min(0.04, dur * 0.5))
# ---------------------------------------------------------------- ЗВУКИ
def s_click(rng: random.Random) -> list:
"""Сухой щелчок: короткий шумовой транзиент + звонкий пик + тело."""
"""Мягкий щелчок клавиши/реле: контакт + деревянное тело."""
out = silence(DUR["click"])
tr = apply_env(biquad(noise(0.004, rng), "hp", 1400.0, 0.8), env_exp(0.004, 0.0012))
place(out, tr, 0.0, 1.0)
ping = apply_env(tone(0.020, 2750.0), env_exp(0.020, 0.0030))
place(out, ping, 0.0, 0.55)
body = apply_env(tone(0.034, 940.0), env_exp(0.034, 0.0065))
place(out, body, 0.0, 0.45)
return finish(out, 0.0005, 0.010)
place(out, contact(rng, 0.004, 500.0, 2600.0, 0.0014), 0.0, 1.0)
# второй контакт реле — совсем тихий
place(out, contact(rng, 0.003, 700.0, 2200.0, 0.0012), 0.007, 0.40)
body = apply_env(tri(0.035, 320.0), env_exp(0.035, 0.009))
body = biquad(body, "lp", 1600.0, 0.7)
place(out, body, 0.001, 0.55)
ping = apply_env(tone(0.016, 1280.0), env_exp(0.016, 0.0045))
place(out, ping, 0.0, 0.10)
x = soft_sat(tape(out, lp=5600.0), 1.5)
return finish(x, 0.0005, 0.010)
def s_drag(rng: random.Random) -> list:
"""Тихий шорох: полосовой шум с медленным «дыханием»."""
n = noise(DUR["drag"], rng)
n = biquad(biquad(n, "hp", 520.0, 0.7), "lp", 3600.0, 0.9)
am = shape_random(DUR["drag"], rng, seg=0.025, lo=0.35, hi=1.0)
"""Бумажный шорох: полосовой шум с «дыханием» и редкими тресками."""
d = DUR["drag"]
n = biquad(biquad(noise(d, rng), "hp", 240.0, 0.7), "lp", 2600.0, 0.8)
am = shape_random(d, rng, seg=0.028, lo=0.35, hi=1.0)
x = [v * a for v, a in zip(n, am)]
return finish(x, 0.030, 0.080)
for _ in range(7):
at = rng.uniform(0.010, d - 0.020)
ln = rng.uniform(0.002, 0.006)
place(x, contact(rng, ln, 700.0, 3600.0, ln * 0.35), at,
rng.uniform(0.10, 0.28))
x = tape(x, wow=2.2, flutter=0.5, lp=4800.0)
return finish(soft_sat(x, 1.3), 0.030, 0.080)
def s_drop(rng: random.Random) -> list:
"""Мягкий стук: нисходящий свип + приглушённый контакт."""
out = silence(DUR["drop"])
swp = tone(0.12, lambda t: 55.0 + 115.0 * math.exp(-t / 0.035))
swp = apply_env(swp, env_exp(0.12, 0.045))
place(out, swp, 0.0, 1.0)
ct = apply_env(biquad(noise(0.010, rng), "lp", 750.0, 0.7), env_exp(0.010, 0.004))
place(out, ct, 0.0, 0.7)
return finish(out, 0.003, 0.035)
"""Бумажно-деревянный стук: низкое тело + контакт + шелест бумаги."""
d = DUR["drop"]
out = silence(d)
th = tone(0.145, lambda t: 88.0 + 62.0 * math.exp(-t / 0.020))
th = apply_env(th, env_exp(0.145, 0.036))
place(out, th, 0.0, 0.95)
wood = contact(rng, 0.012, 200.0, 900.0, 0.004)
place(out, wood, 0.0, 0.55)
paper = biquad(noise(0.035, rng), "bp", 1700.0, 0.9)
paper = apply_env(paper, env_exp(0.035, 0.012))
place(out, paper, 0.0, 0.35)
x = room(out, mix=0.10, decay=0.090)
x = soft_sat(tape(x, lp=5000.0), 1.4)
return finish(limit(x, d), 0.002, 0.025)
def s_drop_error(rng: random.Random) -> list:
"""Глухой удар «не туда»: низкий спад без ярких гармоник."""
out = silence(DUR["drop_error"])
swp = tone(0.22, lambda t: 40.0 + 60.0 * math.exp(-t / 0.06))
swp = apply_env(swp, env_exp(0.22, 0.080))
place(out, biquad(swp, "lp", 320.0, 0.7), 0.0, 1.0)
thud = apply_env(biquad(noise(0.030, rng), "lp", 420.0, 0.6), env_exp(0.030, 0.012))
place(out, thud, 0.0, 0.6)
return finish(out, 0.004, 0.070)
"""Отказ «не туда»: глухой удар и приглушённый нисходящий язычок."""
d = DUR["drop_error"]
out = silence(d)
th = tone(0.200, lambda t: 58.0 + 40.0 * math.exp(-t / 0.050))
th = apply_env(biquad(th, "lp", 260.0, 0.7), env_exp(0.200, 0.055))
place(out, th, 0.0, 1.0)
place(out, contact(rng, 0.008, 150.0, 1100.0, 0.0024), 0.0, 0.55)
place(out, contact(rng, 0.007, 150.0, 1000.0, 0.0022), 0.062, 0.40)
reed = muted_reed(0.140, G3, tau=0.090, lp=900.0)
place(out, reed, 0.052, 0.42)
x = room(out, mix=0.10, decay=0.100)
x = soft_sat(tape(x, lp=3600.0), 1.3)
return finish(limit(x, d), 0.003, 0.045)
def s_recv(rng: random.Random) -> list:
"""Короткий сигнал + запись: писк уведомления, второй писк, шум ленты."""
out = silence(DUR["recv"])
beep = apply_env(tone(0.100, 880.0), env_ar(0.100, 0.005, 0.020))
place(out, beep, 0.0, 1.0)
blip = apply_env(tone(0.070, 1760.0), env_ar(0.070, 0.004, 0.015))
place(out, blip, 0.130, 0.75)
tape = biquad(biquad(noise(DUR["recv"], rng), "hp", 2200.0, 0.7), "lp", 6500.0, 0.7)
place(out, tape, 0.0, 0.10)
return finish(out, 0.002, 0.030)
"""Телеграфный двухнотный сигнал: прима/квинта с релейным контактом."""
d = DUR["recv"]
out = silence(d)
for at, f, g in ((0.0, C5, 1.0), (0.115, G5, 0.86)):
seg = 0.130
pulse = [a + 0.35 * b for a, b in zip(tone(seg, f), tone(seg, f * 2.0))]
pulse = apply_env(pulse, env_ar(seg, 0.005, 0.050))
pulse = [v * math.exp(-(i / SR) / 0.160) for i, v in enumerate(pulse)]
place(out, pulse, at, g)
place(out, contact(rng, 0.004, 900.0, 3200.0, 0.0013), at, 0.30)
hiss = biquad(biquad(noise(d, rng), "hp", 2000.0, 0.7), "lp", 6000.0, 0.7)
place(out, hiss, 0.0, 0.025)
x = room(out, mix=0.08, decay=0.080)
x = soft_sat(tape(x, wow=1.0, lp=5600.0), 1.3)
return finish(limit(x, d), 0.002, 0.030)
def s_request_ready(rng: random.Random) -> list:
"""Восходящий аккорд: три ноты вверх, мягкая атака."""
"""Тёплый восходящий сигнал: прима → квинта → октава, FM-колокол."""
del rng
out = silence(DUR["request_ready"])
notes = [(523.25, 0.0), (659.25, 0.130), (783.99, 0.260)]
for i, (f, at) in enumerate(notes):
seg = DUR["request_ready"] - at
t = rich(seg, f, [(1.0, 1.0), (2.0, 0.30), (3.0, 0.12)])
t = apply_env(t, env_ar(seg, 0.010, 0.12))
t = [v * math.exp(-(i2 / SR) / 0.55) for i2, v in enumerate(t)]
place(out, t, at, 1.0 - i * 0.12)
return finish(out, 0.008, 0.090)
d = DUR["request_ready"]
out = silence(d)
notes = ((C4, 0.000, 0.90), (G4, 0.105, 0.90), (C5, 0.215, 1.0))
for f, at, g in notes:
seg = d - at
place(out, warm_bell(seg, f, tau=0.26, index=1.05), at, g)
x = room(out, mix=0.14, decay=0.120)
x = soft_sat(tape(x, wow=1.5, lp=5200.0), 1.4)
return finish(limit(x, d), 0.006, 0.070)
def s_minigame_win(rng: random.Random) -> list:
"""Победа: восходящее арпеджио из четырёх нот."""
"""Победа: короткая фраза тем же колоколом, C5 → G5 → C6."""
del rng
out = silence(DUR["minigame_win"])
notes = [523.25, 659.25, 783.99, 1046.50]
for i, f in enumerate(notes):
at = i * 0.085
seg = DUR["minigame_win"] - at
t = tri(seg, f, amp=1.0)
t = [v + 0.30 * math.sin(TWO_PI * f * 2.0 * (i2 / SR)) for i2, v in enumerate(t)]
tau = 0.22 if i == len(notes) - 1 else 0.10
t = apply_env(t, env_exp(seg, tau))
t = fade(t, 0.003, 0.010)
place(out, t, at, 1.0 - i * 0.10)
return finish(out, 0.003, 0.070)
d = DUR["minigame_win"]
out = silence(d)
notes = ((C5, 0.000, 0.82, 0.20), (G5, 0.105, 0.90, 0.22),
(C6, 0.225, 1.0, 0.28))
for f, at, g, tau in notes:
seg = d - at
place(out, warm_bell(seg, f, tau=tau, index=1.15), at, g)
# тёплая опора на октаву ниже
place(out, warm_bell(0.420, C3, tau=0.30, index=0.7), 0.010, 0.20)
x = room(out, mix=0.16, decay=0.140)
x = soft_sat(tape(x, wow=1.2, lp=5000.0), 1.5)
return finish(limit(x, d), 0.004, 0.070)
def s_minigame_fail(rng: random.Random) -> list:
"""Провал: нисходящие ноты с изгибом и низкий гул в хвосте."""
out = silence(DUR["minigame_fail"])
notes = [392.00, 330.00, 262.00, 196.00]
for i, f in enumerate(notes):
at = i * 0.090
seg = DUR["minigame_fail"] - at # ноты укладываются ровно в 500 мс
bend = lambda t, _f=f, _s=seg: 1.0 - 0.08 * min(1.0, t / max(0.01, _s))
t = rich(seg, f, [(1.0, 1.0), (2.0, 0.45), (3.0, 0.25)], bend=bend)
t = apply_env(t, env_ar(seg, 0.006, 0.06))
t = apply_env(t, env_exp(seg, 0.13))
place(out, t, at, 1.0 - i * 0.10)
# низкий вибрирующий «бзз» в конце
buzz_d = 0.230
buzz = square(buzz_d, 98.0, amp=1.0)
am = [0.55 + 0.45 * math.sin(TWO_PI * 9.0 * (i / SR)) for i in range(len(buzz))]
buzz = [v * a for v, a in zip(buzz, am)]
buzz = apply_env(biquad(buzz, "lp", 900.0, 0.8), env_ar(buzz_d, 0.010, 0.06))
place(out, buzz, 0.270, 0.55)
return finish(out, 0.004, 0.060)
"""Провал: та же «деревянная» семья, G4 → C4 → C3 (квинта и октава вниз)."""
d = DUR["minigame_fail"]
out = silence(d)
notes = ((G4, 0.000, 0.95, 0.14), (C4, 0.110, 0.92, 0.14),
(C3, 0.230, 1.0, 0.18))
for f, at, g, tau in notes:
seg = d - at
place(out, muted_reed(seg, f, tau=tau, lp=1200.0), at, g)
th = apply_env(tone(0.200, 70.0), env_exp(0.200, 0.055))
place(out, th, 0.0, 0.40)
place(out, contact(rng, 0.006, 300.0, 1600.0, 0.0018), 0.0, 0.35)
x = room(out, mix=0.10, decay=0.120)
x = soft_sat(tape(x, lp=3800.0), 1.4)
return finish(limit(x, d), 0.004, 0.060)
def s_modem(rng: random.Random) -> list:
"""Модемный скриз 0.8 с: набор → свип → резкий цифровой шум → тон-шаги."""
out = silence(DUR["modem"])
"""Модем 0.8 с: набор → тёплый свип → FM-варблер → тон-шаги.
Никакого свистящего шума 1.8 кГц: «цифровой» характер даёт мягкий
FM-варблер и стробированный шум ниже 2.5 кГц.
"""
d = DUR["modem"]
out = silence(d)
# 1) DTMF-набор (два тона)
def dtmf(seg: float, lo: float, hi: float) -> list:
t = [a + 0.5 * b for a, b in zip(tone(seg, lo), tone(seg, hi))]
return apply_env(t, env_ar(seg, 0.003, 0.010))
return apply_env(biquad(t, "lp", 3000.0, 0.7), env_ar(seg, 0.003, 0.010))
place(out, dtmf(0.050, 697.0, 1209.0), 0.0, 0.9)
place(out, dtmf(0.050, 770.0, 1336.0), 0.075, 0.9)
place(out, dtmf(0.050, 697.0, 1209.0), 0.0, 0.55)
place(out, dtmf(0.050, 770.0, 1336.0), 0.070, 0.55)
# 2) растущий несущий свип
sweep = tone(0.150, lambda t: 350.0 + 1250.0 * (t / 0.150))
sweep = apply_env(biquad(sweep, "lp", 4000.0, 0.7), env_ar(0.150, 0.010, 0.030))
place(out, sweep, 0.140, 0.85)
sweep = tone(0.130, lambda t: 320.0 + 680.0 * (t / 0.130))
sweep = apply_env(biquad(sweep, "lp", 2200.0, 0.7), env_ar(0.130, 0.008, 0.030))
place(out, sweep, 0.130, 0.60)
# 3) «скрип» цифрового канала: FM-носитель со случайными фазовыми срывами
seg_d = 0.330
# FM-варблер вместо визга: несущая ~1 кГц, мягкая девиация.
seg_d = 0.300
n = nsamp(seg_d)
screech = [0.0] * n
warble = [0.0] * n
ph = 0.0
reset_every = max(1, nsamp(0.007))
gate = 1.0
reset_every = max(1, nsamp(0.010))
for i in range(n):
t = i / SR
if i % reset_every == 0:
gate = rng.uniform(0.55, 1.0)
f = 1800.0 + 620.0 * math.sin(TWO_PI * 87.0 * t) + rng.uniform(-260.0, 260.0)
gate = rng.uniform(0.65, 1.0)
f = 980.0 + 170.0 * math.sin(TWO_PI * 9.5 * t) + 90.0 * math.sin(TWO_PI * 31.0 * t)
ph += TWO_PI * f / SR
if ph >= TWO_PI:
ph -= TWO_PI
screech[i] = math.sin(ph) * gate * (0.62 + 0.38 * math.sin(TWO_PI * 95.0 * t))
hiss = biquad(biquad(noise(seg_d, rng), "hp", 1500.0, 0.7), "lp", 5200.0, 0.8)
hiss = [v * r for v, r in zip(hiss, shape_random(seg_d, rng, 0.006, 0.3, 1.0))]
screech = [a * 0.75 + b * 0.45 for a, b in zip(screech, hiss)]
screech = apply_env(screech, env_ar(seg_d, 0.006, 0.020))
place(out, screech, 0.290, 0.95)
warble[i] = math.sin(ph) * gate * (0.68 + 0.32 * math.sin(TWO_PI * 13.0 * t))
hiss = biquad(biquad(noise(seg_d, rng), "hp", 500.0, 0.7), "lp", 2400.0, 0.8)
hiss = [v * r for v, r in zip(hiss, shape_random(seg_d, rng, 0.008, 0.3, 1.0))]
warble = [a * 0.80 + b * 0.30 for a, b in zip(warble, hiss)]
warble = apply_env(warble, env_ar(seg_d, 0.010, 0.040))
place(out, warble, 0.270, 0.75)
# 4) финальные тон-шаги (переключение двух частот каждые 12 мс)
tail_d = DUR["modem"] - 0.620
# финальные тон-шаги (переключение частот каждые 14 мс)
tail_d = d - 0.580
tn = nsamp(tail_d)
tail = [0.0] * tn
step = max(1, nsamp(0.012))
ramp = max(1, nsamp(0.0015))
step = max(1, nsamp(0.014))
ramp = max(1, nsamp(0.0020))
ph2 = 0.0
for i in range(tn):
f = 1650.0 if (i // step) % 2 == 0 else 1900.0
f = 1150.0 if (i // step) % 2 == 0 else 1420.0
ph2 += TWO_PI * f / SR
if ph2 >= TWO_PI:
ph2 -= TWO_PI
@@ -458,137 +611,218 @@ def s_modem(rng: random.Random) -> list:
elif pos > step - ramp:
edge = (step - pos) / ramp
tail[i] = math.sin(ph2) * edge
tail = apply_env(tail, env_ar(tail_d, 0.002, 0.020))
place(out, tail, 0.620, 0.85)
return finish(out, 0.002, 0.015)
tail = apply_env(biquad(tail, "lp", 2600.0, 0.7), env_ar(tail_d, 0.002, 0.030))
place(out, tail, 0.580, 0.58)
x = soft_sat(tape(out, wow=1.8, flutter=1.0, lp=4600.0), 1.6)
return finish(x, 0.002, 0.015)
def s_connect_fail(rng: random.Random) -> list:
"""«бзз» — глухой прерывистый сигнал ошибки."""
out = silence(DUR["connect_fail"])
pulse_d = 0.100
"""«Линия занята»: три приглушённых импульса на квинте, без квадрата."""
d = DUR["connect_fail"]
out = silence(d)
for k in range(3):
p = square(pulse_d, 140.0, amp=1.0)
grit = biquad(noise(pulse_d, rng), "lp", 1300.0, 0.7)
p = [a + 0.18 * b for a, b in zip(p, grit)]
p = apply_env(biquad(p, "lp", 2600.0, 0.7), env_ar(pulse_d, 0.004, 0.012))
place(out, p, k * 0.120, 1.0 - k * 0.12)
return finish(out, 0.003, 0.040)
at = k * 0.115
f = G3 if k % 2 == 0 else (G3 * 1.5)
seg = 0.100
pulse = rich(seg, f, [(1.0, 1.0), (2.0, 0.20), (3.0, 0.08)])
pulse = apply_env(biquad(pulse, "lp", 1500.0, 0.8), env_ar(seg, 0.012, 0.030))
pulse = apply_env(pulse, env_exp(seg, 0.090))
place(out, pulse, at, 0.85 - k * 0.08)
place(out, contact(rng, 0.004, 400.0, 1800.0, 0.0015), at, 0.30)
low = apply_env(tone(0.380, G3 / 2.0), env_exp(0.380, 0.160))
place(out, low, 0.0, 0.30)
x = room(out, mix=0.10, decay=0.100)
x = soft_sat(tape(x, lp=3400.0), 1.4)
return finish(limit(x, d), 0.003, 0.040)
def s_tick(rng: random.Random) -> list:
"""Тиканье: очень короткий сухой щелчок (< 5 с)."""
out = silence(DUR["tick"])
tr = apply_env(biquad(noise(0.002, rng), "hp", 2500.0, 0.8), env_exp(0.002, 0.0006))
place(out, tr, 0.0, 1.0)
ping = apply_env(tone(0.010, 3600.0), env_exp(0.010, 0.0018))
place(out, ping, 0.0, 0.50)
body = apply_env(tone(0.024, 1700.0), env_exp(0.024, 0.0040))
place(out, body, 0.0, 0.45)
return finish(out, 0.0003, 0.008)
"""Тиканье: очень короткий мягкий контакт, тише click (см. trim в Audio)."""
d = DUR["tick"]
out = silence(d)
place(out, contact(rng, 0.0025, 600.0, 2000.0, 0.0010), 0.0, 1.0)
body = apply_env(tri(0.022, 520.0), env_exp(0.022, 0.005))
place(out, biquad(body, "lp", 1400.0, 0.7), 0.0, 0.50)
place(out, apply_env(tone(0.018, 190.0), env_exp(0.018, 0.006)), 0.0, 0.20)
x = soft_sat(tape(out, lp=5000.0), 1.4)
return finish(x, 0.0003, 0.008)
def s_packet_loss(rng: random.Random) -> list:
"""Потери пакетов: россыпь коротких тресков."""
out = silence(DUR["packet_loss"])
for _ in range(14):
at = rng.uniform(0.0, DUR["packet_loss"] - 0.014)
ln = rng.uniform(0.003, 0.011)
b = biquad(biquad(noise(ln, rng), "hp", 1500.0, 0.7), "lp", 6000.0, 0.7)
b = apply_env(b, env_exp(ln, max(0.0008, ln * 0.4)))
place(out, b, at, rng.uniform(0.45, 1.0))
return finish(out, 0.001, 0.015)
"""Потери пакетов: мягкие трески и короткие провалы статики."""
d = DUR["packet_loss"]
out = silence(d)
bed = biquad(biquad(noise(d, rng), "hp", 500.0, 0.7), "lp", 3000.0, 0.8)
gate = shape_random(d, rng, seg=0.012, lo=0.0, hi=1.0)
gate = [1.0 if v > 0.45 else 0.0 for v in gate]
place(out, [v * g for v, g in zip(bed, gate)], 0.0, 0.10)
for _ in range(11):
at = rng.uniform(0.005, d - 0.018)
ln = rng.uniform(0.002, 0.007)
place(out, contact(rng, ln, 700.0, 3200.0, ln * 0.40), at,
rng.uniform(0.30, 1.0))
x = room(out, mix=0.06, decay=0.060)
x = soft_sat(tape(x, lp=4600.0), 1.4)
return finish(limit(x, d), 0.001, 0.015)
def s_attack(rng: random.Random) -> list:
"""Тревожный сигнал: двухтоновая сирена."""
out = silence(DUR["attack"])
period = 0.080
k = 0
"""Аналоговая тревога: чередование примы/квинты, низкий гул, без визга."""
d = DUR["attack"]
out = silence(d)
period = 0.095
t0 = 0.0
while t0 < DUR["attack"] - 0.010:
seg = min(period, DUR["attack"] - t0)
f = 880.0 if k % 2 == 0 else 770.0
t = rich(seg, f, [(1.0, 1.0), (3.0, 0.35), (5.0, 0.18)])
t = apply_env(biquad(t, "lp", 3200.0, 0.7), env_ar(seg, 0.004, 0.014))
place(out, t, t0, 1.0)
low = apply_env(tone(seg, 110.0), env_ar(seg, 0.004, 0.020))
place(out, low, t0, 0.45)
k = 0
while t0 < d - 0.020:
seg = min(0.082, d - t0)
f = G4 if k % 2 == 0 else (G4 * 1.5)
v = rich(seg, f, [(1.0, 1.0), (2.0, 0.28), (3.0, 0.10)])
v = apply_env(biquad(v, "lp", 1700.0, 0.8), env_ar(seg, 0.012, 0.030))
place(out, v, t0, 0.85)
place(out, apply_env(tone(seg, f * 1.5), env_ar(seg, 0.012, 0.030)),
t0, 0.10)
t0 += period
k += 1
return finish(out, 0.003, 0.030)
drone = [a + b for a, b in zip(tone(d, 55.0), tone(d, 110.0, 0.35))]
drone = apply_env(drone, env_ar(d, 0.020, 0.100))
place(out, drone, 0.0, 0.30)
x = room(out, mix=0.10, decay=0.100)
x = soft_sat(tape(x, wow=2.5, lp=4000.0), 1.6)
return finish(limit(x, d), 0.003, 0.030)
def s_attack_blocked(rng: random.Random) -> list:
"""Металлический удар: неармоничные обертоны."""
out = silence(DUR["attack_blocked"])
partials = [(430.0, 1.0, 0.060), (713.0, 0.70, 0.080), (1097.0, 0.60, 0.065),
(1621.0, 0.45, 0.050), (2411.0, 0.30, 0.035)]
"""Мягкий металлический удар: гармонические партиалы C4/G4/C5 + тело."""
d = DUR["attack_blocked"]
out = silence(d)
partials = ((G4, 1.00, 0.340), (G4 * 1.5, 0.62, 0.270),
(G4 * 2.0, 0.42, 0.210), (G4 * 3.0, 0.18, 0.130))
for f, w, tau in partials:
t = apply_env(tone(0.300, f), env_exp(0.300, tau))
t = apply_env(tone(0.310, f), env_exp(0.310, tau))
place(out, t, 0.0, w)
strike = apply_env(biquad(noise(0.007, rng), "hp", 2000.0, 0.7), env_exp(0.007, 0.0022))
place(out, strike, 0.0, 1.0)
body = apply_env(tone(0.120, 90.0), env_exp(0.120, 0.030))
place(out, body, 0.0, 0.7)
return finish(out, 0.001, 0.030)
strike = contact(rng, 0.008, 300.0, 3200.0, 0.0022)
place(out, strike, 0.0, 0.70)
thud = apply_env(tone(0.180, 80.0), env_exp(0.180, 0.050))
place(out, thud, 0.0, 0.60)
x = room(out, mix=0.12, decay=0.120)
x = soft_sat(tape(x, lp=4400.0), 1.5)
return finish(limit(x, d), 0.001, 0.030)
def s_money_up(rng: random.Random) -> list:
"""Касса: «ka-ching» — щелчки механики и звон."""
out = silence(DUR["money_up"])
for at, g in ((0.0, 1.0), (0.130, 0.7)):
clack = biquad(biquad(noise(0.007, rng), "hp", 1000.0, 0.7), "lp", 4200.0, 0.7)
clack = apply_env(clack, env_exp(0.007, 0.0022))
place(out, clack, at, g)
mech = apply_env(tone(0.060, 380.0), env_exp(0.060, 0.016))
place(out, mech, 0.0, 0.6)
for f, w, tau in ((1570.0, 1.0, 0.22), (2350.0, 0.62, 0.15),
(3140.0, 0.42, 0.11), (4720.0, 0.22, 0.07)):
t = apply_env(tone(0.360, f), env_exp(0.360, tau))
place(out, t, 0.060, w)
return finish(out, 0.001, 0.030)
"""Касса в лоу-фае: щелчки механики, тёплый «ka-ching» на квинте."""
d = DUR["money_up"]
out = silence(d)
slide = biquad(noise(0.160, rng), "bp", 900.0, 0.7)
slide = apply_env(slide, env_ar(0.160, 0.040, 0.080))
place(out, slide, 0.0, 0.16)
for at, g in ((0.0, 0.90), (0.085, 0.65)):
place(out, contact(rng, 0.008, 400.0, 3200.0, 0.0022), at, g)
mech = apply_env(tri(0.060, 210.0), env_exp(0.060, 0.018))
place(out, mech, 0.0, 0.45)
place(out, warm_bell(0.340, C6, tau=0.150, index=0.85), 0.070, 0.75)
place(out, warm_bell(0.340, G6, tau=0.120, index=0.70), 0.070, 0.40)
place(out, warm_bell(0.340, C3, tau=0.240, index=0.65), 0.070, 0.18)
x = room(out, mix=0.10, decay=0.110)
x = soft_sat(tape(x, lp=5600.0), 1.5)
return finish(limit(x, d), 0.001, 0.030)
def s_money_down(rng: random.Random) -> list:
"""Глухой вычет: сползающий вниз тон."""
out = silence(DUR["money_down"])
swp = tone(0.300, lambda t: 180.0 + 240.0 * math.exp(-t / 0.09))
swp = apply_env(biquad(swp, "lp", 950.0, 0.7), env_exp(0.300, 0.100))
place(out, swp, 0.0, 1.0)
thud = apply_env(biquad(noise(0.020, rng), "lp", 520.0, 0.6), env_exp(0.020, 0.008))
place(out, thud, 0.0, 0.7)
return finish(out, 0.003, 0.060)
"""Гильотина кассы: «ш-ш-ш» лезвия, сползающий тон и деревянный стук."""
d = DUR["money_down"]
out = silence(d)
blade = biquad(noise(0.180, rng), "bp", 1600.0, 0.8)
blade = apply_env(blade, env_exp(0.180, 0.040))
place(out, blade, 0.020, 0.50)
swp = tone(0.220, lambda t: 75.0 + 150.0 * math.exp(-t / 0.070))
swp = apply_env(biquad(swp, "lp", 700.0, 0.7), env_exp(0.220, 0.080))
place(out, swp, 0.0, 0.90)
place(out, contact(rng, 0.010, 200.0, 1400.0, 0.0030), 0.190, 0.70)
thud = apply_env(tone(0.140, 68.0), env_exp(0.140, 0.040))
place(out, thud, 0.190, 0.75)
x = room(out, mix=0.10, decay=0.100)
x = soft_sat(tape(x, lp=4200.0), 1.5)
return finish(limit(x, d), 0.003, 0.050)
def s_hour_end(rng: random.Random) -> list:
"""Сигнал смены: колокольные две ноты."""
"""Сигнал смены: интеркомный колокол, G5 → C5 (квинта вниз)."""
del rng
out = silence(DUR["hour_end"])
for at, f in ((0.0, 783.99), (0.260, 1046.50)):
for mult, w, tau in ((1.0, 1.0, 0.34), (2.76, 0.35, 0.18), (5.4, 0.15, 0.10)):
seg = DUR["hour_end"] - at
t = apply_env(tone(seg, f * mult), env_exp(seg, tau))
place(out, t, at, w)
return finish(out, 0.003, 0.100)
d = DUR["hour_end"]
out = silence(d)
for at, f, g in ((0.0, G5, 1.0), (0.270, C5, 0.95)):
seg = d - at
place(out, warm_bell(seg, f, tau=0.240, index=1.6), at, g)
place(out, warm_bell(0.500, C3, tau=0.340, index=0.8), 0.010, 0.25)
x = room(out, mix=0.18, decay=0.160)
x = biquad(x, "hp", 220.0, 0.7) # маленький интеркомный динамик
x = soft_sat(tape(x, wow=1.0, lp=4200.0), 1.4)
return finish(limit(x, d), 0.003, 0.100)
def s_run_end(rng: random.Random) -> list:
"""Длинный спад конца забега: удар + низ + уходящий аккорд."""
out = silence(DUR["run_end"])
impact = biquad(biquad(noise(0.030, rng), "hp", 300.0, 0.7), "lp", 4200.0, 0.7)
"""Конец забега: удар, низкий выдох и тёплый аккорд C3/G3/C4."""
d = DUR["run_end"]
out = silence(d)
impact = biquad(biquad(noise(0.030, rng), "hp", 250.0, 0.7), "lp", 3200.0, 0.7)
impact = apply_env(impact, env_exp(0.030, 0.010))
place(out, impact, 0.0, 0.9)
low = tone(0.500, lambda t: 38.0 + 92.0 * math.exp(-t / 0.11))
low = apply_env(low, env_exp(0.500, 0.150))
place(out, low, 0.0, 1.0)
for f in (130.81, 196.00, 261.63):
t = rich(0.790, f, [(1.0, 1.0), (2.0, 0.40), (3.0, 0.18)])
t = apply_env(t, env_exp(0.790, 0.360))
place(out, t, 0.010, 0.75)
shim = apply_env(tone(0.500, 1180.0), env_exp(0.500, 0.220))
place(out, shim, 0.0, 0.20)
return finish(out, 0.004, 0.150)
place(out, impact, 0.0, 0.80)
low = tone(0.600, lambda t: 38.0 + 92.0 * math.exp(-t / 0.110))
low = apply_env(low, env_exp(0.600, 0.150))
place(out, low, 0.0, 0.90)
for f, g in ((C3, 0.55), (G3, 0.42), (C4, 0.50)):
t = warm_bell(0.740, f, tau=0.340, index=0.80)
place(out, t, 0.010, g)
place(out, warm_bell(0.500, C5, tau=0.220, index=0.65), 0.050, 0.20)
x = room(out, mix=0.20, decay=0.180)
x = soft_sat(tape(x, wow=1.4, lp=4400.0), 1.5)
return finish(limit(x, d), 0.004, 0.150)
def s_ok(rng: random.Random) -> list:
"""Мягкое подтверждение: короткая фраза колоколом C5 → G5."""
del rng
d = DUR["ok"]
out = silence(d)
for f, at, g in ((C5, 0.0, 0.85), (G5, 0.065, 1.0)):
seg = d - at
place(out, warm_bell(seg, f, tau=0.110, index=0.90), at, g)
x = room(out, mix=0.08, decay=0.070)
x = soft_sat(tape(x, lp=5200.0), 1.3)
return finish(limit(x, d), 0.003, 0.030)
def s_bad(rng: random.Random) -> list:
"""Мягкий отказ: две низкие деревянные ноты G3 → C3, без «бзз»."""
d = DUR["bad"]
out = silence(d)
for f, at, g in ((G3, 0.0, 0.90), (C3, 0.085, 1.0)):
seg = d - at
place(out, muted_reed(seg, f, tau=0.105, lp=1100.0), at, g)
place(out, apply_env(tone(0.150, 65.0), env_exp(0.150, 0.045)), 0.0, 0.30)
x = room(out, mix=0.07, decay=0.080)
x = soft_sat(tape(x, lp=2900.0), 1.3)
return finish(limit(x, d), 0.003, 0.035)
def s_stamp(rng: random.Random) -> list:
"""Печать/штамп: бумажный удар, деревянное тело и короткий хвост."""
d = DUR["stamp"]
out = silence(d)
th = tone(0.200, lambda t: 70.0 + 45.0 * math.exp(-t / 0.015))
th = apply_env(th, env_exp(0.200, 0.040))
place(out, th, 0.0, 1.0)
wood = contact(rng, 0.015, 180.0, 1000.0, 0.0040)
place(out, wood, 0.0, 0.70)
crunch = biquad(noise(0.060, rng), "bp", 1200.0, 0.8)
crunch = apply_env(crunch, env_exp(0.060, 0.015))
place(out, crunch, 0.004, 0.40)
x = room(out, mix=0.10, decay=0.100)
x = soft_sat(tape(x, lp=4200.0), 1.5)
return finish(limit(x, d), 0.002, 0.035)
def s_hum(rng: random.Random) -> list:
@@ -640,6 +874,9 @@ SFX_GENERATORS = {
"money_down": s_money_down,
"hour_end": s_hour_end,
"run_end": s_run_end,
"ok": s_ok,
"bad": s_bad,
"stamp": s_stamp,
}
@@ -660,6 +897,9 @@ def check_sfx(name: str, x: list) -> list:
errs.append("пик %.2f dBFS" % db)
if max(abs(v) for v in x) > 1.0:
errs.append("клип")
dc = sum(x) / max(1, len(x))
if abs(dc) > 0.001:
errs.append("DC %.4f" % dc)
return errs
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c5e6gil6gxxmp"
path="res://.godot/imported/defeat.mp3-7e698f993d537d59b939608a0abcf708.mp3str"
[deps]
source_file="res://audio/music/defeat.mp3"
dest_files=["res://.godot/imported/defeat.mp3-7e698f993d537d59b939608a0abcf708.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b6ua70k24xsp7"
path="res://.godot/imported/game.mp3-3720d552177761471b052a3420216bb1.mp3str"
[deps]
source_file="res://audio/music/game.mp3"
dest_files=["res://.godot/imported/game.mp3-3720d552177761471b052a3420216bb1.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b6vxk0i2pd0of"
path="res://.godot/imported/low_sla.mp3-a6f69b93336356d63c353431bcf1405a.mp3str"
[deps]
source_file="res://audio/music/low_sla.mp3"
dest_files=["res://.godot/imported/low_sla.mp3-a6f69b93336356d63c353431bcf1405a.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bxdgubpqsktv3"
path="res://.godot/imported/magazine.mp3-b6f09f5db14feb1428451d6c639c4105.mp3str"
[deps]
source_file="res://audio/music/magazine.mp3"
dest_files=["res://.godot/imported/magazine.mp3-b6f09f5db14feb1428451d6c639c4105.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dp3n8od21isy8"
path="res://.godot/imported/menu.mp3-c0c7e41802fae0234a014a5e97694acb.mp3str"
[deps]
source_file="res://audio/music/menu.mp3"
dest_files=["res://.godot/imported/menu.mp3-c0c7e41802fae0234a014a5e97694acb.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://denqdlq1w1njm"
path="res://.godot/imported/bad.wav-9f1e2a623126c4b4b35ec8ba5cfa4126.sample"
[deps]
source_file="res://audio/sfx/bad.wav"
dest_files=["res://.godot/imported/bad.wav-9f1e2a623126c4b4b35ec8ba5cfa4126.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dwc4yqps6p1p7"
path="res://.godot/imported/ok.wav-dbf7928cddffd4f6fe92ad7a1655b0de.sample"
[deps]
source_file="res://audio/sfx/ok.wav"
dest_files=["res://.godot/imported/ok.wav-dbf7928cddffd4f6fe92ad7a1655b0de.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dpebsgc1k2kfu"
path="res://.godot/imported/stamp.wav-482ea9bc9409e806d9433c6217eb3bc6.sample"
[deps]
source_file="res://audio/sfx/stamp.wav"
dest_files=["res://.godot/imported/stamp.wav-482ea9bc9409e806d9433c6217eb3bc6.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
+633 -206
View File
@@ -1,123 +1,653 @@
{
"_meta": {
"file": "balance.json",
"version": "1.1",
"rules": "docs/GDD.md (компактная сводка) + docs/DECISIONS.md (D01–D17)",
"note": "null = параметр не задан в постановке, уточнить до кода (см. _todo). Никаких выдуманных чисел.",
"_todo": [
"attacks.junk.spawn_probability — не задан в спецификации (явно null)",
"income_target_by_hour: часы 4,6,7,8 — интерполяция, уточнить",
"difficulty.h2 packet_timer/request_timer: формула даёт 26.325/50.15 (19.5×1.35 и 29.5×1.7 — у запросов отдельный grace)"
]
"version": "2.0",
"rules": "PEAK_HOUR_V2_SHOP_FILES_MINIGAMES.md §2, §3, §6, §8 + Appendix A",
"note": "v2: 8 lot families, 3 offers, rerolls, dense send-minigame selection. Legacy sections (difficulty/spawn/progression/timers/connections/channel) kept for the not-yet-rewritten HourDirector. New code reads constants/hours/grid/shop_rules/shop/minigames/attacks/payout/sla."
},
"constants": {
"hour_seconds": 300,
"run_hours": 9,
"sla_start": 128,
"sla_max": 200,
"money_start": 150,
"buffer_base": 4,
"buffer_per_upgrade": 2,
"buffer_max": 8,
"slots_start": 2,
"slots_max": 4,
"max_sections": 6,
"files_per_folder": 16,
"money_start": 50,
"sla_start": 45,
"sla_base": 45,
"sla_per_level": 30,
"sla_hard_cap": 165,
"sla_regen_per_hour": 12,
"slots_start": 1,
"slots_max": 5,
"channel_capacity": 10,
"channel_per_level": 6,
"channel_capacity_max": 46,
"buffer_base": 6,
"buffer_per_level": 1,
"buffer_max": 12,
"request_timer": 30,
"request_timer_per_level": 7,
"request_timer_max": 65,
"packet_timer": 20,
"folder_cols": 5,
"folder_rows": 4,
"folders_start": 2,
"folders_max": 6,
"folder_new_cost": 180,
"folder_price_start": 150,
"folder_price_step": 100,
"max_sections": 1,
"section_start": "C",
"folders_per_section": 12,
"files_per_folder": 48,
"send_window_visible_cards": 4,
"send_scroll_card_height_px": 56,
"required_name_char_limit": 12,
"file_cooldown_after_send_sec": 20,
"required_per_request": { "min": 1, "max": 3 }
"file_cooldown_after_send_sec": 0,
"required_per_request": { "min": 1, "max": 4 },
"spawn_window_ratio": 0.75,
"spawn_jitter": 0.2,
"buffer_per_upgrade": 1,
"udp_success_base": 0.40,
"udp_success_max": 0.80,
"udp_fail_sla": -5,
"udp_retry_cooldown_sec": 4,
"udp_request_ratio": 0.35,
"send_quality_min": 0.45,
"file_expire_base": 0.03,
"file_expire_step": 0.05,
"file_expire_cap": 0.55
},
"economy": {
"start_stats": {
"money": 50,
"sla": 45,
"sla_cap": 45,
"slots": 1,
"channel_capacity": 10,
"buffer": 6,
"request_timer": 30,
"packet_timer": 20,
"folder_grid": [5, 4],
"money_mult": 1.0,
"speed_mult": 1.0
},
"formulas": {
"max_sla": "clamp(45 + 30 * sla_level, 45, 165)",
"sla_regen": "sla = min(sla_cap, sla + 12) в начале каждого часа",
"slots": "1 + slot_level (max 5)",
"channel_capacity": "10 + 6 * channel_level",
"buffer": "min(12, 6 + channel_level)",
"request_timer": "min(65, 30 + 7 * timer_level)",
"packet_timer": "20 (не покупается)",
"speed": "pow(0.90, speed_level)",
"money_mult": "1 + 0.05 * money_level",
"folder_dims": "[[5,4],[6,4],[7,5],[8,5],[8,6],[9,6]][folder_level]",
"transfer_time": "base_time_sec * pow(0.90, speed_level) * max(1.0, load / capacity)",
"protocol_time": "UDP ×0.8, TCP ×1.0 (только отправка)"
}
},
"types": {
"text": {
"name_ru": "Текст",
"ext": ".txt",
"icon": "▤",
"recv_time": 1.0,
"send_time": 1.0,
"recv_fee": 2,
"base_pay": 20,
"weight": 6,
"minigame_coeff": 1.0,
"sla_loss": 2
"ext": "txt",
"icon": "text",
"size_kb": 64,
"cells": 1,
"base_time_sec": 3,
"base_pay": 18,
"recv_fee": 1,
"weight": 1,
"minigame_coeff": 1,
"sla_loss": 3,
"shapes": ["O1"]
},
"sound": {
"name_ru": "Звук",
"ext": ".wav",
"icon": "♪",
"recv_time": 1.5,
"send_time": 2.0,
"recv_fee": 3,
"base_pay": 35,
"weight": 10,
"minigame_coeff": 2.0,
"sla_loss": 4
"ext": "mp3",
"icon": "sound",
"size_kb": 3072,
"cells": 2,
"base_time_sec": 5,
"base_pay": 30,
"recv_fee": 2,
"weight": 2,
"minigame_coeff": 2,
"sla_loss": 6,
"shapes": ["I2H", "I2V"]
},
"photo": {
"name_ru": "Фото",
"ext": ".jpg",
"icon": "▣",
"recv_time": 2.0,
"send_time": 3.0,
"recv_fee": 5,
"base_pay": 55,
"weight": 14,
"minigame_coeff": 3.0,
"sla_loss": 8
"ext": "jpg",
"icon": "photo",
"size_kb": 5120,
"cells": 3,
"base_time_sec": 8,
"base_pay": 44,
"recv_fee": 4,
"weight": 4,
"minigame_coeff": 4,
"sla_loss": 11,
"shapes": ["I3H", "I3V", "L3a", "L3b", "L3c", "L3d"]
},
"video": {
"name_ru": "Видео",
"ext": ".mp4",
"icon": "▶",
"recv_time": 3.0,
"send_time": 5.0,
"recv_fee": 8,
"base_pay": 90,
"weight": 22,
"minigame_coeff": 4.0,
"sla_loss": 16
"ext": "mp4",
"icon": "video",
"size_kb": 20480,
"cells": 4,
"base_time_sec": 12,
"base_pay": 69,
"recv_fee": 6,
"weight": 8,
"minigame_coeff": 8,
"sla_loss": 21,
"shapes": ["I4H", "I4V", "O4", "T4a", "T4b", "T4c", "T4d", "L4a", "L4b", "L4c", "L4d", "J4a", "J4b", "J4c", "J4d", "S4", "Z4"]
}
},
"hours": [
{
"hour": 1,
"duration": 120,
"files": 8,
"active_requests": 2,
"attacks": 0,
"income_target": 100,
"send_interval": 20,
"type_weights": { "text": 1.0, "sound": 0.0, "photo": 0.0, "video": 0.0 },
"request_mix": { "1": 1.0, "2": 0.0, "3": 0.0, "4": 0.0 },
"request_type_weights": { "text": 1.0, "sound": 0.0, "photo": 0.0, "video": 0.0 }
},
{
"hour": 2,
"duration": 150,
"files": 8,
"active_requests": 2,
"attacks": 1,
"income_target": 220,
"send_interval": 24,
"type_weights": { "text": 0.80, "sound": 0.20, "photo": 0.0, "video": 0.0 },
"request_mix": { "1": 0.30, "2": 0.70, "3": 0.0, "4": 0.0 },
"request_type_weights": { "text": 0.80, "sound": 0.20, "photo": 0.0, "video": 0.0 }
},
{
"hour": 3,
"duration": 180,
"files": 9,
"active_requests": 2,
"attacks": 1,
"income_target": 380,
"send_interval": 18,
"type_weights": { "text": 0.50, "sound": 0.50, "photo": 0.0, "video": 0.0 },
"request_mix": { "1": 0.20, "2": 0.80, "3": 0.0, "4": 0.0 },
"request_type_weights": { "text": 0.50, "sound": 0.50, "photo": 0.0, "video": 0.0 }
},
{
"hour": 4,
"duration": 210,
"files": 10,
"active_requests": 3,
"attacks": 2,
"income_target": 560,
"send_interval": 24,
"type_weights": { "text": 0.40, "sound": 0.30, "photo": 0.30, "video": 0.0 },
"request_mix": { "1": 0.0, "2": 0.70, "3": 0.30, "4": 0.0 },
"request_type_weights": { "text": 0.40, "sound": 0.30, "photo": 0.30, "video": 0.0 }
},
{
"hour": 5,
"duration": 240,
"files": 11,
"active_requests": 3,
"attacks": 2,
"income_target": 760,
"send_interval": 24,
"type_weights": { "text": 0.35, "sound": 0.35, "photo": 0.30, "video": 0.0 },
"request_mix": { "1": 0.0, "2": 0.55, "3": 0.45, "4": 0.0 },
"request_type_weights": { "text": 0.35, "sound": 0.35, "photo": 0.30, "video": 0.0 }
},
{
"hour": 6,
"duration": 270,
"files": 11,
"active_requests": 3,
"attacks": 3,
"income_target": 980,
"send_interval": 24,
"type_weights": { "text": 0.30, "sound": 0.30, "photo": 0.30, "video": 0.10 },
"request_mix": { "1": 0.0, "2": 0.50, "3": 0.35, "4": 0.15 },
"request_type_weights": { "text": 0.30, "sound": 0.30, "photo": 0.30, "video": 0.10 }
},
{
"hour": 7,
"duration": 300,
"files": 12,
"active_requests": 4,
"attacks": 3,
"income_target": 1200,
"send_interval": 24,
"type_weights": { "text": 0.22, "sound": 0.23, "photo": 0.30, "video": 0.25 },
"request_mix": { "1": 0.0, "2": 0.60, "3": 0.15, "4": 0.25 },
"request_type_weights": { "text": 0.22, "sound": 0.23, "photo": 0.30, "video": 0.25 }
},
{
"hour": 8,
"duration": 330,
"files": 12,
"active_requests": 4,
"attacks": 4,
"income_target": 1450,
"send_interval": 24,
"type_weights": { "text": 0.15, "sound": 0.25, "photo": 0.35, "video": 0.25 },
"request_mix": { "1": 0.0, "2": 0.60, "3": 0.20, "4": 0.20 },
"request_type_weights": { "text": 0.15, "sound": 0.25, "photo": 0.35, "video": 0.25 }
},
{
"hour": 9,
"duration": 360,
"files": 12,
"active_requests": 4,
"attacks": 4,
"income_target": 1700,
"send_interval": 24,
"type_weights": { "text": 0.10, "sound": 0.15, "photo": 0.30, "video": 0.45 },
"request_mix": { "1": 0.0, "2": 0.70, "3": 0.10, "4": 0.20 },
"request_type_weights": { "text": 0.10, "sound": 0.15, "photo": 0.30, "video": 0.45 }
}
],
"payout": {
"speed_base": 0.6,
"speed_bonus": 0.4,
"quality_base": 0.5,
"quality_bonus": 0.5,
"multi_step": 0.15,
"protocol_mult": { "TCP": 1.15, "UDP": 0.85 },
"udp_time_mult": 0.8,
"streak_step": 0.02,
"streak_cap": 10,
"money_mult_step": 0.05,
"formula": "payout = int(round(float(base_sum) * speed * quality * multi * protocol * streak * money_mult))",
"speed_formula": "speed = 0.6 + 0.4 * clamp(time_left / timer_total, 0, 1)",
"quality_formula": "quality = 0.5 + 0.5 * q",
"multi_formula": "multi = 1 + 0.15 * (n - 1)",
"streak_formula": "streak = 1 + 0.02 * min(10, streak)",
"money_mult_formula": "money_mult = 1 + 0.05 * money_level",
"note": "Только float-арифметика: никаких int/int и никакого ^ для степени."
},
"sla": {
"start": 45,
"regen_per_hour": 12,
"defeat_at_zero": true,
"timeout_fine_base": 14,
"timeout_fine_per_hour": 3,
"timeout_fine_formula": "-(14 + 3*hour) SLA",
"wrong_attach_fine": -2,
"lost_file_fine": -5,
"buffer_overflow_fine": -5,
"reject_money": -8,
"reject_sla": -7,
"connect_miss_sla": -6,
"folder_destroyed_fine": -5,
"sla_lose": "SLA <= 0 во время часа = поражение забега"
},
"grid": {
"gap": 6,
"cell_min": 56,
"cell_max": 104,
"view_w": 558,
"view_h": 470,
"folder_cols": 5,
"folder_rows": 4,
"folder_dims": [[5, 4], [6, 4], [7, 5], [8, 5], [8, 6], [9, 6]],
"table_cols": 4,
"table_rows": 6,
"cell_formula": "cell = clamp(floor(min((view_w - gap*(cols-1))/cols, (view_h - gap*(rows-1))/rows)), 56, 104)",
"shapes": {
"O1": { "cells": [[0, 0]], "w": 1, "h": 1 },
"I2H": { "cells": [[0, 0], [1, 0]], "w": 2, "h": 1 },
"I2V": { "cells": [[0, 0], [0, 1]], "w": 1, "h": 2 },
"I3H": { "cells": [[0, 0], [1, 0], [2, 0]], "w": 3, "h": 1 },
"I3V": { "cells": [[0, 0], [0, 1], [0, 2]], "w": 1, "h": 3 },
"L3a": { "cells": [[0, 0], [0, 1], [1, 1]], "w": 2, "h": 2 },
"L3b": { "cells": [[0, 0], [1, 0], [0, 1]], "w": 2, "h": 2 },
"L3c": { "cells": [[1, 0], [0, 1], [1, 1]], "w": 2, "h": 2 },
"L3d": { "cells": [[0, 0], [1, 0], [1, 1]], "w": 2, "h": 2 },
"I4H": { "cells": [[0, 0], [1, 0], [2, 0], [3, 0]], "w": 4, "h": 1 },
"I4V": { "cells": [[0, 0], [0, 1], [0, 2], [0, 3]], "w": 1, "h": 4 },
"O4": { "cells": [[0, 0], [1, 0], [0, 1], [1, 1]], "w": 2, "h": 2 },
"T4a": { "cells": [[0, 0], [1, 0], [2, 0], [1, 1]], "w": 3, "h": 2 },
"T4b": { "cells": [[0, 1], [1, 1], [2, 1], [1, 0]], "w": 3, "h": 2 },
"T4c": { "cells": [[0, 0], [0, 1], [1, 1], [0, 2]], "w": 2, "h": 3 },
"T4d": { "cells": [[1, 0], [1, 1], [0, 1], [1, 2]], "w": 2, "h": 3 },
"L4a": { "cells": [[0, 0], [0, 1], [0, 2], [1, 2]], "w": 2, "h": 3 },
"L4b": { "cells": [[0, 0], [1, 0], [2, 0], [0, 1]], "w": 3, "h": 2 },
"L4c": { "cells": [[0, 0], [1, 0], [1, 1], [1, 2]], "w": 2, "h": 3 },
"L4d": { "cells": [[2, 0], [0, 1], [1, 1], [2, 1]], "w": 3, "h": 2 },
"J4a": { "cells": [[1, 0], [1, 1], [1, 2], [0, 2]], "w": 2, "h": 3 },
"J4b": { "cells": [[0, 0], [1, 0], [2, 0], [2, 1]], "w": 3, "h": 2 },
"J4c": { "cells": [[0, 0], [0, 1], [1, 1], [2, 1]], "w": 3, "h": 2 },
"J4d": { "cells": [[0, 0], [0, 1], [0, 2], [1, 0]], "w": 2, "h": 3 },
"S4": { "cells": [[1, 0], [2, 0], [0, 1], [1, 1]], "w": 3, "h": 2 },
"Z4": { "cells": [[0, 0], [1, 0], [1, 1], [2, 1]], "w": 3, "h": 2 }
},
"type_shapes": {
"text": ["O1"],
"sound": ["I2H", "I2V"],
"photo": ["I3H", "I3V", "L3a", "L3b", "L3c", "L3d"],
"video": ["I4H", "I4V", "O4", "T4a", "T4b", "T4c", "T4d", "L4a", "L4b", "L4c", "L4d", "J4a", "J4b", "J4c", "J4d", "S4", "Z4"]
}
},
"shop_rules": {
"offers": 3,
"unlock_hour": 2,
"reroll_costs": [40, 80, 120],
"max_rerolls": 3,
"last_offer_weight": 0.5,
"pity_hours": 2,
"pity_weight": 1.5,
"note": "Веса берутся из shop[].weight_by_hour (9 значений, индекс = hour-1). Фазы H2-3/H4-6/H7-9 — таблица §3.1 — источник правды."
},
"shop": [
{
"id": "lot_slots",
"name_ru": "Порты коммутатора",
"desc_ru": "Ещё один порт: приём и отправка могут идти параллельно.",
"effect": "connection_slots",
"value": 1,
"max_level": 4,
"prices": [90, 200, 400, 700],
"level_names": ["Второй порт", "Третий порт", "Четвёртый порт", "Магистральный порт"],
"weight_by_hour": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.8, 0.8]
},
{
"id": "lot_speed",
"name_ru": "Оптоволокно",
"desc_ru": "Скорость приёма и отправки +10% за уровень (умножается).",
"effect": "transfer_speed",
"value": 0.10,
"max_level": 6,
"prices": [55, 75, 100, 135, 180, 245],
"level_names": ["Витая пара", "Экранированная пара", "Оптоволокно", "Гигабитная линия", "Магистраль", "Тёмное волокно"],
"weight_by_hour": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.8, 0.8]
},
{
"id": "lot_sla",
"name_ru": "Резервный ИБП",
"desc_ru": "Держит узел живым: кап SLA +30 и мгновенное восстановление +30.",
"effect": "sla_cap",
"value": 30,
"max_level": 4,
"prices": [80, 160, 270, 430],
"level_names": ["Резервный ИБП", "Горячий резерв", "Двойное питание", "Автономный контур"],
"weight_by_hour": [0.8, 0.8, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
},
{
"id": "lot_shield",
"name_ru": "Заземление",
"desc_ru": "Автоматически гасит атаку: 70% награды, без штрафа.",
"effect": "shield_charge",
"value": 1,
"max_level": 3,
"prices": [100, 220, 410],
"level_names": ["Заземление", "Громоотвод", "Защитный контур"],
"weight_by_hour": [0.3, 0.3, 0.3, 0.8, 0.8, 0.8, 1.0, 1.0, 1.0]
},
{
"id": "lot_money",
"name_ru": "Корпоративный контракт",
"desc_ru": "Оплата за контейнеры +5% за уровень.",
"effect": "money_mult",
"value": 0.05,
"max_level": 8,
"prices": [70, 95, 130, 170, 220, 285, 365, 465],
"level_names": ["Тарифный план", "Корпоративный контракт", "SLA-пакет", "Госзаказ", "Экспортный контракт", "Франшиза", "Монополия", "Золотая жила"],
"weight_by_hour": [0.6, 0.6, 0.6, 1.0, 1.0, 1.0, 1.2, 1.2, 1.2]
},
{
"id": "lot_folder",
"name_ru": "Складской стеллаж",
"desc_ru": "Больше места под фигуры: 20 → 24 клетки на первом уровне (до 54).",
"effect": "folder_cells",
"value": 1,
"max_level": 5,
"prices": [50, 100, 180, 290, 440],
"level_names": ["Стеллаж", "Секция стеллажа", "Складская линия", "Автоматизированный склад", "Ангар"],
"weight_by_hour": [1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.8, 0.8, 0.8]
},
{
"id": "lot_channel",
"name_ru": "Пропускная способность",
"desc_ru": "Канал +6 и буфер приёма +1 за уровень.",
"effect": "channel_capacity",
"value": 6,
"buffer_value": 1,
"max_level": 6,
"prices": [45, 70, 95, 130, 175, 230],
"level_names": ["Канал 56k", "ISDN", "ADSL", "Выделенная линия", "Оптика", "Магистраль"],
"weight_by_hour": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9]
},
{
"id": "lot_timer",
"name_ru": "Регламент обработки",
"desc_ru": "Больше времени на поиск файлов и прикрепление: 30 → 37 с (до 65).",
"effect": "request_timer",
"value": 7,
"max_level": 5,
"prices": [45, 75, 115, 165, 230],
"level_names": ["Регламент", "Продление сессии", "Гибкий график", "Приоритетная обработка", "Выделенный менеджер"],
"weight_by_hour": [1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.7, 0.7, 0.7]
},
{
"id": "lot_udp",
"name_ru": "UDP-повторитель",
"desc_ru": "Надёжность UDP +10% за уровень: 40% → 80%.",
"effect": "udp_success",
"value": 0.10,
"max_level": 4,
"prices": [60, 120, 210, 350],
"weight_by_hour": [0.8, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
}
],
"minigames": {
"selection": {
"order": ["dots", "sound", "text", "video", "photo", "maze"],
"achievable_sums": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 28, 32],
"max_index": 26,
"tier_max_rank": [7, 13],
"rank_games": ["text", "sound", "dots", "photo", "maze", "dots", "photo", "video", "dots", "text", "video", "maze", "photo", "sound", "video", "text", "maze", "sound", "video", "sound", "photo", "dots", "dots", "maze", "maze", "photo", "video"],
"formula": "S=sum(coeff по 1-4 файлам); rank=index of S in achievable_sums (или ближайшей меньшей); tier=1 при rank<=7, 2 при 8<=rank<=13, иначе 3; id=rank_games[rank]; S=0 → dots/T1",
"note": "6 игр отправки (wires удалена), 3 ступени, ровно 27 достижимых сумм коэффициентов для 1-4 файлов. Таблица rank_games задаёт игру по rank напрямую (без modulo)."
},
"games": {
"dots": {
"name_ru": "Одна линия",
"kind": "send",
"tiers": [
{ "grid": [4, 3], "obstacles": 2, "undo_penalty": 0.04 },
{ "grid": [4, 4], "obstacles": 4, "undo_penalty": 0.04 },
{ "grid": [5, 4], "obstacles": 6, "undo_penalty": 0.03 }
]
},
"sound": {
"name_ru": "Уровни",
"kind": "send",
"tiers": [
{ "channels": 4, "tolerance": 0.11, "drift": 0.0, "drift_hz": 0.0 },
{ "channels": 5, "tolerance": 0.08, "drift": 0.02, "drift_hz": 0.15 },
{ "channels": 6, "tolerance": 0.06, "drift": 0.03, "drift_hz": 0.25 }
]
},
"text": {
"name_ru": "Чужое слово",
"kind": "send",
"tiers": [
{ "words": 15, "aliens": 2, "lines": 2, "alien_kind": "foreign_language+typos" },
{ "words": 19, "aliens": 3, "lines": 3, "alien_kind": "foreign_language+typos+semantic" },
{ "words": 24, "aliens": 4, "lines": 3, "alien_kind": "foreign_language+typos+semantic+archaic" }
]
},
"video": {
"name_ru": "Раскадровка",
"kind": "send",
"tiers": [
{ "frames": 4, "slots": 4, "decoys": 0, "mirror": false },
{ "frames": 5, "slots": 4, "decoys": 1, "mirror": false },
{ "frames": 6, "slots": 5, "decoys": 2, "mirror": true }
]
},
"photo": {
"name_ru": "Кадр",
"kind": "send",
"tiers": [
{ "speed": 140, "frame_w": 180, "frame_h": 120, "shots": 4, "retarget_min": 0.6, "retarget_max": 1.0 },
{ "speed": 200, "frame_w": 150, "frame_h": 100, "shots": 5, "retarget_min": 0.5, "retarget_max": 0.8 },
{ "speed": 260, "frame_w": 120, "frame_h": 80, "shots": 6, "retarget_min": 0.35, "retarget_max": 0.65 }
]
},
"maze": {
"name_ru": "Лабиринт",
"kind": "send",
"tiers": [
{ "cols": 11, "rows": 4, "width": 440, "height": 160, "speed": 470, "collision_penalty": 0.05, "dead_end_penalty": 0.02 },
{ "cols": 13, "rows": 4, "width": 494, "height": 152, "speed": 520, "collision_penalty": 0.04, "dead_end_penalty": 0.02 },
{ "cols": 15, "rows": 5, "width": 520, "height": 180, "speed": 560, "collision_penalty": 0.04, "dead_end_penalty": 0.02 }
]
},
"junk": {
"name_ru": "Фильтр",
"kind": "attack",
"tiers": [
{ "cards": 9, "junk": 2, "hints": 1, "threshold": 0.6 },
{ "cards": 10, "junk": 3, "hints": 2, "threshold": 0.65 },
{ "cards": 11, "junk": 4, "hints": 2, "threshold": 0.7 },
{ "cards": 12, "junk": 5, "hints": 3, "threshold": 0.75 }
]
},
"ddos": {
"name_ru": "Щит",
"kind": "attack",
"tiers": [
{ "packets": 8, "speed": 110, "shield_width": 260, "interval": 0.55, "threshold": 0.6 },
{ "packets": 10, "speed": 140, "shield_width": 220, "interval": 0.45, "threshold": 0.65 },
{ "packets": 12, "speed": 170, "shield_width": 180, "interval": 0.35, "threshold": 0.7 },
{ "packets": 14, "speed": 200, "shield_width": 150, "interval": 0.28, "threshold": 0.75 }
]
},
"phishing": {
"name_ru": "Паспорт",
"kind": "attack",
"tiers": [
{ "clients": 3, "checks": ["name"], "threshold": 0.75 },
{ "clients": 4, "checks": ["name", "department"], "threshold": 0.75 },
{ "clients": 5, "checks": ["name", "department", "id"], "threshold": 0.8 },
{ "clients": 6, "checks": ["name", "department", "id", "photo", "expiry"], "threshold": 0.8 }
]
},
"ransomware": {
"name_ru": "Карантин",
"kind": "attack",
"tiers": [
{ "cards": 12, "exe": 1, "threshold": 0.5, "masking": "extension_visible" },
{ "cards": 16, "exe": 2, "threshold": 0.55, "masking": "extension_small" },
{ "cards": 20, "exe": 2, "threshold": 0.6, "masking": "document_icon" },
{ "cards": 24, "exe": 3, "threshold": 0.65, "masking": "similar_names" }
]
}
}
},
"attacks": {
"schedule": [0, 1, 1, 2, 2, 3, 3, 4, 4],
"tier_formula": "clamp(1 + floor((hour - 2) / 2.0), 1, 4)",
"spawn_window": {
"start_ratio": 0.25,
"end_ratio": 0.85,
"start_min_sec": 20,
"start_max_sec": 60,
"end_margin_sec": 20
},
"shield_reward_scale": 0.7,
"money_scale": 1.0,
"kinds": {
"junk": {
"name_ru": "Мусорный пакет",
"minigame": "junk",
"weight_by_hour": [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
"tiers": [
{ "reward_money": 5, "reward_sla": 2, "fail_money": -10, "fail_sla": -7, "threshold": 0.6 },
{ "reward_money": 10, "reward_sla": 3, "fail_money": -15, "fail_sla": -10, "threshold": 0.65 },
{ "reward_money": 15, "reward_sla": 4, "fail_money": -20, "fail_sla": -12, "threshold": 0.7 },
{ "reward_money": 20, "reward_sla": 5, "fail_money": -25, "fail_sla": -16, "threshold": 0.75 }
]
},
"ddos": {
"name_ru": "DDoS",
"minigame": "ddos",
"weight_by_hour": [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
"tiers": [
{ "reward_money": 30, "reward_sla": 4, "fail_money": -10, "fail_sla": -20, "channel_debuff": 0.4, "debuff_sec": 15, "threshold": 0.6 },
{ "reward_money": 40, "reward_sla": 5, "fail_money": -15, "fail_sla": -27, "channel_debuff": 0.4, "debuff_sec": 15, "threshold": 0.65 },
{ "reward_money": 50, "reward_sla": 6, "fail_money": -20, "fail_sla": -34, "channel_debuff": 0.4, "debuff_sec": 15, "threshold": 0.7 },
{ "reward_money": 60, "reward_sla": 7, "fail_money": -25, "fail_sla": -40, "channel_debuff": 0.4, "debuff_sec": 15, "threshold": 0.75 }
]
},
"phishing": {
"name_ru": "Фишинг",
"minigame": "phishing",
"weight_by_hour": [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
"tiers": [
{ "reward_money": 20, "reward_sla": 3, "fail_money": -40, "fail_sla": -14, "file_lock_sec": 60, "send_penalty_sec": 5, "threshold": 0.75 },
{ "reward_money": 30, "reward_sla": 4, "fail_money": -50, "fail_sla": -16, "file_lock_sec": 60, "send_penalty_sec": 5, "threshold": 0.75 },
{ "reward_money": 40, "reward_sla": 5, "fail_money": -60, "fail_sla": -20, "file_lock_sec": 60, "send_penalty_sec": 5, "threshold": 0.8 },
{ "reward_money": 50, "reward_sla": 6, "fail_money": -70, "fail_sla": -24, "file_lock_sec": 60, "send_penalty_sec": 5, "threshold": 0.8 }
]
},
"ransomware": {
"name_ru": "Шифровальщик",
"minigame": "ransomware",
"weight_by_hour": [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
"tiers": [
{ "reward_money": 25, "reward_sla": 3, "fail_money": -30, "fail_sla": -14, "folder_block_sec": 15, "threshold": 0.5 },
{ "reward_money": 35, "reward_sla": 4, "fail_money": -40, "fail_sla": -16, "folder_block_sec": 15, "threshold": 0.55 },
{ "reward_money": 45, "reward_sla": 5, "fail_money": -50, "fail_sla": -20, "folder_block_sec": 15, "threshold": 0.6 },
{ "reward_money": 55, "reward_sla": 6, "fail_money": -60, "fail_sla": -24, "folder_block_sec": 15, "threshold": 0.65 }
]
}
}
},
"difficulty": [
{ "hour": 1, "recv_interval": 12, "send_interval": 22, "max_requests": 2, "buffer": 4, "packet_timer": 30, "request_timer": 60, "attacks": 0 },
{ "hour": 2, "recv_interval": 11, "send_interval": 20, "max_requests": 2, "buffer": 4, "packet_timer": 26.325, "request_timer": 50.15, "attacks": 1 },
{ "hour": 3, "recv_interval": 10, "send_interval": 18, "max_requests": 3, "buffer": 5, "packet_timer": 22.8, "request_timer": 40.6, "attacks": 1 },
{ "hour": 4, "recv_interval": 9, "send_interval": 17, "max_requests": 3, "buffer": 5, "packet_timer": 18.5, "request_timer": 28.5, "attacks": 2 },
{ "hour": 5, "recv_interval": 8, "send_interval": 15, "max_requests": 4, "buffer": 6, "packet_timer": 18, "request_timer": 28, "attacks": 2 },
{ "hour": 6, "recv_interval": 7.5, "send_interval": 14, "max_requests": 4, "buffer": 6, "packet_timer": 17.5, "request_timer": 27.5, "attacks": 3 },
{ "hour": 7, "recv_interval": 7, "send_interval": 13, "max_requests": 4, "buffer": 6, "packet_timer": 17, "request_timer": 27, "attacks": 3 },
{ "hour": 8, "recv_interval": 6.5, "send_interval": 12, "max_requests": 4, "buffer": 7, "packet_timer": 16.5, "request_timer": 26.5, "attacks": 4 },
{ "hour": 9, "recv_interval": 6, "send_interval": 12, "max_requests": 4, "buffer": 7, "packet_timer": 16, "request_timer": 26, "attacks": 4 }
{ "hour": 1, "recv_interval": 12, "send_interval": 22, "max_requests": 2, "buffer": 4, "packet_timer": 20, "request_timer": 30, "attacks": 0 },
{ "hour": 2, "recv_interval": 11, "send_interval": 20, "max_requests": 5, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 1 },
{ "hour": 3, "recv_interval": 10, "send_interval": 18, "max_requests": 7, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 1 },
{ "hour": 4, "recv_interval": 9, "send_interval": 17, "max_requests": 9, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 2 },
{ "hour": 5, "recv_interval": 8, "send_interval": 15, "max_requests": 10, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 2 },
{ "hour": 6, "recv_interval": 7.5, "send_interval": 14, "max_requests": 11, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 3 },
{ "hour": 7, "recv_interval": 7, "send_interval": 13, "max_requests": 11, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 3 },
{ "hour": 8, "recv_interval": 6.5, "send_interval": 12, "max_requests": 12, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 4 },
{ "hour": 9, "recv_interval": 6, "send_interval": 12, "max_requests": 12, "buffer": 6, "packet_timer": 20, "request_timer": 30, "attacks": 4 }
],
"difficulty_rules": {
"table_end_hour": 8,
"after_end_hour": "значения часа 8; +5% к скорости спавна каждые 2 часа",
"spawn_speedup_after_end": 0.05,
"table_end_hour": 9,
"after_end_hour": "значения часа 9",
"spawn_speedup_after_end": 0.0,
"spawn_speedup_period_hours": 2,
"type_availability": "час 1 — text; час 2 — text и sound; час 3 — +photo; час 4+ — все 4 (D23); атаки — с часа 2 (attack_junk)",
"timer_grace": "packet_timer: grace h1 1.5, h2 1.35, h3 1.2, h4+ 1.0 (Economy.TIMER_GRACE). request_timer: отдельный grace h1 2.0, h2 1.7, h3 1.4, h4+ 1.0 (Economy.REQUEST_TIMER_GRACE). Формула: (base − 0.5*(h−1))*grace + 4*level. h2: packet 19.5*1.35=26.325, request 29.5*1.7=50.15."
"type_availability": "веса типов — hours[].type_weights (единственный источник правды для приёма) и совпадают с hours[].request_type_weights (запросы); H1 только text, H2 text/sound, photo с H4, video с H6",
"timer_grace": "v2: таймеры не масштабируются по часу (packet 20, request 30+7*level, cap 65)"
},
"progression": {
"unlocks": {
"shop": {"run_hour": 2, "label": "Магазин"},
"search": {"meta": "survive_hour_3", "label": "Поиск"},
"shield": {"meta": "survive_hour_5", "label": "Щит"},
"minigame_text": {"run_hour": 1},
"minigame_sound": {"run_hour": 2},
"minigame_photo": {"run_hour": 3},
"minigame_video": {"run_hour": 4},
"minigame_advanced": {"meta": "complete_run", "label": "Продвинутые мини-игры"},
"attack_junk": {"run_hour": 2},
"attack_ddos": {"run_hour": 2},
"attack_phishing": {"run_hour": 3},
"attack_ransomware": {"run_hour": 4}
"shop": { "run_hour": 2, "label": "Магазин" },
"search": { "meta": "survive_hour_3", "label": "Поиск" },
"shield": { "meta": "survive_hour_5", "label": "Щит" },
"minigame_text": { "run_hour": 1 },
"minigame_sound": { "run_hour": 2 },
"minigame_photo": { "run_hour": 3 },
"minigame_video": { "run_hour": 4 },
"attack_junk": { "run_hour": 2 },
"attack_ddos": { "run_hour": 2 },
"attack_phishing": { "run_hour": 3 },
"attack_ransomware": { "run_hour": 4 }
},
"meta_rewards": {
"survive_hour_3": {"min_hours": 3, "unlocks": ["search"]},
"survive_hour_5": {"min_hours": 5, "unlocks": ["shield"]},
"complete_run": {"min_hours": 9, "unlocks": ["minigame_advanced"]}
"survive_hour_3": { "min_hours": 3, "unlocks": ["search"] },
"survive_hour_5": { "min_hours": 5, "unlocks": ["shield"] },
"complete_run": { "min_hours": 9, "unlocks": ["minigame_advanced"] }
}
},
"timers": {
@@ -127,34 +657,20 @@
"minigame_timeout": null,
"delete_long_press_sec": 0.5
},
"sla": {
"recv_bonus": 0.5,
"send_bonus": 1.0,
"reflect_bonus": 1.0,
"timeout_fine_base": 10,
"timeout_fine_per_hour": 2,
"timeout_fine_formula": "-(10 + 2*hour) — таймаут и подобные штрафы (D12)",
"wrong_attach_fine": -2,
"lost_file_fine": -5,
"buffer_overflow_fine": -5,
"sla_lose": "SLA <= 0"
},
"payout": {
"base_pay": "см. types.*.base_pay (значения в types)",
"recv_fee": "см. types.*.recv_fee (значения в types)",
"speed_formula": "speed = 0.6 + 0.4*(time_left / timer_total)",
"quality_formula": "quality = 0.5 + 0.5*mini_quality",
"multi_formula": "multi = 1 + 0.1*(n - 1)",
"protocol_mult": { "TCP": 1.15, "UDP": 1.0 },
"streak_formula": "streak = 1 + 0.02*min(10, streak)",
"income_target_by_hour": {
"hours": [1, 2, 3, 5, 9],
"approx": [500, 800, 1200, 1800, 2200],
"note": "ожидаемо ±10%; часы 4,6,7,8 — интерполяция, уточнить"
}
"spawn": {
"type_pool_by_hour": { "1": ["text", "sound"], "2": ["text", "sound", "photo"], "3+": ["text", "sound", "photo", "video"] },
"malicious_ratio": 0.0,
"required_count_by_hour": {
"1": { "1": 0.7, "2": 0.3, "3": 0.0 },
"2": { "1": 0.6, "2": 0.35, "3": 0.05 },
"3+": { "1": 0.5, "2": 0.4, "3": 0.1 }
},
"required_size_distribution": { "1": 0.5, "2": 0.4, "3": 0.1 },
"required_count_note": "legacy; v2 читает hours[].request_mix",
"attack_schedule": "attacks.schedule (0/1/1/2/2/3/3/4/4), окно 60–240 с часа"
},
"connections": {
"W_formula": "W = clamp(0.30 - 0.005*(hour-1) - 0.005*successes + 0.05*zone_upgrade_level, 0.18, 0.36)",
"W_formula": "legacy (полоска соединения удалена в v2)",
"W_base": 0.3,
"W_hour_coef": -0.005,
"W_success_coef": -0.005,
@@ -169,103 +685,14 @@
"CHANNEL_BG_PER_HOUR": 0,
"SLOT_LOAD": 0,
"BG_note": "фоновый шум и занятые слоты НЕ грузят канал; load = только активные передачи (loading/sending)",
"CHANNEL_CAPACITY": 80,
"capacity_per_upgrade": 25,
"capacity_upgrade_max_level": 4,
"formula": "load = sum(weight(active_transfers)); factor = max(1.0, load / CHANNEL_CAPACITY); actual_time = base_time * factor. idle load = 0"
},
"attacks": {
"junk": {
"name_ru": "Мусорный пакет",
"spawn_probability": null,
"duration_sec": 15,
"tell": "серая иконка ?, размер 0 Б, нет таймера",
"effect": "авто-занос через duration_sec, занимает место в приёме; уничтожить можно как malicious в любой фазе (D02)",
"reward_money": 5,
"reward_sla": 1,
"fail_money": -10,
"fail_sla": -5,
"filter_disabled_sec": 10
},
"ddos": {
"name_ru": "DDoS",
"cards_min": 8,
"cards_max": 12,
"spawn_window_sec": 2.0,
"shield_hold_sec": 1.5,
"tell": "8–12 идентичных карточек без типа и таймера",
"effect": "волна карточек; отбивается удержанием щита",
"reward_money": 30,
"reward_sla": 2,
"fail_sla": -15,
"channel_debuff": 0.4,
"channel_debuff_sec": 15,
"packets_lost": 2,
"recv_disabled_sec": 5
},
"phishing": {
"name_ru": "Фишинг",
"reward_money": 20,
"reward_sla": 2,
"fail_money": -40,
"fail_sla": -10,
"file_lock_sec": 60,
"send_disabled_sec": 5,
"timeout_money": -20,
"timeout_sla": -5,
"timeout_lock_sec": 30,
"tell": "client_id формата id_XXXX с неверным символом (например id_7F3К — кириллическая К), серый аватар",
"effect": "фальшивый запрос/пакет"
},
"ransomware": {
"name_ru": "Шифровальщик",
"reward_money": 25,
"reward_sla": 2,
"folder_block_delay_sec": 3,
"auto_unlock_sec": 30,
"auto_unlock_money": -30,
"auto_unlock_sla": -10,
"tell": "расширение .exe, красный квадрат 4×4",
"effect": "файл становится locked (lock_until), кликабелен для уничтожения (D08)"
}
},
"shop": [
{ "id": "shop_slot3", "name_ru": "Третий слот", "effect": "connection_slots", "value": 1, "price": 250, "max_level": 1 },
{ "id": "shop_slot4", "name_ru": "Четвёртый слот", "effect": "connection_slots", "value": 1, "price": 450, "max_level": 1 },
{ "id": "shop_buffer", "name_ru": "Буфер +2", "effect": "buffer_slots", "value": 2, "price": 200, "max_level": 2 },
{ "id": "shop_timer", "name_ru": "Таймер пакетов +4 с", "effect": "packet_timer", "value": 4, "unit": "сек (аддитивно)", "price": 350, "max_level": 3 },
{ "id": "shop_zone", "name_ru": "Зона +5%", "effect": "zone_width", "value": 0.05, "unit": "доля W (аддитивно в формуле D04)", "price": 300, "max_level": 3 },
{ "id": "shop_channel", "name_ru": "Канал +25", "effect": "channel_capacity", "value": 25, "price": 250, "max_level": 4 },
{ "id": "shop_section", "name_ru": "Новая секция", "effect": "sections", "value": 1, "price": 300, "max_level": 5 },
{ "id": "shop_search", "name_ru": "Поиск", "effect": "search_highlight", "value": 1, "price": 250, "max_level": 1 },
{ "id": "shop_shield", "name_ru": "Щит", "effect": "shield", "value": 1, "price": 400, "max_level": 1 },
{ "id": "shop_sla", "name_ru": "SLA +24", "effect": "sla_add", "value": 24, "price": 300, "max_level": 3 }
],
"shop_reserve": {
"items": ["scanner", "autosort"],
"note": "резерв — в прототип НЕ входят (D13); позиций в shop НЕТ"
},
"spawn": {
"type_pool_by_hour": {
"1": ["text"],
"2": ["text", "sound"],
"3+": ["text", "sound", "photo", "video"]
},
"malicious_ratio": 0.0,
"required_count_by_hour": {
"1": { "1": 1.0 },
"2": { "1": 0.75, "2": 0.25 },
"3+": { "1": 0.6, "2": 0.3, "3": 0.1 }
},
"required_size_distribution": { "1": 0.6, "2": 0.3, "3": 0.1 },
"required_count_note": "required_count_by_hour — распределение по часу; required_size_distribution — fallback/час 3+",
"attack_schedule": "с часа 2, далее по difficulty.attacks, окно 60–240 с часа, не во время мини-игры"
"CHANNEL_CAPACITY": 10,
"capacity_per_upgrade": 6,
"capacity_upgrade_max_level": 6,
"formula": "load = sum(weight(active_transfers)); factor = max(1.0, load / capacity); actual_time = base_time_sec * pow(0.90, speed_level) * factor"
},
"minigame_selection": {
"coeff": { "text": 1.0, "sound": 2.0, "photo": 3.0, "video": 4.0 },
"difficulty_formula": "sum(coeff(required)) / n",
"thresholds": { "text": "< 1.5", "sound": "< 2.5", "photo": "< 3.5", "else": "video" },
"call": "MiniGame.start(type, difficulty)",
"coeff": { "text": 1, "sound": 2, "photo": 4, "video": 8 },
"formula": "см. minigames.selection.formula (27 достижимых сумм для 1-4 файлов, 3 ступени, rank_games)",
"quality_stub": 1.0
}
}
+73 -1
View File
@@ -72,6 +72,10 @@
"retry_no_effect": "Качество прежнее — улучшить нельзя",
"delete_prep_only": "Обычные файлы удаляются только в подготовке",
"money_short": "Не хватает средств",
"folder_need_money": "НУЖНО {cost} $",
"folder_limit": "ДОСТИГНУТ ЛИМИТ ПАПОК",
"delete_file_confirm": "Удалить файл?",
"delete_file_detail": "Файл «{name}» будет уничтожен без штрафа SLA.",
"save_restored": "Прогресс смена восстановлен"
},
"shop": {
@@ -87,7 +91,75 @@
"shop_channel": "Ёмкость канала +25",
"shop_section": "Новая секция хранения",
"shop_search": "Поиск нужных файлов",
"shop_shield": "Щит (отражения атак)"
"shop_shield": "Щит (отражения атак)",
"title_offers": "МАГАЗИН · 3 ПРЕДЛОЖЕНИЯ",
"subtitle": "Новые предложения после каждого часа",
"reroll": "ОБНОВИТЬ АССОРТИМЕНТ · %d $",
"reroll_limit": "ЛИМИТ ОБНОВЛЕНИЙ",
"level": "УРОВЕНЬ %d/%d",
"now": "СЕЙЧАС",
"after": "ПОСЛЕ",
"not_enough": "НЕ ХВАТАЕТ %d $",
"maxed": "МАКСИМУМ",
"all_bought": "ВСЁ КУПЛЕНО",
"lock": "ЗАКРЕПИТЬ",
"unlock": "ОТКРЕПИТЬ",
"lot_slots": "Порты коммутатора",
"lot_speed": "Оптоволокно",
"lot_sla": "Резервный ИБП",
"lot_shield": "Заземление",
"lot_money": "Корпоративный контракт",
"lot_folder": "Складской стеллаж",
"lot_channel": "Пропускная способность",
"lot_timer": "Регламент обработки"
},
"sort": {
"title": "СТОЛ СОРТИРОВКИ",
"hint": "Перетащите файлы, чтобы разложить по папкам",
"full": "СТОЛ ЗАПОЛНЕН",
"return_blocked": "На столе остались файлы, места нет",
"empty": "Здесь можно разложить файлы после часа пика"
},
"grid": {
"cells": "%d / %d клеток",
"free": "свободно %d клеток",
"busy": "занято %d из %d · свободно %d",
"empty_folder": "ПУСТО · перетащите файл или пакет",
"pack": "УПАКОВАТЬ"
},
"packet": {
"shape": "ЗАЙМЁТ %d КЛЕТОК",
"no_space": "НЕТ МЕСТА В ПАПКЕ"
},
"request": {
"minigame": "МИНИ-ИГРА: %s · T%d",
"difficulty": "СЛОЖНОСТЬ: %d",
"waiting_slot": "ОЖИДАНИЕ СЛОТА"
},
"minigame": {
"quality": "КАЧЕСТВО %d%%",
"surrender": "СДАТЬСЯ",
"skip": "ПРОПУСТИТЬ",
"no_timer": "БЕЗ ТАЙМЕРА",
"continue": "ПРОДОЛЖИТЬ",
"clean": "ЧИСТО",
"normal": "НОРМАЛЬНО",
"weak": "СЛАБО",
"dots": "Одна линия",
"sound": "Уровни",
"text": "Чужое слово",
"video": "Раскадровка",
"photo": "Кадр",
"maze": "Лабиринт",
"junk": "Фильтр",
"ddos": "Щит",
"phishing": "Паспорт",
"ransomware": "Карантин"
},
"hour": {
"next_offers": "3 НОВЫХ ЛОТА В МАГАЗИНЕ",
"sla_regen": "SLA восстановлен: +%d",
"all_slots_busy": "ВСЕ КАНАЛЫ ЗАНЯТЫ"
},
"attack": {
"junk": {
Binary file not shown.
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff. Show more