feedback2 WD: 活性源石 stays on a unit for its 300 s after it leaves the tile, one effect per unit, the tiles never switch off (GitHub #33 item 6)
This commit is contained in:
1 parent
2a03467897
commit
1579a8807f
4 files changed
+108
-13
No files matched your search
+8
-2
@@ -1477,9 +1477,15 @@ TBD-22.2
|
||||
TBD-22.3
|
||||
|
||||
|
||||
### 22.4 活性源石 stays on an enemy after it leaves the tile (#33 item 6)
|
||||
### 22.4 活性源石 stays on an enemy after it leaves the tile (#33 item 6) — `server/sim/content/devices.js` (`touchInfection`, `enterTerrain`, `terrainFor`)
|
||||
|
||||
TBD-22.4
|
||||
- **Seen** (public issue #33, other part 3: 「红地板图敌方单位经过后,红地板附带的效果会消失」): on 战场#04 an enemy lost the red floor's effect the moment it walked off it. Measured on v0.1.1 (the real stage): an enemy on (10,6) from 15.67 s to 19.67 s lost 4 × 70 HP; 12 s later it still had 99720 / 100000 HP and no ATK / ASPD bonus.
|
||||
- **Official**: the red floor is 活性源石 `tile_infection`, the only one in the season's data — 战场#04 (`act1autochess_m04`) at (10,6), (11,6), (10,14), (11,14) and on the boss rows at (3,6), (4,6), (3,14), (4,14); level blackboard `damage` 70, `atk` 0.2, `attack_speed` 20, `duration` 300. PRTS 特殊地形 tile template (`特殊地形/trapper/tilesformat.json`): "部署于其上的我军和经过的敌军在{duration}s内每秒受到{damage}真实伤害,攻击力提升{atk×100}%,攻击速度增加{attack_speed}" (main-story stage pages: "在五分钟内"). The crisis-contract tag `global_tile_infection_1` 「目标:可控感染」 (PRTS 切尔诺伯格 6区废墟/历史合约): "踏过活性源石地块的敌人不再持续损失生命值" — without it an enemy that has walked over the tile goes on losing HP. Same-name effects do not stack (PRTS 作战机制: "同名buff的默认叠加策略buff只能表现出一个"). Its damage is 环境伤害 (PRTS 自然环境 lists 活性源石).
|
||||
- **Cause**: `devices.js` read `duration` as a lifetime of the tiles (`terrainFor` switched them off after 300 s of battle time) and tied the effect to standing on the tile (`enterTerrain` removed it as soon as the unit's tile changed).
|
||||
- **Now**: every tick a unit is on the tile (allies by their tile, ground enemies by their rounded position; flyers, hovering / levitated and hidden units never touch it, as before), `touchInfection` gives it the timed effect `terrain:infection` for `duration` s — 70 true damage per second, ATK +20 %, ASPD +20 — or, when it already carries it, gives it back its full time. Leaving the tile no longer removes it: an enemy that crossed it drains 70/s with ATK +20 % / ASPD +20 for 300 s from its last contact — longer than any own-field or 联防 battle lasts (≤ 230 s of battle time). All 12 ground legs of 战场#04 (own field, 联防 field, boss field) cross one of these tiles, so every ground enemy there carries it once past the tile. One effect per unit: a second pass or the next tile of a pair restarts its time and never adds a second effect (ATK +20 % once, 70/s, no extra tick). The tiles never switch off — the old lifetime only ever ran out on the boss field (no time limit), at 300 s. An operator deployed on it is always in contact and keeps draining until it is knocked out or withdrawn (official; v0.1.1 did the same within the first 300 s); leaving the field (knock-out, withdrawal, 突袭's jump) drops the effect with every other buff. The damage itself is unchanged: true damage no unit deals (无来源), tagged `terrain` (环境伤害), a damage instance, not a 流失.
|
||||
- [ASSUMED]: the time counts from the last contact (it is refreshed every tick on the tile) and the per-second rhythm runs on through a refresh; not 持续伤害 (no `dot` tag: PRTS 伤害分类's list of BUFF damage names 深水区, 源石污染区 and 活性源石风暴 but not 活性源石, so 锡人's 凋敝魂灵 does not raise it); a 重生 keeps it (the 重生 whitelist keeps source-less buffs, SIM.md "Knock-outs that are not deaths").
|
||||
- **Checked**: no other terrain reads a `duration` — 沼泽 (stacks, cleared on leaving), 深水区 (`sea_drown[enemy]` has none), 烟雾 and the blowers. Nothing relied on the old lifetime: the client draws the tile glow from the stage map (it never went out), terrain effects have no status icon, the enemy abilities that care about the tile (萨卡兹枯朽前锋's arts attacks, “灵幛”, 孽罪奇美拉) read the tile they stand on (`onTerrain`), and 联防 carries no effects (HP, SP and a running skill only).
|
||||
- Tests: `test/content/tokens_devices.test.js` — 活性源石 (the operator and the enemy standing on it), an enemy keeps the effect after leaving the tile (20 s later: 70/s, ATK, ASPD, the time left), one effect per unit (two passes give one effect that ends `duration` s after the last contact, 70/s while it lasted; an operator drains past `duration`).
|
||||
|
||||
|
||||
### 22.5 Emotes and guide pages without a local client; deployment docs (#42); the English title (#38)
|
||||
|
||||
@@ -76,7 +76,7 @@ Boss board:
|
||||
| `m` | mire | LOW | ALL | yes | +1 stack every 3 s: atk-speed −5, move −5 %; max 10; cleared on leaving |
|
||||
| `g` | smog | LOW | ALL | yes | an operator here cannot be targeted by enemy **ranged** attacks |
|
||||
| `d` | deepsea | LOW | ALL | yes | enemies take 40 dmg/s, atk-speed −60, move ×0.6 |
|
||||
| `i` | infection | LOW | ALL | yes | 70 true dmg/s to allies on it and enemies crossing it; +20 % ATK, +20 ASPD; 300 s |
|
||||
| `i` | infection | LOW | ALL | yes | allies on it and enemies crossing it get 70 true dmg/s, +20 % ATK, +20 ASPD for 300 s from their last contact (an enemy keeps it after leaving the tile; the tiles never switch off) |
|
||||
|
||||
### 2.4 Stage pool (`stageDatasDict`)
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
// are ×100 ASPD), move speed × (1 + moveMulPerStack × stacks); cleared on leaving (allies and ground enemies)
|
||||
// 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them)
|
||||
// 深水 d ground enemies on it: sea_drown[enemy].damage true dmg/s, ASPD attack_speed (×100), move × move_speed
|
||||
// 活性源石 i units on it (allies and ground enemies): damage true dmg/s, ATK + atk, ASPD + attack_speed, for
|
||||
// `duration` s of battle time
|
||||
// 活性源石 i a unit on it (allies and ground enemies) gets a timed effect: damage true dmg/s, ATK + atk, ASPD +
|
||||
// attack_speed for `duration` s from its last contact — an enemy keeps it after walking off; one effect
|
||||
// per unit, its time starts again while the unit is on the tile; the tiles never switch off
|
||||
// “双眼皮” turrets (`trap_1104_aclasert`, band 机械援助 / `deviceOverrides` alias on): ranged arts shooter on its data
|
||||
// range; ASPD + attack_speed_per_stack × L (≤ max_attack_speed), hits apply fragile 1 + damage_scale_per_stack
|
||||
// × L (≤ max_damage_scale) for the text's duration; L = the owner's highest bond layers (live)
|
||||
@@ -300,13 +301,15 @@ function buildTerrain(battle, st) {
|
||||
aspd: aspdOf(num(ds['sea_drown[enemy].attack_speed'], RESEARCH.deepsea.attack_speed)),
|
||||
moveMul: num(ds['sea_drown[enemy].move_speed'], RESEARCH.deepsea.move_speed),
|
||||
};
|
||||
// active originium
|
||||
// active originium: `duration` is how long the effect lasts on a unit (PRTS tile template "部署于其上的我军和经过的
|
||||
// 敌军在{duration}s内…"), not a lifetime of the tiles
|
||||
const inf = sp.infection?.bb || {};
|
||||
const infDur = num(inf.duration, RESEARCH.infection.duration);
|
||||
st.infection = {
|
||||
damage: num(inf.damage, RESEARCH.infection.damage),
|
||||
atk: num(inf.atk, RESEARCH.infection.atk),
|
||||
aspd: aspdOf(num(inf.attack_speed, RESEARCH.infection.attack_speed)),
|
||||
until: num(inf.duration, RESEARCH.infection.duration),
|
||||
duration: infDur > 0 ? infDur : RESEARCH.infection.duration,
|
||||
};
|
||||
// blowers
|
||||
for (const d of battle.stage?.devices || []) {
|
||||
@@ -332,7 +335,8 @@ const terrainDamage = (battle, amount) => (ctx) => {
|
||||
function enterTerrain(battle, st, u, code) {
|
||||
const m = u.mem;
|
||||
if (m.terrain === code) return;
|
||||
if (m.terrain) battle.removeBuff(u, BUFF[m.terrain]);
|
||||
// leaving a tile ends its effect — except 活性源石's, which runs out on its own (touchInfection)
|
||||
if (m.terrain && m.terrain !== TERRAIN.infection) battle.removeBuff(u, BUFF[m.terrain]);
|
||||
m.terrain = code;
|
||||
m.terrainSince = battle.time;
|
||||
m.mireStacks = 0;
|
||||
@@ -341,11 +345,27 @@ function enterTerrain(battle, st, u, code) {
|
||||
else if (code === TERRAIN.deepsea) {
|
||||
const D = st.deepsea;
|
||||
battle.addBuff(u, { key: BUFF[code], mods: { aspd: D.aspd, moveMul: D.moveMul }, interval: 1, onTick: terrainDamage(battle, D.damage) });
|
||||
} else if (code === TERRAIN.infection) {
|
||||
const I = st.infection;
|
||||
battle.addBuff(u, { key: BUFF[code], mods: { atkPct: I.atk, aspd: I.aspd }, interval: 1, onTick: terrainDamage(battle, I.damage) });
|
||||
}
|
||||
// mire stacks are applied by tickMire
|
||||
// mire stacks are applied by tickMire, 活性源石 by touchInfection (every tick on the tile)
|
||||
}
|
||||
|
||||
/**
|
||||
* 活性源石 contact (every tick on the tile). The tile gives a timed effect — damage true dmg/s, ATK + atk, ASPD +
|
||||
* attack_speed for `duration` s (PRTS 特殊地形 tile template: "部署于其上的我军和经过的敌军在{duration}s内每秒受到{damage}
|
||||
* 真实伤害,攻击力提升…,攻击速度增加…") — that stays on an enemy after it walks off: PRTS 危机合约 tag
|
||||
* global_tile_infection_1 「目标:可控感染」 "踏过活性源石地块的敌人不再持续损失生命值" switches the lasting HP loss off,
|
||||
* so without it the loss continues. One effect per unit (PRTS 作战机制: "同名buff的默认叠加策略buff只能表现出一个"): a
|
||||
* unit that already carries it gets its full `duration` back and keeps its per-second rhythm — no second effect, no
|
||||
* extra tick [ASSUMED: the time counts from the last contact]. An operator deployed on it is always in contact and
|
||||
* keeps draining, as officially. The tick (terrainDamage) is true damage no unit deals (无来源), tagged 'terrain' =
|
||||
* 环境伤害 (PRTS 自然环境 lists 活性源石), not 'dot' [ASSUMED: PRTS 伤害分类's list of BUFF damage does not name it].
|
||||
*/
|
||||
function touchInfection(battle, st, u) {
|
||||
const I = st.infection;
|
||||
const key = BUFF[TERRAIN.infection];
|
||||
const b = u.findBuff(key);
|
||||
if (b) { if (b.timeLeft < I.duration) b.timeLeft = I.duration; return; }
|
||||
battle.addBuff(u, { key, duration: I.duration, mods: { atkPct: I.atk, aspd: I.aspd }, interval: 1, onTick: terrainDamage(battle, I.damage) });
|
||||
}
|
||||
|
||||
function tickMire(battle, st, u) {
|
||||
@@ -372,7 +392,6 @@ function terrainFor(battle, st, u) {
|
||||
let code = st.terrain[k];
|
||||
if (code === TERRAIN.smog && u.side !== 'ally') code = 0;
|
||||
if (code === TERRAIN.deepsea && u.side === 'ally') code = 0;
|
||||
if (code === TERRAIN.infection && battle.time >= st.infection.until) code = 0;
|
||||
return code;
|
||||
}
|
||||
|
||||
@@ -420,6 +439,7 @@ function refreshUnit(battle, st, u) {
|
||||
if (st.hasTerrain) {
|
||||
enterTerrain(battle, st, u, terrainFor(battle, st, u));
|
||||
if (u.mem.terrain === TERRAIN.mire) tickMire(battle, st, u);
|
||||
else if (u.mem.terrain === TERRAIN.infection) touchInfection(battle, st, u);
|
||||
}
|
||||
if (st.flow.size) { if (u.side === 'ally') tickAirflowAlly(battle, st, u); else tickAirflowEnemy(battle, st, u); }
|
||||
}
|
||||
|
||||
@@ -674,6 +674,75 @@ test('活性源石 (act1 m04): units on it take damage/s and gain ATK/ASPD (alli
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
// GitHub #33 item 6: the effect is timed (PRTS tile template "经过的敌军在{duration}s内…"), not tied to standing on the tile
|
||||
test('活性源石: an enemy keeps the effect after leaving the tile — damage/s, ATK and ASPD go on for `duration` s', REAL, () => {
|
||||
const bb = ds.getStage('act1autochess_m04').special.infection.bb;
|
||||
// a harmless walker crosses (11,6) going left, then waits on (11,4), off the tile
|
||||
const route = { motion: 'WALK', start: [11, 8], end: [9, 2], checkpoints: [{ type: 'MOVE', pos: [11, 4] }, { type: 'WAIT', time: 99 }] };
|
||||
const h = makeBattle({ stageId: 'act1autochess_m04', defs: { enemies: { enemy_walker: walker({ atk: 500 }) } }, enemies: [{ key: 'enemy_walker', route }], autoFinish: false, timeLimit: 120 });
|
||||
h.step();
|
||||
const e = h.enemy('enemy_walker');
|
||||
const onTile = () => Math.round(e.y) === 11 && Math.round(e.x) === 6;
|
||||
assert.ok(h.runUntil(onTile, 10), 'steps on (11,6)');
|
||||
assert.ok(h.runUntil(() => !onTile(), 10), 'walks off');
|
||||
assert.ok(e.findBuff('terrain:infection'), 'still carries the effect after leaving');
|
||||
const hp0 = e.hp;
|
||||
h.run(20);
|
||||
assert.deepEqual([Math.round(e.y), Math.round(e.x)], [11, 4], 'waits off the tile');
|
||||
const loss = hp0 - e.hp;
|
||||
assert.ok(Math.abs(loss - 20 * bb.damage) <= bb.damage + 1e-6, `20 s after leaving: lost ${loss}, ≈ 20 × ${bb.damage}`);
|
||||
approx(e.s.atk, 500 * (1 + bb.atk), 1e-9);
|
||||
approx(e.s.aspd, 100 + bb.attack_speed, 1e-9);
|
||||
const b = e.findBuff('terrain:infection');
|
||||
assert.ok(Math.abs(b.timeLeft - (bb.duration - 20)) <= 3 * h.TICK, `${b.timeLeft} s left of ${bb.duration}`);
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('活性源石: one effect per unit — contact restarts its duration, it ends `duration` s after the last contact; the tiles never switch off', REAL, () => {
|
||||
const stage = structuredClone(ds.getStage('act1autochess_m04'));
|
||||
const bb = stage.special.infection.bb;
|
||||
bb.duration = 10; // short, so that the end is inside the test
|
||||
// the walker crosses (11,6) to (11,4), walks straight back over it to (11,8) and waits there
|
||||
const route = { motion: 'WALK', start: [11, 8], end: [9, 2], checkpoints: [{ type: 'MOVE', pos: [11, 4] }, { type: 'MOVE', pos: [11, 8] }, { type: 'WAIT', time: 99 }] };
|
||||
const h = makeBattle({ stage, defs: { enemies: { enemy_walker: walker({ atk: 500 }) } }, enemies: [{ key: 'enemy_walker', route }], autoFinish: false, timeLimit: 120 });
|
||||
h.step();
|
||||
const e = h.enemy('enemy_walker');
|
||||
const onTile = () => Math.round(e.y) === 11 && Math.round(e.x) === 6;
|
||||
const hp0 = e.hp;
|
||||
let first = null, lastOn = null, end = null, contacts = 0, was = false, most = 0, atkMax = 0;
|
||||
while (h.b.time < 40 && end == null) {
|
||||
h.step();
|
||||
const on = onTile();
|
||||
if (on && !was) contacts++;
|
||||
was = on;
|
||||
const n = e.buffs.filter((x) => x.key === 'terrain:infection').length;
|
||||
most = Math.max(most, n);
|
||||
atkMax = Math.max(atkMax, e.s.atk);
|
||||
if (on) { lastOn = h.b.time; first ??= h.b.time; }
|
||||
if (first != null && n === 0) end = h.b.time;
|
||||
}
|
||||
assert.equal(contacts, 2, 'walked over the tile twice');
|
||||
assert.equal(most, 1, 'never a second effect');
|
||||
approx(atkMax, 500 * (1 + bb.atk), 1e-9, 'ATK + atk once');
|
||||
assert.ok(end != null && end > first + bb.duration, `outlived its first ${bb.duration} s: the second contact restarted it (${first} → ${end})`);
|
||||
assert.ok(Math.abs(end - (lastOn + bb.duration)) <= 2 * h.TICK, `ends ${bb.duration} s after the last contact (${lastOn} → ${end})`);
|
||||
const loss = hp0 - e.hp;
|
||||
assert.ok(Math.abs(loss - bb.damage * (end - first)) <= bb.damage + 1e-6, `${bb.damage}/s while it lasted: lost ${loss} in ${end - first} s`);
|
||||
h.run(3);
|
||||
assert.equal(e.hp, hp0 - loss, 'nothing after the end');
|
||||
approx(e.s.atk, 500, 1e-9);
|
||||
// an operator deployed on it is always in contact: it keeps draining past `duration` (no lifetime of the tiles)
|
||||
const g = guard({ stats: { atk: 1000, maxHp: 1e5 } });
|
||||
const h2 = makeBattle({ stage, defs: { chess: { test_guard: g } }, units: [{ chessId: 'test_guard', row: 11, col: 6 }], autoFinish: false, timeLimit: 60 });
|
||||
h2.step();
|
||||
h2.run(2 * bb.duration);
|
||||
const u = h2.unit('test_guard');
|
||||
approx(u.stats.taken, 2 * bb.duration * bb.damage, 1e-9, 'ally damage');
|
||||
approx(u.s.atk, 1000 * (1 + bb.atk), 1e-9);
|
||||
checkInvariants(h.b);
|
||||
checkInvariants(h2.b);
|
||||
});
|
||||
|
||||
test('盟约寒风: every interval all enemies turn cold; a gust on cold enemies freezes them; kjeragColdWind uses live layers', REAL, () => {
|
||||
const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, enemies: [{ key: 'enemy_dummy', pos: [10, 6] }, { key: 'enemy_dummy', pos: [11, 8] }], autoFinish: false, timeLimit: 60 });
|
||||
h.step();
|
||||
|
||||
Reference in new issue
Block a user