# 原动画、动效与时间契约

研究日期：2026-10-09。本文只解析本地原包、原 Laya 引擎和静态解码源码；**动画时长是资源声明或代码参数，不是录像测得的耗时。** 原件复用应同时保留图像、atlas 帧顺序、pivot、骨骼、关键帧、时间源、Tween 和调用循环参数，不能只保留最终静态截图。

## 可复核的资源清单

- [animation-index.json](../../artifacts/reverse/animation-index.json)：18 个文件中 25 条 JSON timeline（含 `.ani` 与 `Prefabs/BuffUI.json`），全部声明 24 fps。
- [skeleton-animation-index.json](../../artifacts/reverse/skeleton-animation-index.json)：42 个 `.sk` 文件的版本、动画名、毫秒时长、节点/关键帧计数、轨道与 SHA256，全部完成静态解析。
- [effect-atlas-index.json](../../artifacts/reverse/effect-atlas-index.json)：8 个 effect atlas，记录**原始帧列表顺序**与默认节奏。
- [inspect-animations.py](../../tools/reverse/inspect-animations.py)：按包内引擎 binary schema 读 `.sk`，不启动游戏，不执行 SDK。

原 `effect` 子包已在原首局实际加载后补提取；当前目录 [res/effect](../../artifacts/original/unpacked/gameAssets/asset/res/effect) 包含全部 8 个 atlas 与各自 PNG。它们之前未缓存的缺口已经补齐；是否每种模式动态加载到位仍需运行验证。

源码证据：原 `artifacts/original/unpacked/game.js` 的 `libs/min/laya.core.min.js`、`libs/min/laya.ani.min.js` define 模块；方便阅读的静态格式化副本 [laya.core.module.formatted.js](../../artifacts/reverse/laya.core.module.formatted.js)、[laya.ani.module.formatted.js](../../artifacts/reverse/laya.ani.module.formatted.js)。游戏调用证据均来自 [bundle.decoded.js](../../artifacts/reverse/bundle.decoded.js) 的 `js/bundle.js`，字符偏移基于该解码文件；[格式化副本](../../artifacts/reverse/bundle.decoded.formatted.js) 可按方法名搜索。

## `.ani` timeline 的帧语义

引擎 `_setUp` 把 interval 设置为 `1000/frameRate`（core 30059）；`_calculateDatas` 扫描最高关键帧 index 后再加 1（30125–30140），`_createGraphicData` 逐帧建立 Graphics（32639–32661）。因此 index=15 的资源包含 0…15 共 16 帧；默认循环周期为 `16/24秒`，不是 `15/24秒`。最后一个关键帧到达时间仍为 `15/24秒`，这两个量必须区分。

JSON 的 `action:0` **不能单独证明循环与否**。循环取决于调用 `play(start,loop,name)`；Laya Animation.play 默认 loop=true（core 33105–33122），游戏可显式传 false，在 COMPLETE 后回收对象。资源的 `tween:true/false`、tweenMethod、alpha/scale/rotation/pivot 和 skin 换帧共同定义视觉。静态索引列出全部通道，以原 JSON 为准。

| 原资源（均在 gameAssets/asset/ 下） | 动画名 | 最高 index | 帧数 | 24 fps 名义周期 |
|---|---|---:|---:|---:|
| EffectAni/AddHpSkillAnim.ani | ani1 | 15 | 16 | 666.667 ms |
| EffectAni/DarkSilkwormAni.ani | move | 10 | 11 | 458.333 ms |
| EffectAni/DarkSilkwormAni.ani | attack | 10 | 11 | 458.333 ms |
| EffectAni/DustbinFull.ani | ani1 | 6 | 7 | 291.667 ms |
| EffectAni/VirtualShadowAnim.ani | old | 10 | 11 | 458.333 ms |
| EffectAni/VirtualShadowAnim.ani | new | 10 | 11 | 458.333 ms |
| EffectAni/boxgodeffect.ani | ani1 | 17 | 18 | 750.000 ms |
| EffectAni/princessmirrorEffect.ani | ani1 | 135 | 136 | 5666.667 ms |
| Prefabs/BuffUI.json | ani1 | 80 | 81 | 3375.000 ms |
| devil1.ani | ani1 | 8 | 9 | 375.000 ms |
| diceAni.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni2.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni3.ani | ani1 | 15 | 16 | 666.667 ms |
| diceAni4.ani | ani1 | 15 | 16 | 666.667 ms |
| haloStar.ani | ani1 | 10 | 11 | 458.333 ms |
| meowAni.ani | ani1 | 15 | 16 | 666.667 ms |
| speed.ani | ani1 | 4 | 5 | 208.333 ms |
| stcAnim.ani | dd | 24 | 25 | 1041.667 ms |
| stcAnim.ani | open | 26 | 27 | 1125.000 ms |
| stcAnim.ani | openLoop | 2 | 3 | 125.000 ms |
| stcAnim.ani | people | 12 | 13 | 541.667 ms |
| tcAnim.ani | dd | 24 | 25 | 1041.667 ms |
| tcAnim.ani | open | 26 | 27 | 1125.000 ms |
| tcAnim.ani | openLoop | 26 | 27 | 1125.000 ms |
| trail.ani | ani1 | 57 | 58 | 2416.667 ms |

例如 `speed.ani` 在 index0/2/4 切换皮肤，pivot=(81,199)，不能把它等同于匀速缩放。`trail.ani` 对多个 GraphicNode 进行位置/透明度变化；`princessmirrorEffect.ani` index135 是长达 136 帧的时间线。原引擎可直接解析这些 JSON，无需再造 CSS keyframes。

## 骨骼 `.sk`：名称和时长可静态读取

所有 42 个文件头是 `LAYAANIMATION:1.7.0`，动画类 `DragonBone`。原引擎 `_onAsynLoaded` 对除 `LAYAANIMATION:02` 外的版本调用 Parser01（ani 747–755）。Parser01（31–151）依次读取 UTF 版本/类/字符串表，animationCount、publicDataOffset、extensionOffset、useParent；每个动画读 name 索引、Float32 playTime、nodeCount，随后按 publicData 的 keyframeWidth 跳过各轨道数据。报告工具完全按此布局读取；不把从二进制中随便找到的字符串当作动画名。

`getAniDuration` 返回 playTime（ani 770–772）；Skeleton 内也用 `duration/1000` 参与时间换算（3474）。所以本清单的单位是毫秒。骨骼采样/缓存频率、节点插值和状态中 playbackRate 仍由引擎和调用方决定，文件 playTime 不是最终可见动作耗时。

| 文件 | 动画及原 playTime |
|---|---|
| Skeleton/player/player_0.sk | attack 1066.667；idle 2000；walk 1300；walk5 833.333 ms |
| Skeleton/player/player_1.sk | attack 1000；idle/idle2/idle3 2000；walk/walk2 1233.333；walk5 766.667 ms |
| Skeleton/player/player_2.sk | attack 1000；idle/idle3 2000；walk 1466.667；walk3 1466.7；walk5 566.667 ms |
| Skeleton/player/player_3.sk | attack 2000；idle 2733.333；walk/walk3 1133.333；walk5 833.333 ms |
| Skeleton/player/player_4.sk | attack 1333.333；idle/idle2 2000；idle3/idle4 1000；walk 2000；walk5 833.333 ms |
| Skeleton/player/player_5.sk | attack 1000；idle/idle4 2033.333；idle3 2000；walk 2166.667；walk5 833.333 ms |
| Skeleton/StrongerTroll/mg_0.sk | daiji/gongji/zoulu 各 2000 ms |
| Skeleton/StrongerTroll/mg_1.sk | animation 0；gongji/idle/walk 各 2000 ms |
| Skeleton/StrongerTroll/mg_3.sk | daiji 2000；gongji/walk 1000 ms |
| Skeleton/StrongerTroll/mg_4.sk | daiji/zoulu 2000；gongji 3000 ms |
| Skeleton/StrongerTroll/mg_6.sk | daji/gongji/zoulu 900；shouji 100；siwang 500 ms |
| Skeleton/muscleBadger/mihuan.sk | gongji 433.333；gongji2 333.333；pao 666.667；shuizhao 1333.333 ms |
| Skeleton/Compound/dustbinOut.sk | animation 200；animation2 366.667 ms |
| Skeleton/combatDoor/skeleton.sk | animation 1900；smbf 2033.333 ms |

来源为表列原 `.sk`，精确值及其轨道在 skeleton-animation-index.json。`res/mg/mg_*.sk` 与 `res/Skeleton/StrongerTroll/mg_*.sk` 是**不同资源**，例如 res/mg/mg_0 多有 zouluzhuqingting、zuoshanxian。不能按相同 mg_0 文件名互换；mounts 的 idle_0…5/walk_0…5 也对应各角色坐骑姿态。

明确的速率覆盖：玩家速度技能调用 `_player.playbackRate(5)`，恢复调用 rate(1)（bundle 39476、39019）；坐骑展示存在 rate(3)（347256）。某些怪物攻击 `play(1,false)` 并由逻辑计时决定再次出手（解码字符873800附近）。因此 2秒骨骼片段不代表2秒攻击间隔。

## atlas 逐帧效果

`Hp.vgg` 创建 Laya.Animation、loadAtlas，对应路径 `gameAssets/asset/res/effect/{name}.atlas`；效果第一次加载后计算 getGraphicBounds，按 bounds/2 设置 pivot。`Hp.ncm` 将效果加到场景，普通 zOrder=pos.y；ahu 标记置 Number.MAX_VALUE。暴气 trollBaoqiEffect 缩放 2.67，并把目标位置减去 (80,80)（解码字符191030）。这些定位/层级/缩放是原视觉契约。

Laya 默认 `animationInterval=50ms`（core 行16），Hp 没有修改 interval，所以未被其它调用修改的这些新实例是 20 fps，和 `.ani` 的24 fps不同。

| 效果 | 原帧数 | 默认名义播放时长 |
|---|---:|---:|
| bulletEffect | 8 | 400 ms |
| trollAttackEffect | 3 | 150 ms |
| trollBaoqiEffect | 12 | 600 ms |
| trollZH | 7 | 350 ms |
| buildDownEffect | 7 | 350 ms |
| build_vertigo | 5 | 250 ms |
| flowerAttack | 5 | 250 ms |
| bomb | 6 | 300 ms |

Hp 默认 loop=0，调用 `play(0,false)`，COMPLETE 后回收到对象池并 removeChild/offAll；非0 loop 参数走 `play(0,true)` 并在指定 loop 毫秒后移除停止（字符191030–192100）。它不是 JSON timeline 的循环标志。原 atlas 也保留裁切 frame/spriteSourceSize/sourceSize；渲染应让 Laya 执行 atlas，不能将透明裁切帧拉伸成相同 CSS 方框。

## 按钮与数值反馈 Tween

来源：bundle CM 的 `ahk/ftt/xtl/gua/cxe/gzp`，解码字符314604、320434、328468、302262、325606、306848；所有时长为传给 Laya.Tween 的毫秒参数。

| 交互 | 原参数 | 校准意义 |
|---|---|---|
| 按下按钮 ftt | scaleX/scaleY→原scale×0.9，50ms | 点击反馈不能只切换贴图 |
| 松开/移出 xtl/gua | scaleX/scaleY→原scale，50ms | 移出恢复也要执行；以绑定时原scale为基准 |
| 对象 cxe 弹跳 | 1→1+i，50ms；→1−2i，50ms；→1，50ms | 总150ms，幅度由调用参数决定，常见炮台i=.2 |
| Show | visible=true，scale→1，200ms | 显示时的展开反馈 |
| Alpha | alpha→目标，1000×传入秒数 | 是可参数化工具，不应固定所有对象1秒 |
| +金币 gzp | 初始alpha/scale=0；500ms→alpha1/scale e、上浮15；1000ms再上浮45；1000ms上浮45且淡出缩为0 | 连续2500ms，结束回对象池；持续收入可叠多条浮动数字 |
| 普通炮台枪口 udi | alpha→1 20ms，→0 20ms，回收 | 总40ms，5秒抽帧看不到它并不代表没有 |

`gzp` 在UI文案显示前把64变65，也保留原浮字白色、+符号、层级和生成位置。所谓「升级动效」并非只有一个资源：扣款/换等级贴图、cxe、buildDownEffect、声音和菜单变化可以共同发生；应以原调用链为准。

## 炮台、炮弹与受击节奏

普通炮台一条代码路径（解码字符431439–445000，格式化34266–34680）初始化 uzd=1、yyp=0，但 uzd getter 是 `1/(zpf+1+ulj)`，所以该普通路径基础出手检查间隔为 **0.5秒**。计时 `Lx.rvy=mxj/1000`，ubs(t,interval) 判断 interval<=rvy−t（字符245127、246672），单位是秒。高级/特殊炮台有不同 getter：基础 `1/(zpf+yyp)`，rapid+3、morerapid+3或5、angel+1.5、crazy+5（字符953157），不可把全部炮台统一写成0.5秒。

普通炮台 attack→nad对象池→rfl 初始化90×90炮弹、pivot=(45,45)、scale=.2、ytl=500；jsx 先对准目标，再 `G_.pqq(node,target,180,ytl)`，并 `afj(node,1,1,.35)`（字符437766、439101）。高级炮台另一条 rfl 设 ytl=900，cannon尺寸135、giantcannon171（952270）。

**速度语义已查明：** `pqq` 第3参保存 pvl（转向速度），第4参保存 suc（位移速度）；`G_.pgu` 每更新用 suc/60 位移、pvl/60 转角（2334641）。普通弹500→每更新约8.333地图单位，高级弹900→15；180是转向，**不是弹速**。位移按 `abs(dx)/(abs(dx)+abs(dy))` 与 y 分量分配，使用曼哈顿归一化；不能换成欧氏归一化而声称同轨迹。靠近目标两轴都<5时隐藏，击中检查另用平方距离<400，即20单位范围（普通炮台 onUpdate 字符435000附近）。

该移动器按 `/60` 的更新步长工作，最终每秒速度还依赖原更新调度。网页省电/后台降帧后不能自动声称500单位/实际秒保持不变；直接复用原更新器能保留其行为，若做dt修正会改变原契约，应单独证明。

## 门、怪物血条与离场

[BuildBloodStrip.json](../../artifacts/original/unpacked/gameAssets/asset/Prefabs/BuildBloodStrip.json) 根90×20、pivot=(45,10)；槽UI/HP.png为90×20、sizeGrid=3,9,6,7；内容UI/BuildHP.png为86×16、x2/y11、pivotY10、sizeGrid=2,10,2,10。门 `yuj` setter直接设置内容 scaleX=生命比例（字符560300–561450）。按门旋转0/90/180/270度，血条分别放(45,10)/(10,45)/(45,80)/(80,45)，血条反转自身rotation保持水平；这条路径没有补间掉血动画。

[TrollBloodStrip.json](../../artifacts/original/unpacked/gameAssets/asset/Prefabs/TrollBloodStrip.json) 同尺寸但红色TrollHP，有LV.标签、font20、描边1。怪物 `yuj` setter即时改实际 `nlk` 红条到比例，同时把 mah追随层 scaleX Tween到该比例，**800ms**（字符656500–658150）；这不是门的反馈。原怪物方向翻转和血条层级也要保留。

标准怪物逻辑默认低血逃跑阈值 ydr=.3；初始化会从配置读取 zac，原配置ydr=[.3,.3,.3]，重新选择/逃跑概率zde=[.5,.4,.3]（字符870300、887946、2405100）。攻击中每 hfe 秒考虑改目标/逃跑；普通默认level<5时10秒、>=5时20秒，阶梯难度可覆盖 escapeAfterLev/escapeThinkingTime（字符895500附近）。`gjt` 在未逃跑且 hp<=ydr 调 arb；arb设置escape状态，寻路到guv位置，切移动/飞行动画（893954）。

逃到目标 btk开始回血阶段；sjx每1秒调用ghr，Normal按addHpSpeed回，Plot地图11每次+.2×最大血，其余+.1×最大血，满血后清逃跑标记、重新找目标（873439、888480）。Normal的addHpSpeed需要继续追配置才能给精确固定值；不能把所有模式写成「每秒10%」。

原骨骼mg_6带shouji100ms和siwang500ms，但这里只证明资源片段存在，**尚未证明当前模式在每次受击/离场都播放这两段**。不能把资源名存在写成实机必然触发。

## 录像时间边界与验收

当前 [01-navigation/index.json](../../references/recordings/01-navigation/index.json) 有51张导航帧，第一帧源时间0、最后362.126667秒；相邻最小间隔5秒，最大跳跃116.7秒（215.371667→332.071667）。这些数值来自索引记录，说明保留了原视频时间且存在空档，不能按文件序号×5给全部帧补时间。原MOV总时长364秒由录制侧报告，本文未独立量测MOV容器。

该稀疏导航序列只适合找阶段与关键状态，不足以测40/50/150/350/800ms动效。25–67秒的10fps开场抽帧正在由主代理生成；即使10fps也无法可靠判定40ms枪口闪现。后续若做像素/节奏验收，应从高帧率原录像选择同一源时刻，不要用两个不同时相截图计算视觉误差。

可校准顺序：同设备比例下固定原舞台640设计宽 → 对齐原存档/角色/模式 → 对齐资源版本/atlas/pivot → 对齐动画名与loop/playbackRate → 对齐同一个关键帧/出手/受击时刻 → 最后对比像素。静态原件复用能保留动作轨迹与美术细节；本报告不宣称已达到所有场景的动态像素一致。


## 人工验收的原暂停入口

原游戏管理器 `T_.cfi()` 调用 `im.instance.cfi()`，将更新开关 `uhw=false`，随后设置 `Z_.instance.ciu.pause=true`；`T_.kzg()` 做对应恢复（格式化 bundle 311060、310469）。`im.tnz()` 只有在 `uhw` 为真时遍历执行注册回调（6037）。原 `Lx` 注册的 `TimeManager`、`HandleOnceEvent`、`HandleLoopEven` 全在这个入口下（9783）；时间累计 `mxj += Laya.timer.delta` 也在注册回调 `krt` 中（12899）。因此原暂停会冻结模拟时钟和对应游戏逻辑，适合暂停选房倒计时后人工检查。

这并不自动暂停 Laya 自身的 Animation、骨骼、Tween、全局 timer。截图验收中仍可能看到独立视觉动效继续运行；不能把逻辑暂停声称为渲染逐帧冻结。`ciu.pause` setter 在 Compound 模式为真时还派发 `gm.lrs` 事件；Normal 模式仍应走原方法，不能只修改 `_pause`。

构造入口在原 [js/game.js](../../artifacts/original/unpacked/js/game.js) 字符 **2095803**，唯一出现的 `new T_(Dx[s[1886]])`。在 web 适配构建时将该表达式桥接为 `(window.__tpfyGame=new T_(Dx[s[1886]]))`，即可由 P 键调用 `window.__tpfyGame.cfi()` / `.kzg()`。解码副本对应字符 **2774250** 的唯一 `new T_(Dx["mpv"])`，格式化行 358599。桥接只暴露原实例；不重建管理器，不改倒计时或经济数值。按键状态由浏览器桥接单独保存，避免反复按下触发多次切换。
