feedback5 WC: 琳琅诗怀雅 S2 见面礼 throws its champagne bomb on range x-6, onto an enemy's tile, with no enemy needed (GitHub #116 item 2)

PRTS 技能2 备注: placement range x-6 (range_table "x-6": her tile and two tiles in each of the four directions); "若放置
范围内存在敌人,则按攻击索敌规律放置香槟至其所在地块,否则随机放置香槟至可部署的地块". The client charpack (char_1033_swire2,
mode S2) makes her attack a priority composite: SpawnToken (Skill_2) when a free x-6 tile exists and she holds a coin,
else the normal attack. So a coin plus a free x-6 tile takes her attack's turn whether or not an enemy is near; the
tile is the first ground enemy's on a free x-6 tile in her attack target order, else a random free tile. Until now
every attack dropped a bomb at random on her 1-1 attack range. [ASSUMED] the throw costs her attack one tick.

Golden: roster-006 / roster-008, bond-investShip-low, bond-kjeragShip-low, bond-siracusaShip-low,
bond-victoriaShip-low, bond-yanShip-high (each fields 琳琅诗怀雅 with S2) and coop4-ABYSS-7 (a bot fields her from
round 6) moved: her bombs land on x-6 / enemy tiles and are thrown without an enemy.
This commit is contained in:
sganggs committed 2026-10-05 17:27:03 +08:00
1 parent 8ab6d021da
commit e9b6f7e706
6 files changed
+363 -188

No files matched your search

@@ -11,6 +11,13 @@ import {
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
/**
* 琳琅诗怀雅 S2 “见面礼”'s placement range: range_table "x-6" — her tile and the two tiles beyond it in each of the four
* directions (symmetric: her facing does not matter). PRTS 备注 "香槟炸弹放置范围:x-6"; the client charpack (char_1033_swire2,
* mode S2) gives its SpawnToken selector `_rangeId` "x-6". The skill's own rangeId (1-1) is her attack range, not this.
*/
const SWIRE2_BOMB_GRID = Object.freeze([[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]]);
/** Merchant trait with a callback on every successful DP payment (琳琅诗怀雅 大买家). Same rules as professions.js. */
function merchantInstall(onPay) {
return (battle, unit) => {
@@ -30,7 +37,8 @@ function merchantInstall(onPay) {
}
export default {
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range;
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): with a coin and a free tile of range x-6 her attack's turn
// throws a champagne bomb instead (onto an enemy's tile when one stands there; installS2);
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
// S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 8 surrounding tiles for
// attack@heal_scale × ATK — on her attack, or with no enemy to attack on her own attack timer (owner's decision
@@ -118,6 +126,53 @@ export default {
battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 });
battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit });
};
/**
* “见面礼” — 「消耗一枚金币在范围内一个可放置且可通行的地面放置香槟炸弹」. The client charpack (char_1033_swire2, mode S2)
* makes her attack a priority composite: first the SpawnToken ability (animation Skill_2) whose trigger needs BOTH a
* tile of its selector (range x-6; melee-buildable, walkable, low ground — the tile options of the selector) and her
* coin buff (swire2_can_use_gold), else her normal attack (which needs a target). So a bomb takes her attack's turn,
* enemies or not: with no enemy around she still spends her coins on the free tiles of x-6 (PRTS 备注 「否则随机放置香槟
* 至可部署的地块」), and only a full x-6 (or no coin) lets her coins pile up to the cap. Tile (PRTS 备注 「放置香槟动画进行
* 前若放置范围内存在敌人,则按攻击索敌规律放置香槟至其所在地块」; the selector's `_targetMotion` WALK): the ground enemies
* standing on a free tile of x-6, in her attack's target order (blocked first, …, nearest the goal) — the first one's
* tile; with none, a random free tile of x-6. A free tile: inside the field, nobody on it, no knocked-out operator's
* tile (freeTile; an enemy on it does not count), walkable ground a melee unit may stand on (groundTile).
* [ASSUMED] timing: the throw is instant and costs her attack one tick (PRTS 备注 「每次放置香槟时若正在攻击敌人,会短暂
* 延长攻击抬手间隔」 — the composite clears its cooldown when the throw ends; the length of the Skill_2 animation is in
* no table): `canAttack` holds her attack back while a throw is due and the 'tick' hook (after the attacks) throws
* when her attack is ready. Until 0.2.0 every attack also dropped a bomb on her 1-1 range, at random.
*/
const bombTiles = (battle, unit) => {
const out = [];
for (const k of gridKeys(SWIRE2_BOMB_GRID, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
if (freeTile(battle, r, c) && groundTile(battle, r, c)) out.push(k);
}
return out;
};
// (`bombHold`: a refused spawn — a tile freeTile let through — never holds her attack back for longer than one cycle)
const hasCoin = (battle, unit) => (unit.mem.coins ?? 0) >= coinCost && !(battle.time < (unit.mem.bombHold ?? -1));
const throwDue = (battle, unit) => hasCoin(battle, unit) && bombTiles(battle, unit).length > 0;
const installS2 = (battle, unit) => {
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
const ground = { ...unit.profile, canHitFly: false };
battle.on('tick', () => {
if (!alive(unit) || !unit.canAct || unit.atkCd > 1e-9 || unit.s.flags.disarm || !hasCoin(battle, unit)) return;
const keys = bombTiles(battle, unit);
if (!keys.length) return;
const free = new Set(keys);
let key = null;
for (const e of enemiesOn(battle, unit, keys, 0, ground)) {
const k = Math.round(e.y) * COLS + Math.round(e.x);
if (!e.isFlying && free.has(k)) { key = k; break; }
}
if (key == null) key = battle.rng.pick(keys);
const bomb = battle.spawnToken(unit, tokenId, (key / COLS) | 0, key % COLS, { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) { unit.mem.bombHold = battle.time + unit.s.interval; return; }
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit });
};
const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币"
battle.on('kill', (ctx) => {
if (ctx.killer === unit && ctx.victim.side === 'enemy' && unit.skill?.active) addCoins(battle, unit, 1);
@@ -171,35 +226,24 @@ export default {
};
},
}),
trait: { install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}) },
trait: {
// S2: a throw due takes her attack's turn (installS2)
...(sel === S2 || sel == null ? { canAttack: (battle, unit) => !throwDue(battle, unit) } : {}),
install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}),
},
install(battle, unit) {
if (sel === S1) { installS1(battle, unit); return; }
if (sel === S3) { installS3(battle, unit); return; }
if (sel !== S2 && sel != null) return;
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost) return;
const tiles = [];
for (const k of gridKeys(skillGrid ?? unit.rangeGrid, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
// (never on the home tile of a dead operator: it could not redeploy until an enemy triggers the bomb)
if (freeTile(battle, r, c) && groundTile(battle, r, c)) tiles.push([r, c]);
}
const tile = battle.rng.pick(tiles);
if (!tile) return;
const bomb = battle.spawnToken(unit, tokenId, tile[0], tile[1], { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) return;
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit, priority: -10 });
installS2(battle, unit);
},
talents: [
{ install(battle, unit) { // 大买家: "开启技能时获得1枚金币" (a passive starts at every deployment, S3 when cast)