feedback1 integrate: WP live skill range on the card; 烛煌 S3 target + 1.7 splash; 信仰搅拌机 S3 ignores 攻击距离
This commit is contained in:
commit
313db2f55f
18 files changed
+917
-53
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;
|
||||
}
|
||||
+11
-3
@@ -193,16 +193,23 @@ const statView = (x) => ({
|
||||
* or the last ones the sim computed) next to its own `unit.base` (no buffs) — max HP, ATK, DEF, RES, attack interval
|
||||
* (s), block, move speed — rounded for display (the sim keeps floats), plus the current HP. The shape of the
|
||||
* `m.unitStats` units (Match.unitStats: what the board's units start their next battle with) and of the browser
|
||||
* runner's live battle stats (public/js/battle/runner.js unitStats).
|
||||
* @param {{ id?: number, uid?: number|null, defId?: string, hp?: number, alive?: boolean, base?: any } | null} u
|
||||
* runner's live battle stats (public/js/battle/runner.js unitStats). An ally with a range also carries `range`: the grid
|
||||
* (`[dRow, dCol]`, facing RIGHT) it attacks with now — a running skill's range, rangeExtend included, not a kit's
|
||||
* target-selection grid (the sim's `unit.liveRangeGrid`, Battle._refreshRange; community report E1 after 0.1.0: 烛煌
|
||||
* S3's 4-11 never reached the card).
|
||||
* @param {{ id?: number, uid?: number|null, defId?: string, hp?: number, alive?: boolean, base?: any, liveRangeGrid?: any } | null} u
|
||||
* @param {any} [s] aggregated stats (missing ⇒ the base)
|
||||
* @returns {{ id: number|null, uid: number|null, defId: string|null, hp: number, alive: boolean, maxHp: number, atk: number,
|
||||
* def: number, res: number, interval: number|null, blockCnt: number, moveSpeed: number,
|
||||
* base: { maxHp: number, atk: number, def: number, res: number, interval: number|null, blockCnt: number, moveSpeed: number } }}
|
||||
* base: { maxHp: number, atk: number, def: number, res: number, interval: number|null, blockCnt: number, moveSpeed: number },
|
||||
* range?: Array<[number, number]> }}
|
||||
*/
|
||||
export function unitStatsEntry(u, s = null) {
|
||||
const base = u && u.base && typeof u.base === 'object' ? u.base : {};
|
||||
const cur = s && typeof s === 'object' ? s : base;
|
||||
const range = u?.side !== 'enemy' && Array.isArray(u?.liveRangeGrid)
|
||||
? u.liveRangeGrid.filter((p) => Array.isArray(p) && Number.isInteger(p[0]) && Number.isInteger(p[1])).map((p) => [p[0], p[1]])
|
||||
: null;
|
||||
return {
|
||||
id: Number.isInteger(u?.id) ? u.id : null,
|
||||
uid: Number.isInteger(u?.uid) ? u.uid : null,
|
||||
@@ -211,6 +218,7 @@ export function unitStatsEntry(u, s = null) {
|
||||
alive: u?.alive !== false,
|
||||
...statView(cur),
|
||||
base: statView(base),
|
||||
...(range ? { range } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user