同步主站 Cocos 当前工程:关卡资源、运行脚本与打包工具。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
382059
tools/.import-backup-93431-94000/levels-database.json
Normal file
382059
tools/.import-backup-93431-94000/levels-database.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,8 @@ usage() {
|
||||
|
||||
与 CDN 使用同一打包逻辑(package-for-cdn),目录结构一致:
|
||||
Build/mstest5.loader.js
|
||||
StreamingAssets/aa/WebGL/*.bundle
|
||||
levels-database.json(.br)
|
||||
StreamingAssets/aa/WebGL/*.bundle.br
|
||||
levels-db-index.json(.br)
|
||||
|
||||
区别仅在于加载方式:本地从 /unity/ 读取,CDN 从 unitycdndir 读取。
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ echo ">>> [3/3] 生成 CDN 上传清单: $MANIFEST"
|
||||
echo "cdn: $CDN_BASE/StreamingAssets/$rel"
|
||||
echo ""
|
||||
done < <(find "$PACK_DIR/StreamingAssets" -type f ! -name '.DS_Store' -print0 | sort -z)
|
||||
for db in levels-database.json levels-database.json.br; do
|
||||
for db in levels-db-index.json levels-db-index.json.br levels-database.json levels-database.json.br; do
|
||||
if [[ -f "$PACK_DIR/$db" ]]; then
|
||||
echo "local: $db"
|
||||
echo "cdn: $CDN_BASE/$db"
|
||||
@@ -220,7 +220,7 @@ catalog_path, cdn = sys.argv[1], sys.argv[2].rstrip('/')
|
||||
d = json.load(open(catalog_path))
|
||||
names = sorted({x.split('/')[-1] for x in d.get('m_InternalIds', []) if '.bundle' in x})
|
||||
for n in names:
|
||||
print(f'curl -I "{cdn}/StreamingAssets/aa/WebGL/{n}"')
|
||||
print(f'curl -I "{cdn}/StreamingAssets/aa/WebGL/{n}.br"')
|
||||
PY
|
||||
} > "$MANIFEST"
|
||||
|
||||
|
||||
@@ -106,10 +106,9 @@ def merge_prefab_maps(L: dict, prefab_dir: Path) -> None:
|
||||
L["_mapSource"] = "boundary_ring"
|
||||
return
|
||||
maps = parse_level_prefab(p)
|
||||
if maps.get("ground"):
|
||||
L["ground"] = maps["ground"]
|
||||
if maps.get("border"):
|
||||
L["border"] = maps["border"]
|
||||
# 空边框也要写回,避免旧 JSON 里的 WallBlock 残留
|
||||
L["ground"] = maps.get("ground") or {}
|
||||
L["border"] = maps.get("border") or {}
|
||||
L["_mapSource"] = "unity_prefab"
|
||||
|
||||
|
||||
@@ -123,10 +122,8 @@ def strip_internal(L: dict) -> dict:
|
||||
out["unityPrefab"] = L["unityPrefab"]
|
||||
if L.get("cocosPrefab"):
|
||||
out["cocosPrefab"] = L["cocosPrefab"]
|
||||
if L.get("ground"):
|
||||
out["ground"] = L["ground"]
|
||||
if L.get("border"):
|
||||
out["border"] = L["border"]
|
||||
out["ground"] = L.get("ground") or {}
|
||||
out["border"] = L.get("border") or {}
|
||||
if L.get("theme"):
|
||||
out["theme"] = L["theme"]
|
||||
return out
|
||||
|
||||
@@ -65,18 +65,12 @@ def prop_placement_from_path(path: str) -> str | None:
|
||||
|
||||
def infer_theme_from_level(level_id: int, spawn_paths: list[str]) -> str:
|
||||
"""按主站关卡批次分配主题;道具路径可覆盖(snow/sanxing)。"""
|
||||
if 91601 <= level_id <= 91900:
|
||||
if 91601 <= level_id <= 92200:
|
||||
return "silu"
|
||||
if 91901 <= level_id <= 92200:
|
||||
return "redarmy"
|
||||
if 92201 <= level_id <= 92500:
|
||||
return "numMan"
|
||||
if 92501 <= level_id <= 92800:
|
||||
if 92201 <= level_id <= 92800:
|
||||
return "chinese"
|
||||
if 92801 <= level_id <= 93100:
|
||||
return "snow"
|
||||
if 93101 <= level_id <= 93700:
|
||||
return "sanxing"
|
||||
if 92801 <= level_id <= 94000:
|
||||
return "numMan"
|
||||
joined = " ".join(spawn_paths).lower()
|
||||
if "prop_sanxing" in joined or "nprop_sanxing" in joined:
|
||||
return "sanxing"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 Unity Level{N}.prefab 解析 Tilemap(Ground / Border)格子数据。
|
||||
|
||||
层级锁定:
|
||||
- Baseblock / JumpBlock → ground(地砖属性)
|
||||
- WallBlock / 墙饰 → border(墙砖属性)
|
||||
不再用死板的 tileIndex==1 → JumpBlock。
|
||||
贴图以 Tilemap 的 m_TileSpriteArray 为准(和 Unity 编辑器里看到的砖一致),
|
||||
不按关卡主题改名、不换层、不把 JumpBlock 收成 Baseblock。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -24,22 +22,45 @@ BORDER_DECOR = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
UNITY_FOLDER_TO_COCOS = {
|
||||
"Chinese": "chinese",
|
||||
"chinese": "chinese",
|
||||
"RedArmy": "redArmy",
|
||||
"redArmy": "redArmy",
|
||||
"redarmy": "redArmy",
|
||||
"numMan": "numMan",
|
||||
"sanxing": "sanxing",
|
||||
"snow": "snow",
|
||||
"silu": "silu",
|
||||
"SILU": "silu",
|
||||
}
|
||||
|
||||
|
||||
def tile_basename(name: str | None) -> str:
|
||||
if not name:
|
||||
return ""
|
||||
return name.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def parse_tilemap_layer(text: str, layer_name: str) -> dict[str, int]:
|
||||
"""返回格子 → 该 Tilemap 本地 m_TileIndex。"""
|
||||
parts = re.split(r"(?=--- !u!1 &)", text)
|
||||
for part in parts:
|
||||
if f"m_Name: {layer_name}" not in part or "Tilemap:" not in part:
|
||||
continue
|
||||
tiles: dict[str, int] = {}
|
||||
for m in re.finditer(
|
||||
r"first: \{x: (-?\d+), y: (-?\d+), z: 0\}[\s\S]*?m_TileIndex: (\d+)",
|
||||
part,
|
||||
):
|
||||
x, y, ti = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||
tiles[f"{x},{y}"] = ti
|
||||
return tiles
|
||||
return {}
|
||||
cells = parse_tilemap_cells(text, layer_name)
|
||||
return {k: v[0] for k, v in cells.items()}
|
||||
|
||||
|
||||
def parse_tilemap_cells(text: str, layer_name: str) -> dict[str, tuple[int, int]]:
|
||||
"""格子 → (m_TileIndex, m_TileSpriteIndex)。"""
|
||||
part = _tilemap_part(text, layer_name)
|
||||
if not part:
|
||||
return {}
|
||||
tiles: dict[str, tuple[int, int]] = {}
|
||||
for m in re.finditer(
|
||||
r"first: \{x: (-?\d+), y: (-?\d+), z: 0\}[\s\S]*?m_TileIndex: (\d+)[\s\S]*?m_TileSpriteIndex: (\d+)",
|
||||
part,
|
||||
):
|
||||
x, y = int(m.group(1)), int(m.group(2))
|
||||
tiles[f"{x},{y}"] = (int(m.group(3)), int(m.group(4)))
|
||||
return tiles
|
||||
|
||||
|
||||
def _tilemap_part(text: str, layer_name: str) -> str | None:
|
||||
@@ -51,15 +72,14 @@ def _tilemap_part(text: str, layer_name: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _tile_asset_guids(tilemap_part: str) -> list[str | None]:
|
||||
"""按 m_TileAssetArray 顺序解析 guid(空槽为 None)。"""
|
||||
def _guid_array(tilemap_part: str, header: str, stop_tokens: tuple[str, ...]) -> list[str | None]:
|
||||
assets: list[str | None] = []
|
||||
in_arr = False
|
||||
for line in tilemap_part.splitlines():
|
||||
if "m_TileAssetArray:" in line:
|
||||
if header in line:
|
||||
in_arr = True
|
||||
continue
|
||||
if in_arr and ("m_TileSpriteArray" in line or "m_AnimationFrameRate" in line):
|
||||
if in_arr and any(tok in line for tok in stop_tokens):
|
||||
break
|
||||
if not in_arr:
|
||||
continue
|
||||
@@ -71,20 +91,57 @@ def _tile_asset_guids(tilemap_part: str) -> list[str | None]:
|
||||
return assets
|
||||
|
||||
|
||||
def _tile_asset_guids(tilemap_part: str) -> list[str | None]:
|
||||
"""按 m_TileAssetArray 顺序解析 guid(空槽为 None)。"""
|
||||
return _guid_array(
|
||||
tilemap_part,
|
||||
"m_TileAssetArray:",
|
||||
("m_TileSpriteArray", "m_AnimationFrameRate"),
|
||||
)
|
||||
|
||||
|
||||
def _tile_sprite_guids(tilemap_part: str) -> list[str | None]:
|
||||
return _guid_array(
|
||||
tilemap_part,
|
||||
"m_TileSpriteArray:",
|
||||
("m_TileMatrixArray", "m_TileColorArray", "m_AnimationFrameRate"),
|
||||
)
|
||||
|
||||
|
||||
def _cocos_folder(unity_folder: str) -> str:
|
||||
if unity_folder in UNITY_FOLDER_TO_COCOS:
|
||||
return UNITY_FOLDER_TO_COCOS[unity_folder]
|
||||
return unity_folder[:1].lower() + unity_folder[1:] if unity_folder else ""
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _guid_to_tile_name(unity_texture_root: str) -> dict[str, str]:
|
||||
"""guid → Cocos 贴图引用,如 numMan/Baseblock。"""
|
||||
root = Path(unity_texture_root)
|
||||
out: dict[str, str] = {}
|
||||
if not root.is_dir():
|
||||
return out
|
||||
for meta in root.rglob("*.asset.meta"):
|
||||
for meta in root.rglob("*.meta"):
|
||||
name = meta.name
|
||||
if not (name.endswith(".png.meta") or name.endswith(".asset.meta")):
|
||||
continue
|
||||
try:
|
||||
text = meta.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
m = re.search(r"^guid: ([a-f0-9]+)", text, re.M)
|
||||
if m:
|
||||
out[m.group(1)] = meta.name.replace(".asset.meta", "")
|
||||
if not m:
|
||||
continue
|
||||
stem = name.replace(".png.meta", "").replace(".asset.meta", "")
|
||||
try:
|
||||
rel = meta.relative_to(root)
|
||||
except ValueError:
|
||||
out[m.group(1)] = stem
|
||||
continue
|
||||
if len(rel.parts) >= 2:
|
||||
out[m.group(1)] = f"{_cocos_folder(rel.parts[0])}/{stem}"
|
||||
else:
|
||||
out[m.group(1)] = stem
|
||||
return out
|
||||
|
||||
|
||||
@@ -94,22 +151,32 @@ def tile_asset_names(text: str, layer_name: str, unity_texture_root: Path | None
|
||||
return []
|
||||
guids = _tile_asset_guids(part)
|
||||
if unity_texture_root is None:
|
||||
# 默认:prefab 所在工程 Assets/Texture
|
||||
return [None] * len(guids)
|
||||
mapping = _guid_to_tile_name(str(unity_texture_root))
|
||||
return [mapping.get(g) if g else None for g in guids]
|
||||
|
||||
|
||||
def tile_sprite_names(text: str, layer_name: str, unity_texture_root: Path | None = None) -> list[str | None]:
|
||||
part = _tilemap_part(text, layer_name)
|
||||
if not part:
|
||||
return []
|
||||
guids = _tile_sprite_guids(part)
|
||||
if unity_texture_root is None:
|
||||
return [None] * len(guids)
|
||||
mapping = _guid_to_tile_name(str(unity_texture_root))
|
||||
return [mapping.get(g) if g else None for g in guids]
|
||||
|
||||
|
||||
def normalize_ground_tile(name: str | None) -> str:
|
||||
return "JumpBlock" if name == "JumpBlock" else "Baseblock"
|
||||
return "JumpBlock" if tile_basename(name) == "JumpBlock" else "Baseblock"
|
||||
|
||||
|
||||
def normalize_border_tile(name: str | None) -> str:
|
||||
if not name or name in GROUND_TILES:
|
||||
base = tile_basename(name)
|
||||
if not name or base in GROUND_TILES:
|
||||
return "WallBlock"
|
||||
if name in BORDER_DECOR:
|
||||
if base in BORDER_DECOR:
|
||||
return name
|
||||
# 其它墙饰保留文件名,禁止地砖名漏到墙层
|
||||
return name
|
||||
|
||||
|
||||
@@ -120,16 +187,16 @@ def _resolve_name(assets: list[str | None], tile_index: int) -> str | None:
|
||||
|
||||
|
||||
def parse_level_prefab(prefab_path: Path, unity_texture_root: Path | None = None) -> dict:
|
||||
"""按 Unity 原层导入:只保留有 Tile Asset 的格子,空槽不补砖。"""
|
||||
if not prefab_path.is_file():
|
||||
return {}
|
||||
text = prefab_path.read_text(encoding="utf-8")
|
||||
g_raw = parse_tilemap_layer(text, "Ground")
|
||||
b_raw = parse_tilemap_layer(text, "Border")
|
||||
if not g_raw and not b_raw:
|
||||
g_cells = parse_tilemap_cells(text, "Ground")
|
||||
b_cells = parse_tilemap_cells(text, "Border")
|
||||
if not g_cells and not b_cells:
|
||||
return {}
|
||||
|
||||
if unity_texture_root is None:
|
||||
# Level91601.prefab → …/Assets/Prefabs/Level → …/Assets/Texture
|
||||
unity_texture_root = prefab_path.resolve().parents[2] / "Texture"
|
||||
|
||||
g_assets = tile_asset_names(text, "Ground", unity_texture_root)
|
||||
@@ -138,24 +205,21 @@ def parse_level_prefab(prefab_path: Path, unity_texture_root: Path | None = None
|
||||
ground: dict[str, str] = {}
|
||||
border: dict[str, str] = {}
|
||||
|
||||
# 按瓦片类型归层(层级固定):地砖→Ground,墙/饰→Border
|
||||
for key, ti in g_raw.items():
|
||||
name = _resolve_name(g_assets, ti)
|
||||
if name in GROUND_TILES:
|
||||
ground[key] = normalize_ground_tile(name)
|
||||
else:
|
||||
border[key] = normalize_border_tile(name)
|
||||
for key, (ti, _si) in g_cells.items():
|
||||
asset = _resolve_name(g_assets, ti)
|
||||
if not asset:
|
||||
continue
|
||||
base = tile_basename(asset)
|
||||
ground[key] = "JumpBlock" if base == "JumpBlock" else base if base in GROUND_TILES else "Baseblock"
|
||||
|
||||
for key, ti in b_raw.items():
|
||||
name = _resolve_name(b_assets, ti)
|
||||
if name in GROUND_TILES:
|
||||
ground[key] = normalize_ground_tile(name)
|
||||
else:
|
||||
border[key] = normalize_border_tile(name)
|
||||
|
||||
# 同格冲突:墙优先(不可走)
|
||||
for key in list(ground.keys()):
|
||||
if key in border:
|
||||
del ground[key]
|
||||
for key, (ti, _si) in b_cells.items():
|
||||
asset = _resolve_name(b_assets, ti)
|
||||
if not asset:
|
||||
# Unity 空槽不渲染,禁止补 WallBlock(否则空隙里会多出砖)
|
||||
continue
|
||||
base = tile_basename(asset)
|
||||
if base in GROUND_TILES:
|
||||
continue
|
||||
border[key] = base if base else "WallBlock"
|
||||
|
||||
return {"ground": ground, "border": border}
|
||||
|
||||
@@ -238,6 +238,22 @@ HUD_GLOBS = (
|
||||
)
|
||||
|
||||
|
||||
def upscale_hud_png(path: Path, scale: int = 4) -> None:
|
||||
"""把低分辨率 HUD 圆钮放大,避免 100px 原图在 HUD/预览里发糊。"""
|
||||
try:
|
||||
from PIL import Image, ImageFilter, ImageEnhance
|
||||
except ImportError:
|
||||
return
|
||||
im = Image.open(path).convert("RGBA")
|
||||
w, h = im.size
|
||||
if w >= 300 and h >= 300:
|
||||
return
|
||||
hd = im.resize((w * scale, h * scale), Image.Resampling.LANCZOS)
|
||||
hd = hd.filter(ImageFilter.UnsharpMask(radius=1.8, percent=140, threshold=1))
|
||||
hd = ImageEnhance.Contrast(hd).enhance(1.04)
|
||||
hd.save(path, format="PNG", optimize=True)
|
||||
|
||||
|
||||
def copy_hud_assets(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int:
|
||||
"""拷贝 Unity UI 按钮贴图(原先 rglob 会跳过 anniu_/倍速/声音)"""
|
||||
src = unity_tex if rel == "." else unity_tex / rel
|
||||
@@ -257,6 +273,8 @@ def copy_hud_assets(unity_tex: Path, out_root: Path, style_key: str, rel: str) -
|
||||
seen.add(key)
|
||||
target = dst / png.name
|
||||
shutil.copy2(png, target)
|
||||
if style_key == "numMan" and png.name in ("anniu_03.png", "anniu_17.png", "anniu_19.png"):
|
||||
upscale_hud_png(target)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@@ -303,6 +321,18 @@ def copy_theme(unity_tex: Path, out_root: Path, style_key: str, rel: str) -> int
|
||||
return count
|
||||
|
||||
|
||||
def copy_direction_gizmo(unity_tex: Path, out_root: Path) -> int:
|
||||
"""生成灰色等距罗盘(不直接拷 Unity 像素图)。"""
|
||||
import sys
|
||||
tools_dir = Path(__file__).resolve().parent
|
||||
if str(tools_dir) not in sys.path:
|
||||
sys.path.insert(0, str(tools_dir))
|
||||
from make_direction_gizmo import write as write_direction_gizmo
|
||||
dst = out_root / "ui" / "ui-direction.png"
|
||||
write_direction_gizmo(dst)
|
||||
return 1
|
||||
|
||||
|
||||
def copy_prop(unity_tex: Path, out_root: Path) -> int:
|
||||
src = unity_tex / "Prop"
|
||||
if not src.is_dir():
|
||||
@@ -347,6 +377,11 @@ def main():
|
||||
print(f" prop: {pn} png")
|
||||
total += pn
|
||||
|
||||
dn = copy_direction_gizmo(unity_tex, out_root)
|
||||
if dn:
|
||||
print(f" ui-direction: {dn} png")
|
||||
total += dn
|
||||
|
||||
print(f"Imported {total} files -> {out_root}")
|
||||
print("请在 Cocos Creator 中刷新 assets/resources/textures,然后运行:")
|
||||
print(" python3 tools/fix_tile_texture_metas.py")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
138
tools/make_direction_gizmo.py
Normal file
138
tools/make_direction_gizmo.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成关卡左下角等距 X/Y 罗盘(灰箭头 + 青色标注,抗锯齿)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
# HUD 显示约 1.55× Unity,2x 出图保证清晰
|
||||
OUT_W, OUT_H = 802, 430
|
||||
SS = 4
|
||||
|
||||
ARROW = (92, 98, 108, 255)
|
||||
ARROW_EDGE = (58, 62, 70, 255)
|
||||
TEAL = (64, 220, 188, 255)
|
||||
LABEL_STROKE = (32, 36, 40, 230)
|
||||
SHADOW = (0, 0, 0, 48)
|
||||
|
||||
|
||||
def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
for fp in (
|
||||
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/Library/Fonts/Arial Bold.ttf",
|
||||
):
|
||||
try:
|
||||
return ImageFont.truetype(fp, size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def _arrow(draw: ImageDraw.ImageDraw, ox: float, oy: float, angle: float, length: float) -> tuple[float, float]:
|
||||
"""沿 angle(屏幕 Y 向上)画一支圆润箭头,返回箭头顶点。"""
|
||||
dx, dy = math.cos(angle), -math.sin(angle)
|
||||
px, py = -dy, dx
|
||||
shaft = 12.5 * SS
|
||||
head_len = 26 * SS
|
||||
head_w = 20 * SS
|
||||
tip_x = ox + dx * length
|
||||
tip_y = oy + dy * length
|
||||
base_x = ox + dx * (length - head_len)
|
||||
base_y = oy + dy * (length - head_len)
|
||||
|
||||
# 箭身:圆头线段
|
||||
draw.line([(ox, oy), (base_x, base_y)], fill=ARROW_EDGE, width=int(shaft + 2.4 * SS), joint="curve")
|
||||
draw.line([(ox, oy), (base_x, base_y)], fill=ARROW, width=int(shaft), joint="curve")
|
||||
|
||||
hx, hy = px * head_w, py * head_w
|
||||
head = [
|
||||
(tip_x, tip_y),
|
||||
(base_x + hx, base_y + hy),
|
||||
(base_x - hx, base_y - hy),
|
||||
]
|
||||
draw.polygon(head, fill=ARROW_EDGE)
|
||||
inset = 1.15 * SS
|
||||
inner = [
|
||||
(tip_x - dx * inset * 0.4, tip_y - dy * inset * 0.4),
|
||||
(base_x + hx * 0.78 + dx * inset, base_y + hy * 0.78 + dy * inset),
|
||||
(base_x - hx * 0.78 + dx * inset, base_y - hy * 0.78 + dy * inset),
|
||||
]
|
||||
draw.polygon(inner, fill=ARROW)
|
||||
return tip_x, tip_y
|
||||
|
||||
|
||||
def render() -> Image.Image:
|
||||
w, h = OUT_W * SS, OUT_H * SS
|
||||
layer = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(layer)
|
||||
|
||||
ox = w * 0.50
|
||||
oy = h * 0.84
|
||||
length = min(w, h) * 0.46
|
||||
ang_x = math.atan2(0.5, 1.0) # 2:1 等距 +X → 右上
|
||||
ang_y = math.atan2(0.5, -1.0) # +Y → 左上
|
||||
|
||||
tx, ty = _arrow(draw, ox, oy, ang_x, length)
|
||||
uy, vy = _arrow(draw, ox, oy, ang_y, length)
|
||||
|
||||
r = 11 * SS
|
||||
draw.ellipse((ox - r - SS, oy - r - SS, ox + r + SS, oy + r + SS), fill=ARROW_EDGE)
|
||||
draw.ellipse((ox - r, oy - r, ox + r, oy + r), fill=ARROW)
|
||||
ir = 5.2 * SS
|
||||
draw.ellipse((ox - ir, oy - ir, ox + ir, oy + ir), fill=TEAL)
|
||||
|
||||
font = _font(int(60 * SS))
|
||||
labels = (("Y", uy, vy, ang_y), ("X", tx, ty, ang_x))
|
||||
stroke = max(2, int(3.6 * SS))
|
||||
for text, px, py, ang in labels:
|
||||
dx, dy = math.cos(ang), -math.sin(ang)
|
||||
lx = px + dx * 30 * SS
|
||||
ly = py + dy * 30 * SS
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
pos = (lx - tw / 2, ly - th / 2 - 6 * SS)
|
||||
for oxs in range(-stroke, stroke + 1):
|
||||
for oys in range(-stroke, stroke + 1):
|
||||
if oxs * oxs + oys * oys > stroke * stroke:
|
||||
continue
|
||||
draw.text((pos[0] + oxs, pos[1] + oys), text, font=font, fill=LABEL_STROKE)
|
||||
draw.text(pos, text, font=font, fill=TEAL)
|
||||
|
||||
alpha = layer.split()[3]
|
||||
sh = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
sh_draw = ImageDraw.Draw(sh)
|
||||
# 用同一 alpha 做淡阴影
|
||||
sh.putalpha(alpha.point(lambda a: min(255, int(a * 0.28)) if a else 0))
|
||||
rgb = Image.new("RGB", (w, h), (0, 0, 0))
|
||||
sh = Image.merge("RGBA", (*rgb.split(), sh.split()[-1]))
|
||||
sh = sh.filter(ImageFilter.GaussianBlur(radius=1.6 * SS))
|
||||
|
||||
composed = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
composed.alpha_composite(sh, (int(1.2 * SS), int(1.4 * SS)))
|
||||
composed.alpha_composite(layer)
|
||||
out = composed.resize((OUT_W, OUT_H), Image.Resampling.LANCZOS)
|
||||
alpha = out.split()[3].point(lambda p: 255 if p >= 16 else 0)
|
||||
box = alpha.getbbox()
|
||||
if box:
|
||||
pad = 16
|
||||
x0 = max(0, box[0] - pad)
|
||||
y0 = max(0, box[1] - pad)
|
||||
x1 = min(out.width, box[2] + pad)
|
||||
y1 = min(out.height, box[3] + pad)
|
||||
out = out.crop((x0, y0, x1, y1))
|
||||
return out
|
||||
|
||||
|
||||
def write(path: Path) -> Path:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
render().save(path, format="PNG", optimize=True)
|
||||
return path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = Path(__file__).resolve().parents[1] / "assets/resources/textures/ui/ui-direction.png"
|
||||
write(out)
|
||||
print(f"wrote {out}")
|
||||
@@ -17,6 +17,8 @@ const {
|
||||
minifyLevelsDatabase,
|
||||
brotliCompressFile,
|
||||
brotliCompressWebglBundles,
|
||||
writeSlimCatalog,
|
||||
stripPlainIfBrotli,
|
||||
formatBytes,
|
||||
} = require('./package-optimize');
|
||||
const { listRuntimeFiles, assertRuntimePack } = require('./runtime-pack');
|
||||
@@ -88,11 +90,16 @@ function attachLevelsDatabase(outDir) {
|
||||
}
|
||||
splitLevelsDatabase(levelsDbSrc, outDir);
|
||||
|
||||
const levelsDbDst = path.join(outDir, 'levels-database.json');
|
||||
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
|
||||
console.log(`>>> levels-database.json (legacy 回退): ${formatBytes(before)} → ${formatBytes(after)}`);
|
||||
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
|
||||
console.log(`>>> levels-database.json.br: ${formatBytes(raw)} → ${formatBytes(br)}`);
|
||||
// 运行时只走分片;全量库仅 KEEP_LEGACY_DB=1 保留(约 6 MB)
|
||||
if (process.env.KEEP_LEGACY_DB === '1') {
|
||||
const levelsDbDst = path.join(outDir, 'levels-database.json');
|
||||
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
|
||||
console.log(`>>> levels-database.json (legacy 回退): ${formatBytes(before)} → ${formatBytes(after)}`);
|
||||
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
|
||||
console.log(`>>> levels-database.json.br: ${formatBytes(raw)} → ${formatBytes(br)}`);
|
||||
} else {
|
||||
console.log('>>> 跳过全量 levels-database.json(运行时用分片;KEEP_LEGACY_DB=1 可保留)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!buildDir || !outDir) {
|
||||
@@ -282,6 +289,11 @@ copyDir(path.join(unityRef, 'StreamingAssets/aa/AddressablesLink'), path.join(aa
|
||||
|
||||
const BUNDLE = parseCatalogBundles(path.join(aaDir, 'catalog.json'));
|
||||
console.log('>>> catalog bundles:', BUNDLE.all.join(', '));
|
||||
const slimBytes = writeSlimCatalog(
|
||||
path.join(aaDir, 'catalog.json'),
|
||||
[BUNDLE.shaders, BUNDLE.assetsAll, BUNDLE.scenesAll].filter(Boolean),
|
||||
);
|
||||
console.log(`>>> catalog.json 已精简为 ${formatBytes(slimBytes)}(仅 bundle 名)`);
|
||||
if (BUNDLE.shaders) {
|
||||
copyFile(
|
||||
path.join(unityRef, 'StreamingAssets/aa/WebGL', BUNDLE.shaders),
|
||||
@@ -361,7 +373,7 @@ console.log(`>>> assets_core (assets_all): ${formatBytes(fs.statSync(assetsCoreZ
|
||||
const assetsBr = brotliCompressFile(assetsCoreZip, assetsCoreZip + '.br');
|
||||
console.log(`>>> ${BUNDLE.assetsAll}.br: ${formatBytes(assetsBr.raw)} → ${formatBytes(assetsBr.br)}`);
|
||||
|
||||
// —— 3b. 每关独立 bundle + levels-manifest.json ——
|
||||
// —— 3b. 每 15 关一包 + levels-manifest.json ——
|
||||
const levelPrefabsSrc = path.join(buildDir, 'assets', 'level-prefabs');
|
||||
let levelPackStats = null;
|
||||
if (!MERGE_LEVELS) {
|
||||
@@ -375,10 +387,26 @@ if (!MERGE_LEVELS) {
|
||||
levelPackStats = splitLevelBundles(levelPrefabsSrc, webglDir, manifestPath, tmp);
|
||||
const brStats = brotliCompressWebglBundles(webglDir);
|
||||
console.log(`>>> bundle Brotli: ${brStats.count} 个, 节省 ${formatBytes(brStats.saved)}`);
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
const manBr = brotliCompressFile(manifestPath, manifestPath + '.br');
|
||||
console.log(`>>> levels-manifest.json.br: ${formatBytes(manBr.raw)} → ${formatBytes(manBr.br)}`);
|
||||
}
|
||||
}
|
||||
|
||||
attachLevelsDatabase(outDir);
|
||||
|
||||
const catalogPath = path.join(aaDir, 'catalog.json');
|
||||
if (fs.existsSync(catalogPath)) {
|
||||
brotliCompressFile(catalogPath, catalogPath + '.br');
|
||||
}
|
||||
|
||||
const stripped = stripPlainIfBrotli(outDir);
|
||||
if (stripped.skipped) {
|
||||
console.log('>>> KEEP_PLAIN=1: 保留未压缩 .bundle / .json');
|
||||
} else {
|
||||
console.log(`>>> 去掉已有 .br 的原文: ${stripped.removed} 个, 节省 ${formatBytes(stripped.saved)}`);
|
||||
}
|
||||
|
||||
// —— 4. Build/ 仅 4 个文件(与 Unity 同名)——
|
||||
const buildOut = path.join(outDir, 'Build');
|
||||
mkdirp(buildOut);
|
||||
@@ -434,7 +462,10 @@ const requiredBundles = [
|
||||
];
|
||||
if (BUNDLE.shaders) requiredBundles.push(path.join(webglDir, BUNDLE.shaders));
|
||||
for (const p of requiredBundles) {
|
||||
if (!fs.existsSync(p) || fs.statSync(p).size < 100) {
|
||||
const br = `${p}.br`;
|
||||
const ok = (fs.existsSync(p) && fs.statSync(p).size >= 100)
|
||||
|| (fs.existsSync(br) && fs.statSync(br).size >= 50);
|
||||
if (!ok) {
|
||||
console.error('>>> bundle 无效:', p);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -442,6 +473,6 @@ for (const p of requiredBundles) {
|
||||
|
||||
const preloadNote = MERGE_LEVELS
|
||||
? 'scenes ∥ assets_all 合并包 (MERGE_LEVELS=1)'
|
||||
: `scenes ∥ assets_core 首屏(引擎启动必需);关卡库分片 + 关卡 bundle 进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关)`;
|
||||
: `scenes ∥ assets_core 首屏(引擎启动必需);关卡库分片 + 关卡 bundle 进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关 / ${levelPackStats ? levelPackStats.packs : '?'} 包)`;
|
||||
console.log('\n完成。运行时包 → 本地 import-to-unity.sh / OSS unitycdndir(同一目录)');
|
||||
printPackageReport(outDir, { preloadNote });
|
||||
|
||||
@@ -32,10 +32,10 @@ usage() {
|
||||
echo " --skip-manifest 不生成 deploy/ 清单" >&2
|
||||
echo " --zip 额外生成 build/mstest5-runtime.zip" >&2
|
||||
echo "" >&2
|
||||
echo "默认分包: scenes 首屏;assets_all / 关卡库分片 / 每关 bundle 进关按需" >&2
|
||||
echo "默认分包: scenes 首屏;assets_all / 关卡库分片 / 每 15 关一包进关按需" >&2
|
||||
echo " MERGE_LEVELS=1 合并 level-prefabs 进 assets_all(不推荐)" >&2
|
||||
echo "运行时包结构(本地 static/unity = OSS unitycdndir):" >&2
|
||||
echo " Build/ StreamingAssets/ levels-db-index.json(.br) levels-database.json(.br)" >&2
|
||||
echo " Build/ StreamingAssets/ levels-db-index.json(.br)" >&2
|
||||
echo " 首屏: scenes_all + assets_all;进关: 关卡库分片 + 关卡 bundle" >&2
|
||||
echo "" >&2
|
||||
echo "步骤 2: scratch-gui/static/unity/import-to-unity.sh" >&2
|
||||
|
||||
@@ -65,6 +65,60 @@ function brotliCompressWebglBundles(webglDir, opts = {}) {
|
||||
return { count, saved };
|
||||
}
|
||||
|
||||
/**
|
||||
* 写出 Cocos loader 只需的 catalog(3 个 bundle 名)。
|
||||
* Unity 原 catalog 含数千 prefab 路径,对 Cocos 无用且首屏白下 ~1 MB。
|
||||
*/
|
||||
function writeSlimCatalog(catalogPath, bundleNames) {
|
||||
const prefix = '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/WebGL/';
|
||||
const ids = [];
|
||||
for (const name of bundleNames) {
|
||||
if (name) ids.push(prefix + name);
|
||||
}
|
||||
const slim = {
|
||||
m_LocatorId: 'AddressablesMainContentCatalog',
|
||||
m_InternalIds: ids,
|
||||
};
|
||||
fs.writeFileSync(catalogPath, JSON.stringify(slim), 'utf8');
|
||||
return Buffer.byteLength(JSON.stringify(slim), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 已有 .br 则删掉未压缩原文。loader 优先请求 .br。
|
||||
* 保留 catalog / settings / levels-db-index 明文(很小,便于校验)。
|
||||
* KEEP_PLAIN=1 跳过。
|
||||
*/
|
||||
function stripPlainIfBrotli(root, opts = {}) {
|
||||
if (process.env.KEEP_PLAIN === '1') {
|
||||
return { removed: 0, saved: 0, skipped: true };
|
||||
}
|
||||
const keep = new Set(opts.keepNames || [
|
||||
'catalog.json',
|
||||
'settings.json',
|
||||
'levels-db-index.json',
|
||||
]);
|
||||
let removed = 0;
|
||||
let saved = 0;
|
||||
function walk(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, ent.name);
|
||||
if (ent.isDirectory()) {
|
||||
walk(p);
|
||||
continue;
|
||||
}
|
||||
if (ent.name.endsWith('.br') || keep.has(ent.name)) continue;
|
||||
const br = `${p}.br`;
|
||||
if (!fs.existsSync(br) || !fs.statSync(br).isFile()) continue;
|
||||
saved += fs.statSync(p).size;
|
||||
fs.unlinkSync(p);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
walk(root);
|
||||
return { removed, saved, skipped: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整预加载 bundle:默认只预加载 main,resources / level-prefabs 按需加载。
|
||||
* @param {object} opts
|
||||
@@ -142,6 +196,8 @@ function printPackageReport(outDir, opts = {}) {
|
||||
['assets/internal', path.join(outDir, 'assets', 'internal')],
|
||||
['levels-database.json', path.join(outDir, 'levels-database.json')],
|
||||
['levels-database.json.br', path.join(outDir, 'levels-database.json.br')],
|
||||
['levels-db-index.json', path.join(outDir, 'levels-db-index.json')],
|
||||
['StreamingAssets', path.join(outDir, 'StreamingAssets')],
|
||||
['Build', path.join(outDir, 'Build')],
|
||||
];
|
||||
let total = 0;
|
||||
@@ -165,6 +221,8 @@ module.exports = {
|
||||
minifyLevelsDatabase,
|
||||
brotliCompressFile,
|
||||
brotliCompressWebglBundles,
|
||||
writeSlimCatalog,
|
||||
stripPlainIfBrotli,
|
||||
patchPreloadSettings,
|
||||
patchSplashSettings,
|
||||
patchSplashInZipBundle,
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
* 运行时包(唯一真相):
|
||||
* Build/
|
||||
* StreamingAssets/
|
||||
* levels-database.json
|
||||
* levels-database.json.br
|
||||
* levels-db-index.json
|
||||
* levels-db-index.json.br
|
||||
* StreamingAssets/aa/levels-db/
|
||||
* StreamingAssets/aa/levels-db/ (默认仅 .json.br)
|
||||
* 可选: levels-database.json(.br) — KEEP_LEGACY_DB=1
|
||||
*
|
||||
* 不含 index.html / TemplateData(仅 standalone-player 独立调试页使用)
|
||||
*/
|
||||
@@ -49,6 +48,19 @@ function listRuntimeFiles(packDir) {
|
||||
return files.sort((a, b) => a.rel.localeCompare(b.rel));
|
||||
}
|
||||
|
||||
function existsPlainOrBr(filePath) {
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).size > 0) return true;
|
||||
const br = `${filePath}.br`;
|
||||
return fs.existsSync(br) && fs.statSync(br).size > 0;
|
||||
}
|
||||
|
||||
function readJsonPlainOrBr(filePath) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
throw new Error(`缺少 JSON: ${filePath}`);
|
||||
}
|
||||
|
||||
function bundleNamesFromCatalog(catalogPath) {
|
||||
if (!fs.existsSync(catalogPath)) return [];
|
||||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
@@ -67,32 +79,32 @@ function assertRuntimePack(packDir, opts) {
|
||||
if (!fs.existsSync(loader)) {
|
||||
throw new Error(`缺少运行时包: ${loader}`);
|
||||
}
|
||||
if (!fs.existsSync(catalog)) {
|
||||
throw new Error(`缺少运行时包: ${catalog}`);
|
||||
if (!existsPlainOrBr(catalog)) {
|
||||
throw new Error(`缺少运行时包: ${catalog}(.br)`);
|
||||
}
|
||||
if (!fs.existsSync(path.join(packDir, 'levels-db-index.json'))) {
|
||||
throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-db-index.json')}`);
|
||||
if (!existsPlainOrBr(path.join(packDir, 'levels-db-index.json'))) {
|
||||
throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-db-index.json')}(.br)`);
|
||||
}
|
||||
if (!fs.existsSync(path.join(packDir, 'levels-database.json'))) {
|
||||
throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-database.json')}`);
|
||||
}
|
||||
const names = bundleNamesFromCatalog(catalog);
|
||||
const names = fs.existsSync(catalog) ? bundleNamesFromCatalog(catalog) : [];
|
||||
if (opts.requireLevelsBundle && !names.some((n) => n.includes('levels_all'))) {
|
||||
throw new Error('catalog 缺少 levels_all 分包');
|
||||
}
|
||||
if (opts.requireLevelsManifest && !fs.existsSync(levelsManifest)) {
|
||||
throw new Error(`缺少运行时包: ${levelsManifest}`);
|
||||
if (opts.requireLevelsManifest && !existsPlainOrBr(levelsManifest)) {
|
||||
throw new Error(`缺少运行时包: ${levelsManifest}(.br)`);
|
||||
}
|
||||
if (opts.requireLevelsManifest) {
|
||||
const manifest = JSON.parse(fs.readFileSync(levelsManifest, 'utf8'));
|
||||
if (opts.requireLevelsManifest && fs.existsSync(levelsManifest)) {
|
||||
const manifest = readJsonPlainOrBr(levelsManifest);
|
||||
const count = Object.keys(manifest.levels || {}).length;
|
||||
if (count < 1) throw new Error('levels-manifest.json 无关卡条目');
|
||||
}
|
||||
for (const name of names) {
|
||||
const p = path.join(packDir, 'StreamingAssets/aa/WebGL', name);
|
||||
if (!fs.existsSync(p) || fs.statSync(p).size < 100) {
|
||||
throw new Error(`bundle 无效: ${p}`);
|
||||
if (!existsPlainOrBr(p)) {
|
||||
throw new Error(`bundle 无效: ${p}(.br)`);
|
||||
}
|
||||
const br = `${p}.br`;
|
||||
const sz = fs.existsSync(p) ? fs.statSync(p).size : fs.statSync(br).size;
|
||||
if (sz < 50) throw new Error(`bundle 过小: ${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +113,7 @@ module.exports = {
|
||||
RUNTIME_DIRS,
|
||||
walkFiles,
|
||||
listRuntimeFiles,
|
||||
existsPlainOrBr,
|
||||
bundleNamesFromCatalog,
|
||||
assertRuntimePack,
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
/**
|
||||
* 将 build/assets/level-prefabs 拆为:
|
||||
* - shell: config.json + index.js(进 assets_all 首屏)
|
||||
* - 每关一包: assets/level-prefabs/import/.../*.json
|
||||
* - 每 15 关一包: assets/level-prefabs/import/.../*.json
|
||||
*
|
||||
* 输出 levels-manifest.json 供 loader 按 levelId 按需下载。
|
||||
* 输出 levels-manifest.json 供 loader 按 levelId 按需下载(同包关卡共享 bundle)。
|
||||
* LEVELS_PER_PACK 可覆盖每包关卡数,默认 15。
|
||||
*/
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
@@ -12,6 +13,8 @@ const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { formatBytes } = require('./package-optimize');
|
||||
|
||||
const DEFAULT_LEVELS_PER_PACK = 15;
|
||||
|
||||
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
||||
|
||||
function hashFileMd5(filePath) {
|
||||
@@ -35,6 +38,11 @@ function walkJsonFiles(dir, out = []) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function levelsPerPack() {
|
||||
const n = parseInt(process.env.LEVELS_PER_PACK || String(DEFAULT_LEVELS_PER_PACK), 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : DEFAULT_LEVELS_PER_PACK;
|
||||
}
|
||||
|
||||
/** 扫描 import/*.json,从预制体名 Level{id} 建立 levelId → 相对路径 */
|
||||
function indexImportFiles(levelPrefabsDir) {
|
||||
const importRoot = path.join(levelPrefabsDir, 'import');
|
||||
@@ -80,52 +88,68 @@ function splitLevelBundles(levelPrefabsDir, webglDir, manifestOutPath, packTmpDi
|
||||
throw new Error(`缺少 level-prefabs/config.json: ${levelPrefabsDir}`);
|
||||
}
|
||||
|
||||
const packSize = levelsPerPack();
|
||||
const configLevels = readConfigLevels(levelPrefabsDir);
|
||||
const importByLevel = indexImportFiles(levelPrefabsDir);
|
||||
mkdirp(webglDir);
|
||||
const stageBase = packTmpDir || path.join(webglDir, '..', '.level-pack-tmp');
|
||||
mkdirp(stageBase);
|
||||
|
||||
const manifest = {
|
||||
version: 1,
|
||||
shell: ['assets/level-prefabs/config.json', 'assets/level-prefabs/index.js'],
|
||||
levels: {},
|
||||
};
|
||||
|
||||
let packed = 0;
|
||||
let totalBytes = 0;
|
||||
const ready = [];
|
||||
const missing = [];
|
||||
|
||||
for (const [levelId, meta] of configLevels) {
|
||||
const importRel = importByLevel.get(levelId);
|
||||
if (!importRel) {
|
||||
missing.push(levelId);
|
||||
continue;
|
||||
}
|
||||
const stageRoot = path.join(stageBase, levelId);
|
||||
const assetRoot = path.join(stageRoot, 'assets', 'level-prefabs');
|
||||
mkdirp(path.dirname(path.join(assetRoot, importRel)));
|
||||
fs.copyFileSync(
|
||||
path.join(levelPrefabsDir, importRel),
|
||||
path.join(assetRoot, importRel),
|
||||
);
|
||||
ready.push({ levelId, meta, importRel });
|
||||
}
|
||||
ready.sort((a, b) => Number(a.levelId) - Number(b.levelId));
|
||||
|
||||
const zipTmp = path.join(stageBase, `${levelId}.zip`);
|
||||
const manifest = {
|
||||
version: 2,
|
||||
packSize,
|
||||
shell: ['assets/level-prefabs/config.json', 'assets/level-prefabs/index.js'],
|
||||
levels: {},
|
||||
};
|
||||
|
||||
let packed = 0;
|
||||
let packs = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
for (let i = 0; i < ready.length; i += packSize) {
|
||||
const group = ready.slice(i, i + packSize);
|
||||
const firstId = group[0].levelId;
|
||||
const lastId = group[group.length - 1].levelId;
|
||||
const stageRoot = path.join(stageBase, `pack-${firstId}-${lastId}`);
|
||||
const assetRoot = path.join(stageRoot, 'assets', 'level-prefabs');
|
||||
|
||||
for (const item of group) {
|
||||
mkdirp(path.dirname(path.join(assetRoot, item.importRel)));
|
||||
fs.copyFileSync(
|
||||
path.join(levelPrefabsDir, item.importRel),
|
||||
path.join(assetRoot, item.importRel),
|
||||
);
|
||||
}
|
||||
|
||||
const zipTmp = path.join(stageBase, `pack-${firstId}-${lastId}.zip`);
|
||||
zipDir(stageRoot, zipTmp);
|
||||
const hash = hashFileMd5(zipTmp);
|
||||
const bundleName = `defaultlocalgroup_level_${levelId}_${hash}.bundle`;
|
||||
const bundleName = `defaultlocalgroup_levels_${firstId}_${lastId}_${hash}.bundle`;
|
||||
fs.copyFileSync(zipTmp, path.join(webglDir, bundleName));
|
||||
const size = fs.statSync(zipTmp).size;
|
||||
totalBytes += size;
|
||||
packs += 1;
|
||||
|
||||
for (const item of group) {
|
||||
manifest.levels[item.levelId] = {
|
||||
bundle: bundleName,
|
||||
uuid: item.meta.uuid,
|
||||
};
|
||||
packed += 1;
|
||||
}
|
||||
|
||||
manifest.levels[levelId] = {
|
||||
bundle: bundleName,
|
||||
path: meta.path,
|
||||
uuid: meta.uuid,
|
||||
files: [`assets/level-prefabs/${importRel}`],
|
||||
bytes: size,
|
||||
};
|
||||
packed += 1;
|
||||
fs.rmSync(stageRoot, { recursive: true, force: true });
|
||||
fs.unlinkSync(zipTmp);
|
||||
}
|
||||
@@ -134,14 +158,15 @@ function splitLevelBundles(levelPrefabsDir, webglDir, manifestOutPath, packTmpDi
|
||||
|
||||
fs.writeFileSync(manifestOutPath, JSON.stringify(manifest), 'utf8');
|
||||
|
||||
console.log(`>>> 关卡分包: ${packed} 关, 合计 ${formatBytes(totalBytes)}, 均 ${formatBytes(Math.round(totalBytes / Math.max(packed, 1)))}/关`);
|
||||
console.log(`>>> 关卡分包: ${packed} 关 / ${packs} 包 (${packSize} 关/包), 合计 ${formatBytes(totalBytes)}, 均 ${formatBytes(Math.round(totalBytes / Math.max(packs, 1)))}/包`);
|
||||
if (missing.length) {
|
||||
console.warn(`>>> 警告: ${missing.length} 关在 config 中无 import 文件 (例: ${missing.slice(0, 5).join(', ')})`);
|
||||
}
|
||||
return { manifest, packed, missing, totalBytes };
|
||||
return { manifest, packed, packs, packSize, missing, totalBytes };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_LEVELS_PER_PACK,
|
||||
splitLevelBundles,
|
||||
indexImportFiles,
|
||||
readConfigLevels,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# 将运行时包同步到 scratch-gui/static/unity/
|
||||
# 与 OSS unitycdndir 内容完全一致:Build/ + StreamingAssets/ + levels-database.*
|
||||
# 与 OSS unitycdndir 内容完全一致:Build/ + StreamingAssets/ + levels-db-index.*
|
||||
#
|
||||
# bash tools/sync-unity-package-to-static.sh <packDir> <staticUnityDir>
|
||||
set -euo pipefail
|
||||
@@ -13,8 +13,8 @@ if [[ ! -f "$PACK_DIR/Build/mstest5.loader.js" ]]; then
|
||||
echo "错误: 缺少 $PACK_DIR/Build/mstest5.loader.js" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$PACK_DIR/StreamingAssets/aa/catalog.json" ]]; then
|
||||
echo "错误: 缺少 $PACK_DIR/StreamingAssets/aa/catalog.json" >&2
|
||||
if [[ ! -f "$PACK_DIR/StreamingAssets/aa/catalog.json" && ! -f "$PACK_DIR/StreamingAssets/aa/catalog.json.br" ]]; then
|
||||
echo "错误: 缺少 $PACK_DIR/StreamingAssets/aa/catalog.json(.br)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -30,7 +30,7 @@ echo " 目标: $UNITY_STATIC"
|
||||
rsync "${RSYNC_FLAGS[@]}" "$PACK_DIR/Build/" "$UNITY_STATIC/Build/"
|
||||
rsync "${RSYNC_FLAGS[@]}" "$PACK_DIR/StreamingAssets/" "$UNITY_STATIC/StreamingAssets/"
|
||||
|
||||
for db in levels-database.json levels-database.json.br; do
|
||||
for db in levels-db-index.json levels-db-index.json.br levels-database.json levels-database.json.br; do
|
||||
if [[ -f "$PACK_DIR/$db" ]]; then
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " [dry-run] cp $PACK_DIR/$db → $UNITY_STATIC/"
|
||||
@@ -40,6 +40,15 @@ for db in levels-database.json levels-database.json.br; do
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# 全量库已默认不打进包;清掉 static 里旧的大文件
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
for stale in levels-database.json levels-database.json.br; do
|
||||
if [[ ! -f "$PACK_DIR/$stale" && -f "$UNITY_STATIC/$stale" ]]; then
|
||||
rm -f "$UNITY_STATIC/$stale"
|
||||
echo " removed stale $stale"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
for legacy in index.js application.js cocos-bridge.js assets cocos-js src index.html TemplateData; do
|
||||
|
||||
@@ -65,8 +65,8 @@ const lines = [
|
||||
'## 上传后验证',
|
||||
`curl -I "${base}/Build/mstest5.loader.js"`,
|
||||
`curl -I "${base}/StreamingAssets/aa/catalog.json"`,
|
||||
`curl -I "${base}/levels-database.json"`,
|
||||
...bundleNames.map((n) => `curl -I "${base}/StreamingAssets/aa/WebGL/${n}"`),
|
||||
`curl -I "${base}/levels-db-index.json.br"`,
|
||||
...bundleNames.map((n) => `curl -I "${base}/StreamingAssets/aa/WebGL/${n}.br"`),
|
||||
];
|
||||
fs.writeFileSync(manifestPath, lines.join('\n'), 'utf8');
|
||||
|
||||
@@ -80,7 +80,7 @@ const readme = [
|
||||
` ${packDir}/`,
|
||||
' ├── Build/',
|
||||
' ├── StreamingAssets/',
|
||||
' └── levels-database.json(.br)',
|
||||
' └── levels-db-index.json(.br)',
|
||||
'',
|
||||
'本地: import-to-unity.sh → scratch-gui/static/unity/(同上结构)',
|
||||
'CDN: 上传运行时包全部内容 → config.js unitycdndir + usecdn:true',
|
||||
@@ -98,8 +98,14 @@ if (makeZip) {
|
||||
zipPath = path.join(path.dirname(packDir), 'mstest5-runtime.zip');
|
||||
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
||||
const items = ['Build', 'StreamingAssets'];
|
||||
if (fs.existsSync(path.join(packDir, 'levels-database.json'))) items.push('levels-database.json');
|
||||
if (fs.existsSync(path.join(packDir, 'levels-database.json.br'))) items.push('levels-database.json.br');
|
||||
for (const name of [
|
||||
'levels-db-index.json',
|
||||
'levels-db-index.json.br',
|
||||
'levels-database.json',
|
||||
'levels-database.json.br',
|
||||
]) {
|
||||
if (fs.existsSync(path.join(packDir, name))) items.push(name);
|
||||
}
|
||||
execSync(`cd "${packDir}" && zip -0 -q -r "${zipPath}" ${items.join(' ')}`, { stdio: 'pipe' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user