diff --git a/README.md b/README.md index 3a286c8..3654664 100644 --- a/README.md +++ b/README.md @@ -73,18 +73,19 @@ godot --path . # можно запустить несколько ок ## Структура проекта ``` -project.godot, export_presets.cfg, icon.svg -net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт) +project.godot, export_presets.cfg +branding/ splash.png (заставка загрузки), icon.svg (иконка, фавиконка) — «Aman's World» +net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт) net.gd состояние комнат, преследователей, RPC, профиль и ники, рейтинг на сервере (см. «Сеть», «Ники и рейтинг») ghost.gd чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник -player/character/ character.glb — модель и анимации (исходник и скрипт сборки: `../Blender models/character_01/`) server.tscn пустая сцена для запуска выделенного сервера core/ map_loader.gd автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт») overlay.gd автолоад Overlay: панель ника (N) и таблица лидеров (Tab) поверх любой карты - lb_view.gd LbView: таблица рейтинга карты (панель Tab, итоги Race/Parkour, будущая Metro) и общий стиль панелей + lb_view.gd LbView: таблица рейтинга карты (панель Tab, итоги Race/Parkour) и общий стиль панелей player/ player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение + character/ character.glb — модель киберниндзя и 14 анимаций (исходник и скрипт сборки: `../Blender models/character_01/`) objects/ portal.gd портал на другую карту (Area3D, рисуется кодом) chasers/ @@ -95,13 +96,13 @@ maps/ у каждой карты своя папка — он hub/ стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS) sandbox/ песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd, floor/ sky/ audio/ models/ (здания, башня, обелиск, деревья, Эмпайр-стейт…) - race/ race.tscn, race.gd, race_rules.gd (правила гонки), race_data.gd (генерируется), race.glb, sky.png, audio/music.ogg + race/ race.tscn, race.gd, race_rules.gd (правила гонки, сдача, итоги, рейтинг), race_data.gd (генерируется), baked.gdshader, race.glb, sky.png, audio/music.ogg parkour/ parkour.tscn, parkour.gd (логика: чекпоинты, лазеры, дождь, звук, окружение), parkour_data.gd (генерируется), baked.gdshader, parkour.glb (из Blender), sky.png (панорама), audio/music.ogg; см. «Заметки для будущего меня» backrooms/ backrooms.tscn, backrooms.gd, backrooms_nav.res (запечённый навмеш), model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md) deploy/ боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера -tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (боты) +tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (боты, замеры, двухклиентные тесты ников/рейтинга/гонки) ``` **Правила, которые стоит соблюдать** @@ -172,7 +173,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ ( | `RACE_STATE` | биты: 0..7 пройдено ворот, 8 готов, 9..10 фаза, **11 сдался** | время финиша или -1 | «маяк» раз в секунду и при смене фазы; по нему догоняют потерявшие событие и зашедшие позже | | `RACE_GIVEUP` | — | время гонки на момент сдачи | игрок, сдавшийся (G) | -Время: на получении START остаток считается как `b - unix_now`; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = `(ticks - start_tick)`, финиш `b` пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник — `Net.nick_of(id)` (как над призраком), цвет — `Color.from_hsv(id*0.618034…)`. В итогах справа — общий рейтинг карты (`LbView`), на финише клиент шлёт `Net.submit_my_record(rating_id, время)` (для Metro: `map.rating_id = "metro"`). +Время: на получении START остаток считается как `b - unix_now`; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = `(ticks - start_tick)`, финиш `b` пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник — `Net.nick_of(id)` (как над призраком), цвет — `Color.from_hsv(id*0.618034…)`. В итогах справа — общий рейтинг карты (`LbView`), на финише клиент шлёт `Net.submit_my_record(rating_id, время)` . **Проверка по существу** (всё в `tools/test/`): - `race_game_test.sh [папка]` — локальный сервер + 2 клиента-бота (A, B) на карте race два круга; третий (C) заходит в разгар круга 1, участвует во втором и выходит посреди него. Проверяет: одинаковые таблицы мест на всех, один START на круг, слоты, синхронность старта, заморозку на отсчёте, музыку после старта, возрождение лицом по трассе (смерть и срыв), зрителя. Сверяет `race_game_check.py`; боты — `race_bot.gd` (автозагрузка во временной копии проекта) по маршруту `race_route.json`. Порт 9080 должен быть свободен; рекорды ботов пишутся в папку теста (`--race-best=`), настоящий не трогается. @@ -193,15 +194,15 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ ( **Рейтинг (сервер).** Карты «на время» и допустимые времена — константа `Net.TIMED_MAPS` в `net/net.gd`: ```gdscript -const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}, "metro": {"min": 25.0}} +const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}} const MAX_RECORD_TIME := 3600.0 ``` -Минимумы — **страховка от мусора, не античит**: время быстрее `min` или больше 3600 с сервер молча отбрасывает. Подкрутить — поправить числа (и выложить сервер вместе с клиентом, см. «Деплой»; клиент использует тот же список, чтобы знать, где рисовать доски). Новая карта на время (Metro) — одна строка в `TIMED_MAPS` + `rating_id` в правилах + запись в `PORTALS` хаба: сервер принимает любой id из этого списка. +Минимумы — **страховка от мусора, не античит**: время быстрее `min` или больше 3600 с сервер молча отбрасывает. Подкрутить — поправить числа (и выложить сервер вместе с клиентом, см. «Деплой»; клиент использует тот же список, чтобы знать, где рисовать доски). - `submit_record(map_id, time)`: клиент сообщает свой результат (сервер берёт `client_id` и ник по peer). Проверки: карта из списка **и** игрок сейчас в её комнате, время конечное и в пределах, есть профиль, лимит **6 записей в минуту** на игрока (лишнее молча отбрасывается). Хранится лучшее время каждого `client_id` (худшее не затирает; ник обновляется при смене ника и/или улучшении), не более **100** записей на карту (худшие вытесняются). Ответ: `recv_record_result(map_id, time, rank, personal, map_record)` (место, «новый личный рекорд», «новый рекорд карты» = личный и 1-е место) и `recv_leaderboard`. - `request_leaderboard(map_id)` -> `recv_leaderboard(map_id, top, my_rank, my_time)`: `top` — до 10 записей `[ник, время, unix-ts]` по возрастанию (при равенстве — кто раньше), `my_rank` — место запросившего (0 — нет записи), `my_time` — его лучшее. Лимит **12 запросов за 10 с** на игрока. Клиент хранит ответы в `Net.boards[map_id]`, сигнал `Net.leaderboard_updated(map_id)`. - **Хранилище:** `user://leaderboard.json` на сервере. На боевом сервере `user://` = `/var/lib/my-world/.local/share/godot/app_userdata/world2-server/` (служба `my-world`: `StateDirectory=my-world`, `HOME=/var/lib/my-world`, DynamicUser) — папка постоянная, переживает перезапуск службы и `tools/deploy_server.sh` (тот копирует только `net/` и `project.godot`). Формат: ```json -{"version": 1, "maps": {"race": {"": {"nick": "Алиса", "time": 52.318, "ts": 1793250000}}, "parkour": {}, "metro": {}}} +{"version": 1, "maps": {"race": {"": {"nick": "Алиса", "time": 52.318, "ts": 1793250000}}, "parkour": {}}} ``` Запись атомарная (во временный `.tmp`, затем переименование), не чаще раза в 2 с (`SAVE_EVERY`; первая после паузы — сразу). Чтение при старте; отсутствующий файл — пустой рейтинг; испорченный — сервер не падает, файл переименовывается в `leaderboard.json.bad`, рейтинг начинается заново; при загрузке отбрасываются неизвестные карты, неверные id и времена, лишнее сверх 100 на карту. **`systemctl stop/restart` не сбрасывает отложенную запись** (Godot по SIGTERM не присылает уведомление о закрытии): в худшем случае теряются результаты последних ≤ 2 с. - **Бэкап / сброс:** `ssh root@192.168.10.110 'cp /var/lib/my-world/.local/share/godot/app_userdata/world2-server/leaderboard.json /root/leaderboard-$(date +%F).json'`; сброс — остановить службу (`systemctl stop my-world`), удалить файл, запустить. Проверить, что файл пишется в постоянную папку: после любого результата `ssh root@192.168.10.110 'ls -la /var/lib/my-world/.local/share/godot/app_userdata/world2-server/; journalctl -u my-world -n 5 --no-pager'` (в логе строка `[рекорд] race <ник> <время> (место N)`). Если путь не тот — `find /var/lib/my-world -name leaderboard.json`. @@ -469,6 +470,8 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок 4. **Race:** гонка на ногах (трасса 633 м из Blender, три развилки, 8 ворот), правила и HUD, новые сетевые события, песня на «СТАРТ!»; сделано агентами, проверено двухклиентным тестом на локальном сервере. 5. **Инфраструктура:** пресеты экспорта карт (с `include_filter` для файлов, грузимых по строке), проверка пакетов «по существу», выкладка сайта и сервера по порядку из «Деплой». 6. **Замеры:** вкладка в браузере ~300 МБ. +7. **Ники, общий рейтинг, таблица Tab, доски в хабе, сдача в гонке (G), таймер Parkour**, название «Aman's World» (заставка, иконка). +8. **Карта Metro (гонка вниз 1495 м со скользящими рельсами и бустером) сделана и УДАЛЕНА по решению владельца** («полный мусор», окт 2026): не понравилась. Из истории не восстанавливай без просьбы. Что узнали: (а) владельцу важнее всего скорость, маневренность и ощущение потока, как у первой гонки Race; (б) прежде чем отдавать большую карту агентам, показать владельцу концепцию и кадры черновика; (в) идея «скользящий рельс» (подхват в воздухе, езда 36 м/с, сход прыжком) работала технически, но потолок `MAX_SPEED = 22` обрезает скорость на сходе; (г) слишком светлые «пятнистые» залы из-за запекания и белые засветки — следить за силой света при запекании. ## Лицензии и авторство diff --git a/core/lb_view.gd b/core/lb_view.gd index 5571ed2..c52684a 100644 --- a/core/lb_view.gd +++ b/core/lb_view.gd @@ -1,7 +1,7 @@ class_name LbView extends VBoxContainer # Таблица рейтинга карты (топ-10 + «Вы: N-й»), данные берутся из Net.boards и обновляются сами по сигналу. -# Одна и та же таблица в панели Tab, в итогах гонки, итогах Parkour и будущей карты Metro: LbView.create("metro"). +# Одна и та же таблица в панели Tab, в итогах гонки и итогах Parkour: LbView.create(id_карты). # Ники выводятся обычными Label (не RichText), поэтому «[b]» в чужом нике оформление не ломает. const TEXT := Color(0.92, 0.88, 0.8) diff --git a/maps/race/race_rules.gd b/maps/race/race_rules.gd index e0d0509..5d1c92a 100644 --- a/maps/race/race_rules.gd +++ b/maps/race/race_rules.gd @@ -43,7 +43,7 @@ var splits: Array = [] # свои сплиты круга var best_time := -1.0 var best_splits: Array = [] var gave_up := {} # id -> true: сдался в этом круге -var rating_id := "race" # id карты в рейтинге сервера (Metro: map.rating_id = "metro") +var rating_id := "race" # id карты в рейтинге сервера var debug := false # --race-log: факты в консоль (тесты) var _beacon_timer := 0.0 diff --git a/net/net.gd b/net/net.gd index 4f6a7c3..4fc5983 100644 --- a/net/net.gd +++ b/net/net.gd @@ -28,8 +28,8 @@ const RACE_GIVEUP = 5 # игрок сдался (b: время гонки на # Рейтинг лучших времён. Список карт «на время» и минимально допустимое время (страховка от мусора, не античит; # подкручивается здесь — см. README «Ники и рейтинг»). Сервер принимает только эти id; клиент по ним же решает, где рисовать доски. -const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}, "metro": {"min": 25.0}} -const MAP_TITLES := {"race": "Гонка", "parkour": "Паркур", "metro": "Метро", "hub": "Хаб", "world": "Песочница", "backrooms": "Backrooms"} +const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}} +const MAP_TITLES := {"race": "Гонка", "parkour": "Паркур", "hub": "Хаб", "world": "Песочница", "backrooms": "Backrooms"} const MAX_RECORD_TIME := 3600.0 const BOARD_TOP := 10 # сколько строк уходит клиенту const BOARD_KEEP := 100 # сколько записей на карту хранит сервер diff --git a/tools/test/nicks_lb_test.py b/tools/test/nicks_lb_test.py index 2656238..febfd0e 100755 --- a/tools/test/nicks_lb_test.py +++ b/tools/test/nicks_lb_test.py @@ -172,7 +172,7 @@ check(vals == [32.0], "отложенная запись (≤2 с) попала # ---------------------------------------------------------------- 6. обрезка до 100 print("== 6. больше 100 записей: обрезка") entries = {("%032x" % (i + 1)): {"nick": "N%d" % i, "time": 30.0 + i * 0.1, "ts": 1000 + i} for i in range(120)} -json.dump({"version": 1, "maps": {"race": entries, "parkour": {}, "metro": {}, "ghost_map": {"%032x" % 999: {"nick": "q", "time": 50, "ts": 1}}}}, open(LB_FILE, "w")) +json.dump({"version": 1, "maps": {"race": entries, "parkour": {}, "ghost_map": {"%032x" % 999: {"nick": "q", "time": 50, "ts": 1}}}}, open(LB_FILE, "w")) start_server() T = client("T", "wait_connected;nick:Tt;map:race;rec:race:29.0;req:race;quit") ot = T() diff --git a/tools/test/ui_tour.gd b/tools/test/ui_tour.gd index 376923a..0049ee4 100644 --- a/tools/test/ui_tour.gd +++ b/tools/test/ui_tour.gd @@ -27,7 +27,6 @@ func _ready() -> void: Net.boards["race"] = fake_board(3, true) Net.boards["parkour"] = fake_board(14, true) Net.boards["parkour"].top = Net.boards["parkour"].top.slice(0, 10) - Net.boards["metro"] = {"top": [], "my_rank": 0, "my_time": 0.0, "tick": 0} Net.nicks[111] = "Алиса" Net.nicks[222] = "Bob [b]жирный" Net.nicks[333] = "Тень"