@@ -30,6 +30,14 @@ LAYER_DEFAULT = 1073741824
|
||||
LAYER_UI_2D = 33554432 # 编辑器 2D 视图可见(对齐 SpriteSplash.prefab)
|
||||
|
||||
CELL = 100
|
||||
|
||||
# themes-database.json id → textures/ 子目录(与 ThemeDatabase.getThemeTextureFolder 一致)
|
||||
THEME_TEXTURE_FOLDER: dict[str, str] = {
|
||||
"redarmy": "redArmy",
|
||||
"redArmy": "redArmy",
|
||||
}
|
||||
|
||||
|
||||
def resolve_level_map_data_type() -> str:
|
||||
"""从 Creator 编译产物读取 LevelMapData 的 __type__(_RF.push 第二参数)。"""
|
||||
chunk = Path("temp/programming/packer-driver/targets/preview/chunks/bb/bbf6a3729c922dc4638933a67da37eeb23e90aee.js")
|
||||
@@ -74,19 +82,27 @@ def theme_from_cfg(cfg: dict, override: str) -> str:
|
||||
return "silu"
|
||||
|
||||
|
||||
def texture_folder_for_theme(theme: str) -> str:
|
||||
return THEME_TEXTURE_FOLDER.get(theme, theme)
|
||||
|
||||
|
||||
def ground_sprite_path(theme: str, tile_name: str) -> str:
|
||||
key = tile_name if tile_name in ("Baseblock", "JumpBlock") else "Baseblock"
|
||||
return f"textures/{theme}/{key}"
|
||||
# Ground 仅地砖
|
||||
key = "JumpBlock" if tile_name == "JumpBlock" else "Baseblock"
|
||||
return f"textures/{texture_folder_for_theme(theme)}/{key}"
|
||||
|
||||
|
||||
def border_sprite_path(theme: str, tile_val) -> str:
|
||||
# Border 仅墙砖/墙饰;地砖名回退 WallBlock
|
||||
if tile_val is True or tile_val is None:
|
||||
name = "WallBlock"
|
||||
elif isinstance(tile_val, str):
|
||||
name = tile_val
|
||||
name = tile_val.strip() or "WallBlock"
|
||||
if name in ("Baseblock", "JumpBlock"):
|
||||
name = "WallBlock"
|
||||
else:
|
||||
name = "WallBlock"
|
||||
return f"textures/{theme}/{name}"
|
||||
return f"textures/{texture_folder_for_theme(theme)}/{name}"
|
||||
|
||||
|
||||
def read_sprite_size(project: Path, texture_path: str) -> tuple[int, int]:
|
||||
@@ -390,12 +406,12 @@ def build_prefab(level_id: int, cfg: dict, project: Path, theme: str = "") -> li
|
||||
val = border[key]
|
||||
if val is True or val is None:
|
||||
bname = "WallBlock"
|
||||
elif isinstance(val, str):
|
||||
bname = val
|
||||
elif isinstance(val, str) and val.strip() and val.strip() not in ("Baseblock", "JumpBlock"):
|
||||
bname = val.strip()
|
||||
else:
|
||||
bname = "WallBlock"
|
||||
spr_wall = read_sprite_uuid(project, border_sprite_path(theme, border[key]))
|
||||
wall_path = border_sprite_path(theme, border[key])
|
||||
wall_path = border_sprite_path(theme, bname)
|
||||
spr_wall = read_sprite_uuid(project, wall_path)
|
||||
border_tiles.append(
|
||||
b.tile(f"b_{x}_{y}", -1, x, y, spr_wall, bname, read_sprite_size(project, wall_path))
|
||||
)
|
||||
@@ -404,7 +420,8 @@ def build_prefab(level_id: int, cfg: dict, project: Path, theme: str = "") -> li
|
||||
for key in sorted(ground.keys()):
|
||||
xs, ys = key.split(",")
|
||||
x, y = int(xs), int(ys)
|
||||
tile_name = ground[key]
|
||||
raw = ground[key]
|
||||
tile_name = "JumpBlock" if raw == "JumpBlock" else "Baseblock"
|
||||
gpath = ground_sprite_path(theme, tile_name)
|
||||
spr = read_sprite_uuid(project, gpath)
|
||||
ground_tiles.append(
|
||||
@@ -455,6 +472,7 @@ def main():
|
||||
ap.add_argument("--out-dir", required=True)
|
||||
ap.add_argument("--limit", type=int, default=0)
|
||||
ap.add_argument("--level-id", type=int, default=0, help="只烘焙指定关卡,0=全部")
|
||||
ap.add_argument("--min-level-id", type=int, default=0, help="只烘焙 levelID >= 此值的关卡")
|
||||
ap.add_argument("--max-level-id", type=int, default=0, help="只烘焙 levelID <= 此值的关卡")
|
||||
ap.add_argument("--rebuild-index", action="store_true", help="仅重建 tools/level-prefab-index.json(不烘焙 prefab)")
|
||||
ap.add_argument("--theme", default="", help="贴图主题 silu/sanxing/snow/…,空则从关卡 theme 字段读取")
|
||||
@@ -467,7 +485,7 @@ def main():
|
||||
|
||||
levels = db["levels"]
|
||||
ids = sorted(int(k) for k in levels.keys())
|
||||
if args.rebuild_index and args.level_id == 0 and args.max_level_id == 0 and args.limit == 0:
|
||||
if args.rebuild_index and args.level_id == 0 and args.min_level_id == 0 and args.max_level_id == 0 and args.limit == 0:
|
||||
index = rebuild_index_from_disk(out)
|
||||
index_path = write_prefab_index(project, out, index)
|
||||
uuid_path = write_prefab_uuid_index(project, out)
|
||||
@@ -476,8 +494,10 @@ def main():
|
||||
|
||||
if args.level_id > 0:
|
||||
ids = [args.level_id] if str(args.level_id) in levels else []
|
||||
elif args.max_level_id > 0:
|
||||
ids = [i for i in ids if i <= args.max_level_id]
|
||||
elif args.min_level_id > 0 or args.max_level_id > 0:
|
||||
lo = args.min_level_id if args.min_level_id > 0 else min(ids) if ids else 0
|
||||
hi = args.max_level_id if args.max_level_id > 0 else max(ids) if ids else 0
|
||||
ids = [i for i in ids if lo <= i <= hi]
|
||||
elif args.limit > 0:
|
||||
ids = ids[: args.limit]
|
||||
|
||||
|
||||
399
tools/cpp_to_python_converter.py
Normal file
399
tools/cpp_to_python_converter.py
Normal file
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert C++ game code (int main / void functions) to Python style."""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def _remove_comments(code: str) -> str:
|
||||
return re.sub(r'//[^\n]*', '', code)
|
||||
|
||||
|
||||
def _find_matching_brace(code: str, open_pos: int) -> int:
|
||||
depth = 0
|
||||
for i in range(open_pos, len(code)):
|
||||
if code[i] == '{':
|
||||
depth += 1
|
||||
elif code[i] == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def _find_matching_paren(code: str, open_pos: int) -> int:
|
||||
depth = 0
|
||||
for i in range(open_pos, len(code)):
|
||||
if code[i] == '(':
|
||||
depth += 1
|
||||
elif code[i] == ')':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def _parse_for_header(header: str) -> Optional[str]:
|
||||
header = re.sub(r'\s+', ' ', header.strip())
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\+\+\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _ = m.groups()
|
||||
return _range_for(var, start.strip(), op, end.strip(), 1)
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*=\s*(\w+)\s*([+\-])\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, _, sign, step = m.groups()
|
||||
step_n = int(step) if sign == '+' else -int(step)
|
||||
return _range_for(var, start.strip(), op, end.strip(), step_n)
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\+=\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, step = m.groups()
|
||||
return _range_for(var, start.strip(), op, end.strip(), int(step))
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)-=\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, step = m.groups()
|
||||
return _range_for(var, start.strip(), op, end.strip(), -int(step))
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*=\s*(\w+)\s*-\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, _, step = m.groups()
|
||||
return _range_for(var, start.strip(), op, end.strip(), -int(step))
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)--\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _ = m.groups()
|
||||
return _range_for(var, start.strip(), op, end.strip(), -1)
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*\*=\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, mult = m.groups()
|
||||
return f'WHILE|{var}|{start.strip()}|{op}|{end.strip()}|{var} *= {mult}'
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*=\s*\1\s*\*\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, mult = m.groups()
|
||||
return f'WHILE|{var}|{start.strip()}|{op}|{end.strip()}|{var} *= {mult}'
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*/=\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, div = m.groups()
|
||||
return f'WHILE|{var}|{start.strip()}|{op}|{end.strip()}|{var} //= {div}'
|
||||
|
||||
m = re.match(
|
||||
r'for\s*\(\s*int\s+(\w+)\s*=\s*([^;]+);\s*(\w+)\s*([<>=!]+)\s*([^;]+);\s*(\w+)\s*=\s*\1\s*/\s*(\d+)\s*\)',
|
||||
header,
|
||||
)
|
||||
if m:
|
||||
var, start, _, op, end, _, div = m.groups()
|
||||
return f'WHILE|{var}|{start.strip()}|{op}|{end.strip()}|{var} //= {div}'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _add_one(expr: str) -> str:
|
||||
expr = expr.strip()
|
||||
if re.fullmatch(r'-?\d+', expr):
|
||||
return str(int(expr) + 1)
|
||||
return f'{expr} + 1'
|
||||
|
||||
|
||||
def _sub_one(expr: str) -> str:
|
||||
expr = expr.strip()
|
||||
if re.fullmatch(r'-?\d+', expr):
|
||||
return str(int(expr) - 1)
|
||||
return f'{expr} - 1'
|
||||
|
||||
|
||||
def _range_for(var: str, start: str, op: str, end: str, step: int) -> str:
|
||||
if step == 1:
|
||||
if op == '<':
|
||||
if start == '0':
|
||||
return f'for {var} in range({end}):'
|
||||
return f'for {var} in range({start}, {end}):'
|
||||
if op == '<=':
|
||||
return f'for {var} in range({start}, {_add_one(end)}):'
|
||||
if step == -1:
|
||||
if op == '>':
|
||||
return f'for {var} in range({start}, {end}, -1):'
|
||||
if op == '>=':
|
||||
return f'for {var} in range({start}, {_sub_one(end)}, -1):'
|
||||
if step > 1 and op == '<':
|
||||
return f'for {var} in range({start}, {end}, {step}):'
|
||||
if step < -1:
|
||||
if op == '>=':
|
||||
return f'for {var} in range({start}, {_sub_one(end)}, {step}):'
|
||||
if op == '>':
|
||||
return f'for {var} in range({start}, {end}, {step}):'
|
||||
return f'# UNCONVERTED FOR: {var} {start} {op} {end} step={step}'
|
||||
|
||||
|
||||
def _split_args(s: str) -> list[str]:
|
||||
parts, cur, depth = [], [], 0
|
||||
for c in s:
|
||||
if c in '([':
|
||||
depth += 1
|
||||
elif c in ')]':
|
||||
depth -= 1
|
||||
elif c == ',' and depth == 0:
|
||||
parts.append(''.join(cur).strip())
|
||||
cur = []
|
||||
continue
|
||||
cur.append(c)
|
||||
if cur:
|
||||
parts.append(''.join(cur).strip())
|
||||
return parts
|
||||
|
||||
|
||||
def _format_rhs(expr: str) -> str:
|
||||
expr = expr.strip().rstrip(';')
|
||||
m = re.match(r'(\w+)\((.*)\)$', expr)
|
||||
if m:
|
||||
fn, args = m.group(1), m.group(2)
|
||||
if not args.strip():
|
||||
return f'{fn}()'
|
||||
parts = _split_args(args)
|
||||
return f'{fn}({", ".join(_format_rhs(p) for p in parts)})'
|
||||
|
||||
expr = re.sub(r'\s+', '', expr)
|
||||
tokens = re.split(r'(==|!=|<=|>=|[+\-*/=<>])', expr)
|
||||
parts = []
|
||||
for t in tokens:
|
||||
if not t:
|
||||
continue
|
||||
if t in '+-*/=<>!':
|
||||
parts.append(f' {t} ')
|
||||
elif t in ('==', '!=', '<=', '>='):
|
||||
parts.append(f' {t} ')
|
||||
else:
|
||||
parts.append(t)
|
||||
result = ''.join(parts)
|
||||
result = re.sub(r'\s+', ' ', result).strip()
|
||||
result = re.sub(r'^-\s+(\d)', r'-\1', result)
|
||||
result = re.sub(r'-\s+(\w)', r'-\1', result)
|
||||
result = re.sub(r'\(\s*-\s+(\w)', r'(-\1', result)
|
||||
return result
|
||||
|
||||
|
||||
def _format_stmt(stmt: str) -> str:
|
||||
stmt = stmt.strip().rstrip(';')
|
||||
m = re.match(r'int\s+(\w+)\s*=\s*(.+)', stmt)
|
||||
if m:
|
||||
return f'{m.group(1)} = {_format_rhs(m.group(2))}'
|
||||
m = re.match(r'(\w+)\s*=\s*(.+)', stmt)
|
||||
if m and not stmt.startswith(('if ', 'for ', 'void ', 'return')):
|
||||
return f'{m.group(1)} = {_format_rhs(m.group(2))}'
|
||||
return _format_rhs(stmt)
|
||||
|
||||
|
||||
def _skip_ws(code: str) -> str:
|
||||
return code.lstrip(' \t\n\r')
|
||||
|
||||
|
||||
def _convert_block(code: str, indent: int = 0) -> list[str]:
|
||||
lines: list[str] = []
|
||||
code = code.strip()
|
||||
pad = ' ' * indent
|
||||
|
||||
while code:
|
||||
code = _skip_ws(code)
|
||||
if not code:
|
||||
break
|
||||
|
||||
# void function
|
||||
m = re.match(r'void\s+(\w+)\s*\(([^)]*)\)\s*\{', code)
|
||||
if m:
|
||||
fn, params = m.group(1), m.group(2).strip()
|
||||
if params:
|
||||
names = [p.strip().split()[-1] for p in params.split(',') if p.strip()]
|
||||
lines.append(f'def {fn}({", ".join(names)}):')
|
||||
else:
|
||||
lines.append(f'def {fn}():')
|
||||
ob = code.index('{', m.start())
|
||||
cb = _find_matching_brace(code, ob)
|
||||
lines.extend(_convert_block(code[ob + 1:cb], indent + 1))
|
||||
lines.append('')
|
||||
code = code[cb + 1:]
|
||||
continue
|
||||
|
||||
# int main()
|
||||
m = re.match(r'int\s+main\s*\(\s*\)\s*\{', code)
|
||||
if m:
|
||||
ob = code.index('{', m.start())
|
||||
cb = _find_matching_brace(code, ob)
|
||||
lines.extend(_convert_block(code[ob + 1:cb], indent))
|
||||
code = code[cb + 1:]
|
||||
continue
|
||||
|
||||
# for loop
|
||||
m = re.match(r'for\s*\(', code)
|
||||
if m:
|
||||
cp = _find_matching_paren(code, m.end() - 1)
|
||||
header = code[m.start():cp + 1]
|
||||
rest = _skip_ws(code[cp + 1:])
|
||||
if rest.startswith('{'):
|
||||
cb = _find_matching_brace(rest, 0)
|
||||
body = rest[1:cb]
|
||||
py_for = _parse_for_header(header)
|
||||
if py_for and py_for.startswith('WHILE|'):
|
||||
_, var, start, op, end, inc = py_for.split('|')
|
||||
lines.append(pad + f'{var} = {start}')
|
||||
lines.append(pad + f'while {var} {op} {end}:')
|
||||
lines.extend(_convert_block(body, indent + 1))
|
||||
lines.append(pad + f' {inc}')
|
||||
elif py_for:
|
||||
lines.append(pad + py_for)
|
||||
lines.extend(_convert_block(body, indent + 1))
|
||||
else:
|
||||
lines.append(pad + (py_for or f'# {header}'))
|
||||
lines.extend(_convert_block(body, indent + 1))
|
||||
code = rest[cb + 1:]
|
||||
continue
|
||||
|
||||
# if / else if / else
|
||||
m = re.match(r'if\s*\(', code)
|
||||
if m:
|
||||
cp = _find_matching_paren(code, m.end() - 1)
|
||||
cond = code[m.end():cp].strip()
|
||||
rest = _skip_ws(code[cp + 1:])
|
||||
if rest.startswith('{'):
|
||||
cb = _find_matching_brace(rest, 0)
|
||||
if_body = rest[1:cb]
|
||||
lines.append(pad + f'if {_format_rhs(cond)}:')
|
||||
lines.extend(_convert_block(if_body, indent + 1))
|
||||
code = _skip_ws(rest[cb + 1:])
|
||||
if code.startswith('else if'):
|
||||
sub_lines, code = _convert_if_chain(code, indent, is_elif=True)
|
||||
lines.extend(sub_lines)
|
||||
elif code.startswith('else'):
|
||||
if code.startswith('else {'):
|
||||
ecb = _find_matching_brace(code, code.index('{'))
|
||||
lines.append(pad + 'else:')
|
||||
lines.extend(_convert_block(code[code.index('{') + 1:ecb], indent + 1))
|
||||
code = code[ecb + 1:]
|
||||
elif code.startswith('else if'):
|
||||
pass # handled above
|
||||
continue
|
||||
|
||||
# return
|
||||
m = re.match(r'return\s+0\s*;', code)
|
||||
if m:
|
||||
code = code[m.end():]
|
||||
continue
|
||||
|
||||
# statement until ;
|
||||
semi = -1
|
||||
depth = 0
|
||||
for j, c in enumerate(code):
|
||||
if c in '{(':
|
||||
depth += 1
|
||||
elif c in '})':
|
||||
depth -= 1
|
||||
elif c == ';' and depth == 0:
|
||||
semi = j
|
||||
break
|
||||
|
||||
if semi >= 0:
|
||||
stmt = code[:semi].strip()
|
||||
if stmt and not re.match(r'return\s+0', stmt):
|
||||
lines.append(pad + _format_stmt(stmt))
|
||||
code = code[semi + 1:]
|
||||
continue
|
||||
|
||||
# trailing statement without semicolon
|
||||
stmt = code.strip()
|
||||
if stmt and not re.match(r'return\s+0', stmt):
|
||||
lines.append(pad + _format_stmt(stmt))
|
||||
break
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _convert_if_chain(code: str, indent: int, is_elif: bool = False) -> tuple[list[str], str]:
|
||||
lines: list[str] = []
|
||||
pad = ' ' * indent
|
||||
prefix = 'elif' if is_elif else 'if'
|
||||
|
||||
while True:
|
||||
code = _skip_ws(code)
|
||||
if code.startswith('else if'):
|
||||
code = code[7:].lstrip()
|
||||
prefix = 'elif'
|
||||
elif code.startswith('if'):
|
||||
code = code[2:].lstrip()
|
||||
elif code.startswith('else'):
|
||||
if code.startswith('else {'):
|
||||
cb = _find_matching_brace(code, code.index('{'))
|
||||
lines.append(pad + 'else:')
|
||||
lines.extend(_convert_block(code[code.index('{') + 1:cb], indent + 1))
|
||||
return lines, code[cb + 1:]
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
if not code.startswith('('):
|
||||
break
|
||||
cp = _find_matching_paren(code, 0)
|
||||
cond = code[1:cp].strip()
|
||||
rest = _skip_ws(code[cp + 1:])
|
||||
if not rest.startswith('{'):
|
||||
break
|
||||
cb = _find_matching_brace(rest, 0)
|
||||
lines.append(pad + f'{prefix} {_format_rhs(cond)}:')
|
||||
lines.extend(_convert_block(rest[1:cb], indent + 1))
|
||||
code = _skip_ws(rest[cb + 1:])
|
||||
if code.startswith('else if'):
|
||||
prefix = 'elif'
|
||||
code = code[4:].lstrip() # 'else if' -> ' if' wait
|
||||
code = 'if' + code[2:] if code.startswith(' if') else code
|
||||
continue
|
||||
if code.startswith('else {'):
|
||||
cb2 = _find_matching_brace(code, code.index('{'))
|
||||
lines.append(pad + 'else:')
|
||||
lines.extend(_convert_block(code[code.index('{') + 1:cb2], indent + 1))
|
||||
return lines, code[cb2 + 1:]
|
||||
return lines, code
|
||||
|
||||
return lines, code
|
||||
|
||||
|
||||
def cpp_to_python(cpp_code: str) -> str:
|
||||
if not cpp_code or not str(cpp_code).strip():
|
||||
return ''
|
||||
code = _remove_comments(str(cpp_code))
|
||||
lines = _convert_block(code, 0)
|
||||
return '\n'.join(lines).strip()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
print(cpp_to_python(sys.stdin.read()))
|
||||
0
tools/deploy-local.sh
Executable file → Normal file
0
tools/deploy-local.sh
Executable file → Normal file
0
tools/deploy-to-001code.sh
Executable file → Normal file
0
tools/deploy-to-001code.sh
Executable file → Normal file
0
tools/export_all.sh
Executable file → Normal file
0
tools/export_all.sh
Executable file → Normal file
@@ -30,6 +30,7 @@ from export_unity_levels import (
|
||||
VDIR_RE,
|
||||
kind_from_path,
|
||||
parse_spawns,
|
||||
infer_theme_from_level,
|
||||
build_border_cache,
|
||||
)
|
||||
from export_unity_prefab_maps import parse_level_prefab
|
||||
@@ -50,10 +51,12 @@ def parse_level_block(chunk: str, lid: int) -> dict:
|
||||
path_m = LEVEL_PATH_RE.search(chunk)
|
||||
level_path = path_m.group(1) if path_m else f"Assets/Prefabs/Level/Level{lid}.prefab"
|
||||
ext_id = normalize_level_id(lid)
|
||||
spawns, spawn_paths = parse_spawns(chunk)
|
||||
return {
|
||||
"levelID": ext_id,
|
||||
"boundary": {"x": bx, "y": by},
|
||||
"spawns": parse_spawns(chunk),
|
||||
"spawns": spawns,
|
||||
"theme": infer_theme_from_level(ext_id, spawn_paths),
|
||||
"unityPrefab": level_path.replace("\\", "/"),
|
||||
"cocosPrefab": prefab_resource_path(ext_id),
|
||||
}
|
||||
@@ -124,6 +127,8 @@ def strip_internal(L: dict) -> dict:
|
||||
out["ground"] = L["ground"]
|
||||
if L.get("border"):
|
||||
out["border"] = L["border"]
|
||||
if L.get("theme"):
|
||||
out["theme"] = L["theme"]
|
||||
return out
|
||||
|
||||
|
||||
@@ -131,6 +136,9 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--unity-root", required=True, help="Unity 项目根目录(含 Assets)")
|
||||
ap.add_argument("--output", required=True, help="输出 levels-database.json")
|
||||
ap.add_argument("--merge-into", default="", help="合并到已有 JSON(保留范围外关卡)")
|
||||
ap.add_argument("--min-level-id", type=int, default=0, help="仅导出 levelID >= 此值")
|
||||
ap.add_argument("--max-level-id", type=int, default=0, help="仅导出 levelID <= 此值")
|
||||
ap.add_argument("--limit", type=int, default=0, help="仅导出前 N 个关卡(调试用)")
|
||||
ap.add_argument("--skip-prefab-maps", action="store_true", help="不解析 Unity prefab 瓦片(快,地图由 Cocos 预制体承担)")
|
||||
args = ap.parse_args()
|
||||
@@ -145,10 +153,14 @@ def main():
|
||||
print("Parsing Levels*.cs …")
|
||||
levels = parse_all_levels_cs(core)
|
||||
ids = sorted(levels.keys())
|
||||
if args.min_level_id > 0:
|
||||
ids = [i for i in ids if i >= args.min_level_id]
|
||||
if args.max_level_id > 0:
|
||||
ids = [i for i in ids if i <= args.max_level_id]
|
||||
if args.limit > 0:
|
||||
ids = ids[: args.limit]
|
||||
levels = {k: levels[k] for k in ids}
|
||||
print(f"Level definitions: {len(levels)}")
|
||||
levels = {k: levels[k] for k in ids}
|
||||
print(f"Level definitions: {len(levels)} ({ids[0] if ids else '?'}-{ids[-1] if ids else '?'})")
|
||||
|
||||
from_prefab = 0
|
||||
from_ring = 0
|
||||
@@ -165,18 +177,56 @@ def main():
|
||||
else:
|
||||
from_ring += 1
|
||||
|
||||
payload = {
|
||||
"version": 1,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab",
|
||||
"stats": {
|
||||
"total": len(levels),
|
||||
"withPrefabTilemap": from_prefab,
|
||||
"withBoundaryRing": from_ring,
|
||||
},
|
||||
"levelIdBase": LEVEL_ID_BASE,
|
||||
"levels": {str(levels[lid]["levelID"]): strip_internal(levels[lid]) for lid in ids},
|
||||
}
|
||||
exported = {str(levels[lid]["levelID"]): strip_internal(levels[lid]) for lid in ids}
|
||||
|
||||
if args.merge_into:
|
||||
merge_path = Path(args.merge_into)
|
||||
if merge_path.is_file():
|
||||
print(f"Merging into {merge_path} …")
|
||||
base = json.loads(merge_path.read_text(encoding="utf-8"))
|
||||
base_levels = base.get("levels", {})
|
||||
base_levels.update(exported)
|
||||
payload = {
|
||||
**base,
|
||||
"version": 2,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab (merged)",
|
||||
"stats": {
|
||||
"total": len(base_levels),
|
||||
"withPrefabTilemap": from_prefab,
|
||||
"withBoundaryRing": from_ring,
|
||||
"exportedRange": len(exported),
|
||||
},
|
||||
"levelIdBase": LEVEL_ID_BASE,
|
||||
"levels": base_levels,
|
||||
}
|
||||
else:
|
||||
print(f"warn: merge target missing, writing export only: {merge_path}", file=sys.stderr)
|
||||
payload = {
|
||||
"version": 2,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab",
|
||||
"stats": {
|
||||
"total": len(exported),
|
||||
"withPrefabTilemap": from_prefab,
|
||||
"withBoundaryRing": from_ring,
|
||||
},
|
||||
"levelIdBase": LEVEL_ID_BASE,
|
||||
"levels": exported,
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"version": 2,
|
||||
"generatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"source": "Unity Levels*.cs + Assets/Prefabs/Level/*.prefab",
|
||||
"stats": {
|
||||
"total": len(exported),
|
||||
"withPrefabTilemap": from_prefab,
|
||||
"withBoundaryRing": from_ring,
|
||||
},
|
||||
"levelIdBase": LEVEL_ID_BASE,
|
||||
"levels": exported,
|
||||
}
|
||||
|
||||
out = Path(args.output)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -25,7 +25,7 @@ DIR = {
|
||||
}
|
||||
|
||||
LEVEL_RE = re.compile(
|
||||
r"\{(\d+),new Level\(\)\{LevelID\s*=\s*\d+,spawns\s*=\s*new List<Spawn>\(\)\{",
|
||||
r"\{\s*(\d+)\s*,\s*new Level\(\)\s*\{\s*LevelID\s*=\s*\d+\s*,\s*spawns\s*=\s*new List<Spawn>\(\)\s*\{",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ SPAWN_RE = re.compile(
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),0\)")
|
||||
POS_RE = re.compile(r"position\s*=\s*new Vector3Int\((-?\d+),(-?\d+),-?\d+\)")
|
||||
PATH_RE = re.compile(r'path\s*=\s*"?([^",\s]+)"?')
|
||||
PDIR_RE = re.compile(r"playerDirection\s*=\s*(Direction\.\w+)")
|
||||
VDIR_RE = re.compile(r"vehicleDirection\s*=\s*(Direction\.\w+)")
|
||||
@@ -49,21 +49,51 @@ def kind_from_path(path: str) -> str:
|
||||
return "vehicle"
|
||||
if "enemy" in p:
|
||||
return "enemy"
|
||||
if "prop" in p:
|
||||
if "prop" in p or "nrop" in p:
|
||||
return "prop"
|
||||
return "prop_decor"
|
||||
|
||||
|
||||
def prop_placement_from_path(path: str) -> str | None:
|
||||
if "nprop" in path.lower():
|
||||
pl = path.lower()
|
||||
if "nprop" in pl or "nrop" in pl:
|
||||
return "ground"
|
||||
if "prop" in path.lower():
|
||||
if "prop" in pl:
|
||||
return "block"
|
||||
return None
|
||||
|
||||
|
||||
def parse_spawns(block: str) -> list[dict]:
|
||||
spawns = []
|
||||
def infer_theme_from_level(level_id: int, spawn_paths: list[str]) -> str:
|
||||
"""按主站关卡批次分配主题;道具路径可覆盖(snow/sanxing)。"""
|
||||
if 91601 <= level_id <= 91900:
|
||||
return "silu"
|
||||
if 91901 <= level_id <= 92200:
|
||||
return "redarmy"
|
||||
if 92201 <= level_id <= 92500:
|
||||
return "numMan"
|
||||
if 92501 <= level_id <= 92800:
|
||||
return "chinese"
|
||||
if 92801 <= level_id <= 93100:
|
||||
return "snow"
|
||||
if 93101 <= level_id <= 93700:
|
||||
return "sanxing"
|
||||
joined = " ".join(spawn_paths).lower()
|
||||
if "prop_sanxing" in joined or "nprop_sanxing" in joined:
|
||||
return "sanxing"
|
||||
if "prop_snow" in joined or "nprop_snow" in joined:
|
||||
return "snow"
|
||||
if "chinese" in joined or "panda" in joined:
|
||||
return "chinese"
|
||||
if "redarmy" in joined or "red_army" in joined:
|
||||
return "redarmy"
|
||||
if "numman" in joined:
|
||||
return "numMan"
|
||||
return "silu"
|
||||
|
||||
|
||||
def parse_spawns(block: str) -> tuple[list[dict], list[str]]:
|
||||
spawns: list[dict] = []
|
||||
paths: list[str] = []
|
||||
for m in SPAWN_RE.finditer(block):
|
||||
body = m.group(1)
|
||||
pm = POS_RE.search(body)
|
||||
@@ -71,6 +101,8 @@ def parse_spawns(block: str) -> list[dict]:
|
||||
continue
|
||||
path_m = PATH_RE.search(body)
|
||||
path = path_m.group(1) if path_m else ""
|
||||
if path:
|
||||
paths.append(path)
|
||||
item: dict = {
|
||||
"x": int(pm.group(1)),
|
||||
"y": int(pm.group(2)),
|
||||
@@ -86,7 +118,7 @@ def parse_spawns(block: str) -> list[dict]:
|
||||
if vdir:
|
||||
item["vehicleDirection"] = vdir.group(1)
|
||||
spawns.append(item)
|
||||
return spawns
|
||||
return spawns, paths
|
||||
|
||||
|
||||
def ring_border_key(bx: int, by: int) -> str:
|
||||
@@ -105,12 +137,13 @@ def parse_file(text: str) -> dict[int, dict]:
|
||||
bx, by = (10, 10)
|
||||
if bound:
|
||||
bx, by = int(bound.group(1)), int(bound.group(2))
|
||||
spawns = parse_spawns(chunk)
|
||||
spawns, spawn_paths = parse_spawns(chunk)
|
||||
levels[lid] = {
|
||||
"levelID": lid,
|
||||
"boundary": {"x": bx, "y": by},
|
||||
"borderKey": ring_border_key(bx, by),
|
||||
"spawns": spawns,
|
||||
"theme": infer_theme_from_level(lid, spawn_paths),
|
||||
}
|
||||
return levels
|
||||
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 Unity Level{N}.prefab 解析 Tilemap(Ground / Border)格子数据。"""
|
||||
"""从 Unity Level{N}.prefab 解析 Tilemap(Ground / Border)格子数据。
|
||||
|
||||
层级锁定:
|
||||
- Baseblock / JumpBlock → ground(地砖属性)
|
||||
- WallBlock / 墙饰 → border(墙砖属性)
|
||||
不再用死板的 tileIndex==1 → JumpBlock。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
GROUND_TILES = frozenset({"Baseblock", "JumpBlock"})
|
||||
BORDER_DECOR = frozenset(
|
||||
{
|
||||
"WallBlock",
|
||||
"kuai11",
|
||||
"Decor23",
|
||||
"素材切图-23",
|
||||
"素材切图2-23",
|
||||
"小游戏素材红色_03",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
@@ -22,7 +42,84 @@ def parse_tilemap_layer(text: str, layer_name: str) -> dict[str, int]:
|
||||
return {}
|
||||
|
||||
|
||||
def parse_level_prefab(prefab_path: Path) -> dict:
|
||||
def _tilemap_part(text: str, layer_name: str) -> str | None:
|
||||
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
|
||||
return part
|
||||
return None
|
||||
|
||||
|
||||
def _tile_asset_guids(tilemap_part: str) -> list[str | None]:
|
||||
"""按 m_TileAssetArray 顺序解析 guid(空槽为 None)。"""
|
||||
assets: list[str | None] = []
|
||||
in_arr = False
|
||||
for line in tilemap_part.splitlines():
|
||||
if "m_TileAssetArray:" in line:
|
||||
in_arr = True
|
||||
continue
|
||||
if in_arr and ("m_TileSpriteArray" in line or "m_AnimationFrameRate" in line):
|
||||
break
|
||||
if not in_arr:
|
||||
continue
|
||||
if "guid:" in line:
|
||||
m = re.search(r"guid: ([a-f0-9]+)", line)
|
||||
assets.append(m.group(1) if m else None)
|
||||
elif "m_Data: {fileID: 0}" in line:
|
||||
assets.append(None)
|
||||
return assets
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _guid_to_tile_name(unity_texture_root: str) -> dict[str, str]:
|
||||
root = Path(unity_texture_root)
|
||||
out: dict[str, str] = {}
|
||||
if not root.is_dir():
|
||||
return out
|
||||
for meta in root.rglob("*.asset.meta"):
|
||||
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", "")
|
||||
return out
|
||||
|
||||
|
||||
def tile_asset_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_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 normalize_ground_tile(name: str | None) -> str:
|
||||
return "JumpBlock" if name == "JumpBlock" else "Baseblock"
|
||||
|
||||
|
||||
def normalize_border_tile(name: str | None) -> str:
|
||||
if not name or name in GROUND_TILES:
|
||||
return "WallBlock"
|
||||
if name in BORDER_DECOR:
|
||||
return name
|
||||
# 其它墙饰保留文件名,禁止地砖名漏到墙层
|
||||
return name
|
||||
|
||||
|
||||
def _resolve_name(assets: list[str | None], tile_index: int) -> str | None:
|
||||
if 0 <= tile_index < len(assets):
|
||||
return assets[tile_index]
|
||||
return None
|
||||
|
||||
|
||||
def parse_level_prefab(prefab_path: Path, unity_texture_root: Path | None = None) -> dict:
|
||||
if not prefab_path.is_file():
|
||||
return {}
|
||||
text = prefab_path.read_text(encoding="utf-8")
|
||||
@@ -30,8 +127,35 @@ def parse_level_prefab(prefab_path: Path) -> dict:
|
||||
b_raw = parse_tilemap_layer(text, "Border")
|
||||
if not g_raw and not b_raw:
|
||||
return {}
|
||||
ground = {
|
||||
k: ("JumpBlock" if ti == 1 else "Baseblock") for k, ti in g_raw.items()
|
||||
}
|
||||
border = {k: True for k in b_raw}
|
||||
|
||||
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)
|
||||
b_assets = tile_asset_names(text, "Border", unity_texture_root)
|
||||
|
||||
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 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]
|
||||
|
||||
return {"ground": ground, "border": border}
|
||||
|
||||
68
tools/import_unity_levels_91601_93430.sh
Executable file
68
tools/import_unity_levels_91601_93430.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# 从 Unity 主站导入 91601–93430 关卡(地图 + spawns + 主题 + 烘焙预制体)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
UNITY_ROOT="${UNITY_ROOT:-/Users/liuyufei/tfrh/主站文件/主站}"
|
||||
DB="$ROOT/assets/level-data/levels-database.json"
|
||||
PREFAB_OUT="$ROOT/assets/bundle-level-prefabs/level-prefabs"
|
||||
MIN_ID=91601
|
||||
MAX_ID=93430
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
echo "==> [1/5] 导出 Unity 关卡 JSON (${MIN_ID}-${MAX_ID})"
|
||||
python3 tools/export_all_levels.py \
|
||||
--unity-root "$UNITY_ROOT" \
|
||||
--output "$DB" \
|
||||
--merge-into "$DB" \
|
||||
--min-level-id "$MIN_ID" \
|
||||
--max-level-id "$MAX_ID"
|
||||
|
||||
echo "==> [2/5] 导入主题贴图(silu/snow/sanxing/chinese/redArmy/numMan)"
|
||||
python3 tools/import_unity_textures.py \
|
||||
--unity-root "$UNITY_ROOT" \
|
||||
--themes silu,snow,sanxing,chinese,redArmy,numMan || true
|
||||
|
||||
echo "==> [3/5] 修复砖块 meta + 主题调色板"
|
||||
python3 tools/fix_tile_texture_metas.py || true
|
||||
python3 tools/build_theme_palettes.py --unity-root "$UNITY_ROOT" || true
|
||||
python3 tools/sync_theme_nprop_ground.py 2>/dev/null || true
|
||||
|
||||
echo "==> [4/5] 烘焙 Cocos 关卡预制体 (${MIN_ID}-${MAX_ID})"
|
||||
python3 tools/bake_cocos_level_prefabs.py \
|
||||
--db "$DB" \
|
||||
--out-dir "$PREFAB_OUT" \
|
||||
--min-level-id "$MIN_ID" \
|
||||
--max-level-id "$MAX_ID"
|
||||
|
||||
echo "==> [5/5] 校验"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
db = json.loads(Path("assets/level-data/levels-database.json").read_text())
|
||||
themes = Counter()
|
||||
empty = 0
|
||||
missing = 0
|
||||
kinds = Counter()
|
||||
for lid in range(91601, 93431):
|
||||
L = db["levels"].get(str(lid))
|
||||
if not L:
|
||||
missing += 1
|
||||
continue
|
||||
if not L.get("spawns"):
|
||||
empty += 1
|
||||
themes[L.get("theme", "?")] += 1
|
||||
for s in L.get("spawns") or []:
|
||||
kinds[s.get("kind", "?")] += 1
|
||||
prefabs = list(Path("assets/bundle-level-prefabs/level-prefabs").glob("Level*.prefab"))
|
||||
in_range = [p for p in prefabs if 91601 <= int(p.stem.replace("Level", "")) <= 93430]
|
||||
print(f"missing: {missing}/1830")
|
||||
print(f"spawns empty: {empty}/1830")
|
||||
print(f"themes: {dict(themes)}")
|
||||
print(f"spawn kinds: {dict(kinds)}")
|
||||
print(f"prefabs in range: {len(in_range)}/1830")
|
||||
PY
|
||||
|
||||
echo "Done."
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,4 +95,6 @@ def touch_database(db_path: Path, level_ids: list[int] | None = None) -> None:
|
||||
data["levels"][key] = sync_level_entry(data["levels"][key], lid)
|
||||
update_db_stats(data)
|
||||
data["generatedAt"] = datetime.now(timezone.utc).isoformat()
|
||||
db_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp = db_path.with_suffix(db_path.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp.replace(db_path)
|
||||
|
||||
0
tools/package-for-project.sh
Executable file → Normal file
0
tools/package-for-project.sh
Executable file → Normal file
0
tools/sync-level-db.sh
Executable file → Normal file
0
tools/sync-level-db.sh
Executable file → Normal file
0
tools/sync-main-site-level-db.sh
Executable file → Normal file
0
tools/sync-main-site-level-db.sh
Executable file → Normal file
0
tools/sync-reference-from-unity.sh
Executable file → Normal file
0
tools/sync-reference-from-unity.sh
Executable file → Normal file
0
tools/sync-unity-package-to-static.sh
Executable file → Normal file
0
tools/sync-unity-package-to-static.sh
Executable file → Normal file
@@ -1,156 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 自测 Unity 绘制排序(与 UnityDrawSort.ts 对齐)
|
||||
* 自测等距 frontKey 排序(与 UnityDrawSort.ts 对齐)
|
||||
* 运行: node tools/verify-unity-draw-sort.mjs
|
||||
*/
|
||||
|
||||
const UNITY_SORTING_ORDER = { nProp: 1, tilemap: 2, vehicle: 2, prop: 3, player: 4 };
|
||||
const ORDER_SCALE = 10_000;
|
||||
const DEPTH_SCALE = 100;
|
||||
const TILE_ELEVATION_Y_THRESHOLD = 12;
|
||||
const HALF_W = 50;
|
||||
const HALF_H = 25;
|
||||
const CELL = 100;
|
||||
const RANK_FLOOR = 0;
|
||||
const RANK_PICKABLE = 20;
|
||||
const RANK_VEHICLE = 30;
|
||||
const RANK_PLAYER = 35;
|
||||
const RANK_WALL = 40;
|
||||
|
||||
function cellToWorldCenter(x, y) {
|
||||
const wx = (x - y) * HALF_W;
|
||||
const wy = (x + y) * HALF_H + HALF_H;
|
||||
return { x: wx, y: wy };
|
||||
}
|
||||
|
||||
function unityTileIndividualSortingOrder(cellX, cellY) {
|
||||
return UNITY_SORTING_ORDER.tilemap - cellX - cellY;
|
||||
}
|
||||
|
||||
function compareTileCellDepth(ax, ay, bx, by) {
|
||||
if (ay !== by) return by - ay;
|
||||
return bx - ax;
|
||||
}
|
||||
|
||||
function compareUnityTileToTile(a, b) {
|
||||
const cellCmp = compareTileCellDepth(a.cellX, a.cellY, b.cellX, b.cellY);
|
||||
if (cellCmp !== 0) return cellCmp;
|
||||
const yCmp = Math.round(a.depthY * DEPTH_SCALE) - Math.round(b.depthY * DEPTH_SCALE);
|
||||
if (Math.abs(yCmp) >= TILE_ELEVATION_Y_THRESHOLD) return yCmp;
|
||||
return unityTileIndividualSortingOrder(a.cellX, a.cellY)
|
||||
- unityTileIndividualSortingOrder(b.cellX, b.cellY);
|
||||
}
|
||||
|
||||
function tileInFrontOfTile(front, back, depthFront = 0, depthBack = 0) {
|
||||
const a = { cellX: front.x, cellY: front.y, depthY: depthFront };
|
||||
const b = { cellX: back.x, cellY: back.y, depthY: depthBack };
|
||||
return compareUnityTileToTile(a, b) > 0;
|
||||
}
|
||||
|
||||
function compareIsoDrawOrder(ax, ay, bx, by) {
|
||||
const ka = ax + ay;
|
||||
const kb = bx + by;
|
||||
if (ka !== kb) return kb - ka;
|
||||
return ax - bx;
|
||||
}
|
||||
|
||||
function wallFaceSamples(wallX, wallY) {
|
||||
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
|
||||
}
|
||||
|
||||
function compareWallActorIso(actor, wallX, wallY) {
|
||||
let actorAhead = 0;
|
||||
for (const face of wallFaceSamples(wallX, wallY)) {
|
||||
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y);
|
||||
if (iso < 0) return iso;
|
||||
if (iso > actorAhead) actorAhead = iso;
|
||||
}
|
||||
return actorAhead;
|
||||
}
|
||||
|
||||
function compareUnityEntityToWall(entity, tile) {
|
||||
const cell = { x: entity.cellX, y: entity.cellY };
|
||||
for (const face of wallFaceSamples(tile.cellX, tile.cellY)) {
|
||||
if (face.x === cell.x && face.y === cell.y) return 1;
|
||||
}
|
||||
const tileOrder = unityTileIndividualSortingOrder(tile.cellX, tile.cellY);
|
||||
if (tileOrder > entity.sortingOrder) return -1;
|
||||
const wallIso = compareWallActorIso(cell, tile.cellX, tile.cellY);
|
||||
if (wallIso < 0) {
|
||||
if (tile.cellX === cell.x && tile.cellY === cell.y - 1) return -1;
|
||||
if (tile.cellY < cell.y) return 1;
|
||||
return -1;
|
||||
}
|
||||
const tileAhead = (tile.cellX + tile.cellY) - (cell.x + cell.y);
|
||||
if (wallIso > 0 && tileAhead > 0) return -1;
|
||||
if (wallIso > 0) return wallIso;
|
||||
const yCmp = Math.round(entity.depthY * DEPTH_SCALE) - Math.round(tile.depthY * DEPTH_SCALE);
|
||||
if (yCmp !== 0) return yCmp;
|
||||
if (tileAhead !== 0) return -tileAhead;
|
||||
return compareIsoDrawOrder(tile.cellX, tile.cellY, cell.x, cell.y);
|
||||
}
|
||||
|
||||
function compareUnityEntityToTile(entity, tile) {
|
||||
if (tile.tileName !== 'WallBlock') return 1;
|
||||
return compareUnityEntityToWall(entity, tile);
|
||||
function frontKey(x, y, rank) {
|
||||
return -(x + y) * CELL + rank;
|
||||
}
|
||||
|
||||
function entityInFrontOfTile(entity, tile) {
|
||||
return compareUnityEntityToTile(entity, tile) > 0;
|
||||
const eRank = entity.isPickable ? RANK_PICKABLE : (entity.isPlayer ? RANK_PLAYER : RANK_VEHICLE);
|
||||
let eKey = frontKey(entity.cellX, entity.cellY, eRank);
|
||||
// 贴墙立面抬升
|
||||
const faces = [
|
||||
{ x: tile.cellX, y: tile.cellY - 1 },
|
||||
{ x: tile.cellX - 1, y: tile.cellY },
|
||||
];
|
||||
const onFace = faces.some((f) => f.x === entity.cellX && f.y === entity.cellY);
|
||||
const tKey = frontKey(tile.cellX, tile.cellY, tile.tileName === 'WallBlock' ? RANK_WALL : RANK_FLOOR);
|
||||
if (tile.tileName === 'WallBlock' && onFace) eKey = Math.max(eKey, tKey + 1);
|
||||
return eKey > tKey;
|
||||
}
|
||||
|
||||
function walkableUnderPlayer(playerCell, tileCell) {
|
||||
return entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: playerCell.x, cellY: playerCell.y, centerY: 0, depthY: 0 },
|
||||
{ cellX: tileCell.x, cellY: tileCell.y, tileName: 'Baseblock', centerY: 0, depthY: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
function playerBottomY(cellX, cellY) {
|
||||
return cellToWorldCenter(cellX, cellY).y - 0.35 * 90;
|
||||
}
|
||||
|
||||
function wallFrontY(cellX, cellY) {
|
||||
const c = cellToWorldCenter(cellX, cellY);
|
||||
return Math.max(c.y, c.y + (1 - 0.67) * 115);
|
||||
function tileInFrontOfTile(a, b) {
|
||||
return frontKey(a.x, a.y, RANK_FLOOR) > frontKey(b.x, b.y, RANK_FLOOR);
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: '砖↔砖:斜向路径 (0,-2)→(0,-1)→(0,0)→(1,0) 下方逐格遮挡',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -2 }, { x: 0, y: -1 })
|
||||
&& tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 })
|
||||
&& tileInFrontOfTile({ x: 0, y: 0 }, { x: 1, y: 0 }),
|
||||
name: '砖:南 (0,-2) 挡北 (0,-1)',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -2 }, { x: 0, y: -1 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:对角路径 (0,-3)→(1,-2)→(2,-1)→(3,0) 下方逐格遮挡',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -3 }, { x: 1, y: -2 })
|
||||
&& tileInFrontOfTile({ x: 1, y: -2 }, { x: 2, y: -1 })
|
||||
&& tileInFrontOfTile({ x: 2, y: -1 }, { x: 3, y: 0 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:南侧 (0,-1) 挡住北侧 (0,0)',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:东南 (2,-2) 挡住西北 (0,-1)',
|
||||
pass: tileInFrontOfTile({ x: 2, y: -2 }, { x: 0, y: -1 }),
|
||||
},
|
||||
{
|
||||
name: '91614 出生 (0,-1) 不被北侧墙 (0,0) 挡住',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: 0 },
|
||||
name: '北墙 (-1,1) 挡住中心角色 (0,0)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: -1, cellY: 1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '跨格路径砖:南侧 (0,-2) 不挡北侧角色 (0,-1)',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
|
||||
{ cellX: 0, cellY: -2, tileName: 'Baseblock', centerY: 0, depthY: 100 },
|
||||
name: '东墙 (1,-1) 挡住中心角色 (0,0)(同对角立面)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 1, cellY: -1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '91614 真实Y:出生 (0,-1) 在 (0,0) 墙前',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: playerBottomY(0, -1) },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: wallFrontY(0, 0) },
|
||||
name: '南 tip 墙 (-1,-3) 挡住金币 (0,-3)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: -1, cellY: -3, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '金币 (0,-3) 在东后墙 (1,-3) 之前(屏上金币更靠前)',
|
||||
pass: entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: -1, cellY: -2, tileName: 'Baseblock' },
|
||||
) && entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: 1, cellY: -3, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '贴南墙立面 (0,-1) 露在墙 (0,0) 前',
|
||||
pass: entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -1 },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '南墙 (0,-1) 挡住角色 (0,0)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 0, cellY: -1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '左墙 (-6,-2) 挡住角色 (-4,-3)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: -4, cellY: -3 },
|
||||
{ cellX: -6, cellY: -2, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '载具朝前钉起点:相对南侧地砖仍被挡',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0, isPlayer: false },
|
||||
{ cellX: 0, cellY: -1, tileName: 'Baseblock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '载具朝后用落点:从(0,0)→(1,0) 比钉起点更靠后(继续被下方砖挡)',
|
||||
pass: (() => {
|
||||
const floor = { cellX: 0, cellY: -1, tileName: 'Baseblock' };
|
||||
// 起点更靠前,相对该南侧砖「不挡得那么死」;落点更靠后应仍被挡住
|
||||
const startKeyWins = entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0, isPlayer: false },
|
||||
floor,
|
||||
);
|
||||
const landOccluded = !entityInFrontOfTile(
|
||||
{ cellX: 1, cellY: 0, isPlayer: false },
|
||||
floor,
|
||||
);
|
||||
return !startKeyWins && landOccluded;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
name: '角色朝前用落点:从(0,1)→(0,0) 途中露前',
|
||||
pass: (() => {
|
||||
const from = { cellX: 0, cellY: 1 };
|
||||
const to = { cellX: 0, cellY: 0 };
|
||||
const wall = { cellX: 1, cellY: 0, tileName: 'WallBlock' };
|
||||
return !entityInFrontOfTile(from, wall) && entityInFrontOfTile(to, wall);
|
||||
})(),
|
||||
},
|
||||
{
|
||||
name: '同格:角色永远盖过载具',
|
||||
pass: frontKey(0, 0, RANK_PLAYER) > frontKey(0, 0, RANK_VEHICLE),
|
||||
},
|
||||
{
|
||||
name: '南侧地砖可挡住同侧偏后的实体(下方地砖应遮挡载具)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 0, cellY: -1, tileName: 'Baseblock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '人物与载具同格 frontKey 条带一致(主题无关)',
|
||||
pass: Math.floor(frontKey(2, -1, RANK_PLAYER) / 100) === Math.floor(frontKey(2, -1, RANK_VEHICLE) / 100),
|
||||
},
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
@@ -159,7 +145,6 @@ for (const c of cases) {
|
||||
console.log(`${ok ? '✓' : '✗'} ${c.name}`);
|
||||
if (!ok) failed++;
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
console.error(`\n${failed} case(s) failed`);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user