feedback1e WP: review round 3 — 烛煌 S3 last bolt keeps the burst bonus, splash fx; prep overlay and wheel show the deployed range
- 烛煌 S3: the burn-burst bonus is gated on the attack's own isSkill (captured when the bolt was fired), not on the live
skill, so the last bullet (it lands after end('ammo')) and bolts in flight when the skill ends early still carry it.
- 烛煌 S3: every landed bolt emits fx 'splash' (r 1.7, burn) at the main target, so the splash shows on screen.
- shared/loadoutRecord.js attackRangeGrid: the range a unit is deployed with — a selected passive range skill
(引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1; not 空弦 ISW-A's 集成战略-only one) —
so the board overlay, the deploy wheel and the card agree with the prep live range. extendedGrid moves there
(re-exported by targeting.js).
- runner.js header: unitStatsEntry carries the ally's live range.
This commit is contained in:
1 parent
5963a40d2d
commit
a657f809f1
11 files changed
+247
-52
No files matched your search
+60
-9
@@ -1,8 +1,12 @@
|
||||
// shared/loadoutRecord.js — operator loadouts (DESIGN §16, DATA.md §2.2): a data/chess.json record as the selected
|
||||
// skill / module make it. Pure ESM shared by the simulation (server/sim/simdata.js re-exports it: getChess(id, loadout)
|
||||
// builds unit defs from it) and the client UI (the detail card shows the stats / 特性 / talents the unit fights with —
|
||||
// user playtest #2 integration: an elite on 不装备 showed its default module's ATK and trait). One implementation, so
|
||||
// the card and the battle never disagree. (Which choices a player may make: shared/protocol.js loadoutOptions.)
|
||||
// user playtest #2 integration: an elite on 不装备 showed its default module's ATK and trait; attackRangeGrid = the range
|
||||
// it is deployed with, also the board overlay and the deploy wheel — extendedGrid is the battle's own 攻击距离 growth,
|
||||
// re-exported by server/sim/targeting.js). One implementation, so the card and the battle never disagree. (Which
|
||||
// choices a player may make: shared/protocol.js loadoutOptions.)
|
||||
|
||||
import { GEO } from './constants.js';
|
||||
|
||||
/**
|
||||
* Resolve a loadout against a chess record.
|
||||
@@ -99,19 +103,66 @@ export function loadoutRecord(rec, lo) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The attack range a (loadout-resolved) chess record fights with: an elite whose equipped module reads "攻击范围扩大"
|
||||
* uses that module's own grid — its range-only talent change (talentIndex −1), e.g. SPC-X = the 3×3 caster range + the
|
||||
* centre tile [0,3] — as the kits do (tier4 moduleRangeGrid, tier5 moduleRangeUp); anything else its `rangeGrid`.
|
||||
* The attack range a (loadout-resolved) chess record fights with from its deployment — the detail card without a live
|
||||
* entry, the board's range overlay and the deploy wheel (DESIGN §16), the same tiles the battle unit starts with (prep
|
||||
* m.unitStats `range`): the selected skill's grid when it reads "被动效果:攻击范围扩大" (引星棘刺 S3 3-9: her own range
|
||||
* while she carries it, tier5 kit); else an elite whose equipped module reads "攻击范围扩大" uses that module's own grid
|
||||
* — its range-only talent change (talentIndex −1), e.g. SPC-X = the 3×3 caster range + the centre tile [0,3] — as the
|
||||
* kits do (tier4 moduleRangeGrid, tier5 moduleRangeUp); anything else its `rangeGrid`. Then grown by the 特性's
|
||||
* permanent 攻击距离 (traitRangeExtend: 信仰搅拌机 SPT-Y "攻击距离+1"). A running skill's range is the live entry's.
|
||||
* @param {object|null} rec loadoutRecord(…) output (or a data/chess.json record: its default module)
|
||||
* @returns {number[][]|null}
|
||||
*/
|
||||
export function attackRangeGrid(rec) {
|
||||
if (!rec || typeof rec !== 'object') return null;
|
||||
let g = Array.isArray(rec.rangeGrid) ? rec.rangeGrid : null;
|
||||
const sk = rec.skill;
|
||||
const m = rec.module;
|
||||
if (rec.isGolden && m && m.active && m.id && /攻击范围扩大/.test(String(rec.trait?.moduleDesc ?? ''))) {
|
||||
if (sk && Array.isArray(sk.rangeGrid) && sk.rangeGrid.length && /被动效果:攻击范围扩大/.test(String(sk.desc ?? ''))) {
|
||||
g = sk.rangeGrid;
|
||||
} else if (rec.isGolden && m && m.active && m.id && /攻击范围扩大/.test(String(rec.trait?.moduleDesc ?? ''))) {
|
||||
const mod = (Array.isArray(rec.modules) ? rec.modules : []).find((x) => x && x.uniEquipId === m.id);
|
||||
const g = (mod?.talentChanges || []).find((t) => t && t.talentIndex === -1 && Array.isArray(t.rangeGrid) && t.rangeGrid.length)?.rangeGrid;
|
||||
if (g) return g;
|
||||
const mg = (mod?.talentChanges || []).find((t) => t && t.talentIndex === -1 && Array.isArray(t.rangeGrid) && t.rangeGrid.length)?.rangeGrid;
|
||||
if (mg) g = mg;
|
||||
}
|
||||
return Array.isArray(rec.rangeGrid) ? rec.rangeGrid : null;
|
||||
const ext = traitRangeExtend(rec);
|
||||
return g && ext ? extendedGrid(g, ext) : g;
|
||||
}
|
||||
|
||||
/**
|
||||
* The permanent 攻击距离 (ability_range_forward_extend) a record's 特性 grants — a module's, e.g. 信仰搅拌机 SPT-Y
|
||||
* "攻击距离+1" (tier4 rangeUp: a persistent rangeExtend buff, s.baseRangeExtend); 0 for one that works "在集成战略中" only
|
||||
* (空弦 ISW-A). The other 攻击距离 of the mode are skills' (their running range).
|
||||
* @param {object|null} rec loadoutRecord(…) output
|
||||
*/
|
||||
export function traitRangeExtend(rec) {
|
||||
const t = rec && typeof rec === 'object' ? rec.trait : null;
|
||||
if (!t || typeof t !== 'object' || /集成战略/.test(String(t.moduleDesc ?? ''))) return 0;
|
||||
const n = Math.floor(Number(t.bb?.ability_range_forward_extend) || 0);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A range grid (`[dRow, dCol]`, facing RIGHT) grown by `extend` (rangeExtend / 攻击距离, DESIGN §3): every row gains
|
||||
* the whole tiles 1 … ⌊extend⌋ beyond its far (+dCol) end — the relative form of what server/sim/targeting.js
|
||||
* absoluteRangeKeys builds, deduplicated, junk entries dropped. One implementation for the battle (re-exported by
|
||||
* targeting.js: Battle._refreshRange keeps it as `unit.liveRangeGrid`, the card's live 攻击范围, when an extend applies)
|
||||
* and the record's attackRangeGrid.
|
||||
* @param {Array<[number, number]>|null|undefined} grid
|
||||
* @param {number} [extend]
|
||||
* @returns {Array<[number, number]>}
|
||||
*/
|
||||
export function extendedGrid(grid, extend = 0) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const add = (dr, dc) => { const k = `${dr},${dc}`; if (!seen.has(k)) { seen.add(k); out.push([dr, dc]); } };
|
||||
if (!Array.isArray(grid)) return out;
|
||||
const cells = grid.filter((p) => Array.isArray(p) && Number.isInteger(p[0]) && Number.isInteger(p[1]));
|
||||
for (const [dr, dc] of cells) add(dr, dc);
|
||||
if (extend > 0 && Number.isFinite(extend)) {
|
||||
const maxByRow = new Map();
|
||||
for (const [dr, dc] of cells) maxByRow.set(dr, Math.max(maxByRow.get(dr) ?? -Infinity, dc));
|
||||
for (const [dr, mx] of maxByRow) for (let k = 1; k <= Math.min(extend, GEO.COLS); k++) add(dr, mx + k);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user