feedback1 integrate: WM 野鬃 S2 pushes along her facing; 琳琅诗怀雅 S3 coins radial
# Conflicts: # docs/SIM.md
This commit is contained in:
commit
da0aa28d5f
8 files changed
+251
-22
No files matched your search
+1
-1
@@ -72,7 +72,7 @@
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员和「停止攻击」类技能期间除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击;**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但它不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放;执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
|
||||
- **敌人**:远程敌人的射程按官方从干员的碰撞体积(半径 0.25 格)算起,比数据上的攻击范围半径远 0.25 格——例如萨卡兹枯朽战车(射程 2.2)能打到中心 2.45 格以内、站在低地的干员。它的第一下攻击就是「秽蚀轰击」(在目标处留下半径 1.7、持续 10 秒的污染区:低地每秒 50、高台 25 点真实伤害,同名不叠加),被阻挡时普通攻击伤害翻倍。“萨科塔之翼”等血量第一次低于一半时会在原地小范围乱飞 5 秒(官方叫「恐惧」);叙拉古、妮芙等干员造成的恐惧会让敌人朝远离干员的方向逃跑。隐匿的敌人只有被阻挡时才能被选为攻击目标:深池逐火战士每次被击倒后先重生 1 秒,然后变成继续往前走的隐匿「余烬」(需要 5 次伤害击倒,护卫的「火灰」10 次),一般要用干员挡住它才能选中(挡住时它不再半透明),10 秒内没打掉就满血站起来;敌人重生时身上的减速、恐惧等状态会被清空,被恐惧的逐火战士变成余烬后也能被挡住;转译基底·α 原始形态受到的伤害全部无效,受到第 4 次物理伤害、第 4 次法术伤害或被阻挡时花 2 秒变成寻仇者、特战术师或幽灵,之后才能被击倒。
|
||||
- **策略「鸭爵」(“神秘顾客”)**:从第 5 回合起,你和队友的每场作战(**最终攻势和隐秘核心也算**,联防不另外替换)有 0–2 名地面敌人(不含领袖及其部件、悬赏敌人)被替换为特殊版本的鸭爵、高普尼克、流泪小子或圆仔,出现在被替换敌人的时间和路线上(两人同场的领袖战里,各自只替换走向自己保护目标点的敌人);击倒者获得 1 资金(联防中的队友也算),漏进保护目标点照常扣 1 点目标生命值。鸭爵和圆仔**不攻击、无法被阻挡**:鸭爵受到伤害后移动速度 +300%;圆仔始终面向我方干员较多的一侧,来自正面的物理 / 法术伤害 −80%。流泪小子隐匿(被阻挡时才能被选中),攻击使目标晕眩 3.5 秒;高普尼克免疫晕眩、冻结和浮空。
|
||||
|
||||
+4
-3
@@ -599,7 +599,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | |
|
||||
| `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); `tile: [r, c]` lands on that tile once (home unchanged — later redeploys use the board tile; refused when off-rect or occupied, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 revive in place |
|
||||
| `refreshRange(unit)`, `setExtraRange(unit, keys)`, `rangeChanged(unit)` | rebuild the ranges after changing `unit.rangeGrid` (流形 copies); extra targetable tiles (absolute keys; merged into every later rebuild until set again; `null` clears; never in `baseRangeKeys`): 蕾缪安 wanted, 维娜 S3 |
|
||||
| `push(enemy, force, {from, dir, fixed, fixedAngle, inward, effect})`, `pull(enemy, force, {to, center, stop})`, `pullToFront(enemy, unit, force)`, `forceLevel(enemy, force)`, `pushDistance(enemy, force, {effect})` | the official 位移 (PRTS 游戏数据基础 §重量公式 / 推与拉; user playtest #6 item 14): 受力等级 = 力度 (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3 …) − current 重量等级 (massLevel, 失重 counts). Push distance per level (`constants.js PUSH_TILES`, PRTS 推与拉's 弹道 column): ≤ −3 → 0, −2 → 0.12, −1 → 0.44, 0 → 1.7, 1 → 2.14, 2 → 2.96, ≥ 3 → 3.53 tiles; `effect` = a 特效 push (`PUSH_TILES_EFFECT`: −2 → 0.085, −1 → 0.374, 0 → 1.562, 1 → 1.987, 2 → 2.773, ≥ 3 → 3.331 — 见行者 S1 / S2, `PUSH_EFFECT_SKILLS`; every other pusher uses the 弹道 column [ASSUMED]); radial (away from `from`) unless `dir` (directional: 推击手, 朝部署方向 — > 45° off or < 0.25 tile ⇒ radial and level −2; `fixed` waives both (圣聆初雪 S1), `fixedAngle` only the angle (见行者 S2)); `inward` = a push towards `from` (薄绿 S2), stopping at the 急停 radius. Pull: level ≥ 0 → to `to` / the 急停 radius around `center`, −1 → 35 % of the way, −2 → 0.03, ≤ −3 → 0; `pullToFront` aims at the 拉力起点 0.5 tile ahead of the unit with the 急停 radius 0.6708 around it (never moves an enemy the unit itself blocks — nor does a pull towards an ally's centre, e.g. the 流形 S3 pulse). A 静态刚体 (data `staticBody`: every air unit of the mode except “炎佑”, plus 昆图斯 — PRTS 特殊机制 "可以进入失衡状态…但物理层面上无法产生任何速度或移动", "与单位的行动方式无关") moves 0 from every source while the skills that reach it still hit it (薄绿 S2, 锏 S3, the 钩索师 …; player report after 0.1.0) |
|
||||
| `push(enemy, force, {from, dir, fixed, fixedAngle, inward, effect})`, `pull(enemy, force, {to, center, stop})`, `pullToFront(enemy, unit, force)`, `forceLevel(enemy, force)`, `pushDistance(enemy, force, {effect})` | the official 位移 (PRTS 游戏数据基础 §重量公式 / 推与拉; user playtest #6 item 14): 受力等级 = 力度 (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3 …) − current 重量等级 (massLevel, 失重 counts). Push distance per level (`constants.js PUSH_TILES`, PRTS 推与拉's 弹道 column): ≤ −3 → 0, −2 → 0.12, −1 → 0.44, 0 → 1.7, 1 → 2.14, 2 → 2.96, ≥ 3 → 3.53 tiles; `effect` = a 特效 push (`PUSH_TILES_EFFECT`: −2 → 0.085, −1 → 0.374, 0 → 1.562, 1 → 1.987, 2 → 2.773, ≥ 3 → 3.331 — 见行者 S1 / S2, `PUSH_EFFECT_SKILLS`; every other pusher uses the 弹道 column [ASSUMED]); radial (away from `from`; the client's buff template `knockback[relative]`: 莫斯提马 S3, 山 S3, 琳琅诗怀雅 S3 — also PRTS 备注 "推开效果为径向推动") unless `dir` (directional; template `knockback[dir]` = Knockback {`_useSourceDirection` true, `_decreaseForceLevelWhenNotInDirection` 2}: 推击手, 野鬃 S2 "往攻击方向" (charpack char_496_wildmn: buff `wildmn_s_2[force]`) — > 45° off or < 0.25 tile ⇒ radial and level −2; `fixed` waives both (圣聆初雪 S1 朝部署方向, `KnockBackWithCharacterDirection`), `fixedAngle` only the angle (见行者 S2)); `inward` = a push towards `from` (薄绿 S2), stopping at the 急停 radius. Pull: level ≥ 0 → to `to` / the 急停 radius around `center`, −1 → 35 % of the way, −2 → 0.03, ≤ −3 → 0; `pullToFront` aims at the 拉力起点 0.5 tile ahead of the unit with the 急停 radius 0.6708 around it (never moves an enemy the unit itself blocks — nor does a pull towards an ally's centre, e.g. the 流形 S3 pulse) A 静态刚体 (data `staticBody`: every air unit of the mode except “炎佑”, plus 昆图斯 — PRTS 特殊机制 "可以进入失衡状态…但物理层面上无法产生任何速度或移动", "与单位的行动方式无关") moves 0 from every source while the skills that reach it still hit it (薄绿 S2, 锏 S3, the 钩索师 …; player report after 0.1.0) |
|
||||
| `displace(enemy, {x, y}, tiles)` | the raw mover behind push / pull: along passable tiles (by `motion`: FLY in the rect, else ground-passable), no weight rule; nothing for 失衡免疫 (`noDisplace`: 近地悬浮 incl. 喷气人's 飞行模式, 浮空, the 胄 parts, 守墓石像's statue and flight), 静态刚体 (`def.staticBody`) and leaders (`_displaceable`); unblocks + re-paths |
|
||||
| `after(seconds, fn, {owner})`, `every(seconds, fn, {owner, immediate})` | return `{cancel()}`; fn(battle, sched) |
|
||||
| `fx(kind, params)`, `rng()` (+ `rng.int/range/chance/pick/shuffle/weighted`) | never use Math.random |
|
||||
@@ -759,8 +759,9 @@ whose skill "恢复…友方/友军…生命" heals the most injured ally in ran
|
||||
`ep_damage_ratio` + "附带…凋亡/灼燃/神经损伤" ⇒ element damage on hit (凋亡 → `apoptosis`; × damage dealt when the text says
|
||||
"伤害N%的…损伤", else × ATK); "屏障" + `shield_max_hp_ratio`/`hp_ratio` ⇒ decaying self shield at start (砾, 新约能天使);
|
||||
"立即流失N%当前生命" ⇒ self HP loss at start (宴, 风丸); `hp_ratio` + "恢复/回复…生命" ⇒ self heal at start; `force` ⇒ on-hit
|
||||
displacement by the official 力度 − 重量 rules (拖拽/hookmaster: `pullToFront`; else `push`, directional for 朝部署方向 / 向前 /
|
||||
身前方向 and 推击手, radial otherwise — §6). **Passive** skills only apply stat mods (timed when the text says "N秒内":
|
||||
displacement by the official 力度 − 重量 rules (拖拽/hookmaster: `pullToFront`; else `push`, directional for 往攻击方向 / 朝部署方向 /
|
||||
向前 / 身前方向 and 推击手, radial otherwise — §6; only a fallback: hand-written kits follow the client templates
|
||||
`knockback[dir]` / `knockback[relative]`, e.g. 琳琅诗怀雅 S3's "向前推开" is radial). **Passive** skills only apply stat mods (timed when the text says "N秒内":
|
||||
宴 +65 % ATK for 14 s) and the self/counter effects — their scales describe procs that need a kit. Instant skills with
|
||||
mods/targeting but no attack override apply them to the next attack (the skill range is switched in for that attack).
|
||||
|
||||
|
||||
@@ -1804,10 +1804,12 @@ export class Battle {
|
||||
/**
|
||||
* Push enemy `e` with 力度 `force` (PRTS 推与拉 §推力): the official distance of its 受力等级 (constants.js PUSH_TILES:
|
||||
* a 中力 push moves a weight-1 enemy 1.7 tiles, a weight-2 one 0.44, weight 3 0.12, weight ≥ 4 not at all). Radial
|
||||
* (the default: away from `from`, the pusher's centre — "沿着干员向自身中心点的射线方向") or directional (`dir` = the
|
||||
* pusher's direction {x, y}: 推击手, "朝部署方向" / "向前推开"); a directional push on a target more than 45° off `dir`
|
||||
* or nearer than 0.25 tile becomes radial with 受力等级 −2 ("特殊修正"). `fixed` waives both corrections (PRTS 圣聆初雪 S1
|
||||
* 备注 "不会因角度过大或距离过近而变化方向与力度"); `fixedAngle` only the angle one (PRTS 见行者 S2 备注 "不会因为角度过大
|
||||
* (the default: away from `from`, the pusher's centre — "沿着干员向自身中心点的射线方向"; the client's buff template
|
||||
* knockback[relative]: 琳琅诗怀雅 S3, 山 S3, 莫斯提马 S3) or directional (`dir` = the pusher's direction {x, y}; template
|
||||
* knockback[dir], Knockback {_useSourceDirection: true, _decreaseForceLevelWhenNotInDirection: 2}: 推击手, 野鬃 S2
|
||||
* "往攻击方向"); a directional push on a target more than 45° off `dir` or nearer than 0.25 tile becomes radial with
|
||||
* 受力等级 −2 ("特殊修正"). `fixed` waives both corrections (圣聆初雪 S1 "朝部署方向": KnockBackWithCharacterDirection,
|
||||
* PRTS 备注 "不会因角度过大或距离过近而变化方向与力度"); `fixedAngle` only the angle one (PRTS 见行者 S2 备注 "不会因为角度过大
|
||||
* 而改变推动的方向或削减力度" — the < 0.25 tile rule still applies). `inward` = a radial push towards `from` (薄绿 S2's "拖拽", PRTS 备注 "实际为
|
||||
* 反方向(指向薄绿方向)的推开"), never nearer than PULL_STOP_RADIUS to its centre [ASSUMED: "至面前"]. `effect` = a 特效
|
||||
* push (PRTS 推与拉: one frame less of travel than a 弹道 push — constants.js PUSH_TILES_EFFECT / PUSH_EFFECT_SKILLS).
|
||||
|
||||
@@ -99,8 +99,9 @@ export const PALSY_MAX = 3;
|
||||
* ≥ 0 → all the way to the pull point (必定拉至身前), −1 → PULL_WEAK_SHARE of the starting distance, −2 → PULL_CRAWL
|
||||
* tiles, ≤ −3 → nothing. A pull "至面前" aims at the point PULL_ORIGIN tiles in front of the puller (拉力起点 "干员前方0.5格
|
||||
* 距离处") and stops once the target is within PULL_STOP_RADIUS of the puller's centre (急停 "拖拽者中心半径0.6708").
|
||||
* A directional push (推击手 / 朝部署方向) on a target more than 45° off the direction or nearer than
|
||||
* PUSH_DIRECTIONAL_MIN_DIST becomes radial with 受力等级 −2 (推与拉 "特殊修正").
|
||||
* A directional push (推击手, 野鬃 S2 — the client buff template knockback[dir] —, 朝部署方向) on a target more than 45°
|
||||
* off the direction or nearer than PUSH_DIRECTIONAL_MIN_DIST becomes radial with 受力等级 −2 (推与拉 "特殊修正";
|
||||
* knockback[dir] _decreaseForceLevelWhenNotInDirection 2).
|
||||
* PRTS 推与拉 gives two columns of 理想移动距离: 弹道 (a push carried by a projectile — "温蒂的23技能、阿消的12技能"; equal
|
||||
* to the 游戏数据基础 table above) and 特效 (an effect push, one frame less of travel — "食铁兽的12技能、见行者的12技能"):
|
||||
* PUSH_TILES_EFFECT, used by the skills in PUSH_EFFECT_SKILLS (见行者 S1 护身射击 / S2 惊爆射击, the only 特效 pushers of
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
// Passive skills only apply stat mods (for bb.duration s when the text says "N秒内": 宴) and the self/counter effects
|
||||
// above — their scales describe procs (bombs, sword rain, counters) that need a hand-authored kit.
|
||||
// force→onHit displacement with the official 力度 − 重量 rules (Battle.push / pullToFront): a pull "至面前" when the text says
|
||||
// 拖拽 or for hookmasters, else a push — along the unit's direction when the text says 朝部署方向 / 向前 / 身前方向 or for
|
||||
// 推击手 (directional), otherwise away from the unit (radial); a skill of constants.js PUSH_EFFECT_SKILLS (见行者 S1) pushes
|
||||
// by PRTS 推与拉's 特效 column.
|
||||
// 拖拽 or for hookmasters, else a push — along the unit's direction when the text says 往攻击方向 (the 推击手 wording, PRTS
|
||||
// 推与拉 方向力) / 朝部署方向 / 向前 / 身前方向 or for 推击手 (directional), otherwise away from the unit (radial); a skill of
|
||||
// constants.js PUSH_EFFECT_SKILLS (见行者 S1) pushes by PRTS 推与拉's 特效 column. These keywords are only a fallback:
|
||||
// the hand-written kits follow the client's buff templates (knockback[dir] = directional, knockback[relative] = radial),
|
||||
// and the text can mislead — 琳琅诗怀雅 S3's "向前推开" is knockback[relative] (kits/tier3.js). No pool skill pushes
|
||||
// through this path with 向前 / 往攻击方向; the only generic push is 见行者 S1, a 推击手.
|
||||
|
||||
import { normalizeSkill } from '../simdata.js';
|
||||
import { sortEnemyTargets } from '../targeting.js';
|
||||
@@ -218,7 +221,7 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
|
||||
const hasForce = !passive && forceRaw !== undefined;
|
||||
const force = forceRaw ?? 0; // 力度 (微小力 −1 … 特大力 5)
|
||||
const pull = /拖拽/.test(desc) || (!/推开|击退/.test(desc) && def && def.subProf === 'hookmaster');
|
||||
const directional = !pull && (/朝部署方向|向前|身前方向/.test(desc) || (def && def.subProf === 'pusher'));
|
||||
const directional = !pull && (/攻击方向|朝部署方向|向前|身前方向/.test(desc) || (def && def.subProf === 'pusher'));
|
||||
const effectPush = PUSH_EFFECT_SKILLS.has(sk.id);
|
||||
const hitElement = element && !counterText;
|
||||
|
||||
|
||||
@@ -927,9 +927,15 @@ export default {
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 1_19 野鬃 夹枪冲锋: wider range (skill grid), ATK +atk, hits push the target away (attack@force 1 = 中力; radial — away
|
||||
// from her centre, PRTS 推与拉: every push but the 推击手' is radial) by the official 力度 − 重量 distance (Battle.push:
|
||||
// weight 0 → 2.14 tiles, 1 → 1.7, 2 → 0.44, 3 → 0.12, ≥ 4 → none; user playtest #6 item 14).
|
||||
// 1_19 野鬃 夹枪冲锋: wider range (skill grid), ATK +atk, "攻击会把目标往攻击方向中等力度地推开" (attack@force 1 = 中力) by
|
||||
// the official 力度 − 重量 distance (Battle.push: weight 0 → 2.14 tiles, 1 → 1.7, 2 → 0.44, 3 → 0.12, ≥ 4 → none; user
|
||||
// playtest #6 item 14). A directional push (方向力, PRTS 推与拉): the client's S2 attack ability (charpack
|
||||
// char_496_wildmn, anim Skill_2) carries buff wildmn_s_2[force] of template knockback[dir] — buff_template_data:
|
||||
// Knockback {_useSourceDirection: true, _decreaseForceLevelWhenNotInDirection: 2} — i.e. along her deploy direction,
|
||||
// radial at 受力等级 −2 for a target > 45° off it or < 0.25 tile away (特殊修正); the radial pushes are
|
||||
// knockback[relative] (琳琅诗怀雅 S3, 山 S3, 莫斯提马 S3). Her text uses the 推击手 wording "往攻击方向". So an enemy she
|
||||
// grabs behind her centre (the hand-over after a push freed her block) is not thrown 1.7 tiles towards the objective
|
||||
// (player feedback D2, "往攻击方向相反方向推"; it was a radial push before).
|
||||
// 一致向前: after deploying (normal flag 0: first deployment only; elite flag 1: every deployment) every undeployed
|
||||
// 【近卫】 operator of the player costs `value` less DP to deploy (≤ max_stack_cnt per operator until it deploys).
|
||||
// Alternate S1 骑枪刺击 (PASSIVE, ON_DEPLOY): for `duration` s after every deployment ASPD +attack_speed.
|
||||
@@ -954,7 +960,7 @@ export default {
|
||||
attack: {
|
||||
onHit({ battle, unit, target }) {
|
||||
if (!target || !target.alive || target.side !== 'enemy') return;
|
||||
battle.push(target, force, { from: unit });
|
||||
battle.push(target, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] } });
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -401,8 +401,9 @@ const KITS = {
|
||||
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
|
||||
// S1 仗义疏财 (passive, 2 coins): an attack spends a coin to heal the most injured ally (< 70 % HP) of the 8
|
||||
// surrounding tiles for attack@heal_scale × ATK. S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin;
|
||||
// closing it spends every coin on random ground enemies of the front range (atk_scale phys + small push forward;
|
||||
// PRTS 备注: 地面敌方单位, 弹道不可对空).
|
||||
// closing it spends every coin on random ground enemies of the front range (atk_scale phys + a small push, radial
|
||||
// despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2: the RandomGold ability
|
||||
// (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位, 弹道不可对空).
|
||||
// Auto-close (the mode casts everything itself; the player's "主动关闭" is not available): once the purse is full
|
||||
// (10) and an enemy stands in range. 精锐 module MER-Y: ATK +4 % per trait payment (≤ 5 stacks).
|
||||
chess_char_3_04_a: (bb, chess, def) => {
|
||||
@@ -494,8 +495,10 @@ const KITS = {
|
||||
if (!e) break;
|
||||
spent++;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * cash, type: 'phys', isSkill: true, tags: ['skill', 'swire2Cash'] });
|
||||
// "将目标小力地向前推开": a directional push along her direction (Battle.push, official 力度 − 重量 distance)
|
||||
if (e.alive) battle.push(e, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] } });
|
||||
// "将目标小力地向前推开" — PRTS 备注 "金币弹道…推开效果为径向推动", client knockback[relative]: a radial push
|
||||
// away from her centre (not along her direction, so no 45° / 0.25-tile 特殊修正), official 力度 − 重量
|
||||
// distance (Battle.push)
|
||||
if (e.alive) battle.push(e, force, { from: unit });
|
||||
}
|
||||
fx(battle, 'coin', unit, { n: 0, spent, skill: 'swire2_3' });
|
||||
},
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Player feedback after 0.1.0, batch 4, workstream WM — D2 "干员野鬃的技能有时会把怪物往攻击方向相反方向推".
|
||||
//
|
||||
// Official: 野鬃 S2 夹枪冲锋 "攻击会把目标往攻击方向中等力度地推开" is a 方向力 (PRTS 推与拉: "如果该目标被沿着设定的方向推动,
|
||||
// 则该力是方向力…常见的推击手特种干员的推力即为此种力"; the 推击手 use the same "往攻击方向…推开" wording). Client data: her
|
||||
// S2 attack ability (charpack char_496_wildmn, anim Skill_2) carries buff wildmn_s_2[force] of template knockback[dir],
|
||||
// which buff_template_data defines as Knockback {_useSourceDirection: true, _decreaseForceLevelWhenNotInDirection: 2}:
|
||||
// a push along her deploy direction, turned radial at 受力等级 −2 when the target is more than 45° off that direction
|
||||
// or nearer than 0.25 tile ("特殊修正"). The remake pushed her targets radially at
|
||||
// full force, so an enemy behind her centre — one that walked through her tile while her block was full and that she
|
||||
// grabbed once a push freed it (the hand-over, DESIGN §19.2), or one coming at her back — was thrown 1.7–2.14 tiles
|
||||
// the other way, towards the protection objective (real act2 m01 R5: 7 % of her pushes facing RIGHT, up to 42 % in
|
||||
// other facings). Now: directional, so such a target moves at most the −2 distance (≤ 0.44 tile).
|
||||
// Audit (direction only): 琳琅诗怀雅 S3's coin push "向前推开" is radial — PRTS 备注 "推开效果为径向推动"; client
|
||||
// swire2_s_3[knockback] of template knockback[relative] (_useSourceDirection false) — and the remake pushed it along her
|
||||
// facing (and at −2 off the 45° cone); now radial from her centre, full force.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource, hasGeneratedData } from '../../server/sim/simdata.js';
|
||||
import { PUSH_TILES, PUSH_DIRECTIONAL_MIN_DIST } from '../../server/sim/constants.js';
|
||||
import { rotateOffset, toLocal } from '../../server/sim/dir.js';
|
||||
import { Battle } from '../../server/sim/Battle.js';
|
||||
import { getData } from '../../server/data.js';
|
||||
import { GameData } from '../../server/match/gamedata.js';
|
||||
import { setupMatchWaves, buildNormalWave } from '../../server/match/waves.js';
|
||||
import { createRng } from '../../server/sim/rng.js';
|
||||
|
||||
const REAL = { skip: !hasGeneratedData() };
|
||||
const approx = (a, b, eps, msg) => assert.ok(Math.abs(a - b) <= eps, `${msg ?? ''} ${a} ≈ ${b}`);
|
||||
const WILDMN = 'chess_char_1_19_a';
|
||||
const foe = (key, o = {}) => enemyRec({ key, hp: 1e8, speed: 0, atk: 0, ...o });
|
||||
const FULL = PUSH_TILES[0]; // 中力 (1) vs weight 1 = 受力等级 0
|
||||
const WEAK = PUSH_TILES[-2]; // the same push turned radial by the 特殊修正 (受力等级 −2)
|
||||
|
||||
// her tile per facing on the flat stage: room ahead for a full push and behind for a short one
|
||||
const HOME = { RIGHT: [10, 4], LEFT: [10, 7], UP: [10, 5], DOWN: [11, 5] };
|
||||
|
||||
/**
|
||||
* 野鬃 (S2 ready) facing `dir`, one static weight-1 enemy at `off` = [dRow, dCol] from her centre in her facing-RIGHT
|
||||
* frame. Returns the enemy's displacement by her first skill hit in that frame: { fwd, side, len }.
|
||||
*/
|
||||
function wildmnHit(dir, off, { side = 'L', kind = 'normal', mass = 1 } = {}) {
|
||||
const [hr, hc] = HOME[dir];
|
||||
// the mirrored right half of a boss field: board (r, c) → field (r − 7, 20 − c), RIGHT ↔ LEFT
|
||||
const fr = kind === 'boss' ? hr - 7 : hr, fc = side === 'R' ? 20 - hc : hc;
|
||||
const fieldDir = side === 'R' ? { RIGHT: 'LEFT', LEFT: 'RIGHT', UP: 'UP', DOWN: 'DOWN' }[dir] : dir;
|
||||
const [ar, ac] = rotateOffset(off[0], off[1], fieldDir);
|
||||
let before = null, moved = null;
|
||||
const h = makeBattle({
|
||||
kind, timeLimit: 30, autoFinish: false, hooks: [],
|
||||
defs: { enemies: { enemy_t: foe('enemy_t', { mass }) } },
|
||||
players: [{ playerId: 'p1', seat: 0, side, colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: WILDMN, row: hr, col: hc, dir, carryState: { sp: 1e3 } }], bonds: {}, playerEffects: [] }],
|
||||
enemies: [{ key: 'enemy_t', pos: [fr + ar, fc + ac] }],
|
||||
setup(b) {
|
||||
b.on('beforeAttack', (c) => { if (c.attacker.defId === WILDMN && c.isSkill && c.targets[0]) before = [c.targets[0].y, c.targets[0].x]; }, { priority: -2000 });
|
||||
b.on('attack', (c) => { if (c.attacker.defId === WILDMN && c.isSkill && before && !moved) moved = [c.targets[0].y - before[0], c.targets[0].x - before[1]]; }, { priority: -2000 });
|
||||
},
|
||||
});
|
||||
const u = h.unit(WILDMN);
|
||||
assert.equal(u.dir, fieldDir, 'field direction');
|
||||
assert.ok(h.runUntil(() => moved != null, 6), `${side} ${dir} ${off}: a skill hit`);
|
||||
const [lf, ll] = toLocal(moved[0], moved[1], fieldDir); // [left (+row in the RIGHT frame), forward]
|
||||
return { fwd: ll, side: lf, len: Math.hypot(lf, ll) };
|
||||
}
|
||||
|
||||
test('D2 野鬃 S2 pushes along her direction ("往攻击方向", the 推击手 wording) — straight ahead the full 中力 distance, every facing', REAL, () => {
|
||||
const ds = getDefaultSource();
|
||||
assert.match(ds.rawChess(WILDMN).skill.desc, /往攻击方向/, 'data: the directional wording');
|
||||
assert.equal(ds.rawChess(WILDMN).skill.bb['attack@force'], 1, 'data: 中等力度 = 1');
|
||||
for (const dir of ['RIGHT', 'UP', 'LEFT', 'DOWN']) {
|
||||
const m = wildmnHit(dir, [0, 0.7]);
|
||||
approx(m.fwd, FULL, 1e-6, `${dir}: forward`);
|
||||
approx(m.side, 0, 1e-9, `${dir}: no sideways`);
|
||||
}
|
||||
});
|
||||
|
||||
test('D2 野鬃 S2: a target within 45° of her direction goes straight along it (not radially sideways)', REAL, () => {
|
||||
for (const dir of ['RIGHT', 'UP', 'LEFT', 'DOWN']) {
|
||||
for (const lat of [0.3, -0.3]) {
|
||||
const m = wildmnHit(dir, [lat, 0.6]); // 26.6° off, inside her block radius and her range (tile ahead)
|
||||
approx(m.fwd, FULL, 1e-6, `${dir} ${lat}: along her direction`);
|
||||
approx(m.side, 0, 1e-9, `${dir} ${lat}: no sideways (radial would be ${(FULL * lat / Math.hypot(lat, 0.6)).toFixed(2)})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('D2 野鬃 S2: a target behind her, beside her (> 45° off) or on her centre — radial at 受力等级 −2, never a full throw backwards', REAL, () => {
|
||||
for (const dir of ['RIGHT', 'UP', 'LEFT', 'DOWN']) {
|
||||
// behind her centre (blocked by her, as after the hand-over): the user's case — it used to fly 1.7 tiles backwards
|
||||
const back = wildmnHit(dir, [0, -0.45]);
|
||||
approx(back.len, WEAK, 1e-6, `${dir} behind: the −2 distance`);
|
||||
assert.ok(back.fwd < 0 && Math.abs(back.side) < 1e-9, `${dir} behind: radial (away from her)`);
|
||||
// beside her, 63° off: radial, −2
|
||||
const beside = wildmnHit(dir, [0.6, 0.3]);
|
||||
approx(beside.len, WEAK, 1e-6, `${dir} beside: the −2 distance`);
|
||||
approx(beside.side / beside.len, 0.6 / Math.hypot(0.6, 0.3), 1e-6, `${dir} beside: radial`);
|
||||
// nearer than 0.25 tile: radial, −2
|
||||
const near = wildmnHit(dir, [0.1, 0.1]);
|
||||
assert.ok(Math.hypot(0.1, 0.1) < PUSH_DIRECTIONAL_MIN_DIST);
|
||||
approx(near.len, WEAK, 1e-6, `${dir} near: the −2 distance`);
|
||||
approx(near.side, near.fwd, 1e-6, `${dir} near: radial`);
|
||||
}
|
||||
});
|
||||
|
||||
test('D2 野鬃 S2 on the mirrored right half of the Final Assault field: the same pushes in its own direction', REAL, () => {
|
||||
for (const dir of ['RIGHT', 'LEFT']) {
|
||||
const ahead = wildmnHit(dir, [0, 0.7], { side: 'R', kind: 'boss' });
|
||||
approx(ahead.fwd, FULL, 1e-6, `R ${dir}: forward`);
|
||||
approx(ahead.side, 0, 1e-9, `R ${dir}: no sideways`);
|
||||
const diag = wildmnHit(dir, [0.3, 0.6], { side: 'R', kind: 'boss' });
|
||||
approx(diag.fwd, FULL, 1e-6, `R ${dir} diagonal: along its direction`);
|
||||
approx(diag.side, 0, 1e-9, `R ${dir} diagonal: no sideways`);
|
||||
const back = wildmnHit(dir, [0, -0.45], { side: 'R', kind: 'boss' });
|
||||
approx(back.len, WEAK, 1e-6, `R ${dir} behind: the −2 distance`);
|
||||
assert.ok(back.fwd < 0, `R ${dir} behind: away from her`);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// The real product path: the act2 m01 lane, the match's own R5 wave (server/match/waves.js), 野鬃 on the melee tiles
|
||||
// next to the lower lane's last bend. Enemies walk through her tile while she holds one; when a push frees her block
|
||||
// she grabs the one behind her centre — and threw it towards the objective.
|
||||
|
||||
function realRun(chessId, r, c, dir, round = 5) {
|
||||
const quiet = { warn() {}, error() {}, info() {}, debug() {} };
|
||||
const data = getData({ log: quiet });
|
||||
const modeId = 'mode_multi_normal';
|
||||
const gd = new GameData(data, modeId);
|
||||
const setup = setupMatchWaves(gd, createRng(11 + round));
|
||||
const w = buildNormalWave(gd, createRng(11 + round + round), setup.factions, round);
|
||||
const b = new Battle({
|
||||
seed: 7 + r * 31 + c, kind: 'normal', modeId, round, stageId: 'act2autochess_m01', timeLimit: w.timeLimit || 60,
|
||||
routes: w.routes, spawns: w.spawns, enemyOverrides: w.overrides ?? {}, fieldId: 'n:P1', logger: quiet,
|
||||
players: [{ playerId: 'P1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId, row: r, col: c, dir }] }],
|
||||
flags: { layerGainsEnabled: true, dpInit: 10, dpPerSec: 1, dpMax: 99 },
|
||||
});
|
||||
const u = b.allyUnits.find((x) => x.defId === chessId);
|
||||
const pushes = [];
|
||||
const orig = b.push.bind(b);
|
||||
b.push = (e, force, o = {}) => {
|
||||
if (o.from !== u) return orig(e, force, o);
|
||||
const x0 = e.x, y0 = e.y, behind = (x0 - u.x) * u.fwd[1] + (y0 - u.y) * u.fwd[0] < 0;
|
||||
const moved = orig(e, force, o);
|
||||
if (moved > 1e-6) pushes.push({ dx: e.x - x0, dy: e.y - y0, moved, behind, massLevel: e.s.massLevel });
|
||||
return moved;
|
||||
};
|
||||
for (let n = 0; !b.finished && n < 30 * 120; n++) b.step();
|
||||
return { u, pushes };
|
||||
}
|
||||
|
||||
test('D2 real battle (act2 m01, R5 official wave): no push of 野鬃 throws an enemy behind her backwards further than the −2 distance', REAL, () => {
|
||||
let behind = 0, total = 0;
|
||||
for (const [r, c] of [[9, 3], [9, 4], [9, 5]]) {
|
||||
for (const chessId of ['chess_char_1_19_a', 'chess_char_1_19_b']) {
|
||||
const { u, pushes } = realRun(chessId, r, c, 'RIGHT');
|
||||
const [fr, fc] = u.fwd;
|
||||
for (const p of pushes) {
|
||||
total++;
|
||||
const along = p.dx * fc + p.dy * fr;
|
||||
if (p.behind) behind++;
|
||||
// either straight along her direction, or the −2 radial push (受力等级 = 1 − weight − 2)
|
||||
if (along < p.moved * (1 - 1e-6)) {
|
||||
assert.ok(p.moved <= PUSH_TILES[-1] + 1e-6, `(${r},${c}) ${chessId}: off-direction push of ${p.moved.toFixed(2)} tiles (weight ${p.massLevel})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(total > 50, `pushes ${total}`);
|
||||
assert.ok(behind > 0, 'the hand-over case happens (an enemy grabbed behind her centre)');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// 琳琅诗怀雅 S3 千金一掷 — PRTS 备注 "金币弹道…推开效果为径向推动": the coin push is radial from her centre.
|
||||
|
||||
function swireCoin(off, dir = 'RIGHT') {
|
||||
const id = 'chess_char_3_04_a';
|
||||
const [hr, hc] = [10, 5];
|
||||
const [ar, ac] = rotateOffset(off[0], off[1], dir);
|
||||
const h = makeBattle({
|
||||
timeLimit: 30, autoFinish: false, hooks: [],
|
||||
defs: { enemies: { enemy_t: foe('enemy_t', { mass: 1 }) } },
|
||||
units: [{ chessId: id, row: hr, col: hc, dir, skillIndex: 2 }],
|
||||
enemies: [{ key: 'enemy_t', pos: [hr + ar, hc + ac] }],
|
||||
});
|
||||
const u = h.unit(id);
|
||||
assert.equal(u.skill.id, 'skchr_swire2_3');
|
||||
h.step();
|
||||
const e = h.enemies()[0];
|
||||
assert.ok(u.skill.active || u.skill.activate('test', { free: true }), 'S3 on');
|
||||
u.mem.coins = 1;
|
||||
const y0 = e.y, x0 = e.x;
|
||||
u.skill.end('manual');
|
||||
const [ls, lf] = toLocal(e.y - y0, e.x - x0, dir);
|
||||
return { fwd: lf, side: ls, len: Math.hypot(ls, lf) };
|
||||
}
|
||||
|
||||
test('D2 audit: 琳琅诗怀雅 S3 coins push radially from her centre at full force (PRTS 备注 "推开效果为径向推动")', REAL, () => {
|
||||
const ds = getDefaultSource();
|
||||
const s3 = ds.rawChess('chess_char_3_04_a').skills.find((s) => s.skillId === 'skchr_swire2_3');
|
||||
assert.equal(s3.bb.force, 0, 'data: 小力 (normal)');
|
||||
const full = PUSH_TILES[-1]; // 小力 (0) vs weight 1
|
||||
for (const dir of ['RIGHT', 'UP']) {
|
||||
// 20.6° off her direction: radial, so partly sideways (the remake pushed it straight along her facing)
|
||||
const a = swireCoin([0.3, 0.8], dir);
|
||||
approx(a.len, full, 1e-6, `${dir} cone: full distance`);
|
||||
approx(a.side / a.len, 0.3 / Math.hypot(0.3, 0.8), 1e-6, `${dir} cone: radial`);
|
||||
// 66° off: radial at full force (the remake turned it into a −2 push: nothing at all)
|
||||
const b = swireCoin([0.45, 0.2], dir);
|
||||
approx(b.len, full, 1e-6, `${dir} beside: full distance`);
|
||||
approx(b.side / b.len, 0.45 / Math.hypot(0.45, 0.2), 1e-6, `${dir} beside: radial`);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user