Структура проекта: папки по назначению, карты самодостаточны, инструменты, README
- Перенос: Net→net, Player→player, World/*→maps/{hub,sandbox,backrooms}, core, objects,
models/*→chasers/{rock,verity} и maps/*/models. Имена внутри папок с .glb не менялись
(рядом лежат извлечённые текстуры). Все пути, пресеты экспорта и автолоады обновлены.
- Преследователи: rock-follow.gd → chasers/chaser.gd; камень в песочнице теперь инстанс
chasers/rock/rock.tscn (как Верити).
- Удалён мусор: неиспользуемое небо sunflowers, Tree_01.glb, rock-path.gd, старая
модель Верити, unwrap_cache (теперь в .gitignore).
- Основной пакет игры 70 КБ (камень и Верити уехали в пакеты карт).
- tools/: export_all.sh, serve_web.py (nginx-подобный dev-сервер), deploy_server.sh,
test/ (боты, временный проект и сервер). Net: флаг --client для headless-клиента.
- deploy/: боевые nginx.conf и my-world.service, project.godot выделенного сервера.
- README переписан: что есть и чего нет, структура, карты, преследователи, сеть, пакеты,
деплой, грабли, дорожная карта (телефон).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user