Files
sganggs 6464e4b46a Playtest #6 fixes: summons placed by hand, boss pool stall, bounties, 联防 leak counter, distinct offers, 机变 card text, phone buy tap, skill triggers, blocking, push force, 孤立, enemies, element bar
- #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
2026-10-01 19:08:23 +08:00

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()