feedback5 O20: the 缚地 status (groundbind) — an enemy air unit counts as a ground unit and cannot move
ba.groundbind "目标变为地面单位,无法移动;使部分近地悬浮敌人掉落;对重量大于3的单位持续时间减半" (予愿安洁莉娜 S2 needs it): - buffs.js: STATUS groundbind (flag groundbind + noMove, move x0), FLAG_KEYS, RESIST_STATUSES (as 束缚, [ASSUMED]); - units.js isFlying: an enemy under 缚地 is a ground unit unless a 浮空 lifts it (same result as before without the flag); - battle/status.js: 浮空 lands on a 缚地 data flyer (PRTS 异常效果 "…且不持有缚地异常…则Buff取消"), 缚地 halved on weight > 3; - content/enemies: 掠海漂移体 / 吉兆飞鳞 drop on 缚地 as their PRTS text says (吉兆飞鳞 stays down while it holds); - render/style.js: the 束缚 icon for it; docs/SIM.md (§2 isFlying, §3 flags / status table / 抵抗). Nothing applies it yet: every golden family is unchanged. Test: test/sim/feedback5-groundbind.test.js.
This commit is contained in:
1 parent
996f5d20fe
commit
83e879a335
8 files changed
+126
-25
No files matched your search
+8
-6
@@ -460,8 +460,9 @@ extraRangeKeys (content extra targets, `battle.setExtraRange`), blocking[] (alli
|
||||
stats {dmg,kills,heal,taken,attacks}, mem {} and trait {} (free scratch space), persist {redeployMul, …}.`
|
||||
Getters: `s` (aggregated stats), `maxHp`, `atk`, `hpRatio`, `sp`, `spMax`, `canAct`, `isFlying`, `statusFlags` (UF bits),
|
||||
`dmgType`, `weight`. `isFlying` = an **air unit** for every targeting / ground-only rule: `motion` FLY, or an enemy with
|
||||
flag `float` (近地悬浮, PRTS 术语释义 "算作空中单位") or `levitate` (浮空 "变为空中单位"); movement and pathing read `motion`
|
||||
(a hovering enemy keeps walking the ground path). `deploySeq` counts deployments (and identifies one: `seq === u.deploySeq`);
|
||||
flag `float` (近地悬浮, PRTS 术语释义 "算作空中单位") or `levitate` (浮空 "变为空中单位") — never an enemy under 缚地 (flag
|
||||
`groundbind`, "目标变为地面单位") unless a 浮空 lifts it again; movement and pathing read `motion` (a hovering enemy keeps
|
||||
walking the ground path). `deploySeq` counts deployments (and identifies one: `seq === u.deploySeq`);
|
||||
`aggroSeq` is the aggro order (= deploySeq, except the summons of the initial deployment, §1).
|
||||
|
||||
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
|
||||
@@ -531,8 +532,8 @@ A `rangeExtend` on a `persist` never-expiring buff is **permanent**: it also wid
|
||||
a running skill's range too, unless that skill's range ignores 攻击距离 (`targeting.noRangeExtend`; PRTS 数值范围 "根据配置
|
||||
不同,任何范围都可以受/不受该属性影响" — 信仰搅拌机 S3 "此技能的攻击范围不受“攻击距离”属性影响").
|
||||
**Flags:** `stun freeze sleep silence disarm stealth stealthOff invulnerable unblockable levitate fear cold reveal bind noHeal
|
||||
healFree untargetable blockFly noMove noSp burstLock hidden attract float noDisplace isolated camou liftoff` (`float` = 近地悬浮 (an air
|
||||
unit, `Unit.isFlying`), `liftoff` = an ally's 起飞 (蒂比's skills; gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,
|
||||
healFree untargetable blockFly noMove noSp burstLock hidden attract float noDisplace isolated groundbind camou liftoff` (`float` = 近地悬浮 (an air
|
||||
unit, `Unit.isFlying`), `groundbind` = 缚地 (the status below: an enemy air unit counts as a ground unit), `liftoff` = an ally's 起飞 (蒂比's skills; gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,
|
||||
可以阻挡飞行敌人"): it blocks no ground enemy (`Battle._blockerFor`) and has 对地规避 — no ground enemy (not `isFlying`)
|
||||
selects it, so no selected damage or status of one lands on it (`targeting.js evadesGround`), while what selects nobody
|
||||
still does (`ignoreSelect` / 无来源: 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks); flyers,
|
||||
@@ -596,9 +597,10 @@ of coverage per 3 s), kept because the current wording no longer says so (feedba
|
||||
| `stealth` / `reveal` | 隐匿: untargetable unless blocked (an ally: only the enemy it blocks attacks it, and an enemy's area effects and buff auras skip it even when it blocks that enemy — `targeting.js areaSelectable` / `auraSelectable`, GitHub #97, owner 2026-10-04; 0.1.2 made an exception for that blocker) / cancels stealth. An enemy's 隐匿 also stays off after a block: each block's end (`Battle._stealthSwitch`, every release path) switches each of its 隐匿 sources off for `STEALTH_RESTORE` (3) s — PRTS 作战机制 §隐匿 "不被阻挡的3秒后重新进入隐匿" — or the source's own "(解除阻挡N秒后恢复)" (buff `data.stealthRestore`: 0 s for 业余竞演者, 节日爵士乐手, 假想敌:骨刺, 流泪小子, 访问团强攻冠军 and 清明's veil, 1 s for the 家族灭迹人); a new block inside it lifts it again and its end restarts the window; our operators' 隐匿 / 迷彩 never lift by blocking (DESIGN §22.8). `targeting.js enemyStealthed` is the one test: the b.snap stealth bit is set only while its 隐匿 is on (drawn solid otherwise); an operator's radius area damage (`foesInRadius`) skips it too (PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 the splash still hit it) | – |
|
||||
| `camou` (迷彩) | an ally's camouflage (ba.camou "不阻挡时不成为敌方普通攻击的目标(无法躲避溅射类攻击)"): like `stealth` for enemy targeting (only the enemy it blocks attacks it) — but an enemy's splash and other area effects still hit it (`areaSelectable` does not check it) — and on screen (b.snap stealth bit, `snapshot.js flagsOf`), but not 隐匿 for 隐匿-conditions (叙拉古, 家族徽章) and under its own buff keys. 忍冬 S3 (key `vulpis:camou`, until her next cast), 寒芒克洛丝 S1 | – |
|
||||
| `invulnerable` | ignores damage | – |
|
||||
| `levitate` (浮空) | stun + unblockable (unblocks enemies) + 失衡免疫 (`noDisplace`); an air unit meanwhile (`isFlying`: melee cannot hit it); **half duration on units with (current) massLevel > 3**; refused on data flyers (`motion` FLY) and units already levitated (PRTS 异常效果 "若单位数据上为飞行单位…或是持有浮空异常则Buff取消") — a 近地悬浮 enemy is WALK in its data, so it can be levitated; 浮空 is not one of the 近地悬浮 enemies' drop triggers | – |
|
||||
| `levitate` (浮空) | stun + unblockable (unblocks enemies) + 失衡免疫 (`noDisplace`); an air unit meanwhile (`isFlying`: melee cannot hit it); **half duration on units with (current) massLevel > 3**; refused on data flyers (`motion` FLY) that hold no 缚地 and on units already levitated (PRTS 异常效果 "若单位数据上为飞行单位且不持有缚地异常或是持有浮空异常则Buff取消") — a 近地悬浮 enemy is WALK in its data, so it can be levitated; 浮空 is not one of the 近地悬浮 enemies' drop triggers | – |
|
||||
| `groundbind` (缚地) | gamedata_const ba.groundbind "目标变为地面单位,无法移动;使部分近地悬浮敌人掉落;对重量大于3的单位持续时间减半": an enemy air unit (data flyer, 近地悬浮) counts as a ground unit (`isFlying` false: melee operators hit it, ground blockers block it) and cannot move (`noMove`); **half duration on units with (current) massLevel > 3**; one of 抵抗's statuses [ASSUMED, as 束缚]; the drop trigger of 掠海漂移体 / 吉兆飞鳞 (content/enemies: "受…缚地影响后"). 予愿安洁莉娜 S2 (kits/ops/op-aglna2.js) | – |
|
||||
| `attract` (诱导) | 无法被阻挡并向目标位置移动: unblockable (released); the engine walks it (own speed, grid path re-planned on obstacle changes, after a push and after an outranking 恐惧; flyers straight) to `opts.point` (`[r, c]` or `{x, y}`, default the source's tile, clamped to the rect) and keeps it there; stun/bind/sleep stop it; its route re-plans from where it stands when the status ends. A new application moves the point | point |
|
||||
| `resist` (抵抗) | the control statuses of `RESIST_STATUSES` (晕眩 冻结 寒冷 沉睡 恐惧 战栗 诱导 浮空 束缚 沉默 缴械 停顿 减速) applied to the unit last ×(1 − value); a resisting unit loses one 麻痹 stack every 5 s. **同名效果不叠加** — `battle.resistOf(u)` = the strongest `status: 'resist'` buff (never a product): several sources (灵知, 流明, 寒檀, enemy talents) never compound. *strongest*; a permanent one that survives death is a `persist` buff with `status: 'resist', data: { value }` | 0.5 (≤ 0.95) |
|
||||
| `resist` (抵抗) | the control statuses of `RESIST_STATUSES` (晕眩 冻结 寒冷 沉睡 恐惧 战栗 诱导 浮空 束缚 沉默 缴械 停顿 减速 缚地) applied to the unit last ×(1 − value); a resisting unit loses one 麻痹 stack every 5 s. **同名效果不叠加** — `battle.resistOf(u)` = the strongest `status: 'resist'` buff (never a product): several sources (灵知, 流明, 寒檀, enemy talents) never compound. *strongest*; a permanent one that survives death is a `persist` buff with `status: 'resist', data: { value }` | 0.5 (≤ 0.95) |
|
||||
| `taunt` | taunt level +value | 1 |
|
||||
| `weaken` | atkMul 1 − value (*strongest*) | 0.3 |
|
||||
| `aspdDown` | aspd + value (*strongest*) | −30 |
|
||||
|
||||
@@ -122,7 +122,7 @@ export const PROJ = Object.freeze({
|
||||
export const STATUS_ICON = Object.freeze({
|
||||
stun: 'stun', freeze: 'freeze', cold: 'cold', stealth: 'stealth', shield: 'shield', fragile: 'fragile',
|
||||
artsFragile: 'fragile', physFragile: 'fragile', elemFragile: 'fragile', sleep: 'sleep', invulnerable: 'invuln',
|
||||
silence: 'silence', slow: 'slow', sluggish: 'slow', bind: 'bind', fear: 'fear', tremble: 'fear', weaken: 'weaken',
|
||||
silence: 'silence', slow: 'slow', sluggish: 'slow', bind: 'bind', groundbind: 'bind', fear: 'fear', tremble: 'fear', weaken: 'weaken',
|
||||
levitate: 'levitate', taunt: 'taunt', defDown: 'weaken', resDown: 'weaken', aspdDown: 'slow', disarm: 'silence',
|
||||
burn: 'burn', burnBurst: 'burn', neural: 'neural', neuralBurst: 'neural', necrosis: 'necrosis', apoptosis: 'necrosis',
|
||||
// a 傀儡师 fighting as its <替身> (sim professions.js buff 'trait:substitute', the 20 s form)
|
||||
|
||||
@@ -135,8 +135,8 @@ export class BattleStatus {
|
||||
* handlers may cancel it or change `duration` / `value`. Official rules (buffs.js STATUS): 抵抗 (the `resist` status)
|
||||
* shortens the RESIST_STATUSES by its value (default half; applied after `beforeStatus`; `resistApplied` skips that
|
||||
* pass — the cold-on-cold 冻结 below already used post-抵抗 lengths). A second 寒冷 while 寒冷 remains applies 冻结 for
|
||||
* max(remaining, this cold after 抵抗) (PRTS 术语释义 寒冷 「持续时间取双方之中最高」). 浮空 lasts half as long on units heavier
|
||||
* than LEVITATE_HALF_WEIGHT (current massLevel); 冻结's RES cut hits enemies only; 麻痹 adds stacks; "同名效果取最高"
|
||||
* max(remaining, this cold after 抵抗) (PRTS 术语释义 寒冷 「持续时间取双方之中最高」). 浮空 and 缚地 last half as long on units
|
||||
* heavier than LEVITATE_HALF_WEIGHT (current massLevel); 冻结's RES cut hits enemies only; 麻痹 adds stacks; "同名效果取最高"
|
||||
* statuses (`valued`) keep the strongest value — a weaker application only extends past the stronger one's end (it
|
||||
* then resumes); `stackAs` = the value such an application competes with instead of its own (its effect stays
|
||||
* `value`): an effect the game stacks as another strength — Raidian S3's 虚弱, PRTS 备注 "在叠加时视为90%(1级~6级)/80%
|
||||
@@ -162,9 +162,9 @@ export class BattleStatus {
|
||||
const immune = target.def && target.def.immune;
|
||||
if (!opts.force && tpl.immune && immune && immune.has(tpl.immune)) return false;
|
||||
// 浮空 Buff (PRTS 异常效果: "若单位数据上为飞行单位且不持有缚地异常或是持有浮空异常则Buff取消"; 行动方式 "行动类型(数据)为
|
||||
// 飞行的单位、以及已持有浮空异常的单位无法被施加浮空Buff"): refused on data flyers (`motion` FLY) and units already
|
||||
// levitated — a hovering 近地悬浮 enemy is WALK in its data, so it can be levitated (no 缚地 / 浮空强化 in this mode)
|
||||
if (key === 'levitate' && (target.motion === 'FLY' || target.s.flags.levitate)) return false;
|
||||
// 飞行的单位、以及已持有浮空异常的单位无法被施加浮空Buff"): refused on data flyers (`motion` FLY) that hold no 缚地 and on
|
||||
// units already levitated — a hovering 近地悬浮 enemy is WALK in its data, so it can be levitated (no 浮空强化 in this mode)
|
||||
if (key === 'levitate' && ((target.motion === 'FLY' && !target.s.flags.groundbind) || target.s.flags.levitate)) return false;
|
||||
if (this._hooks.beforeStatus) {
|
||||
const c = this.emit('beforeStatus', { source: opts.source ?? null, target, status: key, duration, value, cancel: false });
|
||||
if (c.cancel || !target.alive) return false;
|
||||
@@ -177,7 +177,8 @@ export class BattleStatus {
|
||||
const rv = this.resistOf(target);
|
||||
if (rv > 0) duration *= 1 - rv;
|
||||
}
|
||||
if (key === 'levitate' && target.s.massLevel > LEVITATE_HALF_WEIGHT) duration /= 2;
|
||||
// 浮空 / 缚地 (ba.levitate, ba.groundbind): "对重量大于3的单位持续时间减半"
|
||||
if ((key === 'levitate' || key === 'groundbind') && target.s.massLevel > LEVITATE_HALF_WEIGHT) duration /= 2;
|
||||
if (!(duration > 0)) return false;
|
||||
if (key === 'cold' && target.findBuff('cold') && !(immune && immune.has('frozen'))) {
|
||||
// PRTS 术语释义 寒冷: 友方寒冷 pairs into 冻结, 「持续时间取双方之中最高」. `duration` is this cold after 抵抗;
|
||||
|
||||
+9
-2
@@ -42,6 +42,8 @@ export const FLAG_KEYS = Object.freeze([
|
||||
// 自缚 (the unit's own immobility: 守墓石像's 转换模式, the 自缚 leaders) beside its `noMove` — 束缚 sets noMove too, and
|
||||
// only 自缚 makes a unit "不视为可达目标" for 余 S2's teleport (PRTS 余 S2 备注)
|
||||
'selfBound', 'healFree', 'stealthOff',
|
||||
// 缚地 (status `groundbind`): an enemy air unit counts as a ground unit meanwhile (Unit.isFlying)
|
||||
'groundbind',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -90,6 +92,11 @@ export const STATUS = Object.freeze({
|
||||
// 浮空: 变为空中单位 (Unit.isFlying),无法移动、攻击及使用技能; 对重量大于3的单位持续时间减半; the state holds 不可阻挡 +
|
||||
// 失衡免疫 (noDisplace: "不会被位移影响", PRTS 异常效果)
|
||||
levitate: { flags: { levitate: true, stun: true, unblockable: true, noDisplace: true }, immune: 'levitate' },
|
||||
// 缚地 (gamedata_const ba.groundbind "目标变为地面单位,无法移动;使部分近地悬浮敌人掉落;对重量大于3的单位持续时间减半"):
|
||||
// an air unit (data flyer, 近地悬浮) counts as a ground unit (Unit.isFlying: melee operators hit it, ground blockers
|
||||
// block it) and cannot move; half duration on units with massLevel > 3 (Battle.applyStatus, as 浮空); a 浮空 still
|
||||
// lands on it (PRTS 异常效果 "若单位数据上为飞行单位且不持有缚地异常…则Buff取消"). 予愿安洁莉娜 S2 (kits/ops/op-aglna2.js)
|
||||
groundbind: { flags: { groundbind: true, noMove: true }, mods: { moveMul: 0 } },
|
||||
// 麻痹: value = stacks (default 1, max 3); each stack cancels one enemy normal attack; lasts until consumed; honours
|
||||
// 麻痹免疫 (enemy_database palsyImmune, e.g. 假想敌:铳 — PRTS 元素: a 神经 burst gives 麻痹 only "若单位不具有麻痹免疫")
|
||||
palsy: { palsy: true, immune: 'palsy' },
|
||||
@@ -120,12 +127,12 @@ export const STATUS = Object.freeze({
|
||||
});
|
||||
|
||||
/**
|
||||
* Control statuses (异常状态) shortened by 抵抗: 晕眩 冻结 寒冷 沉睡 恐惧 战栗 诱导 浮空 束缚 沉默 缴械 停顿 减速. The
|
||||
* Control statuses (异常状态) shortened by 抵抗: 晕眩 冻结 寒冷 沉睡 恐惧 战栗 诱导 浮空 束缚 沉默 缴械 停顿 减速 缚地. The
|
||||
* official term lists 晕眩/寒冷/冻结/恐惧/诱导…; the rest follow the operator kits that grant 抵抗 [ASSUMED]. 麻痹 is
|
||||
* stack-based: it decays instead (RESIST_PALSY_DECAY).
|
||||
*/
|
||||
export const RESIST_STATUSES = Object.freeze(new Set(['stun', 'freeze', 'cold', 'sleep', 'fear', 'tremble', 'attract', 'levitate',
|
||||
'bind', 'silence', 'disarm', 'sluggish', 'slow']));
|
||||
'bind', 'silence', 'disarm', 'sluggish', 'slow', 'groundbind']));
|
||||
|
||||
/** Statuses that should be reported to clients as `['status', id, key, on]`. */
|
||||
export const VISIBLE_STATUS = new Set(Object.keys(STATUS));
|
||||
|
||||
@@ -58,13 +58,13 @@ const SYUFO_CRAWL_STUN = 0.5;
|
||||
/**
|
||||
* 掠海漂移体 (PRTS): 初始模式 近地悬浮 + 失衡免疫, 不会攻击飞行单位; 受晕眩/无法行动/沉睡/冻结/缚地影响后进入爬行模式 for
|
||||
* good and is stunned 0.5 s — a ground unit (blockable, melee operators hit it) that "仅进行阻挡攻击" (only its blocker).
|
||||
* The engine has 晕眩 / 冻结 / 沉睡 (no operator here applies 无法行动 or 缚地). Erosion on every attack.
|
||||
* The engine has 晕眩 / 冻结 / 沉睡 / 缚地 (予愿安洁莉娜 S2; no operator here applies 无法行动). Erosion on every attack.
|
||||
*/
|
||||
function kitSyufo(ab) {
|
||||
return [float(), ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), {
|
||||
spawn(b, e) { e.profile.canTarget = (u) => !u.isFlying; },
|
||||
status(c, b, e, a) {
|
||||
if (a.crawl || !(c.status === 'stun' || c.status === 'freeze' || c.status === 'sleep')) return;
|
||||
if (a.crawl || !(c.status === 'stun' || c.status === 'freeze' || c.status === 'sleep' || c.status === 'groundbind')) return;
|
||||
a.crawl = true;
|
||||
setFloat(b, e, false);
|
||||
e.profile.melee = true;
|
||||
|
||||
@@ -103,7 +103,7 @@ function kitParrot(ab) {
|
||||
},
|
||||
status(c, b, e, a) {
|
||||
if (c.status === 'freeze') { a.chill = true; return; }
|
||||
if (a.down || !(c.status === 'stun' || c.status === 'sleep')) return;
|
||||
if (a.down || !(c.status === 'stun' || c.status === 'sleep' || c.status === 'groundbind')) return;
|
||||
a.down = true;
|
||||
setFloat(b, e, false);
|
||||
b.removeBuff(e, 'ab:parrotRun');
|
||||
@@ -114,8 +114,8 @@ function kitParrot(ab) {
|
||||
tick(b, e, a) {
|
||||
if (a.chill && !e.s.flags.freeze) { a.chill = false; b.applyStatus(e, 'stun', { duration: PARROT_CHILL_STUN, source: null }); }
|
||||
// "离开上述异常效果影响后进入初始模式": s.flags.stun also holds while asleep / frozen (units.js), so a sleep that
|
||||
// outlasts the mode stun keeps it down
|
||||
if (a.down && !e.s.flags.stun) {
|
||||
// outlasts the mode stun keeps it down; so does a 缚地 (flag `groundbind`)
|
||||
if (a.down && !e.s.flags.stun && !e.s.flags.groundbind) {
|
||||
a.down = false;
|
||||
b.removeBuff(e, 'ab:parrotDown');
|
||||
b.removeBuff(e, 'ab:parrotRun');
|
||||
|
||||
+10
-5
@@ -179,13 +179,18 @@ export class Unit {
|
||||
/**
|
||||
* Air unit (空中单位) for every targeting / ground-only rule: FLY movers, and enemies that hover (近地悬浮, buff flag
|
||||
* `float` — PRTS 术语释义 ba.float "算作空中单位"; they keep walking the ground path) or are levitated (浮空, gamedata_const
|
||||
* ba.levitate "变为空中单位"). Movement and pathing read `motion`, never this.
|
||||
* ba.levitate "变为空中单位"). A 缚地 enemy (status `groundbind`, ba.groundbind "目标变为地面单位") is a ground unit
|
||||
* meanwhile — unless a 浮空 lifts it again (浮空 lands on a 缚地 flyer: Battle.applyStatus). Movement and pathing read
|
||||
* `motion`, never this.
|
||||
*/
|
||||
get isFlying() {
|
||||
if (this.motion === 'FLY') return true;
|
||||
if (this.side !== 'enemy') return false;
|
||||
const f = this.s.flags;
|
||||
return !!(f.float || f.levitate);
|
||||
if (this.side === 'enemy') {
|
||||
const f = this.s.flags;
|
||||
if (f.levitate) return true;
|
||||
if (f.groundbind) return false;
|
||||
return this.motion === 'FLY' || !!f.float;
|
||||
}
|
||||
return this.motion === 'FLY';
|
||||
}
|
||||
|
||||
get hpRatio() { const mh = this.s.maxHp; return mh > 0 ? this.hp / mh : 0; }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// test/sim/feedback5-groundbind.test.js — the 缚地 status (0.2.0, O20: 予愿安洁莉娜 S2, kits/ops/op-aglna2.js): gamedata_const
|
||||
// ba.groundbind "目标变为地面单位,无法移动;使部分近地悬浮敌人掉落;对重量大于3的单位持续时间减半" — server/sim/buffs.js STATUS
|
||||
// `groundbind` (flag `groundbind` + `noMove`), units.js Unit.isFlying (a 缚地 enemy is a ground unit unless a 浮空 lifts it),
|
||||
// battle/status.js applyStatus (浮空 lands on a 缚地 data flyer — PRTS 异常效果 "若单位数据上为飞行单位且不持有缚地异常…则Buff
|
||||
// 取消"; the weight halving), the 抵抗 list, and the two hovering enemies whose PRTS text names it (content/enemies:
|
||||
// 掠海漂移体 爬行模式, 吉兆飞鳞 晕眩模式).
|
||||
// Run: node --test test/sim/feedback5-groundbind.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { STATUS, RESIST_STATUSES, FLAG_KEYS } from '../../server/sim/buffs.js';
|
||||
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const ENEMIES = {
|
||||
enemy_fly: enemyRec({ key: 'enemy_fly', hp: 1e9, speed: 0, mass: 0, motion: 'FLY' }),
|
||||
enemy_flywalk: enemyRec({ key: 'enemy_flywalk', hp: 1e9, speed: 0.6, mass: 0, motion: 'FLY' }),
|
||||
enemy_flyheavy: enemyRec({ key: 'enemy_flyheavy', hp: 1e9, speed: 0, mass: 4, motion: 'FLY' }),
|
||||
};
|
||||
|
||||
function arena(units = []) {
|
||||
const h = makeBattle({ defs: { enemies: ENEMIES }, timeLimit: 600, autoFinish: false, seed: 3, flags: { dpPerSec: 0 }, units });
|
||||
h.step();
|
||||
return h;
|
||||
}
|
||||
|
||||
test('缚地 is a catalogue status: flag groundbind + noMove, move ×0, one of 抵抗\'s statuses', () => {
|
||||
assert.deepEqual(STATUS.groundbind, { flags: { groundbind: true, noMove: true }, mods: { moveMul: 0 } });
|
||||
assert.ok(FLAG_KEYS.includes('groundbind'));
|
||||
assert.ok(RESIST_STATUSES.has('groundbind'));
|
||||
});
|
||||
|
||||
test('缚地: an air unit is a ground unit (melee operators hit it) and cannot move; half as long on weight > 3; 抵抗 shortens it; 浮空 lands on a 缚地 flyer; back in the air when it ends', () => {
|
||||
const h = arena([{ uid: 1, chessId: 'chess_char_1_02_a', row: 9, col: 4 }]);
|
||||
const yak = h.unit(1);
|
||||
const fl = h.spawn('enemy_flywalk', { routeIndex: 2 });
|
||||
h.run(0.5);
|
||||
assert.equal(fl.isFlying, true);
|
||||
assert.equal(h.b.applyStatus(fl, 'levitate', { duration: 3 }), false, 'a data flyer takes no 浮空');
|
||||
assert.ok(h.b.applyStatus(fl, 'groundbind', { duration: 5 }));
|
||||
assert.deepEqual([fl.isFlying, !!fl.s.flags.noMove, fl.s.moveSpeed], [false, true, 0], 'ground unit, rooted');
|
||||
const x0 = fl.x;
|
||||
h.run(1);
|
||||
approx(fl.x, x0, 'it does not move');
|
||||
assert.ok(h.b.applyStatus(fl, 'levitate', { duration: 2 }), '浮空 lands on a 缚地 flyer');
|
||||
assert.equal(fl.isFlying, true, 'levitated: an air unit again');
|
||||
// a melee operator (ground-only, range: his own tile) hits a 缚地 flyer on his tile — and blocks it, a ground unit now
|
||||
const near = h.spawn('enemy_fly', { pos: [9, 4] });
|
||||
h.run(1);
|
||||
assert.equal(yak.stats.dmg, 0, '角峰 cannot hit the flyer');
|
||||
h.b.applyStatus(near, 'groundbind', { duration: 10 });
|
||||
assert.ok(h.runUntil(() => yak.stats.dmg > 0, 5), '缚地: a ground unit he can hit');
|
||||
assert.equal(near.blockedBy, yak, 'and block');
|
||||
// halving and 抵抗
|
||||
const heavy = h.spawn('enemy_flyheavy', { pos: [10, 9] });
|
||||
h.b.applyStatus(heavy, 'groundbind', { duration: 10 });
|
||||
approx(heavy.findBuff('groundbind').timeLeft, 5, 'weight 4: half');
|
||||
const res = h.spawn('enemy_fly', { pos: [11, 9] });
|
||||
h.b.applyStatus(res, 'resist', { duration: 99, value: 0.5 });
|
||||
h.b.applyStatus(res, 'groundbind', { duration: 10 });
|
||||
approx(res.findBuff('groundbind').timeLeft, 5, '抵抗 halves it');
|
||||
h.run(6);
|
||||
assert.equal(res.isFlying, true, 'back in the air when it ends');
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0);
|
||||
});
|
||||
|
||||
test('the hovering enemies (PRTS "受…缚地影响后"): 掠海漂移体 drops to its 爬行模式 for good; 吉兆飞鳞 is down while the 缚地 holds', () => {
|
||||
const h = arena();
|
||||
const sy = h.spawn('enemy_2025_syufo', { pos: [12, 8], routeIndex: 0, mods: { speedMul: 0 } });
|
||||
const pa = h.spawn('enemy_10045_parrot', { pos: [12, 6], routeIndex: 0, mods: { speedMul: 0 } });
|
||||
h.step();
|
||||
assert.ok(sy.isFlying && pa.isFlying, 'both hover');
|
||||
h.b.applyStatus(sy, 'groundbind', { duration: 4 });
|
||||
h.b.applyStatus(pa, 'groundbind', { duration: 20 });
|
||||
h.step();
|
||||
assert.equal(sy.form, 'crawl', '掠海漂移体: 爬行模式');
|
||||
assert.equal(pa.isFlying, false, '吉兆飞鳞: down');
|
||||
h.run(10);
|
||||
assert.equal(sy.isFlying, false, '掠海漂移体 stays down for good');
|
||||
assert.equal(pa.isFlying, false, '吉兆飞鳞 down while the 缚地 holds (its 8 s stun over)');
|
||||
h.run(12);
|
||||
assert.equal(pa.isFlying, true, '吉兆飞鳞 floats again once nothing holds it');
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0);
|
||||
});
|
||||
Reference in new issue
Block a user