feedback2 WH: 荒芜拉普兰德 S3's drones attack the enemy they reach like normal drones (PRTS flight: spread 1.3 s, chase 2.0 to 4.0, own funnel ramp, neither attack nor skill damage); 余 S2 teleports leaders that are not 自缚 and shows its pull effect only when it pulls (GitHub #32 items 7 and 1)

This commit is contained in:
sganggs committed 2026-10-03 23:31:44 +08:00
1 parent 47df17171a
commit ef636f941c
3 files changed
+424 -41

No files matched your search

+135 -40
View File
@@ -51,9 +51,12 @@
// 6_16 溯光星源 link transfers the pre-mitigation arts amount × share to the other locked target(s); 能源解析 = the
// 脆弱 status (同名效果取最高).
// 6_17 耀骑士临光 "上一名部署干员" = the op of the same owner deployed right before her (deploy order).
// 6_18 荒芜拉普兰德 S3 drones are virtual (fx events), move at projectile_move_speed tiles/s, she stops attacking
// while they roam; 头狼 stage 2 "特殊能力失效" = silence; stage 3 = +1 drone (normal attacks hit once
// more, S3 releases one more drone). Base drone count 1.
// 6_18 荒芜拉普兰德 S3 drones are virtual (fx events) flying PRTS's 技能流程 (spread attack@times s, chase 2.0 → 4.0
// tiles/s; the turn rate is not modelled: straight at the target); every drone is out, so she makes no
// normal attack herself, while each drone on its target attacks like a normal drone (her attack
// interval, ATK × its own funnel ramp; neither attack nor skill damage — PRTS 备注); 头狼 stage 2
// "特殊能力失效" = silence; stage 3 = +1 drone (normal attacks hit once more; S3 releases one more drone,
// mid-skill too). Base drone count 1.
// 6_19 锏 10 slashes every d_hit_interval, pulls every p_hit_interval, final blow (skill range) at the end;
// S3 slashes and pulls air units too (PRTS 备注 "可对空"; a 静态刚体 — every drone of the mode — is hit
// but stays put: Battle._displaceable).
@@ -72,7 +75,8 @@
// any enemy on the field wasted both charges); the snow spreads along her facing line (≤ trig_cnt tiles);
// S2 "目标点变为冻结状态" = the frozen 保护目标 token (icetgt, one at a time) on the tile reaching max snow, whose
// snow is used up; spreads go to the thinnest 4-neighbour; the 20 % DoT ticks once per second.
// 6_03 S2 teleport = the ground-reachable (grid path) enemies of the skill grid moved onto his tile.
// 6_03 S2 teleport = the ground-reachable (grid path) enemies of the skill grid moved onto his tile, in the cast's
// tick (PRTS: 0.13 s after the damage), leaders included unless 自缚.
// 6_04 S1 transfer: the ally takes ×(1 − share), she takes the rest as true damage from the attacker; module 新生代
// "30点伤害减免" = +30 effective DEF (physical) / −30 ÷ (1 − RES) before mitigation (arts).
// 6_06 S2 the ASPD stacks last the whole battle; S1 "异常状态时可以释放" = cleanse + cast (tick check).
@@ -106,7 +110,7 @@ import { absoluteRangeKeys, sortEnemyTargets, canTargetEnemy, aggroCmp } from '.
import { aggregateMods } from '../../buffs.js';
import { COLS, ROWS, PULL_STOP_RADIUS, CHAIN_RADIUS } from '../../constants.js';
import { rotateOffset } from '../../dir.js';
import { bodyDist, bodyInKeys, bodyInRadius, bodyKeys } from '../../body.js';
import { bodyDist, bodyInKeys, bodyKeys } from '../../body.js';
import { hasHp } from '../../damage.js';
import { summonToken, TOKEN_IDS } from '../tokens.js';
@@ -199,12 +203,15 @@ function cleanseAbnormal(battle, u) {
}
/**
* "传送至自身位置": a ground enemy that can reach `unit`'s tile on the ground grid is moved onto it (unblocked, its
* route re-planned from there — the engine then blocks it on that tile when capacity allows). Flyers / bosses stay, and
* so does a 自缚 unit (flag `selfBound`, e.g. 守墓石像's 转换模式): PRTS 余 S2 备注 "处于消失状态的/持有自缚的单位不视为可达
* 目标" (束缚 alone does not exempt it, so not `noMove`).
* route re-planned from there — the engine then blocks it on that tile when capacity allows). Flyers stay, and so does a
* 自缚 unit (flag `selfBound`: 守墓石像's 转换模式, the 7 huge leaders, the 胄 parts' anchor): PRTS 余 S2 备注 "处于消失状态的/
* 持有自缚的单位不视为可达目标" (束缚 alone does not exempt it, so not `noMove`). Being a leader is no exemption: PRTS gives
* 卢西恩 and 假想敌:铳 (both sections) 传送抗性 无 — they are teleported like any ground enemy and walk on from his tile (a
* patrol keeps looping: content/bosses.js patrolLoop). 静态刚体 forbids physical movement, not a teleport (the 备注 names
* only 消失 / 自缚 / 免疫传送); every 静态刚体 of the mode flies or is 自缚 anyway.
*/
function teleportEnemy(battle, unit, e) {
if (!e || !e.alive || e.isFlying || e.isBoss || e.s.flags.selfBound) return false;
if (!e || !e.alive || e.isFlying || e.s.flags.selfBound) return false;
const r = unit.tileR, c = unit.tileC;
if (!battle.grid.groundPassable(r, c)) return false;
const er = Math.round(e.y), ec = Math.round(e.x);
@@ -812,8 +819,10 @@ function yu(bb, chess, def) {
// ep_damage_ratio × ATK 灼燃损伤 on the attacker (install below)
skchr_yu_1: { kind: 'duration', mods: { hpPct: num(bb.max_hp), defPct: num(bb.def) } },
// S2 厚礼上宾: atk_scale × ATK arts on every enemy of the skill range + the ground-reachable ones teleported onto his
// tile; block +block_cnt, HP / ATK +, normal attacks deal arts damage. The burst hits air units too [ASSUMED: no 对空
// note on PRTS]; the teleport takes ground units only ("地面可达目标")
// tile (leaders too, unless 自缚: teleportEnemy); block +block_cnt, HP / ATK +, normal attacks deal arts damage. The
// burst hits air units too [ASSUMED: no 对空 note on PRTS]; the teleport takes ground units only ("地面可达目标"). The
// 'pull' fx only when someone was teleported (each one also gets its own 'teleport' fx). PRTS's 0.13 s between the
// damage and the teleport is not modelled (same tick).
skchr_yu_2: {
kind: 'duration',
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), blockCnt: num(bb.block_cnt) },
@@ -824,7 +833,7 @@ function yu(bb, chess, def) {
for (const e of foes) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1), type: 'arts', isSkill: true, tags: ['skill'] });
let n = 0;
for (const e of foes) if (teleportEnemy(battle, unit, e)) n++;
battle.fx('pull', { x: unit.x, y: unit.y, id: unit.id, n });
if (n > 0) battle.fx('pull', { x: unit.x, y: unit.y, id: unit.id, n });
},
},
};
@@ -2754,14 +2763,109 @@ function nearl2(bb, chess, def) {
// ------------------------------------------------------------------------------------------------------------------
// 荒芜拉普兰德 chess_char_6_18 (驭械术师) — S3 终幕·浩劫; 头狼; 叙拉古的荣幸
/**
* S3 终幕·浩劫's drone flight — PRTS 荒芜拉普兰德 S3 备注 "技能流程": ① for `attack@times` (1.3) s after the cast, or after
* a drone is added ("补充浮游单元"), the drones spread evenly outward from her, one along her facing ("散开的方向始终包括
* 自身的朝向"): 初速度 0.1, 加速度 1.9, 最大速度 2.0; ② then each picks the target nearest to itself, ties nearest to her
* ("距离自身最近>距离本体最近"), anywhere on the field, and flies at it: 初速度 2.0, 加速度 1.0, 最大速度 4.0, restarting
* at 2.0 whenever its target leaves or turns unselectable on the way; ③ once there it stays on the target and attacks it
* like a normal drone ("此状态下的攻击行为同正常浮游单元"); when that target leaves / turns unselectable it reappears at a
* random point of the 1.5-side square around it and picks again (②). With no selectable target it circles (radius 0.9,
* 1.0 tiles/s, counter-clockwise) with its heading as the tangent, the circle on its left. Speeds in tiles/s.
*/
const WHITW2_SPREAD = Object.freeze({ v0: 0.1, acc: 1.9, max: 2 });
const WHITW2_CHASE = Object.freeze({ v0: 2, acc: 1, max: 4 });
const WHITW2_REAPPEAR_SIDE = 1.5;
const WHITW2_ORBIT = Object.freeze({ r: 0.9, v: 1 });
function whitw2(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const sid = selectedSkill(chess, def);
const speed = Math.max(0.1, num(bb['attack@projectile_move_speed'], 1)), R = num(bb['attack@range_radius'], 0.9);
const spreadTime = Math.max(0, num(bb['attack@times'], 1.3)), R = num(bb['attack@range_radius'], 0.9);
const dmgScale = num(bb['attack@magic_atk_scale'], 1), fear = num(bb['attack@fear']), slow = num(bb['attack@move_speed']);
// "非移动敌人": blocked, or not walking (stunned, bound, asleep, waiting, speed 0 …)
const still = (e) => !!e.blockedBy || e.moving === false || !(e.s.moveSpeed > 0) || !!(e.s.flags.stun || e.s.flags.freeze || e.s.flags.bind || e.s.flags.sleep || e.s.flags.noMove);
const skillGridW = skillGridOf(def);
// ---- S3 drones (virtual: positions in unit.mem.drones, fx events for the client) --------------------------------
const droneCount = (unit) => 1 + Math.floor(num(bb['attack@cnt'])) + ((unit.mem.wolfStage || 0) >= 3 ? 1 : 0);
// ① `k` drones leave her evenly spread, the first along her facing (row 0 is the bottom row: angles in the (col, row)
// plane, counter-clockwise)
const releaseDrones = (unit, k) => {
const a0 = Math.atan2(unit.fwd[0], unit.fwd[1]);
for (let i = 0; i < k; i++) {
const a = a0 + (2 * Math.PI * i) / k;
unit.mem.drones.push({ x: unit.x, y: unit.y, hx: Math.cos(a), hy: Math.sin(a), v: WHITW2_SPREAD.v0, age: 0, phase: 'spread', t: null, cd: 0, rampId: null, ramp: 0, orbit: null });
}
};
// speed v → v + acc·dt (capped); the distance covered at the mean of the two (exact under constant acceleration)
const accelerate = (d, lim, dt) => { const v1 = Math.min(lim.max, d.v + lim.acc * dt), s = ((d.v + v1) / 2) * dt; d.v = v1; return s; };
// ② the selectable enemy nearest to the drone, ties broken by the one nearest to her
const pickTarget = (battle, unit, d, ok) => {
let best = null, bd = Infinity, bh = Infinity;
for (const e of battle.enemies) {
if (!ok(e)) continue;
const de = bodyDist(e, d.x, d.y), dh = bodyDist(e, unit.x, unit.y);
if (de < bd - 1e-9 || (de <= bd + 1e-9 && dh < bh - 1e-9)) { best = e; bd = de; bh = dh; }
}
return best;
};
// nothing selectable: circle counter-clockwise on its left, its heading the tangent
const circle = (d, dt) => {
const { r, v } = WHITW2_ORBIT;
if (!d.orbit) d.orbit = { cx: d.x - d.hy * r, cy: d.y + d.hx * r };
const a = Math.atan2(d.y - d.orbit.cy, d.x - d.orbit.cx) + (v / r) * dt;
d.x = d.orbit.cx + r * Math.cos(a); d.y = d.orbit.cy + r * Math.sin(a);
d.hx = -Math.sin(a); d.hy = Math.cos(a);
};
// one tick of one drone: spread → (pick) → chase → on the target, attacking like a normal drone. Its attack clock runs
// all the time (one attack per interval of hers at most, whatever it chased in between). The turn rate (PRTS
// 转向速度 1/6 per frame = attack@projectile_turn_speed × 1/30 s) is not modelled: a chasing drone heads straight at
// its target [ASSUMED].
const flyDrone = (battle, unit, d, dt, ok) => {
d.cd = Math.max(0, d.cd - dt);
if (d.phase === 'spread') {
const s = accelerate(d, WHITW2_SPREAD, dt);
d.x += d.hx * s; d.y += d.hy * s;
d.age += dt;
if (d.age + 1e-9 < spreadTime) return;
d.phase = 'seek';
}
if (d.phase === 'lock' && !ok(d.t)) {
// ③ its target left / is no longer selectable: it reappears at a random point of the square around that spot
const h = WHITW2_REAPPEAR_SIDE / 2;
d.x = d.t.x + battle.rng.range(-h, h);
d.y = d.t.y + battle.rng.range(-h, h);
d.t = null; d.phase = 'seek';
} else if (d.phase === 'chase' && !ok(d.t)) { d.t = null; d.phase = 'seek'; }
if (d.phase === 'seek') {
const t = pickTarget(battle, unit, d, ok);
if (!t) { circle(d, dt); return; }
d.t = t; d.phase = 'chase'; d.v = WHITW2_CHASE.v0; d.orbit = null;
battle.fx('droneLock', { x: t.x, y: t.y, id: t.id, src: unit.id }); // PRTS: the red wolf-eye mark over the target
}
const t = d.t;
if (d.phase === 'chase') {
const s = accelerate(d, WHITW2_CHASE, dt);
if (bodyDist(t, d.x, d.y) > s + 1e-9) {
const dx = t.x - d.x, dy = t.y - d.y, L = Math.hypot(dx, dy);
if (L > 1e-9) { d.hx = dx / L; d.hy = dy / L; }
d.x += d.hx * s; d.y += d.hy * s;
return;
}
d.phase = 'lock'; // reached: "追上时使目标恐惧…并锁定其攻击"
if (fear > 0) battle.applyStatus(t, 'fear', { duration: fear, source: unit });
}
d.x = t.x; d.y = t.y;
if (d.cd > 1e-9 || !ok(t)) return;
d.cd = unit.s.interval;
const f = unit.profile?.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
d.ramp = d.rampId === t.id ? Math.min(f.max, d.ramp + f.delta) : f.init;
d.rampId = t.id;
battle.fx('drone', { x: d.x, y: d.y, id: unit.id });
battle.dealDamage(unit, t, { amount: unit.s.atk * unit.s.atkScaleMul * d.ramp, type: 'arts', tags: ['droneAttack'] });
};
const skills = {
// S1 慵怠者悲鸣: passive 浮游单元+1 (trait: one more hit per attack); toggled on: ATK +atk and the drones lock a random
// non-moving enemy anywhere on the field (re-locking when it moves or falls; install), else her range. The whole-field
@@ -2836,43 +2940,33 @@ function whitw2(bb, chess, def) {
}, { owner: unit });
}
},
// S3 终幕·浩劫: ATK +atk; 1 + attack@cnt drones (+1 from 头狼 stage 3) fly the PRTS 技能流程 (WHITW2_* above). Every
// drone is out, so she makes no normal attack of her own (`noAttack`); a drone on its target attacks it every attack
// interval of hers (her live ASPD; the first hit as it arrives) for ATK × its OWN funnel ramp — the trait's init,
// +delta per hit on the same target, the cap (头狼 stage 1 raises it), back to init on a new target (PRTS 分支特性信息
// 驭械术师 "浮游单元攻击不同目标…时,上述的伤害立刻恢复至初始值"). That damage is arts and neither a normal attack nor
// skill damage (PRTS S3 备注 "该技能释放的浮游单元造成的伤害不属于普通攻击/技能直接伤害": no 'attack' hook, isAttack /
// isSkill false — the 叙拉古 6 assassin proc and the on-attack items skip it), and 缴械 does not stop it (PRTS 驭械术师
// "通过技能释放的浮游单元…不受缴械类效果制约"). Around every drone (attack@range_radius): move speed attack@move_speed
// and, once per second, attack@magic_atk_scale × ATK arts (不叠加: one hit per enemy whatever the number of drones).
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
attack: { noAttack: true },
onStart({ battle, unit }) {
const n = 1 + Math.floor(num(bb['attack@cnt'])) + ((unit.mem.wolfStage || 0) >= 3 ? 1 : 0);
unit.mem.drones = Array.from({ length: n }, () => ({ x: unit.x, y: unit.y, t: null, locked: null }));
unit.mem.drones = [];
unit.mem.droneAcc = 0;
battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n });
releaseDrones(unit, droneCount(unit));
battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n: unit.mem.drones.length });
},
onTick({ battle, unit, dt }) {
const D = unit.mem.drones;
if (!D) return;
// 头狼 stage 3 reached while the skill runs: the extra drone is released like the others (PRTS ① "补充浮游单元")
const add = droneCount(unit) - D.length;
if (add > 0) { releaseDrones(unit, add); battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n: add }); }
const ok = (e) => e && e.alive && !e.hidden && canTargetEnemy(unit, e, ANY);
const taken = new Set(D.filter((d) => ok(d.t)).map((d) => d.t));
for (const d of D) {
if (!ok(d.t)) {
let best = null, bd = Infinity, bestFree = null, bfd = Infinity;
for (const e of battle.enemies) {
if (!ok(e)) continue;
const dist = bodyDist(e, d.x, d.y);
if (dist < bd - 1e-9) { bd = dist; best = e; }
if (!taken.has(e) && dist < bfd - 1e-9) { bfd = dist; bestFree = e; }
}
d.t = bestFree ?? best;
d.locked = null;
if (d.t) taken.add(d.t);
}
if (!d.t) continue;
const dx = d.t.x - d.x, dy = d.t.y - d.y, dist = Math.hypot(dx, dy), step = speed * dt;
if (dist <= step) { d.x = d.t.x; d.y = d.t.y; } else { d.x += (dx / dist) * step; d.y += (dy / dist) * step; }
if (d.locked !== d.t && bodyInRadius(d.t, d.x, d.y, R)) {
d.locked = d.t;
if (fear > 0) battle.applyStatus(d.t, 'fear', { duration: fear, source: unit });
battle.fx('droneLock', { x: d.x, y: d.y, id: d.t.id, src: unit.id });
}
}
for (const d of D) flyDrone(battle, unit, d, dt, ok);
const near = new Set();
for (const d of D) for (const e of battle.foesInRadius(d.x, d.y, R)) if (ok(e)) near.add(e);
if (slow) for (const e of near) battle.addBuff(e, { key: 'whitw2:slow', duration: 0.2, refresh: 'replace', mods: { moveMul: Math.max(0, 1 + slow) }, source: unit });
@@ -2880,7 +2974,8 @@ function whitw2(bb, chess, def) {
if (unit.mem.droneAcc + 1e-9 >= 1) {
unit.mem.droneAcc -= 1;
for (const e of near) if (e.alive) battle.dealDamage(unit, e, { amount: unit.s.atk * dmgScale, type: 'arts', isSkill: true, tags: ['skill', 'drone'] });
for (const d of D) battle.fx('drone', { x: d.x, y: d.y, id: unit.id });
// (a drone on its target already pulses with each of its attacks)
for (const d of D) if (d.phase !== 'lock') battle.fx('drone', { x: d.x, y: d.y, id: unit.id });
}
},
onEnd({ unit }) { unit.mem.drones = null; },