Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d347d132b | ||
|
|
0ffba7311f | ||
|
|
5d8b2165e3 | ||
|
|
3d017ffdca | ||
|
|
86a1821d27 | ||
|
|
c3e0ae40b2 | ||
|
|
b58c30a525 | ||
|
|
054b96e07c | ||
|
|
db437afd7a | ||
|
|
8c1bbe52b0 | ||
|
|
666bebfb60 | ||
|
|
d38152ea97 | ||
|
|
fd7523c8fb | ||
|
|
a6174ba749 | ||
|
|
9130202a51 | ||
|
|
2a6397da79 | ||
|
|
556d3af615 | ||
|
|
af462364c0 | ||
|
|
1438e5a04b | ||
|
|
73e2d69ec6 | ||
|
|
0eaf6d19ea | ||
|
|
b8282b1438 | ||
|
|
ace7ed5c84 | ||
|
|
fc0139dc05 | ||
|
|
5ba6466113 | ||
|
|
307c62b62e | ||
|
|
ed4ee86c8e | ||
|
|
af81050a1a | ||
|
|
9a4142419d | ||
|
|
75aa525cb8 | ||
|
|
97fafe6a30 | ||
|
|
b7ca18ae3d | ||
|
|
db27711ca7 | ||
|
|
823d68ffd6 | ||
|
|
6b34b8593f | ||
|
|
8c9d7e2c82 | ||
|
|
c24ad31fd7 |
@@ -1,3 +1,6 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# Кэш UV-разворачивания, создаётся при импорте
|
||||
*.unwrap_cache
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
class_name Ghost
|
||||
extends Node3D
|
||||
# Чужой игрок: капсула, которая плавно догоняет последнее полученное состояние.
|
||||
|
||||
const SMOOTHING = 14.0
|
||||
|
||||
var target_pos := Vector3.ZERO
|
||||
var target_yaw := 0.0
|
||||
var flags := 0
|
||||
var body: MeshInstance3D
|
||||
var visor: MeshInstance3D
|
||||
|
||||
func setup(id: int, pos: Vector3) -> void:
|
||||
name = "Ghost_%d" % id
|
||||
position = pos
|
||||
target_pos = pos
|
||||
var color := Color.from_hsv(fmod(id * 0.618034, 1.0), 0.7, 1.0)
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.emission_enabled = true
|
||||
mat.emission = color
|
||||
mat.emission_energy_multiplier = 0.6
|
||||
|
||||
var capsule := CapsuleMesh.new()
|
||||
capsule.radius = 0.34
|
||||
capsule.height = 2.3
|
||||
body = MeshInstance3D.new()
|
||||
body.mesh = capsule
|
||||
body.material_override = mat
|
||||
add_child(body)
|
||||
|
||||
# «Визор» показывает, куда смотрит игрок (-Z — вперёд)
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(0.5, 0.15, 0.3)
|
||||
visor = MeshInstance3D.new()
|
||||
visor.mesh = box
|
||||
visor.position = Vector3(0, 0.9, -0.25)
|
||||
add_child(visor)
|
||||
|
||||
var label := Label3D.new()
|
||||
label.text = "Игрок %d" % (id % 1000)
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.no_depth_test = true
|
||||
label.position = Vector3(0, 1.6, 0)
|
||||
label.pixel_size = 0.005
|
||||
add_child(label)
|
||||
|
||||
func set_target(pos: Vector3, yaw: float, pitch: float, new_flags: int) -> void:
|
||||
target_pos = pos
|
||||
target_yaw = yaw
|
||||
flags = new_flags
|
||||
visor.rotation.x = pitch
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var t := 1.0 - exp(-SMOOTHING * delta)
|
||||
position = position.lerp(target_pos, t)
|
||||
rotation.y = lerp_angle(rotation.y, target_yaw, t)
|
||||
|
||||
var sliding := (flags & Net.FLAG_SLIDING) != 0
|
||||
var squash := 0.55 if sliding else 1.0
|
||||
body.scale = body.scale.lerp(Vector3(1, squash, 1), t)
|
||||
var roll := 0.0
|
||||
if (flags & Net.FLAG_WALLRUN) != 0:
|
||||
roll = 0.35
|
||||
body.rotation.z = lerp(body.rotation.z, roll, t)
|
||||
@@ -1,122 +0,0 @@
|
||||
extends Node
|
||||
# Автолоад "Net". Один и тот же скрипт работает и на клиенте, и на выделенном сервере
|
||||
# (RPC требуют одинаковый путь узла и скрипт). Сервер — это headless-запуск Godot.
|
||||
|
||||
const PORT = 9080
|
||||
const DEFAULT_URL = "ws://127.0.0.1:9080"
|
||||
const SEND_RATE = 20.0
|
||||
const RETRY_DELAY = 3.0
|
||||
|
||||
const FLAG_SLIDING = 1
|
||||
const FLAG_WALLRUN = 2
|
||||
const FLAG_DASH = 4
|
||||
const FLAG_MANTLE = 8
|
||||
|
||||
var is_server := false
|
||||
var ghosts := {} # peer_id -> Ghost
|
||||
var send_timer := 0.0
|
||||
var connected := false
|
||||
|
||||
func _ready() -> void:
|
||||
is_server = DisplayServer.get_name() == "headless"
|
||||
if is_server:
|
||||
_start_server()
|
||||
else:
|
||||
_connect_client()
|
||||
|
||||
# ---------- Сервер ----------
|
||||
|
||||
func _start_server() -> void:
|
||||
var peer := WebSocketMultiplayerPeer.new()
|
||||
var err := peer.create_server(PORT)
|
||||
if err != OK:
|
||||
push_error("Не удалось открыть порт %d: %s" % [PORT, error_string(err)])
|
||||
get_tree().quit(1)
|
||||
return
|
||||
multiplayer.multiplayer_peer = peer
|
||||
multiplayer.peer_connected.connect(func(id): print("+ игрок ", id))
|
||||
multiplayer.peer_disconnected.connect(_server_peer_left)
|
||||
print("Сервер запущен на порту ", PORT)
|
||||
|
||||
func _server_peer_left(id: int) -> void:
|
||||
print("- игрок ", id)
|
||||
for other in multiplayer.get_peers():
|
||||
recv_left.rpc_id(other, id)
|
||||
|
||||
# Клиент -> сервер: своё состояние. Сервер пересылает остальным.
|
||||
@rpc("any_peer", "unreliable_ordered")
|
||||
func submit_state(pos: Vector3, yaw: float, pitch: float, flags: int) -> void:
|
||||
if not is_server:
|
||||
return
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
for other in multiplayer.get_peers():
|
||||
if other != sender:
|
||||
recv_state.rpc_id(other, sender, pos, yaw, pitch, flags)
|
||||
|
||||
# ---------- Клиент ----------
|
||||
|
||||
func _server_url() -> String:
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with("--server-url="):
|
||||
return arg.substr("--server-url=".length())
|
||||
if OS.has_feature("web"):
|
||||
var proto = JavaScriptBridge.eval("window.location.protocol")
|
||||
var host = JavaScriptBridge.eval("window.location.host")
|
||||
return ("wss://" if proto == "https:" else "ws://") + str(host) + "/ws"
|
||||
return DEFAULT_URL
|
||||
|
||||
func _connect_client() -> void:
|
||||
var peer := WebSocketMultiplayerPeer.new()
|
||||
var err := peer.create_client(_server_url())
|
||||
if err != OK:
|
||||
push_warning("Не удалось подключиться: %s" % error_string(err))
|
||||
_schedule_retry()
|
||||
return
|
||||
multiplayer.multiplayer_peer = peer
|
||||
if not multiplayer.connected_to_server.is_connected(_on_connected):
|
||||
multiplayer.connected_to_server.connect(_on_connected)
|
||||
multiplayer.connection_failed.connect(_on_lost)
|
||||
multiplayer.server_disconnected.connect(_on_lost)
|
||||
|
||||
func _on_connected() -> void:
|
||||
connected = true
|
||||
|
||||
func _on_lost() -> void:
|
||||
connected = false
|
||||
for g in ghosts.values():
|
||||
g.queue_free()
|
||||
ghosts.clear()
|
||||
multiplayer.multiplayer_peer = null
|
||||
_schedule_retry()
|
||||
|
||||
func _schedule_retry() -> void:
|
||||
get_tree().create_timer(RETRY_DELAY).timeout.connect(_connect_client)
|
||||
|
||||
@rpc("authority", "unreliable_ordered")
|
||||
func recv_state(id: int, pos: Vector3, yaw: float, pitch: float, flags: int) -> void:
|
||||
var ghost: Ghost = ghosts.get(id)
|
||||
if ghost == null:
|
||||
ghost = Ghost.new()
|
||||
ghost.setup(id, pos)
|
||||
ghosts[id] = ghost
|
||||
get_tree().current_scene.add_child(ghost)
|
||||
ghost.set_target(pos, yaw, pitch, flags)
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func recv_left(id: int) -> void:
|
||||
var ghost = ghosts.get(id)
|
||||
if ghost:
|
||||
ghost.queue_free()
|
||||
ghosts.erase(id)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if is_server or not connected:
|
||||
return
|
||||
send_timer += delta
|
||||
if send_timer < 1.0 / SEND_RATE:
|
||||
return
|
||||
send_timer = 0.0
|
||||
var player := get_tree().get_first_node_in_group("local_player")
|
||||
if player == null:
|
||||
return
|
||||
submit_state.rpc_id(1, player.global_position, player.rotation.y, player.head.rotation.x, player.get_net_flags())
|
||||
@@ -1,2 +1,571 @@
|
||||
## Что за репо
|
||||
Это репо самого проекта в godot, тоесть тут все файлы
|
||||
# My World
|
||||
|
||||
# Кароче это пишу я, владелец карты
|
||||
1. Управление геймпадом
|
||||
2. Новую карту гонки, стиль тот же киберпанк, надо чтобы было длиньше первой, и упор шел на маневренность и скорость
|
||||
3. Чтобы на Tab показывался список игроков онлайн
|
||||
|
||||
|
||||
Мультиплеерная игра от первого лица в браузере (Godot 4.7.2, Web). Игроки собираются в стартовой
|
||||
комнате-хабе, заходят через порталы на разные карты и убегают там от преследователей — камня и Верити.
|
||||
Паркур-управление: спринт, рывок, подкат, присед, бег по стенам, подтягивание на уступы.
|
||||
|
||||
- **Движок:** Godot **4.7.2**, рендерер **GL Compatibility** (нужен для Web), физика **Jolt**
|
||||
- **Платформа:** Web (WebAssembly), без потоков (`thread_support=false`), без GDExtension (`extensions_support=false`)
|
||||
- **Язык** комментариев в коде, интерфейса и документации: русский
|
||||
- **Репозитории** (соседние папки в `/home/aibrash/Projects/My_World/`):
|
||||
`my-world` — этот проект; `grid-world` — готовый веб-экспорт, который CI выкладывает на сайт;
|
||||
`Blender models` — исходники моделей; `source-assets` — исходные файлы вне игры (например, видео, из которого сделан звук Верити)
|
||||
|
||||
## Состояние: что есть и чего нет
|
||||
|
||||
| | Что |
|
||||
|---|---|
|
||||
| ✅ | Хаб с порталами; карты **Песочница**, **Backrooms**, **Parkour**, **Race** и **Race 2**; выход из карт обратно в хаб |
|
||||
| 🔶 | **Race 2 «Верхние ярусы»** — вторая гонка (крыши и мосты над городом, бег по фасадам, лазерные мосты, спуск, башня; три пути на каждом участке; вентиляторы и ускоряющие полосы). **Фаза 1 готова** (раскладка, геометрия, проходимость ботами, правила); фаза 2 (чистовое запекание, своё небо и музыка) ждёт одобрения владельца — раздел «Карта Race 2» |
|
||||
| ✅ | **Race** — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); **Parkour** — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня») |
|
||||
| ✅ | Вкладка в браузере занимает **~300 МБ** (замер владельца, окт 2026) |
|
||||
| ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) |
|
||||
| ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, **комнаты по картам** (в разных картах не пересекаются) |
|
||||
| ✅ | Преследователи: **камень** и **Верити**; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока |
|
||||
| ✅ | Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!» |
|
||||
| ✅ | Паркур: спринт, рывок, подкат, **присед (C)**, бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ) |
|
||||
| ✅ | Управление **не зависит от раскладки** (русская и любая другая): действия `InputMap` на физические клавиши |
|
||||
| ✅ | Деплой: CI из `grid-world` на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера |
|
||||
| ✅ | **Геймпад Xbox** (раскладка под паркур и гонки, обзор правым стиком), **меню паузы** по Esc/Start (выход в хаб, настройки, справка), **настройки** (мышь, стик, FOV, громкость, инверсия) — разделы «Управление», «Меню паузы и настройки». В браузере с живым геймпадом не проверялось (только тест без окна) |
|
||||
| ✅ | **Ники** (панель по N, подпись над игроками, в итогах гонки) и **общий рейтинг** лучших времён на сервере (топ-10 на Race/Parkour, таблица по Tab, доски в хабе, таймер и итоги Parkour, сдача в гонке на G) — раздел «Ники и рейтинг» |
|
||||
| ❌ | Текстовый чат — **запланирован**, не сделан (план в «Дорожная карта → Ближайшее») |
|
||||
| ✅ | Хаб увеличен (48×32×10 м) и оформлен, в нём играет музыка «как в лифте» — раздел «Карты и порталы», «Лицензии» |
|
||||
| ❌ | Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает |
|
||||
| ❌ | Сохранения прогресса, аккаунтов |
|
||||
| ❌ | Автоматических тестов в CI (есть боты для ручного прогона, см. «Тесты без окна») |
|
||||
| ✅ | Чужой игрок — модель киберниндзя со скелетом (`player/character/character.glb`, 752 треугольника, 17 костей, 14 анимаций); свечение красится в цвет игрока от его id. Своё тело — та же модель, спрятанная от своей камеры |
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# запуск игры локально (стартует в хабе; без сервера — одиночная игра, клиент переподключается каждые 3 с)
|
||||
godot --path . --windowed --resolution 960x540
|
||||
|
||||
# с локальным сервером (в другом терминале)
|
||||
tools/test/make_test_server.sh /tmp/test-server && godot --headless --path /tmp/test-server
|
||||
godot --path . # можно запустить несколько окон
|
||||
```
|
||||
|
||||
Карты в редакторе запускаются отдельно (F6 на сцене карты): `maps/sandbox/sandbox.tscn`,
|
||||
`maps/backrooms/backrooms.tscn`, `maps/parkour/parkour.tscn`, `maps/race/race.tscn`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются).
|
||||
|
||||
## Управление
|
||||
|
||||
Клавиши привязаны к **физическому расположению** (так работает и на русской раскладке). Действия лежат в
|
||||
`project.godot → [input]`: `move_forward/back/left/right`, `jump`, `sprint`, `crouch`, `dash`, `ready` (R — «готов» в гонке, перезапуск забега в Parkour), `nick` (N), `leaderboard` (Tab), `giveup` (G). Действия, которых нет в `project.godot`, добавляет `core/settings.gd` кодом при старте: `pause` (Esc, Start), `zoom` (LT, R3), `look_left/right/up/down` (правый стик) и все кнопки геймпада.
|
||||
|
||||
| Клавиша | Действие |
|
||||
|---|---|
|
||||
| W A S D, стрелки | движение |
|
||||
| Shift | спринт |
|
||||
| Пробел | прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок |
|
||||
| C (держать) | **присед** (скорость 3,5, капсула вдвое ниже, пролезает под низкое) |
|
||||
| Shift + C на бегу | **подкат** |
|
||||
| Q | рывок |
|
||||
| R | «готов» (карта Race); на карте Parkour — **забег заново** (на старт, таймер сброшен) |
|
||||
| G (держать 1,5 с) | **сдаться** в гонке (только пока идёт гонка; отпустил раньше — отмена) |
|
||||
| N | ввод/смена ника (панель по центру; Enter — подтвердить, Esc — пропустить/отмена); на время ввода игрок стоит |
|
||||
| Tab | таблица лидеров текущей карты (топ-10 и «Вы: N-й»); в хабе и на картах без рейтинга — вкладки по картам, A/D или стрелки; повторный Tab или Esc закрывают |
|
||||
| ЛКМ (зажать) | зум |
|
||||
| Esc | **меню паузы** (мышь освобождается); «Продолжить» или повторный Esc — назад в игру и захват мыши |
|
||||
| клик по игре | захватить мышь |
|
||||
|
||||
**Геймпад Xbox** (правый большой палец всегда на стике обзора, поэтому прыжок продублирован на LB, а рывок на RB):
|
||||
|
||||
| Кнопка | Действие |
|
||||
|---|---|
|
||||
| Левый стик / правый стик | движение / обзор (кривая 1,6: точно у центра, быстро на краю; скорость — в настройках) |
|
||||
| A или LB | прыжок (у уступа — подтянуться, у стены — от стены, в воздухе — двойной) |
|
||||
| RT или L3 | спринт (держать) |
|
||||
| X или RB | рывок |
|
||||
| B (держать) | присед; **RT + B на бегу** — подкат |
|
||||
| LT или R3 | зум |
|
||||
| Y | «готов» в Race; забег заново в Parkour |
|
||||
| Back | таблица лидеров (Tab) |
|
||||
| Крестовина вниз (держать 1,5 с) | сдаться в гонке |
|
||||
| Start | меню паузы (в меню: крестовина/левый стик, A — выбрать, B — назад) |
|
||||
|
||||
Ник по геймпаду меняется в меню паузы. Бег по стене — в воздухе у стены с зажатым «вперёд» на скорости. Ctrl не используется: в браузере `Ctrl+W` закрывает вкладку.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
project.godot, export_presets.cfg
|
||||
branding/ splash.png (заставка загрузки), icon.svg (иконка, фавиконка) — «Aman's World»
|
||||
net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт)
|
||||
net.gd состояние комнат, преследователей, RPC, профиль и ники, рейтинг на сервере (см. «Сеть», «Ники и рейтинг»)
|
||||
ghost.gd чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник
|
||||
server.tscn пустая сцена для запуска выделенного сервера
|
||||
core/
|
||||
map_loader.gd автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт»)
|
||||
settings.gd автолоад Settings: настройки (user://settings.cfg) и раскладка геймпада в InputMap
|
||||
pause_menu.gd автолоад PauseMenu: меню паузы Esc/Start (см. «Меню паузы и настройки»)
|
||||
overlay.gd автолоад Overlay: панель ника (N) и таблица лидеров (Tab) поверх любой карты
|
||||
lb_view.gd LbView: таблица рейтинга карты (панель Tab, итоги Race/Parkour) и общий стиль панелей
|
||||
player/
|
||||
player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение
|
||||
character/ character.glb — модель киберниндзя и 14 анимаций (исходник и скрипт сборки: `../Blender models/character_01/`)
|
||||
objects/
|
||||
portal.gd портал на другую карту (Area3D, рисуется кодом)
|
||||
vent.gd вентиляционный поток (Area3D): тянет игрока вверх, горизонтальная скорость сохраняется (Race 2)
|
||||
boost_strip.gd ускоряющая полоса (Area3D): добирает скорость до MAX_SPEED вдоль полосы (Race 2)
|
||||
chasers/
|
||||
chaser.gd общий «мозг» преследователя: навмеш, зрение, погоня, блуждание, поимка, синхронизация
|
||||
rock/ камень: rock.tscn, Rock.glb (+ извлечённая текстура), Guitar.ogg
|
||||
verity/ Верити: verity.tscn, verity2.glb (+ текстура), verity.ogg (звук из видео)
|
||||
maps/ у каждой карты своя папка — она самодостаточна и экспортируется отдельным пакетом
|
||||
hub/ стартовая комната: hub.tscn, hub.gd (порталы из списка PORTALS, музыка), hub_decor.gd (оформление), audio/music.ogg + CREDITS.md
|
||||
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 2**), race_data.gd (генерируется), baked.gdshader, race.glb, sky.png, audio/music.ogg
|
||||
race2/ race2.tscn, race2.gd (внешний вид, ворота, лазеры, вентиляторы, полосы), race2_data.gd (генерируется), baked.gdshader, race2.glb, sky.png, audio/music.ogg; правила берёт из maps/race/race_rules.gd
|
||||
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/ (боты, замеры, двухклиентные тесты ников/рейтинга/гонки)
|
||||
```
|
||||
|
||||
**Правила, которые стоит соблюдать**
|
||||
|
||||
- **Папки и файлы — в `snake_case`.** Каждая карта — в `maps/<имя>/`. Всё, что нужно только ей, лежит внутри.
|
||||
Общее (игрок, преследователи, порталы, сеть) — вне `maps/`: оно входит в основной пакет игры.
|
||||
- **Файлы рядом с `.glb` не переименовывать и не удалять.** Это текстуры, которые Godot вынул из модели при импорте
|
||||
(`gltf/embedded_image_handling=Extract`), их имена зависят от имени `.glb`. Переименуешь `.glb` — импорт создаст дубли.
|
||||
Папки с моделями переносить целиком можно (я так и делал).
|
||||
- **`.import` и `.uid` коммитятся** и переносятся вместе с файлом (`git mv`). Кэш `.godot/` не коммитится; после переноса
|
||||
файлов вне редактора его удаляют и делают `godot --headless --import --path .`.
|
||||
- Пути в сценах — через `res://`; ссылки на карты (`PORTALS` в `hub.gd`, `target_scene` у порталов) — строками.
|
||||
|
||||
## Карты и порталы
|
||||
|
||||
| Карта | Сцена | Пакет | Что там |
|
||||
|---|---|---|---|
|
||||
| Хаб | `maps/hub/hub.tscn` | в основном пакете | «Лобби»: зал 48×32×10 м, строится кодом (`hub.gd` — стены, порталы, доски, музыка; `hub_decor.gd` — оформление). Порталы в ряд у северной стены с шагом 6 м (влезает 7; SANDBOX `x=-12`, RACE 2 `x=12`, `z=-15`), игрок появляется на `(0, 1.2, 11)`. В центре платформа с вращающейся голограммой, пол с латунными кольцами и дорожкой к порталам, 6 колонн с неоновыми прорезями, потолочные светильники (свечение — `glow`), в боковых стенах шесть окон с ночным городом (шейдер), лавки (на них можно запрыгнуть), пылинки в воздухе, табло «ЛОББИ» на южной стене. Над порталами карт «на время» — доски с топ-3 (см. «Ники и рейтинг»). Музыка `maps/hub/audio/music.ogg` («Local Forecast - Elevator», Kevin MacLeod, CC-BY) играет по кругу, плавно нарастая за 3 с, −9 дБ. Безопасна, преследователей нет |
|
||||
| Песочница | `maps/sandbox/sandbox.tscn` | `world.pck` | Киберпанк-мир: пол-шахматка, здания, 60 деревьев (MultiMesh), птицы, парящий обелиск, звук `tron_01`. Преследователи: **камень** и **Верити**. Портал «HUB» возвращает в хаб |
|
||||
| Backrooms | `maps/backrooms/backrooms.tscn` | `backrooms.pck` | Модель «Backrooms VR» (×1,4). Преследователь: **Верити** (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом |
|
||||
| Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (7,4 МБ: небо 3,5 + модель 3,4 + музыка 1,1) | Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). **Забег на время** (старт — линия на краю стартовой платформы, финиш — вершина; рейтинг на сервере; R — заново). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте. Музыка: `maps/parkour/audio/music.ogg` (Ogg Vorbis, моно 32 кГц, ~1 МБ; делается `ffmpeg -i исходник.mp3 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`), играет по кругу |
|
||||
| Race | `maps/race/race.tscn` | `race.pck` | Гонка на ногах до 8 игроков (кто быстрее до финиша), мрачный город: стартовая площадка → рынок → эстакада → крыши → каньон → ангар. 8 ворот-чекпоинтов, 2 лазера, развилки с обходами. Правила и сеть — раздел «Карта Race» ниже |
|
||||
| Race 2 | `maps/race2/race2.tscn` | `race2.pck` (9,5 МБ) | «Верхние ярусы»: гонка по крышам и мостам над городом, 13 ворот, ~1,2 км безопасного пути; на каждом участке безопасный / рискованный / секретный путь; вентиляторы-подбросы и ускоряющие полосы. Правила те же, что у Race (общий код). Раздел «Карта Race 2» |
|
||||
|
||||
**Правка и пересборка Parkour** (всё в `Blender models/parkour_02/`):
|
||||
1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт).
|
||||
2. `build_parkour.py` — в Blender (`exec`): строит коллизию (`Collision-colonly` — Godot делает из неё StaticBody), детальную геометрию `Solid`, светящиеся элементы `Neon`; пишет `parkour_data.gd` (чекпоинты, лазеры, старт, портал). Сохраняет `Parkour_02.blend`.
|
||||
3. `bake.py` — `blender -b Parkour_02.blend -P bake.py`: Cycles запекает прямой+непрямой свет (неон светит сам, плюс холодный/пурпурный/бирюзовый свет) в **цвета вершин** (сжаты в 8 раз — шейдер умножает обратно) и экспортирует `parkour.glb`. ~1 мин.
|
||||
4. `build_sky.py` — `blender -b -P build_sky.py`: панорама дальнего города 3072×1536 → `sky.png` (в импорте включены мипы — по ним размытые отражения). ~2 мин.
|
||||
5. `godot --headless --import --path .`.
|
||||
Палитра светящихся задана именами материалов из Blender (старые названия сохранены): `NeonCyan` — холодный белёсый, `NeonMagenta` — красный, `NeonOrange` — натриевая лампа. Настроение — в `parkour.gd` (туман, дождь `_add_rain`, гул `_add_ambience`, насыщенность и контраст), в `bake.py` (силы света), `build_sky.py` (дымное небо).
|
||||
В Godot: `baked.gdshader` (unshaded: цвет материала × запечённый свет × «зерно» бетона по мировым координатам + влажный блеск — отражение панорамы на горизонтальных поверхностях); `parkour.gd` подменяет материалы по именам из Blender (`SURFACES`, `GLOWS`), строит ворота-чекпоинты, лазеры (пульсирующие считают фазу по системным часам), окружение и портал.
|
||||
|
||||
**Добавить карту:**
|
||||
1. Создай `maps/<имя>/<имя>.tscn`: корень Node3D, внутри игрок (`player/player.tscn`), портал обратно (`objects/portal.gd`,
|
||||
`target_map="hub"`, `target_scene="res://maps/hub/hub.tscn"`), при необходимости преследователи.
|
||||
В `_ready()` вызови `Net.enter_map("<имя>")` (идемпотентно; так сервер знает, в какой ты комнате).
|
||||
2. Если нужны преследователи — навмеш: либо `NavigationRegion3D` + `maps/sandbox/nav_bake.gd` (запекание при старте, как в песочнице),
|
||||
либо заранее запечённый `.res` (как у Backrooms: в вебе без потоков запекать на лету долго).
|
||||
3. В `export_presets.cfg` добавь пресет-копию «World» (`export_filter="resources"`, `export_files=["res://maps/<имя>/<имя>.tscn"]`,
|
||||
`exclude_filter="player/*, objects/*, chasers/chaser.gd"`, `export_path="../grid-world/<имя>.pck"`),
|
||||
а в пресет «Web» — `maps/<имя>/*` в `exclude_filter`. Добавь вызов в `tools/export_all.sh`.
|
||||
4. В `PORTALS` (`maps/hub/hub.gd`) допиши запись `{title, map, scene, pack, color}` — портал появится сам.
|
||||
|
||||
## Карта Race (гонка)
|
||||
|
||||
Файлы: `maps/race/` — `race.tscn`, `race.gd` (внешний вид, ворота, лазеры, срыв, дождь/гул, **музыка**), `race_rules.gd` (правила, HUD, сеть), `race_data.gd`
|
||||
(генерируется из `Blender models/race_01/`, не править), `race.glb`, `sky.png`, `baked.gdshader`, `audio/music.ogg`.
|
||||
Пакет `race.pck` (в нём `include_filter`: sky.png, baked.gdshader, race_data.gd, race_rules.gd, race.gd, audio/*; без него файлы, которые скрипт грузит по строке, не попадают в пакет).
|
||||
|
||||
**Правила.** ОЖИДАНИЕ → ОТСЧЁТ → ГОНКА → ИТОГИ → ОЖИДАНИЕ.
|
||||
- Ожидание: бегаем свободно. **R** (действие `ready`) переключает «готов»; подсказка «R — готов (n/m)». Все готовы (хотя бы один) → отсчёт.
|
||||
- Отсчёт 6 с (секунда «приготовьтесь», затем 5…1): все телепортируются в слоты (номер = индекс в отсортированном списке id комнаты; больше 8 — рядами позади, шаг 3 м) лицом по трассе и **заморожены** (`player.input_frozen`; обзор мышью работает).
|
||||
- Гонка: ворота только по порядку (пересечение плоскости ворот по ходу трассы в пределах полуширины+1 м; чужие ворота игнорируются). Последние ворота = финиш: фиксируется время, дальше бежим свободно. Сплит: «ВОРОТА n время ±разница с личным лучшим забегом».
|
||||
- Место: финишировавшие — по времени, остальные — по числу ворот, затем по расстоянию до следующих ворот. Конец круга: финишировали все присутствующие **или** прошло 45 с после первого финиша; игрок вышел — убирается из расчётов (если остался один — бежит до финиша). Итоги ~8 с, затем ожидание; готовность сброшена, игроки стоят где стояли.
|
||||
- Зашедший в разгар круга бегает без зачёта, видит «ГОНКА ИДЁТ» (определяется по маякам других игроков) и участвует со следующего круга.
|
||||
- **Сдача** (G, удержание 1,5 с): индикатор «СДАЮСЬ…» с полоской внизу, отпустил раньше — отмена. Только в фазе «гонка», у не финишировавшего. Сдавшийся возвращается на стартовую площадку (свободно бегает до конца круга), в итогах стоит ниже всех со строкой «сдался», **время не пишется** ни в итоги, ни в рейтинг. Сдавшиеся считаются завершившими для конца круга (все финишировали/сдались — итоги сразу; иначе 45 с после первого финиша), места остальных считаются без них.
|
||||
- Срыв: ниже **последних пройденных ворот** на 32 м (до первых ворот — ниже стартового слота) или лазер → «СОРВАЛСЯ!», возрождение на последних воротах лицом по трассе (`Basis(UP, yaw)`; **знак проверен**: `rotation.y = yaw_deg` из `race_data.gd` даёт нужное направление: yaw -90 = на +X). Личный рекорд и сплиты лучшего забега — `user://race_best.cfg` (ConfigFile).
|
||||
- **Музыка** `audio/music.ogg` (Ogg Vorbis, моно 32 кГц, 0,6 МБ; `ffmpeg -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`) включается в момент «СТАРТ!» с начала, играет до возврата в ожидание и плавно (0,8 с) затихает; дождь и гул остаются под ней. Зашедший в разгар круга слышит её, пока идёт чужой круг.
|
||||
|
||||
**Кто что считает (сервер ничего не знает про гонку).** Каждый клиент ведёт свою копию состояния (`race_rules.gd`) и обменивается событиями `Net.RACE_*` через `submit_race(kind, a, b)` / `recv_race(id, kind, a, b)` (+ сигнал `Net.race_event`; сервер просто пересылает остальным в комнате):
|
||||
|
||||
| kind | a | b | Кто шлёт |
|
||||
|---|---|---|---|
|
||||
| `RACE_READY` | 1/0 | — | игрок при нажатии R |
|
||||
| `RACE_START` | — | unix-время старта (отправитель: `now + 6`) | только **наименьший id** в комнате, когда видит, что готовы все (поэтому двое «последних готовых» дают один старт; второй START от большего id игнорируется) |
|
||||
| `RACE_CP` | индекс ворот | время гонки | игрок при прохождении ворот |
|
||||
| `RACE_FINISH` | — | время финиша | игрок на последних воротах |
|
||||
| `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, время)` .
|
||||
|
||||
**Проверка по существу** (всё в `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=`), настоящий не трогается.
|
||||
- `race_giveup_test.py` — сдача: A отменяет удержание и сдаётся, B финиширует; итоги одинаковы, в рейтинге только B; во втором круге сдались оба — круг сразу закончился.
|
||||
- `race_tour.tscn` — кадры для проверки HUD/ворот (`--mode=gates|hud`, см. заголовок).
|
||||
- Пакет: `godot --headless --export-pack Race /tmp/x/race.pck`, затем `strings /tmp/x/race.pck | grep -c` для `sky.png`, `baked.gdshader`, `race_data`, `race_rules.gd`, `music.ogg`, `race.glb` (каждое > 0).
|
||||
- Пакетная загрузка через портал хаба: `tools/test/race_check.txt` (по образцу `parkour_check.txt`).
|
||||
|
||||
**Выкладка (общее правило):** старый клиент с новым сервером (и наоборот) **не работает вообще** — Godot сверяет контрольную сумму набора RPC узла `/root/Net` («rpc node checksum failed»), проверено; поэтому порядок строго по README («Деплой»). `net/net.gd` менялся при Race (`submit_race`/`recv_race`) и при никах/рейтинге (см. «Ники и рейтинг») → после пуша `grid-world` сразу `tools/deploy_server.sh root@192.168.10.110`.
|
||||
|
||||
## Карта Race 2 («Верхние ярусы»)
|
||||
|
||||
**Состояние: фаза 1** (раскладка, геометрия, ворота, правила, проходимость ботами, предварительный свет). **Фаза 2** — после одобрения владельцем: чистовое запекание (384+ сэмплов), своё небо (`build_sky.py`), своя музыка. Палитра своя (холодная индиго-ночь, пурпур и ледяная бирюза; ворота тусклые индиго / очередные маджента / пройденные циан), небо перерисовано (`build_sky.py`), свет запечён на 384 сэмплах. Музыка — «On My Way to Hell» (Poloz), с 1:40 исходника (`ffmpeg -ss 100 -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 maps/race2/audio/music.ogg`, ~3:40, 1,2 МБ; исходник вне проекта: `source-assets/music/`); играет с «СТАРТ!» с начала по кругу (`loop = true`, затухание 0,8 с). Стык петли на слух/по уровню не проверен.
|
||||
Исходники: `Blender models/race_02/` (README там же; репозитория у папки нет — файлы не коммитятся). Концепция согласована с владельцем: вертикальность и поток скорости в духе Ghostrunner, но атмосфера как у Race/Parkour (дождь, смог, мокрый бетон, натриевый/красный/больной зелёный свет, надписи-приказы), туман 0,011.
|
||||
|
||||
Файлы: `maps/race2/` — `race2.tscn`, `race2.gd` (внешний вид, ворота со световыми столбами, лазеры, вентиляторы, полосы, срыв, дождь без дождя в траншее и шахте, гул, портал), `race2_data.gd` (**генерируется** `build_race.py`, не править),
|
||||
`race2.glb`, `sky.png`, `baked.gdshader`, `audio/music.ogg`. **Правила — общие с Race:** `race2.gd` создаёт тот же `maps/race/race_rules.gd` и передаёт ему `data` (= `race2_data.gd`) и `rating_id` (= `"race2"`); `race_rules.gd` берёт данные у карты
|
||||
(`m.get("data")`, у Race — запасной `load` его `race_data.gd`). Пакет `race2.pck` (9,5 МБ) собирается пресетом **Race2** (в нём `include_filter`: `maps/race/race_rules.gd` и файлы карты); скрипты `objects/vent.gd` и `objects/boost_strip.gd` — в **основном** пакете.
|
||||
Рейтинг: `Net.TIMED_MAPS["race2"] = {"min": 44}` (≈ 60 % от лучшего круга ботом по всем секретам ~72 с), `Net.MAP_TITLES["race2"] = "Гонка 2"`; в хабе портал «RACE 2» пятым в ряду.
|
||||
|
||||
**Новые объекты** (локальный эффект на игроке, сетевой код не тронут; в `player/player.gd` — только хуки `apply_vent()` и `apply_boost()` и константы `VENT_ACCEL`, `VENT_MAX_RISE`, `BOOST_ACCEL`, `EFFECT_HOLD`; остальная физика и `MAX_SPEED = 22` не менялись):
|
||||
- **Вентиляционный поток** (`objects/vent.gd`): пока игрок в столбе, вертикальная скорость растёт до **16 м/с** (ускорение 80 м/с²; раньше 11 и 60), горизонтальная сохраняется, двойной прыжок возвращается. Выход из столба на 16 м/с даёт ещё ~13 м высоты (v²/2g), поэтому ступень башни 12,5 м берётся с перелётом ~3 м; ступени 14–15 м в пробах ботом вели к промахам и оставлены 12,5. Шахта-лифт — скрытый поток (`hidden`: без столба, щелей и света), над окном знака нет. Визуал: тёмная решётка со светящимися щелями, мерцающий зелёный столб, слабый свет.
|
||||
- **Ускоряющая полоса** (`objects/boost_strip.gd`): набор скорости вдоль полосы до 22 м/с (не выше `MAX_SPEED`), потом на земле она гасится как обычно (14 м/с²), в воздухе сохраняется. Измерено: на 22 м/с одиночный прыжок 25 м, двойной 42 м; **рывок сбрасывает скорость до 13**. Стрелки пурпурно-белые, бегут по ходу (статичные ледяные — подсказки трассы).
|
||||
|
||||
**Участки и три пути** (подробности и координаты — `Blender models/race_02/README.md`, `layout.py`): старт на верхней платформе (8 слотов, шаг 2,8 м) → 2) мосты и крыши между башнями → 3) фасады: бег по четырём стенам → 4) площадь с краном → 5) пульсирующие лазерные мосты → 6) спуск по крышам → 7) башня-спираль из четырёх вентиляторов; на крыше ядра — финиш и портал.
|
||||
Безопасный путь 1085 м до подножия башни + 136 м башня = **1221 м** (план ~1100; можно сжать зоны 4 и 6), 13 ворот (ширина до 100 м там, где ветки расходятся широко), круг бота по безопасному пути ~99 с.
|
||||
Безопасный: зазоры 8–12 м, подъём до 1,5 м, спуск до 9,2 м. Рискованный: зазоры 16–24 м (двойной прыжок + рывок), бег по стенам. Секретный: скрытые тёмные площадки без подсветки и входы без подписей — окно «пустого квартала», траншея под ямой (подкат), техпроход под лазерами, стена w6, шахта-лифт.
|
||||
Срыв ниже последних ворот на 32 м = «СОРВАЛСЯ!», возрождение на воротах (центр каждых ворот лежит на плите — проверено).
|
||||
|
||||
**Проверка ботами** (`Blender models/race_02/tests/`, `run.sh`; в headless с `--fixed-fps 60` — быстрее реального времени). Все ветки пройдены (результаты ниже — последний прогон, оценочные времена бота от одной точки зоны до другой, секунды):
|
||||
|
||||
| Зона | безопасный | рискованный | секретный | выигрыш секрета |
|
||||
|---|---|---|---|---|
|
||||
| 2 мосты | 13,2 | 10,8 | 9,5 | 3,7 |
|
||||
| 3 фасады | 14,5 | 9,9 (4 стены, 4 из 4 попыток) | 10,3 | 4,2 |
|
||||
| 4 площадь | 12,8 | 7,6 | 7,8 | 5,0 |
|
||||
| 5 лазеры | 10,1 | 11,3 | 9,1 | 1,0 (бот не теряет времени на ритм лазеров) |
|
||||
| 6 спуск | 15,5 | 12,4 | 10,9 (стена w6: 3 из 4 попыток, с запасом в 1,5 м не проходит) | 4,6 |
|
||||
| 7 башня | 17,4 | — | 9,5 | 7,9 |
|
||||
|
||||
Итого все секреты вместе экономят ~26 с из ~99 (оценка бота; человек на секретах рискует сорваться на последние ворота). **Правила на race2:** `MAP=race2 tools/test/race_game_test.sh` (бот проводит игроков через ворота телепортами по 0,35 м за кадр, на лазерном мосту выбирает полосу — проверяются отсчёт, места, итоги, один START, возрождение лицом по трассе, зритель; проходимость проверена отдельно). Рейтинг этим тестом не проверяется: круг бота ~60 с — запись возможна, но тест рейтинг не сверяет.
|
||||
Пакет: `godot --headless --export-pack Race2 /tmp/x/race2.pck`, затем `strings` по `sky.png`, `baked.gdshader`, `race2_data`, `race_rules.gd`, `music.ogg`, `race2.glb`, `race2.gd` (каждое > 0; vent/boost_strip в этом пакете нет — они в основном). Через портал хаба: `tools/test/race2_check.txt`.
|
||||
|
||||
**Что не сделано / известно:** после смены силы вентиляторов (11 -> 16 м/с) проходимость башни и траншеи проверена ботом только выборочно (cmp7_safe, secret7, cmp4_secret, risky4 прошли), полный прогон и таблица времён выше — до этой правки, не перемерены; подножие башни всё ещё тёмное; `race2.glb` 8,4 МБ против 5,2 у Race (в фазе 2 прореживать фон); у зон 1 и 7 нет рискованной ветки; зазор для безопасного пути на спуске к `lane` — 8 м при спуске 9,2 м; не мерялись: нагрузка в браузере и звук. **Выкладка:** `net/net.gd` изменён только константами (`TIMED_MAPS`, `MAP_TITLES`), RPC те же — но сервер без нового `net.gd` **не примет рекорды race2** (запись отклонит: карты нет в `TIMED_MAPS`), поэтому после пуша `grid-world` запустить `tools/deploy_server.sh`; порядок и бэкап — как в «Деплой».
|
||||
|
||||
## Ники и рейтинг
|
||||
|
||||
**Профиль** — `user://profile.cfg` (ConfigFile, секция `profile`): `client_id` (случайный 128-битный hex, 32 символа; создаётся при первом запуске, **не показывается**, опознаёт игрока в рейтинге; потерял файл/очистил данные сайта — стал новым игроком), `nick`, `asked` (уже спрашивали ник). В тестах путь задаётся `-- --profile=путь`.
|
||||
|
||||
**Ники.** При первом запуске в хабе — панель «Введите ник» (≤ 16 символов, Enter — подтвердить, Esc — пропустить: тогда «Игрок N» и больше не спрашиваем). Клавиша **N** (физическая, действие `nick`) в любой момент открывает панель смены с текущим ником; вставка и кириллица работают. Нормализация (`Net.normalize_nick`, один код на клиенте и **сервере**): `\n \r \t` -> пробел, прочие управляющие и невидимые символы (в т.ч. направляющие юникода, нулевой пробел) выбрасываются, пробелы по краям обрезаются, ≤ 16 символов, пустой -> «Игрок N» (N = `id % 1000`). Одинаковые ники в комнате допустимы. Чужой ник везде выводится обычным `Label`/`Label3D` (не RichText) — «[b]» оформление не ломает. Единая точка: `Net.nick_of(peer_id)` (свой — `Net.my_nick`), сигнал `Net.nick_changed(id)`. Где виден: подпись над призраком (`ghost.gd`), HUD и итоги гонки, таблицы рейтинга.
|
||||
- Сеть: после подключения клиент шлёт `submit_profile(client_id, nick)` **раньше** `submit_map`; при смене ника — снова. Сервер хранит `peer_client` (peer -> client_id) и `client_nick`; при входе в комнату (`submit_map`) присылает вошедшему `recv_nick` всех присутствующих, а им — его; смену ника рассылает комнате; на выходе чистит. Клиент принимает `recv_nick` только при `map_ready` и очищает `Net.nicks` в `_reset_room_state()` — ники не просачиваются между комнатами (как состояние игроков).
|
||||
- **Блокировка ввода** на время набора: `Net.input_blocked` (ставит `Overlay`). Игрок читает ввод опросом, поэтому `player.gd` при `input_blocked` стоит как при заморозке и не ловит `crouch/dash`, а клик мыши не захватывает курсор. Это **отдельно от `player.input_frozen`** (его ведёт гонка на отсчёте; закрытие панели его не сбрасывает). R/N/Tab/G в поле — обычные буквы/не действия; открывающая клавиша в поле не попадает (фокус берётся отложенно). После закрытия мышь снова захвачена.
|
||||
|
||||
**Рейтинг (сервер).** Карты «на время» и допустимые времена — константа `Net.TIMED_MAPS` в `net/net.gd`:
|
||||
```gdscript
|
||||
const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}}
|
||||
const MAX_RECORD_TIME := 3600.0
|
||||
```
|
||||
Минимумы — **страховка от мусора, не античит**: время быстрее `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": {"<client_id>": {"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`.
|
||||
|
||||
**Таблица лидеров (Tab, действие `leaderboard`).** `core/overlay.gd` + `core/lb_view.gd`: справа по центру, тёмная полупрозрачная подложка, место / ник / `мм:сс.мс`, свой ряд подсвечен, вне топ-10 — строка «Вы: N-й, время»; сверху остаётся таймер гонки, в центре — отсчёт. Игрок при показе не блокируется. Закрывается повторным Tab или Esc.
|
||||
**Доски в хабе** (`hub.gd`, `_build_boards`): над порталами карт из `TIMED_MAPS` — Label3D на тёмной подложке с топ-3; запрос при входе в хаб и раз в 60 с; нет ответа — «—». У песочницы и Backrooms досок нет.
|
||||
|
||||
**Parkour: забег.** Таймер стартует при пересечении **линии на конце стартовой платформы** — Blender `y = 14` (`slab('start', -12, 12, -14, 14, …)` в `layout.py`), в Godot `z = -14`: константа `START_LINE_Z` в `maps/parkour/parkour.gd` (меняете раскладку — поправьте её). Финиш — **вершина**: Area3D 14×5×14 м над верхом платформы `summit`, центр `Data.PORTAL + (0, 2.5, 3)` (в `parkour_data.gd` PORTAL = центр summit + 3 м по Blender-y; `FINISH_SIZE`). Смерть и возрождение таймер не останавливают. На финише — `submit_record("parkour", время)` и панель итогов ~8 с (время, рейтинг, «НОВЫЙ РЕКОРД»). R — телепорт на старт, таймер и чекпоинты сброшены. Таймер маленький, сверху по центру.
|
||||
|
||||
**Проверка по существу** (всё в `tools/test/`, порт 9080 свободен):
|
||||
- `nicks_lb_test.py [папка]` — локальный сервер (его `user://` в папке теста через `XDG_DATA_HOME`) и клиенты `lb_client.tscn`: ники у двоих, у позже зашедшего, смена, изоляция комнат, «[b]»; рейтинг: лучшее время без дублей, сортировка, топ-10 при 14 игроках, неверные записи, лимиты, повторный вход, **перезапуск сервера**, испорченный файл, обрезка 120 -> 100, подписи над призраками в настоящей сцене хаба; в конце `nick_ui_test.tscn` (панель ника, блокировка ввода, R/N/Tab/G в поле, Esc, Tab).
|
||||
- `parkour_run_test.py` — забег: линия, таймер, смерть, финиш, отказ слишком быстрому времени, R, запись в рейтинге.
|
||||
- `ui_tour.tscn --mode=hub|race|parkour` — кадры (`--write-movie`; рейтинг подставлен, сервер не нужен; добавляйте `--server-url=ws://127.0.0.1:1`, иначе окно подключится к реальному серверу и затрёт подставленное).
|
||||
|
||||
## Меню паузы и настройки
|
||||
|
||||
`core/pause_menu.gd` (автолоад `PauseMenu`) + `core/settings.gd` (автолоад `Settings`, объявлен раньше `Overlay`). Меню открывается по **Esc / Start** (действие `pause`), игра при этом идёт дальше (мультиплеер), а игрок стоит: `Net.input_blocked = true`, как при вводе ника; игровые клавиши за меню не работают (меню забирает событие раньше `Overlay`). Закрывается кнопкой «Продолжить», повторным Esc/Start или B; мышь снова захватывается.
|
||||
- **Главная:** Продолжить · **Выйти в хаб** (только если карта не хаб: `MapLoader.enter("hub", …)`, хаб в основном пакете) · Таблица лидеров · Сменить ник · Настройки · Управление.
|
||||
- **Настройки** (`user://settings.cfg`, пишется при закрытии меню; кнопка «По умолчанию»): чувствительность мыши ×0,2–3 (к `player.MOUSE_SENSITIVITY`), чувствительность стика ×0,3–2,5 (`Settings.STICK_LOOK_SPEED` = 3,6 рад/с), поле зрения 60–100° (`player.base_fov` берётся из `Settings.fov` каждый кадр), громкость (шина Master), инверсия вертикали (мышь и стик). Слайдеры и кнопки управляются геймпадом через стандартные `ui_*`.
|
||||
- **Управление:** таблица «клавиатура / геймпад».
|
||||
- **Esc в браузере:** при захваченной мыши браузер сам снимает захват и событие Esc до игры **не доходит**. Поэтому `PauseMenu._process` открывает меню и по факту «мышь была захвачена ≥ 0,5 с и перестала быть» (если не открыт ввод ника и не идёт загрузка карты); после закрытия 0,6 с это не срабатывает (браузер мог отказать в повторном захвате без клика — тогда клик по игре захватит мышь, как раньше). **В живом браузере не проверялось.**
|
||||
- Геймпад: обзор правым стиком — `player._process` (кадр за кадром, не в физике), мышь — `player._mouse_look`. Движение стиком аналоговое по направлению, но скорость по-прежнему «шаг/спринт» (`direction` нормализуется), как и раньше с клавиатурой.
|
||||
- Проверка: `godot --headless --path . res://tools/test/pause_test.tscn -- --client --server-url=ws://127.0.0.1:1` — раскладка геймпада в InputMap, открытие/закрытие, блокировка, чувствительность и инверсия, FOV, обзор стиком, A и RT, сохранение, «Выйти в хаб». Кадры меню и хаба: `hub_tour.tscn` (`--menu=main|settings|controls`).
|
||||
|
||||
## Преследователи
|
||||
|
||||
`chasers/chaser.gd` — один скрипт для всех. Сцены (`rock.tscn`, `verity.tscn`) — это просто модель + звук + `NavigationAgent3D`.
|
||||
|
||||
- **Поведение:** гонится за ближайшим живым игроком по навмешу, ловит на `catch_distance` (по горизонтали, не выше `catch_height`).
|
||||
После поимки игрок умирает и возрождается через 2,5 с в стартовой точке; если преследователь рядом со стартом — уходит на свой старт.
|
||||
- **Зрение** (`sight_limited = true`): замечает игрока, если он ближе `hearing_radius` (слышит даже за стеной) **или** в конусе `sight_fov`
|
||||
до `sight_range` и луч до него не упирается в стену. Потерял из виду — идёт к последнему месту `lose_time` секунд, потом блуждает (`patrol_speed`, `wander_radius`).
|
||||
Без `sight_limited` знает, где игрок, из любой точки.
|
||||
- **Параметры** (экспорты узла): `chaser_id` (имя в сети), `speed`, `turn_speed`, `catch_distance`, `catch_height`, `sight_*`, `hearing_radius`,
|
||||
`lose_time`, `patrol_speed`, `wander_radius`, `model_scale` (масштаб дочернего узла `Model`), `emission_energy` (чисто светящийся материал; у Верити 1,0).
|
||||
- **Значения сейчас:** камень в песочнице — скорость 15, зрение 45 м / 130°, слух 8 м, лимит потери 5 с. Верити в песочнице — скорость 12, зрение 55 м / 110°, слух 10 м, потеря 6 с.
|
||||
Верити в Backrooms — скорость 10, масштаб 0,5, зрение по умолчанию (30 м / 120° / слух 6 м / потеря 4 с).
|
||||
- **Модели — плоские «карточки»** вдоль оси X; в сцене повёрнуты на 90° и подняты, чтобы стоять на полу. У `verity2` лицо нарисовано с одной стороны
|
||||
(свечением), при погоне оно повёрнуто к игроку.
|
||||
- **Добавить преследователя:** новая сцена по образцу `verity.tscn` (корень со скриптом `chaser.gd`, `chaser_id="имя"`, `NavigationAgent3D`),
|
||||
инстанс в карту. Больше ничего: сеть синхронизирует всех по имени.
|
||||
|
||||
## Сеть
|
||||
|
||||
**Архитектура:** выделенный сервер на Godot (WebSocket, порт **9080**), клиенты — `WebSocketMultiplayerPeer`. Сервер **не знает про уровни**
|
||||
и ничего не считает, только пересылает сообщения внутри «комнаты» (карты). Физика, управление, **преследователи** считаются на клиентах.
|
||||
|
||||
- `net/net.gd` — автолоад `Net`. `DisplayServer.get_name() == "headless"` → **сервер**, иначе клиент; флаг `--client` делает клиентом и headless (боты).
|
||||
Клиент переподключается каждые 3 с. Адрес в вебе берётся из `window.location` (`wss://<домен>/ws` или `ws://`), на десктопе — `ws://127.0.0.1:9080` или `-- --server-url=...`.
|
||||
- **Комнаты по картам.** Клиент при входе на карту шлёт `submit_map(имя)`, сервер отвечает `recv_map_ack`. Пока подтверждения нет (`Net.map_ready == false`),
|
||||
клиент игнорирует чужие сообщения — так при смене карты не просачиваются старые пакеты. Игроки разных карт друг друга не видят.
|
||||
- **Владелец преследователей.** В каждой комнате один клиент (первый вошедший, затем наименьший `peer_id` из оставшихся) симулирует **всех** преследователей и
|
||||
шлёт их позиции 20 раз/с; остальные клиенты плавно повторяют. Если владелец молчит `ROCK_TIMEOUT` = 3 с (вкладка в фоне, лаги) или вышел — сервер передаёт роль другому;
|
||||
преследователь продолжает с того же места. Поимку «своего» игрока всегда решает его собственный клиент по тому, что он видит.
|
||||
- **Смерть и возрождение** шлются отдельно (`submit_life`), чтобы владелец не гнался за мёртвыми и мог увести преследователя от точки возрождения.
|
||||
|
||||
| RPC | Кто → кому | Что |
|
||||
|---|---|---|
|
||||
| `submit_map(map)` / `recv_map_ack(map)` | клиент → сервер / обратно | вход в комнату |
|
||||
| `recv_rock_owner(id)` | сервер → клиенты | кто симулирует преследователей |
|
||||
| `submit_state(pos, yaw, pitch, flags)` / `recv_state(id, …)` | клиент → сервер → остальные | состояние игрока, 20 Гц, `unreliable_ordered` |
|
||||
| `submit_chasers(ids, positions, yaws)` / `recv_chasers(…)` | владелец → сервер → остальные | позиции всех преследователей, 20 Гц |
|
||||
| `submit_life(dead, pos)` / `recv_life(id, dead, pos)` | клиент → сервер → остальные | смерть/возрождение, `reliable` |
|
||||
| `recv_left(id)` | сервер → комната | игрок вышел |
|
||||
| `submit_profile(client_id, nick)` / `recv_nick(id, nick)` | клиент → сервер / сервер → комната | профиль и ники (раздел «Ники и рейтинг») |
|
||||
| `submit_record(map_id, time)` / `recv_record_result(map_id, time, rank, personal, map_record)` / `recv_leaderboard(map_id, top, my_rank, my_time)` | клиент → сервер → клиенту | рекорды |
|
||||
| `request_leaderboard(map_id)` | клиент → сервер | запрос таблицы (ответ — `recv_leaderboard`) |
|
||||
|
||||
Флаги состояния (`Net.FLAG_*`): подкат, бег по стене (+ сторона стены), рывок, подтягивание, «в воздухе». Присед передаётся как подкат; призрак отличает их по скорости. Скорость и двойной прыжок призрак считает сам по смещению позиции — в протокол они не входят.
|
||||
|
||||
**RPC требуют одинаковые путь узла и скрипт на сервере и клиенте**, поэтому `net.gd` один. Любое изменение RPC — **клиент и сервер выкладываются вместе**
|
||||
(старый клиент с новым сервером не работает). Имена вроде `rock_owner`/`recv_rock_owner` остались от времени, когда преследователь был один.
|
||||
|
||||
## Загрузка карт пакетами (веб)
|
||||
|
||||
Основной пакет игры (`index.pck`) — только хаб (с музыкой), игрок, сеть, меню и порталы (около 1,6 МБ, из них 1,2 МБ — музыка хаба). Движок — `index.wasm` (39,5 МБ, ~10 МБ с gzip).
|
||||
Карты — отдельные `.pck`: `world.pck` (13,6 МБ), `backrooms.pck` (21,3 МБ). Портал вызывает `MapLoader.enter(map, scene, pack, title)`:
|
||||
|
||||
1. если сцена уже есть в проекте (редактор, десктоп, пакет уже скачан) — переход сразу;
|
||||
2. иначе игра встаёт на паузу (преследователь не должен убить игрока на экране загрузки), показывается «Загрузка: <имя> N / M МБ»;
|
||||
3. пакет качается `HTTPRequest` с `Cache-Control: no-cache` (браузер сверяет файл с сервером), записывается в `user://` и подключается через
|
||||
`ProjectSettings.load_resource_pack(path, false)` (**`false`** — файлы основной игры в приоритете, устаревший пакет не затрёт код);
|
||||
4. при ошибке на экране и в консоли пишется причина, портал можно повторить.
|
||||
|
||||
**Грабли, на которые уже наступили:**
|
||||
- `HTTPRequest` в браузере: **`accept_gzip = false`** обязательно. Браузер сам распаковывает gzip, а движок видит `Content-Encoding: gzip` и пытается распаковать ещё раз (ошибка 8).
|
||||
- **`download_file` в вебе не создаёт файл** — тело берём в память и пишем в `user://` сами.
|
||||
- Файлы-«не ресурсы» (`.mp4`, `.md`) не экспортируются; потому исходное видео Верити лежит вне проекта (`source-assets`).
|
||||
- Предупреждения `invalid UID … using text path instead` при загрузке пакета безвредны.
|
||||
- **Ресурсы, которые скрипт грузит строкой** (`load("res://…")`), в пакет карты **не попадают**: пресет `export_filter=resources` берёт только сцену и её прямые зависимости (`preload` тоже находится). Добавляй такие файлы в `include_filter` пресета карты (у Parkour: `sky.png, baked.gdshader, parkour_data.gd, audio/*`). Признак беды: пакет «слишком маленький». Проверка: `strings parkour.pck | grep -c sky.png` (должно быть > 0). Раз это случилось: карта тихо грузится без неба, шейдера и логики, а проверка «сцена называется Parkour» этого не замечает — **проверяй по существу** (см. «Тесты»).
|
||||
- **Кэш зависимостей редактора** у недавно созданных/изменённых сцен может устареть, и экспорт пакета карты пропустит модели и звуки (пакет «слишком маленький»).
|
||||
`tools/export_all.sh` делает `touch` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов.
|
||||
|
||||
## Экспорт, тесты и деплой
|
||||
|
||||
### Экспорт
|
||||
|
||||
```bash
|
||||
tools/export_all.sh # веб-клиент + backrooms.pck + world.pck в ../grid-world
|
||||
```
|
||||
Три пресета в `export_presets.cfg`: **Web** (хаб + общее; из него исключены `maps/sandbox`, `maps/backrooms`, `chasers/rock`, `chasers/verity`, `tools`),
|
||||
**World** и **Backrooms** (`export_filter=resources` — сцена карты и всё, на что она ссылается, кроме общего из основного пакета). Шаблоны экспорта —
|
||||
`~/.local/share/godot/export_templates/4.7.2.stable/` (есть только Web). Экспорт при выходе печатает безвредную ошибку `Parameter "singleton" is null`.
|
||||
|
||||
### Проверка сборки как на сервере
|
||||
|
||||
```bash
|
||||
python3 tools/serve_web.py ../grid-world 8080 # nginx-подобно: COOP/COEP и gzip чанками
|
||||
```
|
||||
Открыть `http://127.0.0.1:8080`. На десктопном `godot --main-pack` пакеты карт качаются плоским `python3 -m http.server` (без gzip) через `-- --pack-url=http://127.0.0.1:8777/`.
|
||||
|
||||
### Тесты без окна (боты)
|
||||
|
||||
Автотестов в CI нет; для проверок есть боты. `tools/test/make_test_project.sh /tmp/tp` делает временную копию проекта с автозагрузкой `tools/test/bot.gd`
|
||||
(телепортирует игрока по расписанию и печатает состояние раз в секунду), `tools/test/make_test_server.sh /tmp/ts` — локальный сервер в боевой раскладке.
|
||||
|
||||
```bash
|
||||
godot --headless --path /tmp/ts & # сервер
|
||||
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=A &
|
||||
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=B
|
||||
# цепочка карт на реальных пакетах (порталы хаба в ряд на z=-15 с шагом 6 м: SANDBOX x=-12, BACKROOMS x=-6; в `--tp` ниже — SANDBOX):
|
||||
/tmp/tp/tools/export_all.sh /tmp/site && (cd /tmp/site && python3 -m http.server 8777 --bind 127.0.0.1 &)
|
||||
godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:-12,1,-15
|
||||
```
|
||||
Для картинок: `godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/a.png --quit-after 25 res://maps/hub/hub.tscn` (окно мелькнёт; кадры — `a00000020.png`).
|
||||
В браузере: Chrome (flatpak `com.google.Chrome`) `--headless=new --enable-logging=stderr --v=0 http://127.0.0.1:8080` — вывод Godot виден в строках `CONSOLE`.
|
||||
|
||||
**Готовые проверки** (`tools/test/`, запускаются как сцены: в режиме `--script` автозагрузки недоступны при компиляции, поэтому только `res://…tscn`; `-- --client`, иначе headless поднимет сервер на порту 9080):
|
||||
- Race 2: `MAP=race2 tools/test/race_game_test.sh` (правила, см. «Карта Race 2»); боты проходимости — `Blender models/race_02/tests/` (`run.sh`, `run_all.sh`, `compare.sh`, `frames.sh`); пакет через портал хаба — `tools/test/race2_check.txt`.
|
||||
- `pad_menu_test.tscn` — геймпад в меню (фокус, крестовина, A); `pause_test.tscn` — меню паузы, настройки, геймпад (см. «Меню паузы и настройки»); `hub_tour.tscn` — кадры хаба и меню паузы.
|
||||
- `measure_movement.tscn` — замеры мувсета (прыжки, рывок, стена); `mantle_test.tscn` — подтягивание на 40 вариантах уступов; `parkour_gaps.tscn` — проходимость контейнеров; `parkour_tour.tscn` — облёт карты со снимками (см. заголовок файла).
|
||||
- **Проверяй по существу, а не по имени сцены.** Для карт из пакетов: собери тестовый проект (`make_test_project.sh`), в его `bot.gd` добавь печать фактов (есть ли у `current_scene` скрипт, подменён ли материал, есть ли `PanoramaSkyMaterial`, играет ли музыка, сколько лазеров) и пройди портал с `--pack-url`. Готовый код проверки — `tools/test/parkour_check.txt` (ловит пропавшие `sky.png`/шейдер/музыку/логику).
|
||||
- Ввод ботом: `Input.action_press("jump")` работает для зажатия и `is_action_pressed`; рывок и подкат ловятся в `_unhandled_input`, поэтому для них нужно `Input.parse_input_event(InputEventAction)` с `pressed=true`.
|
||||
- Грабли оболочки: в этом окружении `ls` — алиас на `eza` (используй `command ls`), `cp` спрашивает подтверждение (`command cp -f`), zsh не режет `$args` по пробелам (передавай аргументы отдельно), `echo ======` ломает команду.
|
||||
|
||||
**Осторожно:** `pkill -f`/`pgrep -f` с шаблоном, который есть и в твоей же командной строке, убивает собственный шелл; не используй `pkill chrome` — закроет браузер владельца. Останавливай по PID или по точному признаку.
|
||||
|
||||
### Деплой
|
||||
|
||||
```
|
||||
Браузер ──https/wss──▶ Nginx Proxy Manager (SSL, домен на duckdns) ──http──▶ контейнер 192.168.10.110
|
||||
nginx :80
|
||||
├─ / → /opt/grid-world (статика игры)
|
||||
└─ /ws → 127.0.0.1:9080 (игровой сервер)
|
||||
```
|
||||
**Сервер:** `root@192.168.10.110`, Debian 12, контейнер LXC в Proxmox, 1 ядро, 1 ГБ RAM, ssh по ключам. Нет `curl`, `rsync`, `ufw`; есть `python3`.
|
||||
В Nginx Proxy Manager включено **Websockets Support** (без него `/ws` не работает). Боевой конфиг nginx: `deploy/nginx.conf`
|
||||
(`/etc/nginx/sites-enabled/godot_game`); обязателен **gzip для `.wasm`/`.pck`** (иначе 39,5 + 21 МБ уходят как есть) и заголовки COOP/COEP.
|
||||
|
||||
**Клиент:** `tools/export_all.sh`, затем коммит и пуш в `grid-world` (`main`). Gitea (`git.ibrash.duckdns.org`, `aibrash/grid-world`) по пушу запускает
|
||||
`.gitea/workflows/deploy.yml`: ssh на сервер и `git pull` в `/opt/grid-world`. Пакеты карт выкладываются вместе с `index.*`.
|
||||
|
||||
**Сервер:** отдельный минимальный проект в `/opt/my-world-server/` (`project.godot` + `net/` + официальный бинарник Godot 4.7.2 linux x86_64 `godot`; бинарник из Fedora на Debian не подходит,
|
||||
берётся из `github.com/godotengine/godot-builds`). Служба systemd `my-world.service` (`deploy/my-world.service`, DynamicUser, `Restart=always`).
|
||||
```bash
|
||||
tools/deploy_server.sh root@192.168.10.110 # бэкап, копирование net/ и project.godot, пересборка кэша классов, перезапуск
|
||||
ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок …», «[карта] камень считает игрок …»
|
||||
```
|
||||
Трогать сервер нужно только при изменении `net/*`. **Порядок при изменении RPC:** сначала пуш `grid-world`, сразу следом `deploy_server.sh`; игрокам — Ctrl+F5.
|
||||
Откат сервера: бэкап в `/opt/backup-my-world-server-<дата>/`.
|
||||
> Сейчас на сервере ещё **старая раскладка** (`/opt/my-world-server/Net/`, заглавная). `deploy_server.sh` при следующем запуске переведёт её на `net/`.
|
||||
> Код сети при этом по протоколу не изменился.
|
||||
|
||||
### Git
|
||||
|
||||
- Репо проекта: `git@git.ibrash.duckdns.org:aibrash/my-world.git`, основная ветка `master`; коммиты на русском с трейлером `Co-Authored-By`.
|
||||
- **Пушить и выкладывать на сайт — только по прямой просьбе владельца.** Коммитить локально можно.
|
||||
|
||||
## Что выяснено по ходу работы
|
||||
|
||||
- **Окружение:** в рендерере Compatibility **объёмный туман не работает** (удалён из песочницы). SSAO и glow работают (неоновое свечение), оставлены.
|
||||
В Backrooms включён обычный туман (`Environment.fog_*`), он работает.
|
||||
- **Память:** раньше вкладка показывала около 1,6 ГБ; в окт 2026 владелец замерил **~300 МБ** (после замены капсул на модель со скелетом, новых карт и всех правок этой сессии). Что именно помогло — неизвестно (гипотезы прежних замеров: несжатые текстуры, HDR-небо, `.ogg` целиком в памяти). Проблему считаем снятой; повторно измерять при добавлении тяжёлого контента (небо Parkour/Race ~6 МБ в видеопамяти каждое).
|
||||
Гипотезы: рост WASM-памяти, несжатые текстуры, HDR-небо 2k, `.ogg`, распаковываемый целиком. Измерения с тех пор не повторялись.
|
||||
- **Материал Верити** — две поверхности: жёлтая основа и диск с лицом (текстура свечения на белой основе). Белая основа на ярком свету даёт бледность,
|
||||
поэтому `emission_energy` делает материал чисто светящимся (чёрная основа).
|
||||
- **Backrooms:** двери ~1,8 м, поэтому преследователь там масштабом 0,5 и навмеш запечён для агента радиусом 0,6 (иначе проход закрыт). Самый дальний зал за узкой дверью недостижим.
|
||||
- **Ввод:** `Input.is_key_pressed(KEY_W)` проверяет символ, на русской раскладке это «Ц». Только действия `InputMap` на физические клавиши.
|
||||
- **Пресеты:** при отключённом `extensions_support` — один `index.wasm`; включение даёт ещё `index.side.wasm` 44 МБ.
|
||||
|
||||
## Заметки для будущего меня
|
||||
|
||||
**Договорённости с владельцем (важно не нарушать)**
|
||||
- Коммитить локально можно, **пушить и выкладывать — только по прямой просьбе**. Сервер выкладывать нужно лишь при правках `net/*` с изменением RPC.
|
||||
- Parkour — **мрачная антиутопия** (дождь, грязный смог, мокрый бетон, натриевый оранжевый, красный, больной зелёный; надписи-приказы COMPLY/CURFEW). Яркий розово-бирюзовый неон владелец назвал «детским». Туман 0,011 выбран из серии проб (0,02 владелец откатил как слишком густой), цвет тумана светлее фона — тёмный туман лишь затемняет.
|
||||
- Сложность Parkour владельцу «нормально»; он просил и «не детский уровень» — если захочет жёстче: зазоры 27–28 м (двойной прыжок + рывок), лазеры в каньоне, быстрее пульсы, реже чекпоинты.
|
||||
- Сальто камеры при двойном прыжке делалось и **убрано по просьбе** (тошнит, не видно куда прыгать). Без просьбы не возвращать.
|
||||
- Чекпоинты должны быть **заметными** (раньше тонкие кольца на полу не нашли): сейчас ворота 5 м с надписью CHECKPOINT.
|
||||
- Мой стиль ответов: по-русски, коротко, без перечисления всего подряд; честно говорить, что проверено, а что нет.
|
||||
|
||||
**Мувсет (замеры ботом, спринт 13 м/с)** — нужны при любом проектировании карт:
|
||||
одиночный прыжок 15 м и +1,9 м вверх; двойной 25 м и +3,5 м; прыжок+рывок 19 м; двойной+рывок 28 м; бег по стене 1,8 с (23 м), провал 1,4 м; прыжок от стены ~5 м в сторону. **Стенами высоту не набрать**: прыжков от стены без бега только `MAX_WALL_JUMPS = 2` за полёт, а прыжок с бега по стене почти ничего не даёт — высоту дают двойной прыжок и подтягивание (уступ до ~2,6 м над полом, с двойным прыжком до ~4,5). Шахта-«лифт» из стен невозможна; в Parkour вместо неё башня из уступов.
|
||||
|
||||
**Подтягивание** (`player.gd → _try_mantle`): цель — центр капсулы на `default_capsule_height * 0.5 + 0.05` над поверхностью уступа. Раньше стояло `+ 0.05` (центр на уровне поверхности): тело оказывалось наполовину в уступе, физика выталкивала назад, и на уступах от 2 м не работало (на всех картах!). Проверка — `mantle_test.tscn`.
|
||||
|
||||
**Конвейер Parkour** (`Blender models/parkour_02/`): `layout.py` (единственный источник раскладки) → `build_parkour.py` (в Blender: геометрия `Solid`/`Neon`/`Collision-colonly`, пишет `parkour_data.gd`) → `bake.py` (`blender -b Parkour_02.blend -P bake.py`: запекание Cycles в цвета вершин + экспорт GLB, ~1 мин) → `build_sky.py` (`blender -b -P build_sky.py`: панорама ~2 мин) → `godot --headless --import --path .`. Правка раскладки требует всех четырёх шагов; по ходу собирай и смотри картинки (снимки `parkour_tour`).
|
||||
- **Освещение — в цветах вершин**, а не в динамических источниках. Сетка граней режется на ячейки ≤ 1,6 м (`kit.Mesh.grid`), поэтому число треугольников растёт быстро: сейчас Solid ~50 тыс. четырёхугольников, Neon ~22 тыс. Фон и дно — грубая сетка (9–80 м). Не раздувай: веб.
|
||||
- glTF хранит цвета вершин нормализованными (до 1), поэтому в `bake.py` значения сжаты ×0.125, а шейдер `baked.gdshader` умножает на `light_scale = 8`. Освещённость без этого обрезается на единице.
|
||||
- Яркость уровня: сила света в `bake.py` (Moon/CityGlow/Smog и цвет мира) × `exposure` в `parkour.gd` (2,1) × цвет материала (`kit.SOLID` и `SURFACES` в `parkour.gd` — **должны совпадать**). Первые попытки были чёрными: средняя запечённая освещённость ~0,05 × альбедо 0,11 → почти ноль.
|
||||
- Тексты (`kit.Mesh.text`): без поворота смотрит в −Y (читается, если смотреть вдоль +Y); для видимой с +X грани нужен `yaw = +π/2`, с −X — `−π/2`, для видимой со стороны +Y — `π`. Ошибка с ориентацией даёт зеркальные надписи. Кривые букв огрублены (`resolution_u = 2`) — иначе десятки тысяч лишних треугольников.
|
||||
- Имена в русском интерфейсе Blender переведены: узлы шейдеров/мира искать **по `type`**, не по имени (`'Background'` не найдётся).
|
||||
- Экспорт цветов вершин в GLB требует активного атрибута (`export_vertex_color='ACTIVE'`); Cycles-запекание в `target='VERTEX_COLORS'`.
|
||||
- Панорама неба: мипы включены в `sky.png.import` (`mipmaps/generate=true`, `compress/mode=2`) — по ним размытые отражения в мокрых полах (`textureLod` в шейдере). Дно города в панораме — отдельный материал: общий шейдер дымки на огромной плоскости давал белое.
|
||||
- Не мерялось: нагрузка Parkour в браузере (~145 тыс. треугольников + дождь 2200 частиц, небо ~6 МБ в видеопамяти), звук (шум дождя, гул, песня) на слух.
|
||||
- Расходный ресурс: `mcp__blender__look` очень дорог по токенам (сотни тысяч за сессию) — снимки просматривай из Godot (`parkour_tour`), в Blender смотри только геометрию.
|
||||
|
||||
**Ники и рейтинг — грабли**
|
||||
- Любой `Input`-опрос не знает о LineEdit: блокировать надо самому (`Net.input_blocked`), `input_frozen` не трогать (гонка). Клавиши, набираемые в LineEdit, до `_unhandled_input` не доходят (поле их съедает), но R/N в правилах всё равно проверяют `Net.input_blocked` — на всякий случай.
|
||||
- `Input.parse_input_event` в тестах доставляет события по-настоящему (в поле попадают буквы, действия не срабатывают) — так проверена панель ника без окна. Режим мыши в headless не хранится: захват проверять руками.
|
||||
- Тесты, которым нужен рейтинг, стартуют сервер с `XDG_DATA_HOME=<папка теста>` (иначе пишут в `~/.local/share/godot/app_userdata/world2-server/` разработчика). `pkill -f` с путём теста убивает собственную оболочку — глуши по PID.
|
||||
- Время записи на сервере сравнивается с `min` из `TIMED_MAPS`: тест Parkour «состаривает» старт на 45 с (`run_start_tick -= 45000`), иначе пришлось бы бежать минуту.
|
||||
- Хаб стал на 1 м выше (7 м): доски над порталами и «CHOOSE A MAP» над ними иначе перекрывались.
|
||||
- SIGTERM не сбрасывает отложенную запись рейтинга (≤ 2 с) — см. раздел «Ники и рейтинг».
|
||||
- Отправка профиля обязана идти **раньше** `submit_map` (оба `reliable`, порядок сохраняется) — иначе вошедший в комнату покажется остальным без ника.
|
||||
- Правка набора RPC (`submit_profile`, `recv_nick`, `submit_record`, `request_leaderboard`, `recv_leaderboard`, `recv_record_result`) — клиент и сервер выкладываются **вместе**.
|
||||
|
||||
**Race — что выяснено**
|
||||
- Имя `ready` у переменной в `Node` занято сигналом движка (`Node.ready`): флаги готовности названы `flags_ready`.
|
||||
- Данные ворот — центр капсулы на полу: ворота ставим на `pos.y - 1.2`; слоты на 1,25 м, при заморозке игрок оседает на ~9 см (это нормально).
|
||||
- Маршрут бота `route.json` проходит ворота ровно по центру; точки маршрута на воротах бот обязан «брать» только после пересечения плоскости (иначе срезает угол, как делал мой первый бот).
|
||||
- В `--write-movie` с `canvas_items`+`expand` видимая область шире окна, и подписи у левого края обрезаются; HUD слева отступает на 64 px.
|
||||
- Кадры гонки: `race_tour.tscn --mode=hud` (R, отсчёт, пробег, итоги). Подробности — раздел «Карта Race».
|
||||
|
||||
**Race 2 — что выяснено**
|
||||
- Дальность прыжка (ровная плита, бот `race_02/tests/measure.gd`): на 13 м/с — 14,7 / 24,9 / 29,1 м (одиночный / двойной / рывок+двойной), на 17 — 19,4 / 32,7 / 37,1, на 22 — 25,1 / 42,3 / 46,5. **Рывок сбрасывает скорость до `DASH_EXIT_SPEED` = 13**: разгон полосой до рывка пропадает, после полосы прыгать без рывка.
|
||||
- Бег по стенам съедает высоту: 4 стены каньона отнимают ~9 м — низ каньона (и всё после него) сделан на 9,2 м ниже крыш (`layout.ZP`); при 3 стенах хватало 4,3 м.
|
||||
- Вентилятор шириной 6 м при горизонтальной скорости 13 м/с держит игрока ~0,46 с: чтобы подняться на 12,5 м, нужны сильный поток (`VENT_ACCEL = 60`, `VENT_MAX_RISE = 11`) и двойной прыжок. Шахта-лифт работает потому, что у шахты есть стены: игрок упирается в них, гасит скорость и поднимается весь путь.
|
||||
- Подкат под плитой: центр капсулы стоит на 1,18 м над полом, полная высота ~2,4 м — под мостом нужен просвет ≥ 2,5 м (2,2 м не пролезть). Подкат с 22 м/с стартует со скорости ~27 и скользит ~55 м.
|
||||
- Центр ворот лежит на плите: возрождение происходит ровно там. Широкие ворота на разлёте веток (cp6 — 100 м, cp10 — 56 м) допустимы: окно по высоте у пересечения — от −3 до +9 м от центра ворот.
|
||||
- Лазерные пары (левая/правая половина моста, период 3 с, горят по 1 с с фазой 1,5 с) оставляют свободную полосу почти всё время (обе заняты ~10 % времени, при учёте поправки ±0,4 с).
|
||||
- Боты (граблям не повторять): точка маршрута над пропастью разворачивает бота в воздухе (ориентиры только на плитах); прыжок держать до конца полёта (отпустил — «срезанный» прыжок вдвое короче); у ботов с `--client` запуск рядом с локальным тестовым сервером добавляет лишнего игрока в комнату; **не `pkill -f` с путём теста** (убьёт свою оболочку) — `ps … | grep '[g]odot'` и `kill` по PID.
|
||||
- Запекание: `BAKE_SAMPLES=64 ./rebuild.sh` — проба за ~30 с; на дне между башнями (подножие финальной башни) свет получается тёмным — добавлены прожекторные панели на стенах ядра; чистовое запекание — фаза 2.
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
### Телефон — не планируется
|
||||
|
||||
Владелец решил, что телефон не нужен (окт 2026). Если когда-нибудь вернёмся: нужны сенсорный стик и зона обзора (ввод уже сведён к действиям `InputMap`, достаточно `Input.action_press`), ориентация и полноэкранный режим по касанию,
|
||||
экран «Нажми, чтобы начать» (аудио в браузере без жеста не стартует), мобильные текстуры ETC2/ASTC (`vram_texture_compression/for_mobile`; удвоит пакеты — делать отдельными `*.mobile.pck`), отключение SSAO/glow и `scaling_3d_scale` 0,6–0,75,
|
||||
восстановление после ухода вкладки в фон (`submit_map` заново). Сначала измерить память и FPS на реальном устройстве.
|
||||
|
||||
### Ближайшее: текстовый чат (ПЛАН, не реализовано — ждёт команды владельца)
|
||||
|
||||
Ники **сделаны** (раздел «Ники и рейтинг»): `Net.nick_of(id)`, `Net.my_nick`, `Net.input_blocked` для панели ввода. Чат строится на них. Смена ника по команде `/ник` не нужна — есть клавиша N.
|
||||
|
||||
**Чат**
|
||||
- Открытие: действие InputMap `chat` на физические клавиши T и Enter (работает на любой раскладке); Enter — отправить и закрыть, Esc — закрыть. Чат — в комнате текущей карты (в хабе/песочнице/Backrooms/Parkour/Race разные).
|
||||
- UI: CanvasLayer, внизу слева, мрачный минимализм (тёмная полупрозрачная подложка, небольшой шрифт, без ярких цветов), последние ~8 строк, видны ~10 с и гаснут; `Ник: текст`, ник цветом игрока (цвет от id, как у призрака), системные строки серым («зашёл»/«вышел»). **Без BBCode**: чужой текст не должен менять оформление (Label или RichTextLabel с выключенным BBCode). Желательно пузырь с последним сообщением над призраком на ~6 с.
|
||||
- **Ввод при наборе:** уже есть `Net.input_blocked` и образец — панель ника в `core/overlay.gd` (флаг отдельный от `input_frozen`, мышь освобождается и потом захватывается, открывающая клавиша в поле не попадает, R/N/Tab/G в поле не действия). Повторить для чата.
|
||||
- Сеть: RPC `submit_chat(text)` → сервер валидирует и пересылает остальным в комнате (`recv_chat(id, text)`), отправитель видит своё сразу. **Серверные ограничения:** ≤ 200 символов, убрать управляющие, пустые отбросить, лимит ~5 сообщений за 5 с на игрока (молча отбрасывать). Команды `/…` обрабатываются на клиенте и не рассылаются.
|
||||
|
||||
**Проверка (по существу):** образец — `tools/test/nicks_lb_test.py`: чат только в своей комнате; лимиты длины/частоты; «[b]» не ломает оформление; при открытом чате бот не двигается, после закрытия — двигается; R/T в поле не срабатывают как действия. Регрессия: тест гонки, `mantle_test`, `measure_movement`. Кадры панели ника и чата через `--write-movie`: не перекрывают HUD гонки (сверху таймер, по центру отсчёт), кириллица рисуется.
|
||||
|
||||
**Выкладка:** меняется набор RPC → клиент и сервер вместе (порядок из «Деплой»), игрокам Ctrl+F5; старый клиент с новым сервером падает с «rpc node checksum failed».
|
||||
|
||||
### Идеи (оценка независимого консультанта, окт 2026; не обязательства)
|
||||
|
||||
- **Parkour/Race:** режим «время и призрак» (запись лучшего забега, показ призраком), старт гонки по таймеру при зависшем «готов» (сейчас один не нажавший R блокирует всех), развилки Race с большим выигрышем, спектейт для мёртвых, приватные комнаты по ссылке.
|
||||
- **Мувсет и ощущение:** анимация приземления, звуки шагов/приземления/подката/бега по стене/рывка (самый заметный эффект при малых затратах), настройки и меню паузы (чувствительность, громкость, FOV).
|
||||
- **Контент и социальное:** игрок в роли преследователя, голос по близости, подбираемые предметы/двери, гроза со вспышкой молнии на картах, кастомизация персонажа.
|
||||
- **Инструменты:** номер версии протокола (чтобы старый клиент получал «обнови страницу»), автопроверка пакетов в CI, одна команда «собрать карту», экран credits (CC-BY Backrooms VR).
|
||||
- Серверные проверки (антиспуфинг позиций), если игроков станет много.
|
||||
|
||||
## Персонаж
|
||||
|
||||
Модель и анимации **генерирует скрипт** `Blender models/character_01/build_character.py` (Blender 5.x: `exec` в окне или `blender -b -P`).
|
||||
Он строит меш из гранёных сегментов (жёсткие веса: одна кость на сегмент), арматуру из 17 костей, считает позы и пишет 14 анимаций
|
||||
(`idle walk run crouch_idle crouch_walk slide jump fall air_jump dash wallrun_R wallrun_L mantle death`), ноги ставятся через IK.
|
||||
Экспорт: GLB, `export_animation_mode=NLA_TRACKS`, в `player/character/character.glb`. Материалы: `Suit` (тёмный) и `Glow` (свечение; в игре заменяется цветом игрока).
|
||||
|
||||
- **Выбор анимации** — `Ghost._choose()`: смерть > подтягивание > рывок > бег по стене > воздух > присед/подкат > бег/ходьба/стоит.
|
||||
Скорость шага подгоняется `speed_scale` (`REF_SPEED`), между анимациями смешивание 0,15 с. Голова повторяет взгляд вверх-вниз.
|
||||
- **Своё тело:** та же модель стоит в `player/player.tscn` (видна в редакторе, отбрасывает тень), но спрятана от своей камеры слоем 2 (`player.gd → _ready`). Не анимируется.
|
||||
- **Выкладка сервера:** `ghost.gd` лежит и на сервере, где модели нет, поэтому модель грузится `load()` внутри `setup()`, а не `preload`.
|
||||
- Изменить позы: правь функции анимаций в скрипте, перезапусти его в Blender, заново экспортируй GLB и импортируй в Godot.
|
||||
|
||||
## Название и заставка
|
||||
|
||||
Игра называется **Aman's World** (`config/name`). Заставка загрузки — `branding/splash.png` (1280×720, генерируется скриптом на Pillow: дождь, силуэты домов, натриевое свечение; при смене надписи/стиля перерисуй и положи на то же место), иконка и фавиконка — `branding/icon.svg`. Всё задаётся в `project.godot → [application]` (`boot_splash/image`, `boot_splash/bg_color`, `config/icon`); веб-экспорт берёт из них `index.png`, `index.icon.png` и `<title>`. **Папка пользовательских данных осталась прежней** (`config/use_custom_user_dir`, `custom_user_dir_name="world2"`): профиль (`profile.cfg`) и рекорд (`race_best.cfg`) не должны потеряться при переименовании проекта. Серверный проект (`deploy/server/project.godot`) — отдельный, его имя не менялось.
|
||||
|
||||
## Журнал изменений (окт 2026)
|
||||
|
||||
Последнее (31 окт 2026): геймпад Xbox, меню паузы с выходом в хаб и настройками, увеличенный и оформленный хаб с музыкой. Подробности — «Управление», «Меню паузы и настройки», «Карты и порталы». Выкладка сервера **не нужна** (`net/*` не менялся).
|
||||
|
||||
Сделано одной серией работ (хронологически):
|
||||
1. **Персонаж:** модель киберниндзя со скелетом (752 треугольника, 14 анимаций, IK ног) вместо капсул; цвет свечения от id игрока; свой персонаж спрятан от своей камеры. Сальто камеры при двойном прыжке делалось и убрано по просьбе.
|
||||
2. **Подтягивание на уступы** починено (не работало от ~2 м на всех картах), замеры мувсета и тестовые сцены в `tools/test/`.
|
||||
3. **Parkour:** подъём по мегабашне (крыши, каньон, башня, лазерный зал, контейнеры, спираль, вершина), запечённый свет, панорама неба, мокрый блеск, дождь, туман, песня; мрачная антиутопия.
|
||||
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` обрезает скорость на сходе; (г) слишком светлые «пятнистые» залы из-за запекания и белые засветки — следить за силой света при запекании.
|
||||
|
||||
9. **Race 2 «Верхние ярусы» (фаза 1)**: вторая гонка по крышам и мостам над городом, 13 ворот, ~1,2 км, три пути на каждом участке, вентиляторы-подбросы и ускоряющие полосы (хуки в `player.gd`), общий с Race код правил (`race_rules.gd` берёт данные у карты), портал в хабе, рейтинг `race2`. Проходимость каждой ветки доказана ботами; чистовое запекание, небо и музыка — фаза 2.
|
||||
|
||||
## Лицензии и авторство
|
||||
|
||||
- Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), **CC-BY 4.0** (`maps/backrooms/model/CREDITS.md`).
|
||||
При публикации нужно указывать автора.
|
||||
- Музыка хаба — «Local Forecast - Elevator», Kevin MacLeod (https://incompetech.com), **CC-BY 4.0** (`maps/hub/audio/CREDITS.md`). При публикации нужно указывать автора.
|
||||
- Остальные модели и звуки — материалы владельца проекта.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://ddsynauqeoppw"
|
||||
path="res://.godot/imported/tron_01.ogg-0e18fb85b15fd0798053a5bd0a4a9def.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://World/tron_01.ogg"
|
||||
dest_files=["res://.godot/imported/tron_01.ogg-0e18fb85b15fd0798053a5bd0a4a9def.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><rect width="124" height="124" x="2" y="2" fill="#0b0b0e" stroke="#c0281a" stroke-width="4" rx="14"/><path d="M26 98 L64 24 L102 98 M42 72 H86" fill="none" stroke="#ff8a3d" stroke-width="10" stroke-linecap="square" stroke-linejoin="miter"/><rect x="20" y="106" width="88" height="4" fill="#c0281a"/></svg>
|
||||
|
After Width: | Height: | Size: 393 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dutx7rbsx2tuy"
|
||||
path="res://.godot/imported/icon.svg-3d9dbe7d709cf7a7c4314775087c0608.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://branding/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-3d9dbe7d709cf7a7c4314775087c0608.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
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
After Width: | Height: | Size: 95 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bc3r54dm4n65j"
|
||||
path="res://.godot/imported/splash.png-0d6de845ee625fbaec4bfb9f6c0e9e33.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://branding/splash.png"
|
||||
dest_files=["res://.godot/imported/splash.png-0d6de845ee625fbaec4bfb9f6c0e9e33.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
|
||||
@@ -0,0 +1,269 @@
|
||||
extends Node3D
|
||||
# Преследователь (камень, Верити…): гонится за ближайшим живым игроком, обходя препятствия по навмешу, и ловит его.
|
||||
# С sight_limited замечает игрока лишь вблизи или в поле зрения без стен между ними;
|
||||
# потеряв из виду, идёт к последнему месту, где видел, и дальше бродит по карте.
|
||||
# В сети всех преследователей комнаты считает один клиент (владелец, его назначает сервер, см. net/net.gd),
|
||||
# остальные плавно повторяют их позиции — поэтому у всех игроков преследователи одни и те же.
|
||||
|
||||
@export var chaser_id := "rock" # Имя в сети: по нему владелец и остальные клиенты сопоставляют преследователей
|
||||
@export var model_scale := 1.0 # Масштаб узла Model (если он есть): уменьшаем в тесных картах
|
||||
@export var emission_energy := -1.0 # Модель светится сама (чёрная основа, цвет = свечение × это число); меньше 0 — как в модели
|
||||
@export var speed := 15.0 # Скорость движения
|
||||
@export var turn_speed := 6.0 # Как быстро разворачивается к игроку
|
||||
@export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока
|
||||
@export var catch_height := 4.0 # Игрока выше этого над камнем не достать
|
||||
@export var repath_distance := 1.0 # Пересчитываем путь, когда игрок сместился на столько
|
||||
@export var respawn_safe_radius := 25.0 # Игрок возродился ближе этого — преследователь уходит на свой старт
|
||||
@export var sync_smoothing := 12.0 # Плавность повтора чужого камня
|
||||
@export var snap_distance := 20.0 # Дальше этого чужой камень не едет, а прыгает (сброс на старт)
|
||||
|
||||
@export_group("Зрение")
|
||||
@export var sight_limited := false # false — знает, где игрок, из любой точки карты
|
||||
@export var sight_range := 30.0 # Дальность зрения, м
|
||||
@export var sight_fov := 120.0 # Угол обзора, градусы
|
||||
@export var hearing_radius := 6.0 # Ближе этого замечает игрока, даже за стеной
|
||||
@export var lose_time := 4.0 # Сколько ещё идёт за игроком после потери из виду, сек
|
||||
@export var patrol_speed := 0.4 # Доля speed, когда бродит
|
||||
@export var wander_radius := 30.0 # Куда выбирает точки для прогулки, м
|
||||
@export var eye_height := 0.9 # Высота «глаз» над полом, м
|
||||
|
||||
@onready var agent: NavigationAgent3D = $NavigationAgent3D
|
||||
@onready var start_transform := global_transform
|
||||
|
||||
enum State { WANDER, CHASE }
|
||||
|
||||
var player: Node3D = null
|
||||
var _state := State.WANDER
|
||||
var _last_seen := Vector3.ZERO
|
||||
var _since_seen := 0.0
|
||||
var _wander_target := Vector3.ZERO
|
||||
var _wander_active := false
|
||||
var _wander_wait := 0.0
|
||||
var _wander_time := 0.0
|
||||
var _own_rids: Array[RID] = []
|
||||
|
||||
func _ready() -> void:
|
||||
agent.target_desired_distance = 1.0
|
||||
agent.path_desired_distance = 1.5
|
||||
agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола
|
||||
var model := get_node_or_null("Model")
|
||||
if model != null and model_scale != 1.0:
|
||||
model.scale = Vector3.ONE * model_scale
|
||||
if emission_energy >= 0.0:
|
||||
_set_emission(emission_energy)
|
||||
for body in find_children("*", "CollisionObject3D", true, false):
|
||||
_own_rids.append(body.get_rid()) # Луч зрения не должен упираться в сам камень
|
||||
Net.chasers[chaser_id] = self
|
||||
Net.peer_respawned.connect(_on_respawned)
|
||||
|
||||
# Копируем материалы модели (они общие для всех её экземпляров) и делаем их чисто светящимися:
|
||||
# без белой основы лицо не выгорает на ярком свету и не пропадает при слабом свечении
|
||||
func _set_emission(energy: float) -> void:
|
||||
for mi in find_children("*", "MeshInstance3D", true, false):
|
||||
for s in mi.mesh.get_surface_count():
|
||||
var mat := mi.get_active_material(s) as BaseMaterial3D
|
||||
if mat != null and mat.emission_enabled:
|
||||
var copy := mat.duplicate() as BaseMaterial3D
|
||||
copy.albedo_color = Color.BLACK
|
||||
copy.emission_energy_multiplier = energy
|
||||
mi.set_surface_override_material(s, copy)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if Net.chasers.get(chaser_id) == self:
|
||||
Net.chasers.erase(chaser_id)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if player == null:
|
||||
player = get_tree().get_first_node_in_group("local_player") as Node3D
|
||||
if player == null:
|
||||
return
|
||||
player.respawned.connect(func(): _on_respawned(player.global_position))
|
||||
|
||||
if Net.rock_is_local():
|
||||
_chase(delta)
|
||||
else:
|
||||
_follow_owner(delta)
|
||||
_try_catch()
|
||||
|
||||
# Своего игрока ловит его собственный клиент — по тому камню, который он видит
|
||||
func _try_catch() -> void:
|
||||
if player.get("is_dead"):
|
||||
return
|
||||
var flat := player.global_position - global_position
|
||||
var dy := flat.y
|
||||
flat.y = 0.0
|
||||
if flat.length() <= catch_distance and absf(dy) <= catch_height:
|
||||
player.die()
|
||||
|
||||
# Живые игроки: свой и чужие
|
||||
func _candidates() -> Array[Node3D]:
|
||||
var out: Array[Node3D] = []
|
||||
if not player.get("is_dead"):
|
||||
out.append(player)
|
||||
for id in Net.ghosts:
|
||||
var g: Node3D = Net.ghosts[id]
|
||||
if is_instance_valid(g) and not Net.dead_peers.has(id):
|
||||
out.append(g)
|
||||
return out
|
||||
|
||||
func _horizontal_dist_sq(c: Node3D) -> float:
|
||||
return Vector2(c.global_position.x - global_position.x, c.global_position.z - global_position.z).length_squared()
|
||||
|
||||
# Ближайший по горизонтали живой игрок
|
||||
func _pick_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for c in _candidates():
|
||||
var d := _horizontal_dist_sq(c)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
# Ближайший из тех, кого камень сейчас замечает
|
||||
func _pick_visible_target() -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := INF
|
||||
for c in _candidates():
|
||||
var d := _horizontal_dist_sq(c)
|
||||
if d < best_d and _can_see(c):
|
||||
best_d = d
|
||||
best = c
|
||||
return best
|
||||
|
||||
# Рядом — слышит (даже за стеной); дальше — только в конусе обзора и если стены не мешают
|
||||
func _can_see(c: Node3D) -> bool:
|
||||
var to := c.global_position - global_position
|
||||
var flat := Vector3(to.x, 0.0, to.z)
|
||||
var dist := flat.length()
|
||||
if dist <= hearing_radius:
|
||||
return true
|
||||
if dist > sight_range:
|
||||
return false
|
||||
var fwd := -global_transform.basis.z
|
||||
fwd.y = 0.0
|
||||
if fwd.normalized().dot(flat / dist) < cos(deg_to_rad(sight_fov * 0.5)):
|
||||
return false
|
||||
var query := PhysicsRayQueryParameters3D.create(global_position + Vector3(0, eye_height, 0), c.global_position, 1)
|
||||
var exclude := _own_rids.duplicate()
|
||||
if c is CollisionObject3D:
|
||||
exclude.append(c.get_rid())
|
||||
query.exclude = exclude
|
||||
return get_world_3d().direct_space_state.intersect_ray(query).is_empty()
|
||||
|
||||
func _chase(delta: float) -> void:
|
||||
if not sight_limited:
|
||||
var target := _pick_target()
|
||||
if target != null: # Все лежат — стоим
|
||||
_move_to(target.global_position, speed, catch_distance, delta)
|
||||
return
|
||||
|
||||
var seen := _pick_visible_target()
|
||||
if seen != null:
|
||||
_state = State.CHASE
|
||||
_last_seen = seen.global_position
|
||||
_since_seen = 0.0
|
||||
elif _state == State.CHASE:
|
||||
_since_seen += delta
|
||||
if _since_seen > lose_time:
|
||||
_state = State.WANDER
|
||||
_wander_active = false
|
||||
_wander_wait = 1.0
|
||||
|
||||
if _state == State.CHASE:
|
||||
# Видим — стоим над жертвой у catch_distance; потеряли — идём к последнему месту
|
||||
_move_to(_last_seen, speed, catch_distance if seen != null else 1.5, delta)
|
||||
else:
|
||||
_wander(delta)
|
||||
|
||||
func _wander(delta: float) -> void:
|
||||
if _wander_wait > 0.0:
|
||||
_wander_wait -= delta
|
||||
return
|
||||
if not _wander_active:
|
||||
if not _pick_wander_target():
|
||||
_wander_wait = 1.0
|
||||
return
|
||||
_wander_active = true
|
||||
_wander_time = 0.0
|
||||
_wander_time += delta
|
||||
if _move_to(_wander_target, speed * patrol_speed, 1.5, delta) or _wander_time > 20.0:
|
||||
_wander_active = false
|
||||
_wander_wait = randf_range(1.0, 3.0)
|
||||
|
||||
# Случайная точка навмеша на расстоянии 8..wander_radius м; нет такой — ближайшая из дальних
|
||||
func _pick_wander_target() -> bool:
|
||||
var map := agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(map) == 0:
|
||||
return false
|
||||
var best := Vector3.INF
|
||||
var best_d := INF
|
||||
for i in 8:
|
||||
var p := NavigationServer3D.map_get_random_point(map, 1, true)
|
||||
var d := p.distance_to(global_position)
|
||||
if d < 8.0:
|
||||
continue
|
||||
if d <= wander_radius:
|
||||
best = p
|
||||
break
|
||||
if d < best_d:
|
||||
best = p
|
||||
best_d = d
|
||||
if best == Vector3.INF:
|
||||
return false
|
||||
_wander_target = best
|
||||
return true
|
||||
|
||||
# Шаг к точке по навмешу. true — дошли (ближе stop_dist по горизонтали)
|
||||
func _move_to(dest: Vector3, spd: float, stop_dist: float, delta: float) -> bool:
|
||||
var flat := dest - global_position
|
||||
flat.y = 0.0
|
||||
if flat.length() <= stop_dist:
|
||||
_face(flat, delta) # Стоим над жертвой, пока её клиент не засчитает поимку
|
||||
return true
|
||||
|
||||
var step: Vector3
|
||||
var map := agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(map) > 0:
|
||||
if agent.target_position.distance_to(dest) > repath_distance:
|
||||
agent.target_position = dest
|
||||
if not agent.is_navigation_finished():
|
||||
step = agent.get_next_path_position() - global_position
|
||||
|
||||
# Навмеш ещё не готов или точка вне его — идём напрямую
|
||||
if step == Vector3.ZERO:
|
||||
step = flat
|
||||
|
||||
var d := step.length()
|
||||
if d < 0.001:
|
||||
return false
|
||||
global_position += step / d * min(spd * delta, d)
|
||||
_face(Vector3(step.x, 0.0, step.z), delta)
|
||||
return false
|
||||
|
||||
# Камень считает другой клиент — плавно идём к тому, что он прислал
|
||||
func _follow_owner(delta: float) -> void:
|
||||
var state = Net.chaser_states.get(chaser_id)
|
||||
if state == null:
|
||||
return
|
||||
if global_position.distance_to(state.pos) > snap_distance:
|
||||
global_position = state.pos
|
||||
else:
|
||||
global_position = global_position.lerp(state.pos, 1.0 - exp(-sync_smoothing * delta))
|
||||
rotation.y = lerp_angle(rotation.y, state.yaw, 1.0 - exp(-sync_smoothing * delta))
|
||||
|
||||
# Кто-то (мы или другой игрок) возродился. Если камень рядом с точкой возрождения —
|
||||
# уводим его на старт, чтобы не поймал снова сразу. Решает только владелец камня.
|
||||
func _on_respawned(pos: Vector3) -> void:
|
||||
if not Net.rock_is_local():
|
||||
return
|
||||
if global_position.distance_to(pos) < respawn_safe_radius:
|
||||
global_transform = start_transform
|
||||
agent.target_position = global_position
|
||||
|
||||
# Плавный разворот: модель смотрит вперёд по -Z
|
||||
func _face(dir: Vector3, delta: float) -> void:
|
||||
if dir.length_squared() < 0.0001:
|
||||
return
|
||||
var target_yaw := atan2(-dir.x, -dir.z)
|
||||
rotation.y = lerp_angle(rotation.y, target_yaw, 1.0 - exp(-turn_speed * delta))
|
||||
@@ -0,0 +1 @@
|
||||
uid://o2kqd0cy4knr
|
||||
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://280pvcvxt2n2"
|
||||
path="res://.godot/imported/Guitar.ogg-a9c649256b934e43412c17ea4e1fd302.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://chasers/rock/Guitar.ogg"
|
||||
dest_files=["res://.godot/imported/Guitar.ogg-a9c649256b934e43412c17ea4e1fd302.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -4,12 +4,12 @@ importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cowp33ejxcfcl"
|
||||
path="res://.godot/imported/Rock.glb-4f2376a14ce37861c14e06a47e678e34.scn"
|
||||
path="res://.godot/imported/Rock.glb-d2a8f3a84a2ecdbbc2d73c9b22190137.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://models/Bochi-Rock/Rock.glb"
|
||||
dest_files=["res://.godot/imported/Rock.glb-4f2376a14ce37861c14e06a47e678e34.scn"]
|
||||
source_file="res://chasers/rock/Rock.glb"
|
||||
dest_files=["res://.godot/imported/Rock.glb-d2a8f3a84a2ecdbbc2d73c9b22190137.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -3,7 +3,7 @@
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bx6t5621iqw7f"
|
||||
path.s3tc="res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-fad3b4615e62f76758b5970cba2671a7.s3tc.ctex"
|
||||
path.s3tc="res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-4f14ab3f29d2ef2273e9da4765cef118.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
@@ -14,8 +14,8 @@ generator_parameters={
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg"
|
||||
dest_files=["res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-fad3b4615e62f76758b5970cba2671a7.s3tc.ctex"]
|
||||
source_file="res://chasers/rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg"
|
||||
dest_files=["res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-4f14ab3f29d2ef2273e9da4765cef118.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://chasers/chaser.gd" id="1_script"]
|
||||
[ext_resource type="PackedScene" uid="uid://cowp33ejxcfcl" path="res://chasers/rock/Rock.glb" id="2_model"]
|
||||
[ext_resource type="AudioStream" uid="uid://280pvcvxt2n2" path="res://chasers/rock/Guitar.ogg" id="3_audio"]
|
||||
|
||||
[node name="Rock" type="Node3D"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Model" parent="." instance=ExtResource("2_model")]
|
||||
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||
height = 5.0
|
||||
radius = 2.0
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Model"]
|
||||
stream = ExtResource("3_audio")
|
||||
autoplay = true
|
||||
max_distance = 30.0
|
||||
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://b7qmenkjh1wdk"
|
||||
path="res://.godot/imported/verity.ogg-e0f71c02d742979c6aefd117ffe3f1b5.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://chasers/verity/verity.ogg"
|
||||
dest_files=["res://.godot/imported/verity.ogg-e0f71c02d742979c6aefd117ffe3f1b5.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=true
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -0,0 +1,25 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://chasers/chaser.gd" id="1_script"]
|
||||
[ext_resource type="PackedScene" path="res://chasers/verity/verity2.glb" id="2_model"]
|
||||
[ext_resource type="AudioStream" path="res://chasers/verity/verity.ogg" id="3_audio"]
|
||||
|
||||
[node name="Verity" type="Node3D"]
|
||||
script = ExtResource("1_script")
|
||||
chaser_id = "verity"
|
||||
emission_energy = 1.0
|
||||
|
||||
[node name="Model" type="Node3D" parent="."]
|
||||
|
||||
[node name="Mesh" parent="Model" instance=ExtResource("2_model")]
|
||||
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 0, 1.59, 0)
|
||||
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||
height = 5.0
|
||||
radius = 2.0
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
|
||||
stream = ExtResource("3_audio")
|
||||
autoplay = true
|
||||
max_distance = 35.0
|
||||
@@ -3,13 +3,13 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cqfe5g5ospxic"
|
||||
path="res://.godot/imported/Tree_01.glb-e3e4d95044780b930423dc5ef7307d2e.scn"
|
||||
uid="uid://wup5p3waumce"
|
||||
path="res://.godot/imported/verity2.glb-60af6cc5d69d9f8af83072155580b49b.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://models/Tree_01/Tree_01.glb"
|
||||
dest_files=["res://.godot/imported/Tree_01.glb-e3e4d95044780b930423dc5ef7307d2e.scn"]
|
||||
source_file="res://chasers/verity/verity2.glb"
|
||||
dest_files=["res://.godot/imported/verity2.glb-60af6cc5d69d9f8af83072155580b49b.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c4fe3eo5c2w6g"
|
||||
path.s3tc="res://.godot/imported/verity2_unnamed.webp-1aa0cf701c3ad8dae1575d20b6b42222.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "5d3e31c7af9c06dc059d9fa5104b94dd"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://chasers/verity/verity2_unnamed.webp"
|
||||
dest_files=["res://.godot/imported/verity2_unnamed.webp-1aa0cf701c3ad8dae1575d20b6b42222.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
@@ -0,0 +1,107 @@
|
||||
class_name LbView
|
||||
extends VBoxContainer
|
||||
# Таблица рейтинга карты (топ-10 + «Вы: N-й»), данные берутся из Net.boards и обновляются сами по сигналу.
|
||||
# Одна и та же таблица в панели Tab, в итогах гонки и итогах Parkour: LbView.create(id_карты).
|
||||
# Ники выводятся обычными Label (не RichText), поэтому «[b]» в чужом нике оформление не ломает.
|
||||
|
||||
const TEXT := Color(0.92, 0.88, 0.8)
|
||||
const DIM := Color(0.62, 0.6, 0.56)
|
||||
const MINE := Color(0.96, 0.76, 0.44)
|
||||
const GOOD := Color(0.5, 0.85, 0.4)
|
||||
|
||||
var map_id := ""
|
||||
var font_size := 18
|
||||
var banner := false # сверху пометка «НОВЫЙ РЕКОРД КАРТЫ / ЛИЧНЫЙ РЕКОРД» (после submit_record)
|
||||
var name_width := 190.0
|
||||
|
||||
static func create(id: String, size := 18, with_banner := false) -> LbView:
|
||||
var v := LbView.new()
|
||||
v.map_id = id
|
||||
v.font_size = size
|
||||
v.banner = with_banner
|
||||
v.add_theme_constant_override("separation", 6)
|
||||
return v
|
||||
|
||||
static func make_label(text: String, size: int, color: Color) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_color_override("font_color", color)
|
||||
l.add_theme_constant_override("outline_size", 3)
|
||||
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return l
|
||||
|
||||
# Тёмная полупрозрачная подложка в мрачном стиле
|
||||
static func panel_style(margin := 20) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color(0.035, 0.04, 0.045, 0.86)
|
||||
sb.border_color = Color(0.45, 0.30, 0.16, 0.9)
|
||||
sb.set_border_width_all(2)
|
||||
sb.set_content_margin_all(margin)
|
||||
return sb
|
||||
|
||||
func _ready() -> void:
|
||||
Net.leaderboard_updated.connect(_on_updated)
|
||||
Net.record_result.connect(_on_record)
|
||||
render()
|
||||
|
||||
func set_map(id: String) -> void:
|
||||
map_id = id
|
||||
render()
|
||||
|
||||
func _on_updated(id: String) -> void:
|
||||
if id == map_id:
|
||||
render()
|
||||
|
||||
func _on_record(id: String, _t: float, _rank: int, _pb: bool, _mr: bool) -> void:
|
||||
if id == map_id:
|
||||
render()
|
||||
|
||||
func render() -> void:
|
||||
for c in get_children():
|
||||
remove_child(c)
|
||||
c.queue_free()
|
||||
if banner:
|
||||
var rec = Net.last_record.get(map_id)
|
||||
if rec != null:
|
||||
var txt := ""
|
||||
if rec.map_record:
|
||||
txt = "НОВЫЙ РЕКОРД КАРТЫ"
|
||||
elif rec.personal:
|
||||
txt = "НОВЫЙ ЛИЧНЫЙ РЕКОРД"
|
||||
if txt != "":
|
||||
var b := make_label(txt, font_size + 4, GOOD)
|
||||
b.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
add_child(b)
|
||||
var board = Net.boards.get(map_id)
|
||||
if board == null:
|
||||
add_child(make_label("—" if Net.connected else "Нет связи с сервером", font_size, DIM))
|
||||
return
|
||||
var top: Array = board.top
|
||||
if top.is_empty():
|
||||
add_child(make_label("Пока нет результатов", font_size, DIM))
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 16)
|
||||
grid.add_theme_constant_override("v_separation", 2)
|
||||
add_child(grid)
|
||||
for i in top.size():
|
||||
var mine: bool = board.my_rank == i + 1
|
||||
var col := MINE if mine else TEXT
|
||||
var place := make_label("%d" % (i + 1), font_size, col if mine else DIM)
|
||||
place.custom_minimum_size.x = font_size * 1.4
|
||||
place.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
grid.add_child(place)
|
||||
var nm := make_label(str(top[i][0]), font_size, col)
|
||||
nm.custom_minimum_size.x = name_width
|
||||
nm.clip_text = true
|
||||
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
grid.add_child(nm)
|
||||
var tm := make_label(Net.fmt_time(float(top[i][1])), font_size, col)
|
||||
tm.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
grid.add_child(tm)
|
||||
if board.my_rank > top.size():
|
||||
add_child(make_label("Вы: %d-й, %s" % [board.my_rank, Net.fmt_time(board.my_time)], font_size, MINE))
|
||||
elif board.my_rank == 0 and not top.is_empty():
|
||||
add_child(make_label("Вы: нет результата", font_size, DIM))
|
||||
@@ -0,0 +1 @@
|
||||
uid://buo8m8ndkcb67
|
||||
@@ -0,0 +1,147 @@
|
||||
extends Node
|
||||
# Подгружает карту отдельным пакетом при первом входе в портал: в вебе её не качают при старте игры.
|
||||
# Пакет (например backrooms.pck) лежит рядом с index.html. Если сцена уже есть в проекте
|
||||
# (редактор, десктоп-запуск) — качать нечего, переходим сразу.
|
||||
|
||||
signal enter_failed
|
||||
|
||||
const PACK_URL_ARG := "--pack-url=" # Для тестов на десктопе: --pack-url=http://127.0.0.1:8000/
|
||||
|
||||
var _loaded := {} # имя пакета -> true, чтобы не качать и не монтировать дважды
|
||||
var _busy := false
|
||||
var _layer: CanvasLayer
|
||||
var _label: Label
|
||||
var _bar: ProgressBar
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
func enter(map: String, scene_path: String, pack_name: String, title: String) -> void:
|
||||
if _busy:
|
||||
return
|
||||
if pack_name != "" and not _loaded.has(pack_name) and not ResourceLoader.exists(scene_path):
|
||||
_busy = true
|
||||
var ok := await _download_and_mount(pack_name, title)
|
||||
_busy = false
|
||||
if not ok:
|
||||
enter_failed.emit()
|
||||
return
|
||||
Net.go_to_map(map, scene_path)
|
||||
|
||||
func _download_and_mount(pack_name: String, title: String) -> bool:
|
||||
_show("Загрузка: %s…" % title)
|
||||
get_tree().paused = true # Пока качаем, мир стоит (камень не должен убить игрока на экране загрузки)
|
||||
|
||||
var http := HTTPRequest.new()
|
||||
http.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
# Если сервер сжимает пакет gzip, браузер распаковывает его сам; вторая распаковка в движке ломает загрузку
|
||||
http.accept_gzip = false
|
||||
add_child(http)
|
||||
# Тело берём в память и пишем в user:// сами: в вебе download_file файл не создаёт
|
||||
var state := {"done": false, "result": -1, "code": 0, "body": PackedByteArray()}
|
||||
http.request_completed.connect(func(result: int, code: int, _h, body: PackedByteArray):
|
||||
state.result = result
|
||||
state.code = code
|
||||
state.body = body
|
||||
state.done = true)
|
||||
|
||||
var ok := false
|
||||
var detail := "запрос не отправлен"
|
||||
# no-cache: браузер сверяет файл с сервером (после обновления пакета не подсунет старый из кэша)
|
||||
if http.request(_pack_url(pack_name), ["Cache-Control: no-cache"]) == OK:
|
||||
while not state.done:
|
||||
_update_progress(http, title)
|
||||
await get_tree().process_frame
|
||||
if state.result == HTTPRequest.RESULT_SUCCESS and state.code == 200:
|
||||
var path := "user://" + pack_name
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
detail = "не удалось сохранить пакет, код %d" % FileAccess.get_open_error()
|
||||
else:
|
||||
file.store_buffer(state.body)
|
||||
file.close()
|
||||
state.body = PackedByteArray() # Освобождаем память: пакет уже на диске
|
||||
ok = ProjectSettings.load_resource_pack(path, false) # false: файлы основной игры в приоритете
|
||||
detail = "пакет скачан, но не подключился (%s)" % _describe_file(path)
|
||||
else:
|
||||
detail = "HTTP %d, код загрузки %d" % [state.code, state.result]
|
||||
http.queue_free()
|
||||
|
||||
if ok:
|
||||
_loaded[pack_name] = true
|
||||
_hide()
|
||||
else:
|
||||
printerr("MapLoader: не удалось загрузить %s: %s" % [pack_name, detail])
|
||||
_label.text = "Не удалось загрузить карту\n%s" % detail
|
||||
_bar.visible = false
|
||||
await get_tree().create_timer(2.5).timeout
|
||||
_hide()
|
||||
get_tree().paused = false
|
||||
return ok
|
||||
|
||||
# Для сообщения об ошибке: размер и первые байты скачанного файла
|
||||
func _describe_file(path: String) -> String:
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return "файл не открывается, код %d" % FileAccess.get_open_error()
|
||||
var head := f.get_buffer(4)
|
||||
return "%d байт, начало %s" % [f.get_length(), head.hex_encode()]
|
||||
|
||||
func _pack_url(pack_name: String) -> String:
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with(PACK_URL_ARG):
|
||||
return arg.substr(PACK_URL_ARG.length()) + pack_name
|
||||
if OS.has_feature("web"):
|
||||
return str(JavaScriptBridge.eval("new URL('%s', window.location.href).href" % pack_name))
|
||||
return "http://127.0.0.1:8000/" + pack_name
|
||||
|
||||
# ---------- Экран загрузки ----------
|
||||
|
||||
func _show(text: String) -> void:
|
||||
_layer = CanvasLayer.new()
|
||||
_layer.layer = 100
|
||||
add_child(_layer)
|
||||
|
||||
var bg := ColorRect.new()
|
||||
bg.color = Color(0, 0, 0, 0.88)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_layer.add_child(bg)
|
||||
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
_label = Label.new()
|
||||
_label.text = text
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label.add_theme_font_size_override("font_size", 40)
|
||||
bg.add_child(_label)
|
||||
_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
_bar = ProgressBar.new()
|
||||
_bar.custom_minimum_size = Vector2(420, 14)
|
||||
_bar.show_percentage = false
|
||||
_bar.indeterminate = true
|
||||
var track := StyleBoxFlat.new()
|
||||
track.bg_color = Color(1, 1, 1, 0.15)
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(1.0, 0.85, 0.25)
|
||||
_bar.add_theme_stylebox_override("background", track)
|
||||
_bar.add_theme_stylebox_override("fill", fill)
|
||||
bg.add_child(_bar)
|
||||
_bar.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM, Control.PRESET_MODE_KEEP_SIZE)
|
||||
_bar.position.y -= 120
|
||||
|
||||
func _update_progress(http: HTTPRequest, title: String) -> void:
|
||||
var got := http.get_downloaded_bytes()
|
||||
var total := http.get_body_size() # -1, если сервер не прислал Content-Length (например, gzip)
|
||||
if total > 0:
|
||||
_bar.indeterminate = false
|
||||
_bar.max_value = total
|
||||
_bar.value = got
|
||||
_label.text = "Загрузка: %s\n%.1f / %.1f МБ" % [title, got / 1048576.0, total / 1048576.0]
|
||||
else:
|
||||
_label.text = "Загрузка: %s\n%.1f МБ" % [title, got / 1048576.0]
|
||||
|
||||
func _hide() -> void:
|
||||
if _layer:
|
||||
_layer.queue_free()
|
||||
_layer = null
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp4qld1nj4cla
|
||||
@@ -0,0 +1,220 @@
|
||||
extends Node
|
||||
# Автолоад «Overlay»: общие панели поверх любой карты — ввод ника (клавиша N, первый запуск в хабе)
|
||||
# и таблица лидеров (Tab). Пока открыта панель ника, Net.input_blocked = true: игрок (player.gd) стоит и не
|
||||
# обрабатывает игровые клавиши. Это отдельный флаг, НЕ player.input_frozen (его ведёт гонка на отсчёте).
|
||||
|
||||
const TEXT := Color(0.92, 0.88, 0.8)
|
||||
const DIM := Color(0.62, 0.6, 0.56)
|
||||
const MINE := Color(0.96, 0.76, 0.44)
|
||||
|
||||
var nick_open := false
|
||||
var lb_open := false
|
||||
|
||||
var _first_run := false
|
||||
var _nick_layer: CanvasLayer
|
||||
var _nick_title: Label
|
||||
var _edit: LineEdit
|
||||
var _lb_layer: CanvasLayer
|
||||
var _lb_title: Label
|
||||
var _lb_tabs: HBoxContainer
|
||||
var _lb_holder: Control
|
||||
var _lb_hint: Label
|
||||
var _lb_view: LbView
|
||||
var _lb_maps: Array = []
|
||||
var _lb_index := 0
|
||||
var _lb_for_map := ""
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_build_nick_panel()
|
||||
_build_lb_panel()
|
||||
|
||||
# ---------- ввод ника ----------
|
||||
|
||||
func _build_nick_panel() -> void:
|
||||
_nick_layer = CanvasLayer.new()
|
||||
_nick_layer.layer = 20
|
||||
_nick_layer.visible = false
|
||||
add_child(_nick_layer)
|
||||
var root := Control.new()
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nick_layer.add_child(root)
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", LbView.panel_style(26))
|
||||
root.add_child(panel)
|
||||
panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
panel.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
panel.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 14)
|
||||
panel.add_child(box)
|
||||
_nick_title = LbView.make_label("Введите ник", 28, TEXT)
|
||||
_nick_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_nick_title)
|
||||
_edit = LineEdit.new()
|
||||
_edit.max_length = Net.NICK_MAX
|
||||
_edit.custom_minimum_size = Vector2(320, 40)
|
||||
_edit.add_theme_font_size_override("font_size", 22)
|
||||
_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_edit.placeholder_text = "до %d символов" % Net.NICK_MAX
|
||||
_edit.context_menu_enabled = false
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color(0.015, 0.018, 0.02, 0.95)
|
||||
sb.border_color = Color(0.45, 0.30, 0.16, 0.9)
|
||||
sb.set_border_width_all(1)
|
||||
sb.set_content_margin_all(8)
|
||||
_edit.add_theme_stylebox_override("normal", sb)
|
||||
_edit.add_theme_stylebox_override("focus", sb)
|
||||
_edit.add_theme_color_override("font_color", TEXT)
|
||||
box.add_child(_edit)
|
||||
_edit.focus_next = _edit.get_path() # Tab не уводит фокус
|
||||
_edit.focus_previous = _edit.get_path()
|
||||
_edit.text_submitted.connect(_on_nick_submitted)
|
||||
_edit.gui_input.connect(_on_edit_gui_input)
|
||||
var hint := LbView.make_label("Enter — подтвердить · Esc — пропустить", 16, DIM)
|
||||
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(hint)
|
||||
|
||||
func open_nick(first_run := false) -> void:
|
||||
if nick_open:
|
||||
return
|
||||
nick_open = true
|
||||
_first_run = first_run
|
||||
Net.input_blocked = true
|
||||
if lb_open:
|
||||
close_lb()
|
||||
_nick_title.text = "Введите ник" if (first_run or Net.my_nick == "") else "Смена ника"
|
||||
_edit.text = Net.my_nick
|
||||
_edit.caret_column = _edit.text.length()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_nick_layer.visible = true
|
||||
_edit.grab_focus.call_deferred() # открывающая клавиша уже обработана и в поле не попадает
|
||||
|
||||
func _close_nick(apply: bool) -> void:
|
||||
if not nick_open:
|
||||
return
|
||||
if apply:
|
||||
Net.set_nick(_edit.text)
|
||||
elif _first_run:
|
||||
Net.skip_nick()
|
||||
nick_open = false
|
||||
Net.input_blocked = false
|
||||
_nick_layer.visible = false
|
||||
_edit.release_focus()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _on_nick_submitted(_text: String) -> void:
|
||||
_close_nick(true)
|
||||
|
||||
func _on_edit_gui_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_edit.accept_event()
|
||||
_close_nick(false)
|
||||
|
||||
# ---------- таблица лидеров ----------
|
||||
|
||||
func _build_lb_panel() -> void:
|
||||
_lb_layer = CanvasLayer.new()
|
||||
_lb_layer.layer = 14
|
||||
_lb_layer.visible = false
|
||||
add_child(_lb_layer)
|
||||
var root := Control.new()
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_lb_layer.add_child(root)
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", LbView.panel_style(18))
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(panel)
|
||||
# справа по центру: сверху остаётся таймер гонки, в центре — отсчёт, слева — место и ворота
|
||||
panel.anchor_left = 1.0
|
||||
panel.anchor_right = 1.0
|
||||
panel.anchor_top = 0.5
|
||||
panel.anchor_bottom = 0.5
|
||||
panel.offset_right = -48.0
|
||||
panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
panel.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 8)
|
||||
panel.add_child(box)
|
||||
_lb_title = LbView.make_label("", 24, TEXT)
|
||||
box.add_child(_lb_title)
|
||||
_lb_tabs = HBoxContainer.new()
|
||||
_lb_tabs.add_theme_constant_override("separation", 14)
|
||||
box.add_child(_lb_tabs)
|
||||
var hold := VBoxContainer.new()
|
||||
hold.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
box.add_child(hold)
|
||||
_lb_holder = hold
|
||||
_lb_hint = LbView.make_label("", 14, DIM)
|
||||
box.add_child(_lb_hint)
|
||||
|
||||
func _lb_map_list() -> Array:
|
||||
if Net.TIMED_MAPS.has(Net.current_map):
|
||||
return [Net.current_map]
|
||||
return Net.TIMED_MAPS.keys() # хаб и карты без рейтинга: вкладки по всем картам
|
||||
|
||||
func open_lb() -> void:
|
||||
if lb_open or nick_open:
|
||||
return
|
||||
lb_open = true
|
||||
_lb_for_map = Net.current_map
|
||||
_lb_maps = _lb_map_list()
|
||||
_lb_index = 0
|
||||
_lb_view = LbView.create(_lb_maps[0], 18)
|
||||
_lb_holder.add_child(_lb_view)
|
||||
_lb_layer.visible = true
|
||||
_lb_refresh()
|
||||
|
||||
func close_lb() -> void:
|
||||
if not lb_open:
|
||||
return
|
||||
lb_open = false
|
||||
_lb_layer.visible = false
|
||||
if _lb_view != null:
|
||||
_lb_holder.remove_child(_lb_view)
|
||||
_lb_view.queue_free()
|
||||
_lb_view = null
|
||||
|
||||
func _lb_refresh() -> void:
|
||||
var m: String = _lb_maps[_lb_index]
|
||||
_lb_title.text = "ЛУЧШИЕ ВРЕМЕНА · " + Net.MAP_TITLES.get(m, m).to_upper() if _lb_maps.size() == 1 else "ЛУЧШИЕ ВРЕМЕНА"
|
||||
for c in _lb_tabs.get_children():
|
||||
c.queue_free()
|
||||
_lb_tabs.visible = _lb_maps.size() > 1
|
||||
for i in _lb_maps.size():
|
||||
_lb_tabs.add_child(LbView.make_label(Net.MAP_TITLES.get(_lb_maps[i], _lb_maps[i]), 18, MINE if i == _lb_index else DIM))
|
||||
_lb_hint.text = ("A / D — карта · " if _lb_maps.size() > 1 else "") + "Tab / Esc — закрыть"
|
||||
_lb_view.set_map(m)
|
||||
Net.request_board(m)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if lb_open and _lb_for_map != Net.current_map:
|
||||
close_lb() # перешли на другую карту через портал
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if nick_open:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_close_nick(false)
|
||||
return
|
||||
if event.is_echo():
|
||||
return
|
||||
if event.is_action_pressed("nick"):
|
||||
open_nick(false)
|
||||
elif event.is_action_pressed("leaderboard"):
|
||||
if lb_open:
|
||||
close_lb()
|
||||
else:
|
||||
open_lb()
|
||||
elif lb_open and event.is_action_pressed("ui_cancel"):
|
||||
close_lb()
|
||||
elif lb_open and _lb_maps.size() > 1:
|
||||
var d := 0
|
||||
if event.is_action_pressed("move_left"):
|
||||
d = -1
|
||||
elif event.is_action_pressed("move_right"):
|
||||
d = 1
|
||||
if d != 0:
|
||||
_lb_index = posmod(_lb_index + d, _lb_maps.size())
|
||||
_lb_refresh()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcudxskqa5bx6
|
||||
@@ -0,0 +1,331 @@
|
||||
extends Node
|
||||
# Автолоад «PauseMenu»: меню паузы по Esc / Start. Игра идёт дальше (это мультиплеер), но игрок стоит
|
||||
# (Net.input_blocked, как при вводе ника). Страницы: главная, настройки, управление.
|
||||
#
|
||||
# В браузере Esc при захваченной мыши не доходит до игры — браузер просто снимает захват. Поэтому меню
|
||||
# открывается и по самому факту «мышь была захвачена и перестала быть» (см. _process).
|
||||
|
||||
const TEXT := Color(0.92, 0.88, 0.8)
|
||||
const DIM := Color(0.62, 0.6, 0.56)
|
||||
const MINE := Color(0.96, 0.76, 0.44)
|
||||
const HUB_SCENE := "res://maps/hub/hub.tscn"
|
||||
const REOPEN_GUARD := 0.6 # после закрытия столько секунд снятие захвата не считается нажатием Esc
|
||||
const CAPTURED_MIN := 0.5 # мышь должна быть захвачена хотя бы столько, чтобы потеря захвата считалась Esc
|
||||
|
||||
var open := false
|
||||
|
||||
var _layer: CanvasLayer
|
||||
var _pages := {} # имя -> Control
|
||||
var _page := "main"
|
||||
var _title: Label
|
||||
var _subtitle: Label
|
||||
var _hub_btn: Button
|
||||
var _lb_btn: Button
|
||||
var _captured_time := 0.0
|
||||
var _guard := 0.0
|
||||
var _sliders := {} # имя настройки -> {slider, value}
|
||||
var _invert: CheckButton
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_build()
|
||||
_layer.visible = false
|
||||
|
||||
# ---------- открытие / закрытие ----------
|
||||
|
||||
func open_menu() -> void:
|
||||
if open:
|
||||
return
|
||||
open = true
|
||||
Net.input_blocked = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_show_page("main")
|
||||
_layer.visible = true
|
||||
|
||||
func close_menu() -> void:
|
||||
if not open:
|
||||
return
|
||||
open = false
|
||||
_guard = REOPEN_GUARD
|
||||
Settings.save()
|
||||
_layer.visible = false
|
||||
get_viewport().gui_release_focus()
|
||||
if not Overlay.nick_open:
|
||||
Net.input_blocked = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_guard = maxf(_guard - delta, 0.0)
|
||||
if open:
|
||||
_captured_time = 0.0
|
||||
return
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
_captured_time += delta
|
||||
return
|
||||
# Захват пропал. Если до этого мышь побыла захвачена и мы сами её не отпускали — это Esc в браузере.
|
||||
var was := _captured_time >= CAPTURED_MIN
|
||||
_captured_time = 0.0
|
||||
if was and _guard <= 0.0 and _can_open():
|
||||
open_menu()
|
||||
|
||||
func _can_open() -> bool:
|
||||
return not Overlay.nick_open and not MapLoader._busy and get_tree().current_scene != null
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if open:
|
||||
if event.is_action_pressed("pause") or event.is_action_pressed("ui_cancel"):
|
||||
if _page == "main":
|
||||
close_menu()
|
||||
else:
|
||||
_show_page("main")
|
||||
# Игровые клавиши (R, N, Tab, G…) за меню не работают
|
||||
if not (event is InputEventMouseMotion):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed("pause") and not event.is_echo():
|
||||
if Overlay.nick_open or Overlay.lb_open or not _can_open():
|
||||
return # Esc закрывает панель ника / таблицу — это делает Overlay
|
||||
open_menu()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# ---------- страницы ----------
|
||||
|
||||
func _show_page(name: String) -> void:
|
||||
_page = name
|
||||
for k in _pages:
|
||||
_pages[k].visible = (k == name)
|
||||
match name:
|
||||
"main":
|
||||
_title.text = "ПАУЗА"
|
||||
var map_title: String = Net.MAP_TITLES.get(Net.current_map, "")
|
||||
_subtitle.text = "Карта: %s" % map_title if map_title != "" else ""
|
||||
_hub_btn.visible = Net.current_map != "hub"
|
||||
_lb_btn.visible = true
|
||||
_focus_first.call_deferred(_pages.main)
|
||||
"settings":
|
||||
_title.text = "НАСТРОЙКИ"
|
||||
_subtitle.text = "Сохраняются автоматически"
|
||||
_refresh_settings()
|
||||
_focus_first.call_deferred(_pages.settings)
|
||||
"controls":
|
||||
_title.text = "УПРАВЛЕНИЕ"
|
||||
_subtitle.text = ""
|
||||
_focus_first.call_deferred(_pages.controls)
|
||||
|
||||
func _focus_first(page: Control) -> void:
|
||||
for b in page.find_children("*", "Button", true, false):
|
||||
if b.visible and not b.disabled:
|
||||
b.grab_focus()
|
||||
return
|
||||
|
||||
func _build() -> void:
|
||||
_layer = CanvasLayer.new()
|
||||
_layer.layer = 30
|
||||
add_child(_layer)
|
||||
|
||||
var theme := _make_theme()
|
||||
var root := Control.new()
|
||||
root.theme = theme
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_layer.add_child(root)
|
||||
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0.0, 0.0, 0.0, 0.62)
|
||||
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_child(dim)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", LbView.panel_style(28))
|
||||
root.add_child(panel)
|
||||
panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
panel.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
panel.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 12)
|
||||
box.custom_minimum_size = Vector2(460, 0)
|
||||
panel.add_child(box)
|
||||
|
||||
_title = LbView.make_label("ПАУЗА", 34, TEXT)
|
||||
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_title)
|
||||
_subtitle = LbView.make_label("", 16, DIM)
|
||||
_subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_subtitle)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
_pages["main"] = _build_main()
|
||||
_pages["settings"] = _build_settings()
|
||||
_pages["controls"] = _build_controls()
|
||||
for k in _pages:
|
||||
box.add_child(_pages[k])
|
||||
|
||||
var hint := LbView.make_label("Esc / Start — назад", 14, DIM)
|
||||
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(hint)
|
||||
|
||||
func _make_theme() -> Theme:
|
||||
var t := Theme.new()
|
||||
t.default_font_size = 20
|
||||
var normal := _btn_style(Color(0.07, 0.075, 0.08, 0.95), Color(0.30, 0.22, 0.14))
|
||||
var hover := _btn_style(Color(0.13, 0.11, 0.09, 0.98), Color(0.75, 0.52, 0.26))
|
||||
var pressed := _btn_style(Color(0.20, 0.15, 0.10, 1.0), MINE)
|
||||
var focus := _btn_style(Color(0.13, 0.11, 0.09, 0.98), MINE)
|
||||
for cls in ["Button", "CheckButton"]:
|
||||
t.set_stylebox("normal", cls, normal)
|
||||
t.set_stylebox("hover", cls, hover)
|
||||
t.set_stylebox("pressed", cls, pressed)
|
||||
t.set_stylebox("focus", cls, focus)
|
||||
t.set_color("font_color", cls, TEXT)
|
||||
t.set_color("font_hover_color", cls, Color.WHITE)
|
||||
t.set_color("font_focus_color", cls, Color.WHITE)
|
||||
t.set_color("font_pressed_color", cls, MINE)
|
||||
t.set_font_size("font_size", cls, 20)
|
||||
t.set_stylebox("disabled", "Button", normal)
|
||||
# Слайдер: тёмная полоса и латунный заполнитель
|
||||
var track := StyleBoxFlat.new()
|
||||
track.bg_color = Color(0.1, 0.1, 0.11)
|
||||
track.set_corner_radius_all(3)
|
||||
track.content_margin_top = 4
|
||||
track.content_margin_bottom = 4
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(0.62, 0.42, 0.2)
|
||||
fill.set_corner_radius_all(3)
|
||||
t.set_stylebox("slider", "HSlider", track)
|
||||
t.set_stylebox("grabber_area", "HSlider", fill)
|
||||
t.set_stylebox("grabber_area_highlight", "HSlider", fill)
|
||||
return t
|
||||
|
||||
func _btn_style(bg: Color, border: Color) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = bg
|
||||
sb.border_color = border
|
||||
sb.set_border_width_all(2)
|
||||
sb.content_margin_left = 16
|
||||
sb.content_margin_right = 16
|
||||
sb.content_margin_top = 9
|
||||
sb.content_margin_bottom = 9
|
||||
return sb
|
||||
|
||||
func _button(text: String, cb: Callable) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = text
|
||||
b.focus_mode = Control.FOCUS_ALL
|
||||
b.pressed.connect(cb)
|
||||
return b
|
||||
|
||||
func _vbox() -> VBoxContainer:
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 10)
|
||||
return v
|
||||
|
||||
func _build_main() -> Control:
|
||||
var v := _vbox()
|
||||
v.add_child(_button("Продолжить", close_menu))
|
||||
_hub_btn = _button("Выйти в хаб", _go_hub)
|
||||
v.add_child(_hub_btn)
|
||||
_lb_btn = _button("Таблица лидеров", _open_lb)
|
||||
v.add_child(_lb_btn)
|
||||
v.add_child(_button("Сменить ник", _open_nick))
|
||||
v.add_child(_button("Настройки", func(): _show_page("settings")))
|
||||
v.add_child(_button("Управление", func(): _show_page("controls")))
|
||||
return v
|
||||
|
||||
func _go_hub() -> void:
|
||||
close_menu()
|
||||
MapLoader.enter("hub", HUB_SCENE, "", "Хаб")
|
||||
|
||||
func _open_lb() -> void:
|
||||
close_menu()
|
||||
Overlay.open_lb()
|
||||
|
||||
func _open_nick() -> void:
|
||||
open = false
|
||||
_guard = REOPEN_GUARD
|
||||
_layer.visible = false
|
||||
get_viewport().gui_release_focus()
|
||||
Overlay.open_nick(false) # сама держит Net.input_blocked и мышь
|
||||
|
||||
# ---------- настройки ----------
|
||||
|
||||
func _build_settings() -> Control:
|
||||
var v := _vbox()
|
||||
_add_slider(v, "mouse", "Чувствительность мыши", 0.2, 3.0, 0.05, func(x): Settings.mouse_sens = x, func(x): return "×%.2f" % x)
|
||||
_add_slider(v, "stick", "Чувствительность стика", 0.3, 2.5, 0.05, func(x): Settings.stick_sens = x, func(x): return "×%.2f" % x)
|
||||
_add_slider(v, "fov", "Поле зрения", 60.0, 100.0, 1.0, func(x): Settings.fov = x, func(x): return "%d°" % int(x))
|
||||
_add_slider(v, "volume", "Громкость", 0.0, 1.0, 0.01, func(x): Settings.volume = x, func(x): return "%d%%" % int(round(x * 100.0)))
|
||||
_invert = CheckButton.new()
|
||||
_invert.text = "Инвертировать вертикаль"
|
||||
_invert.focus_mode = Control.FOCUS_ALL
|
||||
_invert.toggled.connect(func(on: bool): Settings.invert_y = on)
|
||||
v.add_child(_invert)
|
||||
v.add_child(_button("По умолчанию", func():
|
||||
Settings.reset_defaults()
|
||||
_refresh_settings()))
|
||||
v.add_child(_button("Назад", func(): _show_page("main")))
|
||||
return v
|
||||
|
||||
func _add_slider(parent: Control, key: String, caption: String, lo: float, hi: float, step: float, setter: Callable, fmt: Callable) -> void:
|
||||
var row := VBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 2)
|
||||
var head := HBoxContainer.new()
|
||||
var name_l := LbView.make_label(caption, 18, TEXT)
|
||||
name_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
head.add_child(name_l)
|
||||
var val_l := LbView.make_label("", 18, MINE)
|
||||
head.add_child(val_l)
|
||||
row.add_child(head)
|
||||
var s := HSlider.new()
|
||||
s.min_value = lo
|
||||
s.max_value = hi
|
||||
s.step = step
|
||||
s.focus_mode = Control.FOCUS_ALL
|
||||
s.custom_minimum_size = Vector2(0, 26)
|
||||
s.value_changed.connect(func(x: float):
|
||||
setter.call(x)
|
||||
val_l.text = fmt.call(x)
|
||||
Settings.apply())
|
||||
row.add_child(s)
|
||||
parent.add_child(row)
|
||||
_sliders[key] = {"slider": s, "label": val_l, "fmt": fmt}
|
||||
|
||||
func _refresh_settings() -> void:
|
||||
var values := {"mouse": Settings.mouse_sens, "stick": Settings.stick_sens, "fov": Settings.fov, "volume": Settings.volume}
|
||||
for k in values:
|
||||
var e: Dictionary = _sliders[k]
|
||||
e.slider.set_value_no_signal(values[k])
|
||||
e.label.text = e.fmt.call(values[k])
|
||||
_invert.set_pressed_no_signal(Settings.invert_y)
|
||||
|
||||
# ---------- справка по управлению ----------
|
||||
|
||||
func _build_controls() -> Control:
|
||||
var v := _vbox()
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 22)
|
||||
grid.add_theme_constant_override("v_separation", 5)
|
||||
var rows := [
|
||||
["", "Клавиатура", "Геймпад Xbox"],
|
||||
["Движение / обзор", "WASD / мышь", "левый / правый стик"],
|
||||
["Прыжок, подтягивание", "Пробел", "A или LB"],
|
||||
["Спринт", "Shift", "RT или L3"],
|
||||
["Рывок", "Q", "X или RB"],
|
||||
["Присед (держать)", "C", "B"],
|
||||
["Подкат (на бегу)", "Shift + C", "RT + B"],
|
||||
["Зум", "ЛКМ", "LT"],
|
||||
["«Готов» / забег заново", "R", "Y"],
|
||||
["Сдаться в гонке (держать)", "G", "крестовина вниз"],
|
||||
["Таблица лидеров", "Tab", "Back"],
|
||||
["Ник", "N", "в меню паузы"],
|
||||
["Пауза", "Esc", "Start"],
|
||||
]
|
||||
for r in rows.size():
|
||||
for c in 3:
|
||||
var l := LbView.make_label(rows[r][c], 16, MINE if r == 0 else (TEXT if c == 0 else DIM))
|
||||
grid.add_child(l)
|
||||
v.add_child(grid)
|
||||
var tip := LbView.make_label("Стены: прыжок в воздухе у стены с зажатым «вперёд» — бег по стене.", 14, DIM)
|
||||
v.add_child(tip)
|
||||
v.add_child(_button("Назад", func(): _show_page("main")))
|
||||
return v
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyvtp5hwd4h5o
|
||||
@@ -0,0 +1,147 @@
|
||||
extends Node
|
||||
# Автолоад «Settings»: пользовательские настройки (мышь, стик, FOV, громкость) и раскладка геймпада Xbox.
|
||||
# Геймпад добавляется в InputMap кодом (а не в project.godot): раскладка собрана в одном месте и видна целиком.
|
||||
# Сохраняется в user://settings.cfg, но только когда игрок что-то менял (save()).
|
||||
#
|
||||
# Раскладка под паркур и гонки (правый большой палец всегда остаётся на стике обзора):
|
||||
# левый стик — движение, правый стик — обзор
|
||||
# A / LB — прыжок (подтягивание, прыжок от стены, двойной прыжок) RT / L3 — спринт (держать)
|
||||
# X / RB — рывок B — присед (держать); RT + B на бегу — подкат
|
||||
# LT / R3 — зум Y — «готов» (гонка) / забег заново (Parkour)
|
||||
# Back — таблица лидеров крестовина вниз (держать) — сдаться в гонке
|
||||
# Start — пауза
|
||||
|
||||
signal changed
|
||||
|
||||
const PATH := "user://settings.cfg"
|
||||
|
||||
const DEFAULT_FOV := 60.0
|
||||
const STICK_DEADZONE := 0.22
|
||||
const STICK_LOOK_SPEED := 3.6 # рад/с при полном отклонении стика и множителе 1.0 (~205°/с)
|
||||
|
||||
var mouse_sens := 1.0 # множитель к player.MOUSE_SENSITIVITY
|
||||
var stick_sens := 1.0 # множитель к STICK_LOOK_SPEED
|
||||
var fov := DEFAULT_FOV # базовое поле зрения
|
||||
var volume := 1.0 # общая громкость 0..1
|
||||
var invert_y := false # инверсия вертикали (мышь и стик)
|
||||
|
||||
var gamepad_used := false # игрок трогал геймпад (для подсказок)
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_setup_gamepad()
|
||||
load_settings()
|
||||
apply()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventJoypadButton or (event is InputEventJoypadMotion and absf(event.axis_value) > 0.5):
|
||||
gamepad_used = true
|
||||
elif event is InputEventKey or event is InputEventMouseButton:
|
||||
gamepad_used = false
|
||||
|
||||
# ---------- сохранение ----------
|
||||
|
||||
func load_settings() -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
if cfg.load(PATH) != OK:
|
||||
return
|
||||
mouse_sens = clampf(float(cfg.get_value("input", "mouse_sens", mouse_sens)), 0.2, 3.0)
|
||||
stick_sens = clampf(float(cfg.get_value("input", "stick_sens", stick_sens)), 0.3, 2.5)
|
||||
invert_y = bool(cfg.get_value("input", "invert_y", invert_y))
|
||||
fov = clampf(float(cfg.get_value("video", "fov", fov)), 60.0, 100.0)
|
||||
volume = clampf(float(cfg.get_value("audio", "volume", volume)), 0.0, 1.0)
|
||||
|
||||
func save() -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
cfg.set_value("input", "mouse_sens", mouse_sens)
|
||||
cfg.set_value("input", "stick_sens", stick_sens)
|
||||
cfg.set_value("input", "invert_y", invert_y)
|
||||
cfg.set_value("video", "fov", fov)
|
||||
cfg.set_value("audio", "volume", volume)
|
||||
cfg.save(PATH)
|
||||
|
||||
func apply() -> void:
|
||||
var bus := AudioServer.get_bus_index("Master")
|
||||
AudioServer.set_bus_mute(bus, volume <= 0.001)
|
||||
AudioServer.set_bus_volume_db(bus, linear_to_db(maxf(volume, 0.001)))
|
||||
changed.emit()
|
||||
|
||||
func reset_defaults() -> void:
|
||||
mouse_sens = 1.0
|
||||
stick_sens = 1.0
|
||||
fov = DEFAULT_FOV
|
||||
volume = 1.0
|
||||
invert_y = false
|
||||
apply()
|
||||
|
||||
# ---------- геймпад ----------
|
||||
|
||||
func _setup_gamepad() -> void:
|
||||
# Стики: двигаем и смотрим
|
||||
_axis("move_left", JOY_AXIS_LEFT_X, -1.0)
|
||||
_axis("move_right", JOY_AXIS_LEFT_X, 1.0)
|
||||
_axis("move_forward", JOY_AXIS_LEFT_Y, -1.0)
|
||||
_axis("move_back", JOY_AXIS_LEFT_Y, 1.0)
|
||||
for a in ["move_left", "move_right", "move_forward", "move_back"]:
|
||||
InputMap.action_set_deadzone(a, STICK_DEADZONE)
|
||||
_new_action("look_left", STICK_DEADZONE)
|
||||
_new_action("look_right", STICK_DEADZONE)
|
||||
_new_action("look_up", STICK_DEADZONE)
|
||||
_new_action("look_down", STICK_DEADZONE)
|
||||
_axis("look_left", JOY_AXIS_RIGHT_X, -1.0)
|
||||
_axis("look_right", JOY_AXIS_RIGHT_X, 1.0)
|
||||
_axis("look_up", JOY_AXIS_RIGHT_Y, -1.0)
|
||||
_axis("look_down", JOY_AXIS_RIGHT_Y, 1.0)
|
||||
|
||||
# Действия
|
||||
_button("jump", JOY_BUTTON_A)
|
||||
_button("jump", JOY_BUTTON_LEFT_SHOULDER) # прыжок на плече: правый палец не уходит со стика обзора
|
||||
_axis("sprint", JOY_AXIS_TRIGGER_RIGHT, 1.0)
|
||||
_button("sprint", JOY_BUTTON_LEFT_STICK)
|
||||
_button("crouch", JOY_BUTTON_B)
|
||||
_button("dash", JOY_BUTTON_X)
|
||||
_button("dash", JOY_BUTTON_RIGHT_SHOULDER)
|
||||
_new_action("zoom", 0.3)
|
||||
_axis("zoom", JOY_AXIS_TRIGGER_LEFT, 1.0)
|
||||
_button("zoom", JOY_BUTTON_RIGHT_STICK)
|
||||
_button("ready", JOY_BUTTON_Y)
|
||||
_button("leaderboard", JOY_BUTTON_BACK)
|
||||
_button("giveup", JOY_BUTTON_DPAD_DOWN)
|
||||
|
||||
# Навигация по меню: A — выбрать, B — назад (в этом проекте у ui_accept/ui_cancel кнопок геймпада нет)
|
||||
_button_once("ui_accept", JOY_BUTTON_A)
|
||||
_button_once("ui_cancel", JOY_BUTTON_B)
|
||||
_button_once("ui_up", JOY_BUTTON_DPAD_UP)
|
||||
_button_once("ui_down", JOY_BUTTON_DPAD_DOWN)
|
||||
_button_once("ui_left", JOY_BUTTON_DPAD_LEFT)
|
||||
_button_once("ui_right", JOY_BUTTON_DPAD_RIGHT)
|
||||
|
||||
# Пауза: Esc и Start
|
||||
_new_action("pause", 0.5)
|
||||
var esc := InputEventKey.new()
|
||||
esc.physical_keycode = KEY_ESCAPE
|
||||
InputMap.action_add_event("pause", esc)
|
||||
_button("pause", JOY_BUTTON_START)
|
||||
|
||||
func _new_action(action: String, deadzone: float) -> void:
|
||||
if not InputMap.has_action(action):
|
||||
InputMap.add_action(action, deadzone)
|
||||
|
||||
func _axis(action: String, axis: JoyAxis, value: float) -> void:
|
||||
_new_action(action, 0.5)
|
||||
var e := InputEventJoypadMotion.new()
|
||||
e.axis = axis
|
||||
e.axis_value = value
|
||||
InputMap.action_add_event(action, e)
|
||||
|
||||
func _button_once(action: String, button: JoyButton) -> void:
|
||||
for e in InputMap.action_get_events(action):
|
||||
if e is InputEventJoypadButton and e.button_index == button:
|
||||
return
|
||||
_button(action, button)
|
||||
|
||||
func _button(action: String, button: JoyButton) -> void:
|
||||
_new_action(action, 0.5)
|
||||
var e := InputEventJoypadButton.new()
|
||||
e.button_index = button
|
||||
InputMap.action_add_event(action, e)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkyvk0c3jox51
|
||||
@@ -1,17 +1 @@
|
||||
# Мультиплеер: деплой
|
||||
|
||||
## Сервер игры
|
||||
Выделенный сервер — тот же проект, запущенный headless (порт 9080, plain WebSocket):
|
||||
|
||||
godot --headless --path /путь/к/my-world res://Net/server.tscn
|
||||
|
||||
Пример systemd-юнита: `deploy/my-world.service`.
|
||||
|
||||
## nginx (TLS + статика)
|
||||
См. `deploy/nginx.conf`. Страница по https подключается к `wss://<домен>/ws`,
|
||||
nginx проксирует `/ws` на `127.0.0.1:9080`.
|
||||
|
||||
## Клиент
|
||||
Web-экспорт (пресет «Web») кладётся в корень сайта. Адрес сервера берётся из
|
||||
`window.location`, на десктопе — `ws://127.0.0.1:9080` или
|
||||
`godot -- --server-url=wss://example.com/ws`.
|
||||
Деплой описан в корневом [README.md](../README.md#деплой).
|
||||
|
||||
@@ -3,9 +3,11 @@ Description=My World game server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/godot --headless --path /opt/my-world res://Net/server.tscn
|
||||
ExecStart=/opt/my-world-server/godot --headless --path /opt/my-world-server
|
||||
Restart=always
|
||||
User=nobody
|
||||
DynamicUser=yes
|
||||
StateDirectory=my-world
|
||||
Environment=HOME=/var/lib/my-world
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
# Боевой конфиг сайта (/etc/nginx/sites-enabled/godot_game).
|
||||
# Статику клиента кладёт CI (git pull в /opt/grid-world), /ws проксируется на игровой сервер.
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
# ssl_certificate / ssl_certificate_key — например, от certbot
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/my-world; # сюда экспорт Web (index.html, .wasm, .pck)
|
||||
root /opt/grid-world/;
|
||||
index index.html;
|
||||
|
||||
# Нужны Godot web (SharedArrayBuffer/изоляция страницы)
|
||||
add_header Cross-Origin-Opener-Policy "same-origin";
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||||
|
||||
# .wasm и .pck хорошо жмутся: 39,5 МБ -> ~10 МБ. Браузер распаковывает сам (см. core/map_loader.gd)
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1024;
|
||||
gzip_types application/wasm application/octet-stream application/javascript text/javascript text/css application/json image/svg+xml;
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:9080;
|
||||
proxy_http_version 1.1;
|
||||
@@ -14,4 +25,8 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 1h;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
; Проект выделенного сервера (headless). Кладётся рядом с папкой net/ из репозитория.
|
||||
; На сервере: /opt/my-world-server/{project.godot, net/, godot}. См. tools/deploy_server.sh
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="world2-server"
|
||||
run/main_scene="res://net/server.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
|
||||
[autoload]
|
||||
|
||||
Net="*res://net/net.gd"
|
||||
@@ -10,7 +10,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tools/*, maps/sandbox/*, maps/backrooms/*, maps/parkour/*, maps/race/*, maps/race2/*, chasers/rock/*, chasers/verity/*"
|
||||
export_path="../grid-world/index.html"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
@@ -29,7 +29,257 @@ script_export_mode=2
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=true
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Backrooms"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/backrooms/backrooms.tscn")
|
||||
include_filter=""
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/backrooms.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="World"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/sandbox/sandbox.tscn")
|
||||
include_filter=""
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/world.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.3]
|
||||
|
||||
name="Parkour"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/parkour/parkour.tscn")
|
||||
include_filter="maps/parkour/sky.png, maps/parkour/baked.gdshader, maps/parkour/parkour_data.gd, maps/parkour/audio/*"
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/parkour.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.3.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.4]
|
||||
|
||||
name="Race"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/race/race.tscn")
|
||||
include_filter="maps/race/sky.png, maps/race/baked.gdshader, maps/race/race_data.gd, maps/race/race_rules.gd, maps/race/race.gd, maps/race/audio/*"
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/race.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.4.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.5]
|
||||
|
||||
name="Race2"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/race2/race2.tscn")
|
||||
include_filter="maps/race2/sky.png, maps/race2/baked.gdshader, maps/race2/race2_data.gd, maps/race/race_rules.gd, maps/race2/race2.gd, maps/race2/audio/*"
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/race2.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.5.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
extends Node3D
|
||||
# Backrooms на готовой модели. Сцена живёт в отдельном пакете (backrooms.pck) и качается
|
||||
# только при первом входе в портал. Коллизии строим по мешам модели при загрузке,
|
||||
# навмеш для камня запечён заранее (backrooms_nav.res) — в вебе запекать на лету долго.
|
||||
|
||||
func _ready() -> void:
|
||||
Net.enter_map("backrooms")
|
||||
_build_colliders()
|
||||
_build_environment()
|
||||
_build_hum()
|
||||
|
||||
func _build_colliders() -> void:
|
||||
for mi in $Map.find_children("*", "MeshInstance3D", true, false):
|
||||
mi.create_trimesh_collision()
|
||||
|
||||
func _build_environment() -> void:
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.06, 0.05, 0.03)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(1.0, 0.95, 0.8)
|
||||
env.ambient_light_energy = 0.7
|
||||
env.fog_enabled = true
|
||||
env.fog_light_color = Color(0.25, 0.2, 0.09)
|
||||
env.fog_density = 0.012
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
|
||||
# Гул ламп: 2 секунды, частоты кратны 0.5 Гц — петля без щелчка
|
||||
func _build_hum() -> void:
|
||||
const RATE := 11025
|
||||
var n := RATE * 2
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 5
|
||||
for i in n:
|
||||
var t := float(i) / RATE
|
||||
var s := 0.50 * sin(TAU * 60.0 * t) + 0.25 * sin(TAU * 120.0 * t) \
|
||||
+ 0.12 * sin(TAU * 180.0 * t) + 0.06 * sin(TAU * 240.0 * t)
|
||||
s += rng.randf_range(-1.0, 1.0) * 0.03
|
||||
data.encode_s16(i * 2, int(clampf(s, -1.0, 1.0) * 12000.0))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = RATE
|
||||
wav.stereo = false
|
||||
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
wav.loop_begin = 0
|
||||
wav.loop_end = n
|
||||
wav.data = data
|
||||
var player := AudioStreamPlayer.new()
|
||||
player.stream = wav
|
||||
player.volume_db = -12.0
|
||||
player.autoplay = true
|
||||
add_child(player)
|
||||
@@ -0,0 +1 @@
|
||||
uid://gmpkjl6m3415
|
||||
@@ -0,0 +1,36 @@
|
||||
[gd_scene format=3 uid="uid://djgkggvwiaaoy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://gmpkjl6m3415" path="res://maps/backrooms/backrooms.gd" id="1_level"]
|
||||
[ext_resource type="PackedScene" uid="uid://b8ef3wy1pthgm" path="res://maps/backrooms/model/backrooms_vr.glb" id="2_map"]
|
||||
[ext_resource type="PackedScene" uid="uid://c3efk6k8xnf57" path="res://player/player.tscn" id="3_player"]
|
||||
[ext_resource type="PackedScene" path="res://chasers/verity/verity.tscn" id="4_rock"]
|
||||
[ext_resource type="Script" uid="uid://bu8v0ivhmnykt" path="res://objects/portal.gd" id="5_portal"]
|
||||
[ext_resource type="NavigationMesh" path="res://maps/backrooms/backrooms_nav.res" id="6_nav"]
|
||||
|
||||
[node name="Backrooms" type="Node3D" unique_id=511186617]
|
||||
script = ExtResource("1_level")
|
||||
|
||||
[node name="Map" parent="." unique_id=82811432 instance=ExtResource("2_map")]
|
||||
transform = Transform3D(1.4, 0, 0, 0, 1.4, 0, 0, 0, 1.4, 0, 0, 0)
|
||||
|
||||
[node name="Player" parent="." unique_id=143512730 instance=ExtResource("3_player")]
|
||||
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -0.4838078, 1.2, 2.0468445)
|
||||
|
||||
[node name="Verity" parent="." unique_id=311663607 instance=ExtResource("4_rock")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -48, 0, 3)
|
||||
model_scale = 0.5
|
||||
speed = 10.0
|
||||
catch_distance = 2.0
|
||||
catch_height = 3.0
|
||||
sight_limited = true
|
||||
|
||||
[node name="Exit" type="Area3D" parent="." unique_id=1540431614]
|
||||
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, -92.93203, -0.15, 10.390619)
|
||||
script = ExtResource("5_portal")
|
||||
target_map = "hub"
|
||||
target_scene = "res://maps/hub/hub.tscn"
|
||||
title = "EXIT"
|
||||
color = Color(0.3, 1, 0.45, 1)
|
||||
|
||||
[node name="Navigation" type="NavigationRegion3D" parent="." unique_id=122126248]
|
||||
navigation_mesh = ExtResource("6_nav")
|
||||
@@ -0,0 +1,5 @@
|
||||
# Backrooms VR
|
||||
|
||||
Модель «Backrooms VR» — автор VTX (https://sketchfab.com/VTX_car)
|
||||
Источник: https://sketchfab.com/3d-models/backrooms-vr-1a5c397f0a43408fa38b09ea5c041149
|
||||
Лицензия: CC-BY 4.0 (http://creativecommons.org/licenses/by/4.0/)
|
||||
@@ -0,0 +1,45 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://b8ef3wy1pthgm"
|
||||
path="res://.godot/imported/backrooms_vr.glb-a44db63c437b581310f90765822ced16.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr.glb"
|
||||
dest_files=["res://.godot/imported/backrooms_vr.glb-a44db63c437b581310f90765822ced16.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
|
After Width: | Height: | Size: 687 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b6weikavgdbap"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_0.png-2be6c844f99fa11d85c6f7f7ea9febaf.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "dd26a9d7692e3436812fe8332db11362"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_0.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_0.png-2be6c844f99fa11d85c6f7f7ea9febaf.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 549 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dpud2naqkm4xs"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_1.png-58eff1a40efb7f44ec9e5051210a83fb.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "79c774f9175bf99b66d6647fc9276dd6"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_1.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_1.png-58eff1a40efb7f44ec9e5051210a83fb.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 609 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b8nb23j42isps"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_10.png-66735878c6a889d3b78eb86dd9d140a6.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "bdf490cf7f9a54efba63ed6886a31481"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_10.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_10.png-66735878c6a889d3b78eb86dd9d140a6.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 1018 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://djx5e3wiwgqex"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_11.png-1b4bc124dd509717784a19ff35407041.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "0eff7867078fb002c73e86db307b5cac"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_11.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_11.png-1b4bc124dd509717784a19ff35407041.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 897 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://djyh6v1wlujbx"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_12.png-4b9b409931a846aa266212c354316816.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "af3cabe8076ab6902c5ef8de7f304b52"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_12.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_12.png-4b9b409931a846aa266212c354316816.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 457 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dw68nfgta46bg"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_13.png-0fc1704bbe3dc42d01705e5699474f84.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "80b3f1544049ca7469516fae963a8b53"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_13.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_13.png-0fc1704bbe3dc42d01705e5699474f84.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 722 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cgx5ip8cpgxly"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_14.png-ef53db6b8d8b9de93738fb3214c4a754.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "f29e6fa0df4712e220723a81961d2409"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_14.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_14.png-ef53db6b8d8b9de93738fb3214c4a754.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 376 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dxvxjwfnpxqyb"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_15.png-bdd518883610319d83796f212056da76.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "be89a770b865d0c2b831e2bd61ec5d60"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_15.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_15.png-bdd518883610319d83796f212056da76.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 814 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://yjj1swxlp5le"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_16.png-a9e77912985bd57ee651e6170a096b24.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "a354b1ade1b355ac5f4faf756def42da"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_16.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_16.png-a9e77912985bd57ee651e6170a096b24.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://decg78uck2mqa"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_17.png-19b989596eb512a37eeb8e755657cf6c.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "a4ff81f7c103ef7064fdcdf9579550bd"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_17.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_17.png-19b989596eb512a37eeb8e755657cf6c.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 727 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://brljydqpvgko0"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_18.png-715bde8336c095d043c0cd3f1be4f502.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "1f8b3f56208dfb1ccc4cf99aead75a96"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_18.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_18.png-715bde8336c095d043c0cd3f1be4f502.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dciao7v0fkii1"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_19.png-49227045cdea27d1b381e3b9a831ee24.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "e093a35dfb48ed90dfee7417cb40b8aa"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_19.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_19.png-49227045cdea27d1b381e3b9a831ee24.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 206 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://eorb1lixjtbv"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_2.png-f1a53db9b9d3fd8a5ada6b370f18b647.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "6f80af8f00ff83fa14604341d717c0a1"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_2.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_2.png-f1a53db9b9d3fd8a5ada6b370f18b647.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 288 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://xbcxlgw40igt"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_20.png-1d10fd330e1a8c0fc61d7486329ee063.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "c089944811fd6841d24c05ff96888a0d"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_20.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_20.png-1d10fd330e1a8c0fc61d7486329ee063.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 971 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://crx06wyll47ou"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_21.png-222e2c54fbdeedd6c0487b075650545f.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "8e67b33967174d08949af75bbfdbf99b"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_21.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_21.png-222e2c54fbdeedd6c0487b075650545f.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 219 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cqareamgsajs"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_22.png-b8965588f76146db08bcab06a453e84c.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "1f9cbe82491c2b29517d2e7170fcc8d2"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_22.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_22.png-b8965588f76146db08bcab06a453e84c.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 547 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dy57qg8yhs4cx"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_23.png-4090a5fff0cc06e162bacc68b3fd83ce.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "afc73e7b0ceed0e68711ee2553fc102c"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_23.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_23.png-4090a5fff0cc06e162bacc68b3fd83ce.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 463 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bt548bd7s4get"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_24.png-bf9b213fc302459f00c11b5b6a489a2d.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "2bfbf2416d5c593dbdd1bfa9d7ccdb2a"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_24.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_24.png-bf9b213fc302459f00c11b5b6a489a2d.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 210 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://doneeapv77gj1"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_25.png-7986d2b08dc2f459f7a7b7872e6a16e1.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "d4edeb415eeadc87ba7e8b618df82951"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_25.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_25.png-7986d2b08dc2f459f7a7b7872e6a16e1.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cg4knissk32f0"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_26.png-258a94dd265e35223b36b7a90dfee344.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "b00013d16cea649dbfcad40105ea95c2"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_26.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_26.png-258a94dd265e35223b36b7a90dfee344.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 815 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://3gy68ac2rkdx"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_3.png-4355c26a3df456240cd49a319f1cd5de.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "27c87342265c1142652ee5c1c7de8578"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_3.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_3.png-4355c26a3df456240cd49a319f1cd5de.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 714 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3verocm47dx8"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_4.png-4a8967702e5eee6c2b19c6c5d1c0c555.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "c0f85dfdeb975bc1438901e114ee54f5"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_4.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_4.png-4a8967702e5eee6c2b19c6c5d1c0c555.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 635 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cnhk7o54biy8d"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_5.png-92bd0164b916a3cf31302520ddb50d1d.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "db8e48feaa1472c07f9e3131a9d5455d"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_5.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_5.png-92bd0164b916a3cf31302520ddb50d1d.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 888 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cjglgsq24tgpr"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_6.png-087f2ed0886aa3d81ebc9b5b30ba5bb4.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "396d37118c519e43f39dc17472ac44ac"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_6.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_6.png-087f2ed0886aa3d81ebc9b5b30ba5bb4.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://7ctu8yeskavl"
|
||||
path.s3tc="res://.godot/imported/backrooms_vr_7.png-6349183f10a2172a16f16a287d42d208.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "01930a0b90aed513040fd3d9eedbb6fe"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/backrooms/model/backrooms_vr_7.png"
|
||||
dest_files=["res://.godot/imported/backrooms_vr_7.png-6349183f10a2172a16f16a287d42d208.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
|
After Width: | Height: | Size: 898 KiB |