- #1 #2 skill / device summons (赫默's drone, 巫恋's doll, 凯瑟琳's devices) are hand cards placed by the player; placed summons deploy once at battle start, then a skill summon re-appears at its tile on each cast (the user's call) - #3 叙拉古 隐匿 ends on time (32 + 0.4 × layers s); 忍冬 S3 迷彩 lasts until her next cast (official), kept apart from 隐匿 - #4 multi-round bounty cards last two battles (the user's call; MULTI_ROUND_BOUNTY_BATTLES), blue / red card text - #5 the shared boss pool's float dust (≈3.6e-12 HP) stalled Final Assault leaders at 0 %; < 1 HP now clears the pool, and the team LP reaching 0 first is a defeat (PRTS) - #6 #10 机变 cards show their official effect text; an invisible 详情 button no longer swallows the confirming tap on phones - #7 live ×N counter of a leaker's enemies still on the 联防 field (LP loss stays capped at 10) - #8 #15 #16 skill triggers per 技能策略: skill-range triggers, injured-ally heals (古米, 塞雷娅), tank take-damage triggers with a 3 s cooldown; 新约能天使 S2 shields the highest-aggro operator in range - #9 official per-enemy model scales (威龙) - #11 element bar under the unit's HP bar (white = 元素值 left, element-coloured refill after a burst), floor rounding - #12 枯朽战车 kit per PRTS; every ranged enemy reaches its target's 0.25 collider - #13 恐惧 wander (萨科塔之翼 at half HP, operator fears make enemies flee) with re-planning after pushes - #14 push / pull distance by 力度 − 重量 (PRTS table); #17 every blocker attacks what it blocks; ASPD floor 20 - #18 炎佑 is 孤立: no heals or ally buffs - #19 every free pick-one chess offer (promotion reward, 信标, 寻呼模块) shows different operators - DESIGN §20 and the normative lines, SIM / META / DATA / PLAYING updated; 2747 unit tests and all 27 browser suites green; dev recordings regenerated
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Official drawn size of the enemy models, read from a locally installed Arknights client (optional, host-side).
|
|
|
|
The client scales every enemy Spine model in its battle prefab (dyn/battle/prefabs/enemies/<prefab>.prefab, bundles
|
|
battle/enm_pfb_*.ab): world size = skeleton units x SkeletonDataAsset.scale (0.01 for every enemy skeleton) x the
|
|
product of the transform scales from the prefab root down to the GameObject that carries the Spine MeshRenderer
|
|
(Graphic / FaceSwitcher / Spine). That product is 0.27 for most prefabs (the standard the remake's UNIT.modelScale
|
|
stands for) and differs for the rest (威龙 0.16, 妖怪 0.20, 青铜镜 0.6 ...).
|
|
|
|
This script prints the product of every prefab named by data/enemies.json (`spine` = enemy_database prefabKey) that is
|
|
not the standard, grouped by value, as the MODEL_SCALES table of tools/build-data.mjs (user playtest #6 item 9).
|
|
|
|
Usage:
|
|
.venv/bin/python tools/local-extract/enemy_scales.py [--game <AB root>] [--json]
|
|
Needs UnityPy + lz4 (tools/local-extract/requirements.txt); --json prints {prefab: product} for every prefab found.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from extract import CANDIDATES, ROOT # noqa: E402
|
|
|
|
STANDARD = 0.27
|
|
|
|
|
|
def group_table(scales, standard=STANDARD):
|
|
"""{prefab: product} -> the build-data MODEL_SCALES lines ([value, ['1005_yokai_3', ...]] sorted by value)."""
|
|
groups = {}
|
|
for prefab, v in scales.items():
|
|
if v is None or abs(v - standard) < 1e-6:
|
|
continue
|
|
groups.setdefault(round(abs(v), 4), []).append(re.sub(r'^enemy_', '', prefab))
|
|
lines = []
|
|
for v in sorted(groups):
|
|
names = ', '.join("'%s'" % n for n in sorted(groups[v]))
|
|
lines.append(' [%s, [%s]],' % (('%g' % v), names))
|
|
return lines
|
|
|
|
|
|
def renderer_scale(root_go, by_id):
|
|
"""Product of the transform scales from a prefab root to its (active) Spine MeshRenderer; None when not exactly one."""
|
|
found = []
|
|
|
|
def walk(god, cum, depth):
|
|
tr = None
|
|
has_mr = False
|
|
for c in god['m_Component']:
|
|
r = by_id.get(c['component']['m_PathID'])
|
|
if r is None:
|
|
continue
|
|
if r.type.name == 'Transform':
|
|
tr = r.read_typetree()
|
|
elif r.type.name == 'MeshRenderer':
|
|
has_mr = True
|
|
if tr is None:
|
|
return
|
|
s = tr['m_LocalScale']
|
|
cum = (cum[0] * s['x'], cum[1] * s['y'])
|
|
if has_mr and god.get('m_IsActive', True):
|
|
found.append(abs(cum[0]))
|
|
if depth > 6:
|
|
return
|
|
for ch in tr['m_Children']:
|
|
cr = by_id.get(ch['m_PathID'])
|
|
if cr is None:
|
|
continue
|
|
g = by_id.get(cr.read_typetree()['m_GameObject']['m_PathID'])
|
|
if g is not None:
|
|
walk(g.read_typetree(), cum, depth + 1)
|
|
|
|
walk(root_go, (1.0, 1.0), 0)
|
|
return round(found[0], 5) if len(found) == 1 else None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.split('\n')[0])
|
|
ap.add_argument('--game', help='AB root of the local client (default: the known install locations)')
|
|
ap.add_argument('--json', action='store_true', help='print {prefab: product} instead of the table')
|
|
args = ap.parse_args()
|
|
ab_root = Path(args.game) if args.game else next((p for p in CANDIDATES if p.exists()), None)
|
|
if not ab_root or not (ab_root / 'battle').exists():
|
|
sys.exit('enemy_scales: no local client found (pass --game <AB root>)')
|
|
import aklz4 # noqa: F401 (registers the LZ4AK decoder)
|
|
import UnityPy
|
|
enemies = json.loads((ROOT / 'data' / 'enemies.json').read_text('utf8'))
|
|
wanted = {e.get('spine') or k for k, e in enemies.items()}
|
|
scales = {}
|
|
for f in sorted((ab_root / 'battle').glob('enm_pfb*.ab')):
|
|
env = UnityPy.load(str(f))
|
|
by_id = {o.path_id: o for o in env.objects}
|
|
for name, obj in env.container.items():
|
|
m = re.search(r'/enemies/(.+)\.prefab$', name)
|
|
if m and m.group(1) in wanted:
|
|
scales[m.group(1)] = renderer_scale(obj.read_typetree(), by_id)
|
|
missing = sorted(wanted - set(scales))
|
|
if missing:
|
|
print('// no prefab found: ' + ', '.join(missing), file=sys.stderr)
|
|
if args.json:
|
|
print(json.dumps(scales, indent=1, sort_keys=True))
|
|
else:
|
|
print('\n'.join(group_table(scales)))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|