Структура проекта: папки по назначению, карты самодостаточны, инструменты, 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:
2026-10-27 17:47:47 +05:00
co-authored by Claude Sonnet 5.5
parent 5ba6466113
commit fc0139dc05
158 changed files with 724 additions and 565 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Выкладка выделенного сервера: файлы сети + project.godot, пересборка кэша классов, перезапуск службы.
# tools/deploy_server.sh root@192.168.10.110
# ВАЖНО: клиент и сервер обновляются вместе (RPC несовместимы между версиями) — сначала пуш grid-world,
# потом этот скрипт. Бэкап прошлой версии кладётся в /opt/backup-my-world-server-<дата>/.
set -euo pipefail
HOST="${1:?укажи user@host}"
cd "$(dirname "$0")/.."
STAMP="$(date +%Y%m%d-%H%M%S)"
ssh "$HOST" "mkdir -p /opt/backup-my-world-server-$STAMP && cp -a /opt/my-world-server/project.godot /opt/my-world-server/Net /opt/my-world-server/net /opt/backup-my-world-server-$STAMP/ 2>/dev/null || true"
ssh "$HOST" "mkdir -p /opt/my-world-server/net"
scp -q net/net.gd net/net.gd.uid net/ghost.gd net/ghost.gd.uid net/server.tscn "$HOST:/opt/my-world-server/net/"
scp -q deploy/server/project.godot "$HOST:/opt/my-world-server/project.godot"
# Старая раскладка серверной папки (Net/ с большой буквы) больше не нужна
ssh "$HOST" "rm -rf /opt/my-world-server/Net && cd /opt/my-world-server && ./godot --headless --import --path . >/dev/null 2>&1; systemctl restart my-world && sleep 2 && systemctl is-active my-world && journalctl -u my-world -n 3 --no-pager | cut -c1-120"
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Экспорт веб-клиента и пакетов карт. По умолчанию — в ../grid-world (репозиторий сайта).
# tools/export_all.sh [папка_вывода]
# Пакеты карт (backrooms.pck, world.pck) нужно выкладывать вместе с index.* — клиент качает их при входе в портал.
set -euo pipefail
cd "$(dirname "$0")/.."
OUT="${1:-../grid-world}"
mkdir -p "$OUT"
# Редактор кэширует зависимости сцен; у недавно изменённых сцен кэш может устареть, и в пакет
# не попадут их модели и звуки. touch заставляет пересчитать.
find maps chasers -name '*.tscn' -exec touch {} +
godot --headless --export-release Web "$OUT/index.html"
godot --headless --export-pack Backrooms "$OUT/backrooms.pck"
godot --headless --export-pack World "$OUT/world.pck"
ls -la "$OUT"/*.pck "$OUT"/index.wasm | awk '{printf "%7.2f МБ %s\n", $5/1e6, $9}'
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Локальный веб-сервер для проверки сборки, повторяет боевой nginx:
заголовки COOP/COEP (нужны Godot web) и gzip без Content-Length (чанками).
Так я поймал баг: браузер сам распаковывает gzip, и HTTPRequest в движке не должен делать это второй раз.
python3 tools/serve_web.py ../grid-world 8080 # открой http://127.0.0.1:8080
"""
import gzip, http.server, os, sys
ROOT = sys.argv[1] if len(sys.argv) > 1 else '../grid-world'
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8080
CACHE = {}
TYPES = {'.wasm': 'application/wasm', '.js': 'application/javascript', '.html': 'text/html',
'.pck': 'application/octet-stream', '.png': 'image/png'}
class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def do_GET(self):
path = os.path.join(ROOT, self.path.split('?')[0].lstrip('/') or 'index.html')
if not os.path.isfile(path):
self.send_response(404); self.send_header('Content-Length', '0'); self.end_headers(); return
data = open(path, 'rb').read()
self.send_response(200)
self.send_header('Content-Type', TYPES.get(os.path.splitext(path)[1], 'application/octet-stream'))
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
if len(data) > 1024:
key = (path, os.path.getmtime(path))
gz = CACHE.get(key) or CACHE.setdefault(key, gzip.compress(data, 1))
self.send_header('Content-Encoding', 'gzip')
self.send_header('Transfer-Encoding', 'chunked')
self.end_headers()
for i in range(0, len(gz), 65536):
chunk = gz[i:i + 65536]
self.wfile.write(b'%x\r\n' % len(chunk) + chunk + b'\r\n')
self.wfile.write(b'0\r\n\r\n')
else:
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
print('GET', self.path, flush=True)
def log_message(self, *args):
pass
print(f'http://127.0.0.1:{PORT} ← {os.path.abspath(ROOT)}')
http.server.ThreadingHTTPServer(('127.0.0.1', PORT), Handler).serve_forever()
+43
View File
@@ -0,0 +1,43 @@
extends Node
# Тестовый бот (автозагрузка во ВРЕМЕННОЙ копии проекта, см. tools/test/make_test_project.sh).
# Пережидает смену сцен, по расписанию телепортирует игрока и раз в секунду печатает, что видит.
# godot --headless --path <копия> res://maps/hub/hub.tscn -- --client --name=A --tp=2:-2.25,1,-7.5 --tp=9:0,1,5
# --tp=<секунда>:<x>,<y>,<z> — телепорт игрока на этой секунде (например, прямо в портал).
# Остальные аргументы пользователя (--server-url=..., --pack-url=...) читают Net и MapLoader.
var t := 0.0
var label := "?"
var tps := []
var next_print := 1.0
func _ready() -> void:
for a in OS.get_cmdline_user_args():
if a.begins_with("--name="):
label = a.substr(7)
if a.begins_with("--tp="):
var p := a.substr(5).split(":")
var v := p[1].split(",")
tps.append({"t": float(p[0]), "pos": Vector3(float(v[0]), float(v[1]), float(v[2])), "done": false})
func _process(delta: float) -> void:
t += delta
var scene := get_tree().current_scene
var player := get_tree().get_first_node_in_group("local_player")
if scene == null or player == null:
return
if not get_tree().paused: # на паузе идёт загрузка карты
for tp in tps:
if not tp.done and t >= tp.t:
tp.done = true
player.global_position = tp.pos
player.velocity = Vector3.ZERO
print("[%s] t=%.1f ТЕЛЕПОРТ %s" % [label, t, tp.pos])
if t >= next_print:
next_print += 1.0
var chasers := []
for id in Net.chasers:
var c: Node3D = Net.chasers[id]
chasers.append("%s=(%.0f,%.0f)%s" % [id, c.global_position.x, c.global_position.z, "!" if c.get("_state") == 1 else ""])
print("[%s] t=%4.1f сцена=%s карта=%s владелец=%s свой=%s призраков=%d мёртв=%s %s" % [
label, t, scene.name, Net.current_map, Net.rock_owner, Net.rock_is_local(),
Net.ghosts.size(), player.is_dead, " ".join(chasers)])
+1
View File
@@ -0,0 +1 @@
uid://do6iy031p0j3m
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Делает ВРЕМЕННУЮ копию проекта с ботом-автозагрузкой для тестов без окна.
# tools/test/make_test_project.sh /tmp/test-world
# Дальше: godot --headless --path /tmp/test-world res://maps/hub/hub.tscn -- --client --name=A
# (сервер для сетевых тестов — отдельный headless-запуск папки из tools/test/make_test_server.sh)
set -euo pipefail
SRC="$(cd "$(dirname "$0")/../.." && pwd)"
DST="${1:?укажи папку}"
mkdir -p "$DST"
rsync -a --exclude .git --exclude deploy "$SRC/" "$DST/"
sed -i 's#^MapLoader="\*res://core/map_loader.gd"#MapLoader="*res://core/map_loader.gd"\nBot="*res://tools/test/bot.gd"#' "$DST/project.godot"
# В боевом экспорте tools/* исключён; в тестовой копии бот должен попасть в пакет
sed -i 's#exclude_filter="tools/\*, #exclude_filter="#' "$DST/export_presets.cfg"
echo "готово: $DST"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Локальный выделенный сервер для сетевых тестов (та же структура, что на боевом сервере).
# tools/test/make_test_server.sh /tmp/test-server && godot --headless --path /tmp/test-server
set -euo pipefail
SRC="$(cd "$(dirname "$0")/../.." && pwd)"
DST="${1:?укажи папку}"
mkdir -p "$DST/net"
cp "$SRC/deploy/server/project.godot" "$DST/project.godot"
cp "$SRC/net/net.gd" "$SRC/net/net.gd.uid" "$SRC/net/ghost.gd" "$SRC/net/ghost.gd.uid" "$SRC/net/server.tscn" "$DST/net/"
(cd "$DST" && godot --headless --import --path . >/dev/null 2>&1) # кэш классов (class_name Ghost)
echo "готово: $DST"