0.2.5-dev: 耶拉 S2 (both forms) and 荒芜拉普兰德 S1 keep the operator's own hit next to the drone hit — only drone-only skills skip it (review fix of the funnel port); golden: more funnel scenarios move, attributed in docs/history/0.2.5.md

This commit is contained in:
sganggs committed 2026-10-12 15:50:41 +08:00
1 parent e0f3771821
commit a451ddcf6b
15 files changed
+597 -384

No files matched your search

@@ -2,35 +2,18 @@
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
import { COLS } from '../../../constants.js';
import { canTargetEnemy } from '../../../targeting.js';
import { num, tbb, skillActive, withDefaults } from '../shared/tier4.js';
import { num, tbb, withDefaults } from '../shared/tier4.js';
import { kjeraRelease } from './chess_char_3_20-kjera.js';
export default withDefaults({
// ===== 耶拉 (funnel, hidden) S2 心随意动 — +1 drone locking targets, ATK up, chance to chill; talent 低眉
// ===== 耶拉 (funnel, hidden) S2 心随意动 — +1 drone locking targets, ATK up, chance to chill; talent 低眉.
// Each released drone restarts at the trait's initial scale (it does not continue the pre-skill ramp) and those
// scales are not written back onto the single trait ramp. The body stays an ordinary in-range attack.
chess_char_4_03_a: (bb, chess, def) => {
const t0 = tbb(def, 0);
const n = 1 + Math.max(0, Math.floor(num(bb['attack@cnt'], 1)));
const prob = num(bb['attack@prob'], 0), cold = num(bb['attack@cold'], 0);
const funnel = (unit) => unit.profile.funnel || { init: 0.2, delta: 0.15, max: 1.1 };
const rel = kjeraRelease(bb, { restart: true, announce: true });
return {
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk) },
targeting: { maxTargets: n },
attack: {
// every drone ramps on its own locked target (trait init/delta/max): the multipliers are queued per target
// by the beforeAttack hook below (two drones on the same enemy ramp independently)
funnelMul: (battle, unit, target) => {
const q = unit.mem.kjeraQueue && unit.mem.kjeraQueue.get(target.id);
return q && q.length ? q.shift() : funnel(unit).init;
},
onHit({ battle, unit, target }) {
if (target && target.alive && cold > 0 && battle.rng.chance(prob)) battle.applyStatus(target, 'cold', { duration: cold, source: unit });
},
},
onStart({ battle, unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = new Map(); battle.fx('drones', { x: unit.x, y: unit.y, id: unit.id, n }); },
onEnd({ unit }) { unit.mem.kjeraDrones = []; unit.mem.kjeraQueue = null; },
},
skill: rel.skill,
talents: [{ install(battle, unit) { // 低眉: ATK +10 %, +16 % with ≥ 2 ground tiles in range
battle.on('deploy', (c) => {
if (c.unit !== unit) return;
@@ -40,38 +23,7 @@ export default withDefaults({
battle.addBuff(unit, { key: 'kjera:t1', mods: { atkPct: v } });
}, { owner: unit });
} }],
install(battle, unit) {
// n drones, each locked on its enemy until that enemy dies (while at least one enemy is in her range). A free
// drone takes an enemy no other drone holds, else doubles up on the best target (a lone enemy/boss gets both).
battle.on('beforeAttack', (c) => {
if (c.attacker !== unit || !skillActive(unit)) return;
const ok = (e) => !!e && e.alive && canTargetEnemy(unit, e, { canHitFly: true });
const cands = c.targets.filter(ok);
const drones = unit.mem.kjeraDrones || (unit.mem.kjeraDrones = []);
const held = new Set();
for (let i = 0; i < n; i++) { if (drones[i] && ok(drones[i].e)) held.add(drones[i].e); else drones[i] = null; }
for (let i = 0; i < n; i++) {
if (drones[i]) continue;
const e = cands.find((x) => !held.has(x)) ?? cands[0] ?? drones.find((d) => d)?.e;
if (!e) continue;
held.add(e);
drones[i] = { e, scale: null };
}
const f = funnel(unit);
const q = unit.mem.kjeraQueue || (unit.mem.kjeraQueue = new Map());
const out = [];
for (const d of drones) {
if (!d) continue;
d.scale = d.scale == null ? f.init : Math.min(f.max, d.scale + f.delta);
out.push(d.e);
const l = q.get(d.e.id);
if (l) l.push(d.scale); else q.set(d.e.id, [d.scale]);
}
if (out.length) c.targets = out;
}, { owner: unit, priority: 10 });
// queued multipliers of shots that never landed (target died mid-flight) are dropped with the target
battle.on('death', (c) => { if (c.unit.side === 'enemy' && unit.mem.kjeraQueue) unit.mem.kjeraQueue.delete(c.unit.id); }, { owner: unit });
},
install(battle, unit) { rel.install(battle, unit); },
};
},
});