feedback7: merge fb7-loadout — per-operator 潜能 (1–6) and 练度 (未精英化 / 精英1 / 精英2 / 精英2 Lv.60) set in 干员调配, defaults 潜能 6 and 精英2 Lv.60 (the owner's decision of 2026-10-08); PR #301 ported as one aligned list; #398 the skill frame, #400 REA-Y's heal text

- Conflicts (the branch started at ae22335c): the four i18n packs (union of keys), game.js DetailPanel props (ops + the
  voices' voice=true), detailPanel.js resolveDetail (cultOpts + the voices' tap), match/views.js (both helpers),
  docs/PLAYING.md and design/network.md merged line by line against the base. test/match/bosshp.test.js's ATK formula
  now includes the unit's 练度 multiplier (cultMul).
- Grok review: MERGE AFTER FIXES — the note and the module card at the set potential fixed on the branch.
- Golden: every owned operator ×1.1 ATK / DEF / max HP by default — roster 49, bonds 46, fields 22, matches 18, diy 134
  (stand-in battles and prototype picks unchanged). Broad node --test 5666 (0 fail after the bosshp formula; 2 skipped);
  lint 0 errors; typecheck; i18n strict clean (1152 msgids).
This commit is contained in:
sganggs committed 2026-10-08 22:35:33 +08:00
commit 0ced21d321
82 files changed
+9156 -6840

No files matched your search

+49 -13
View File
@@ -27,6 +27,10 @@
// slot's base id, `def.charId` = the operator (its kit, content/index.js kitOf), `def.loadout.diy` = the checked pick and
// `def.tokenOwner` = the key of its summons' variants (data/backups.json `tokens`, read by getToken); null for an illegal
// pick.
// Potential (潜能 1–6, 0.2.2 — the player's 干员调配 setting; shared/potential.js): a loadout's `potential` composes the
// chess / 自选 record at it (`def.loadout.potential`; full potential 6 is the default and the data's own) and the
// summons' variants at their owner's potential (getToken / getDiyToken: talents, a 自选 summon's deploy limit / count).
// A 补位 stand-in and a prototype 自选 pick have no potential.
//
// This module is pure ESM shared with browsers (served at /sim/): no Node API is imported here. Under Node the default
// loader (./nodeData.js) is imported dynamically at module evaluation; browsers never load it.
@@ -36,6 +40,7 @@
// getSimData() the injected data (browser) or the generated data (Node), or null.
import { resolveRecordLoadout, composeStats, composeTalents, loadoutRecord } from '../../shared/loadoutRecord.js';
import { atPotential, isPotential, POTENTIAL_DEFAULT, cultivateMul } from '../../shared/potential.js';
import { standInRecord } from '../../shared/standIn.js';
import { diyRecordOf, diyTokenOwner } from '../../shared/diy.js';
import { normHitArea } from './body.js';
@@ -230,7 +235,7 @@ export { composeStats, composeTalents, loadoutRecord };
/** Cache key suffix of a resolved non-default loadout ('' for the default). */
export function loadoutKey(lo) {
return lo && !lo.isDefault ? `|s${lo.skillIndex}|m${lo.moduleId ?? ''}` : '';
return lo && !lo.isDefault ? `|s${lo.skillIndex}|m${lo.moduleId ?? ''}${lo.potentialIsDefault === false ? `|p${lo.potential}` : ''}` : '';
}
/** Build an immunity Set from an object `{stun:true,…}` or an array `['stun', 'feared']`. */
@@ -560,6 +565,7 @@ export class DataSource {
stages: asMap(unwrap(raw.stages, 'stages'), 'id') ?? {},
waves: asMap(unwrap(raw.waves, 'waves'), 'id') ?? {},
backups: raw.backups && typeof raw.backups === 'object' && raw.backups.units ? raw.backups : null,
effects: raw.effects && typeof raw.effects === 'object' ? raw.effects : null,
};
this.fallback = fallback;
this._chess = new Map();
@@ -578,6 +584,20 @@ export class DataSource {
rawWave(id) { return this.raw.waves[id] ?? this.fallback?.rawWave(id) ?? null; }
/** data/backups.json (`{ units, diy }`) of this source or its fallback, or null. */
rawBackups() { return this.raw.backups ?? (typeof this.fallback?.rawBackups === 'function' ? this.fallback.rawBackups() : null); }
/** A data/effects.json record of this source, its fallback, or the default game data (Node / the injected data). */
rawEffect(id) {
const own = this.raw.effects && Object.prototype.hasOwnProperty.call(this.raw.effects, id) ? this.raw.effects[id] : null;
if (own) return own;
if (typeof this.fallback?.rawEffect === 'function') return this.fallback.rawEffect(id);
const fx = generatedCache && generatedCache.effects;
return fx && Object.prototype.hasOwnProperty.call(fx, id) ? fx[id] : null;
}
/**
* The 练度 (自持有) multipliers of tier `cultivate` (0–3): ATK / DEF / max HP from the effects.json CHAR_MAP record's
* char_attribute_mul (shared/potential.js cultivateMul), or null.
* @param {number} cultivate
*/
cultivateMul(cultivate) { return cultivateMul((id) => this.rawEffect(id), cultivate); }
/**
* Chess def for the default loadout, or for `loadout` `{ skillIndex, moduleId }` (DESIGN §16; resolved with
@@ -586,10 +606,11 @@ export class DataSource {
* `loadout.standIn === true`: the 补位 def of the chess (getStandIn) — its skill and module are the chess's backup
* selection whatever `skillIndex` / `moduleId` say; a chess without a stand-in (PRESET, DIY, no backups data) gives
* its own def for that loadout. `loadout.diy` (`{ charId, skillIndex, uniEquipId }`): the 自选 def of a DIY slot
* (getDiy; null when the pick is not legal for that slot).
* (getDiy; null when the pick is not legal for that slot). `loadout.potential` (1–6): the record at that potential
* (loadoutRecord; not for a stand-in).
*/
getChess(id, loadout = null) {
if (loadout && loadout.diy && typeof loadout.diy === 'object') return this.getDiy(id, loadout.diy);
if (loadout && loadout.diy && typeof loadout.diy === 'object') return this.getDiy(id, loadout.diy, loadout.potential);
if (loadout && loadout.standIn === true) {
const sd = this.getStandIn(id);
if (sd) return sd;
@@ -636,19 +657,26 @@ export class DataSource {
* module, `def.charId` the operator's, `def.diyFor` the slot's base id, `def.loadout` = the selection (`isDefault`) with
* `diy` = the checked pick, `def.tokenOwner` = its summons' variant key (shared/diy.js diyTokenOwner). Null for an
* illegal pick (not a pick of the slot's tier, a prototype off its locked skill, an unknown skill or module — checkDiyPick)
* or when the data lacks it. Cached per (id, pick).
* or when the data lacks it. An owned pick is composed at `potential` (1–6, default 6; shared/diy.js diyRecordOf —
* `def.loadout.potential`, its summons' variants follow); a prototype has none. Cached per (id, pick, potential).
* @param {string} id
* @param {{ charId?: string, skillIndex?: number|null, uniEquipId?: string|null }} pick
* @param {number|null} [potential]
*/
getDiy(id, pick) {
const key = `${id}|diy|${pick?.charId ?? ''}|${pick?.skillIndex ?? ''}|${pick?.uniEquipId ?? ''}`;
getDiy(id, pick, potential = null) {
const pot = isPotential(potential) ? potential : POTENTIAL_DEFAULT;
const key = `${id}|diy|${pick?.charId ?? ''}|${pick?.skillIndex ?? ''}|${pick?.uniEquipId ?? ''}|p${pot}`;
if (this._chess.has(key)) return this._chess.get(key);
const r = this.rawChess(id);
const rec = r && r.isDiy ? diyRecordOf(r, pick, this) : null;
const rec = r && r.isDiy ? diyRecordOf(r, pick, this, { potential: pot }) : null;
let d = null;
if (rec) {
d = normalizeChess({ ...rec, chessId: id });
d.loadout = { ...resolveLoadout(rec, null), diy: { charId: rec.charId, skillIndex: rec.skill.index, uniEquipId: rec.module?.id ?? null } };
const own = rec.diyProto ? POTENTIAL_DEFAULT : pot;
d.loadout = {
...resolveLoadout(rec, null), potential: own, potentialIsDefault: own === POTENTIAL_DEFAULT,
diy: { charId: rec.charId, skillIndex: rec.skill.index, uniEquipId: rec.module?.id ?? null },
};
d.tokenOwner = diyTokenOwner(rec.charId, rec.status);
freezeDef(d);
}
@@ -671,7 +699,7 @@ export class DataSource {
* than 'display'; PlayerState / the kits' skill summons read them from here).
*/
getToken(id, ownerChessId = null, ownerLoadout = null) {
if (ownerChessId && ownerLoadout && ownerLoadout.diy && typeof ownerLoadout.diy === 'object') return this.getDiyToken(id, ownerChessId, ownerLoadout.diy);
if (ownerChessId && ownerLoadout && ownerLoadout.diy && typeof ownerLoadout.diy === 'object') return this.getDiyToken(id, ownerChessId, ownerLoadout.diy, ownerLoadout.potential);
const olo = ownerChessId && ownerLoadout ? resolveLoadout(this.rawChess(ownerChessId), ownerLoadout) : null;
const ck = id + '|' + (ownerChessId ?? '') + loadoutKey(olo);
if (this._token.has(ck)) return this._token.get(ck);
@@ -695,6 +723,8 @@ export class DataSource {
}
if (r && r.variants && !owner) owner = Object.keys(r.variants)[0] ?? null;
let variant = r && r.variants && owner ? r.variants[owner] ?? null : null;
// the owner's potential (0.2.2): the variant's talents (and counts) at it — before the skill / module parts merge
if (variant && olo && olo.potentialIsDefault === false) variant = atPotential(variant, olo.potential);
if (variant && olo && !olo.isDefault) {
if (!olo.skillIsDefault && variant.bySkill && variant.bySkill[olo.skillIndex]) variant = { ...variant, ...variant.bySkill[olo.skillIndex] };
if (!olo.moduleIsDefault && variant.byModule && variant.byModule[olo.moduleId]) variant = { ...variant, ...variant.byModule[olo.moduleId] };
@@ -712,14 +742,17 @@ export class DataSource {
* @param {string} id token id
* @param {string} slotId the owner's DIY slot record id (`_a` / `_b`)
* @param {{ charId?: string, skillIndex?: number|null, uniEquipId?: string|null }} pick the owner def's `loadout.diy`
* @param {number|null} [potential] the owner's potential (its `loadout.potential`; default 6): the variant at it
*/
getDiyToken(id, slotId, pick) {
getDiyToken(id, slotId, pick, potential = null) {
const slot = this.rawChess(slotId);
const owner = slot && typeof pick?.charId === 'string' ? diyTokenOwner(pick.charId, slot.status) : null;
const ck = `${id}|diy|${owner ?? ''}|${pick?.skillIndex ?? ''}|${pick?.uniEquipId ?? ''}`;
const pot = isPotential(potential) ? potential : POTENTIAL_DEFAULT;
const ck = `${id}|diy|${owner ?? ''}|${pick?.skillIndex ?? ''}|${pick?.uniEquipId ?? ''}|p${pot}`;
if (this._token.has(ck)) return this._token.get(ck);
const r = this.rawToken(id);
let variant = r && r.variants && owner ? r.variants[owner] ?? null : null;
if (variant) variant = atPotential(variant, pot);
if (variant) {
const si = pick.skillIndex;
if (Number.isInteger(si) && variant.bySkill && variant.bySkill[si]) variant = { ...variant, ...variant.bySkill[si] };
@@ -821,7 +854,7 @@ export function toDataSource(data) {
* loadout, and an existing view unchanged (`isLoadoutView`). Used by spec.js createBattleFromSpec and by
* content/index.js installContent (every Battle, however constructed). An entry with `standIn: true` (补位) maps its
* chess id to the stand-in def (`getChess(id, { standIn: true })`) the same way, and one with `diy` (自选: `{ charId,
* skillIndex, uniEquipId }`) to its 自选 def (`getChess(id, { diy })`).
* skillIndex, uniEquipId }`) to its 自选 def (`getChess(id, { diy })`); a `potential` (1–6) rides along (0.2.2).
* @param {object} ds DataSource
* @param {object[]} players PlayerBattleInput[] (spec players)
*/
@@ -834,13 +867,16 @@ export function withUnitLoadouts(ds, players) {
for (const u of (p && Array.isArray(p.units) ? p.units : [])) {
if (!u || u.kind === 'token' || typeof u.chessId !== 'string') continue;
const diy = u.diy && typeof u.diy === 'object' ? u.diy : null;
if (u.skillIndex != null || u.moduleId != null || u.standIn === true || diy) any = true;
const pot = u.standIn !== true && isPotential(u.potential) ? u.potential : null;
if (u.skillIndex != null || u.moduleId != null || u.standIn === true || diy || pot != null) any = true;
const lo = { skillIndex: u.skillIndex ?? null, moduleId: u.moduleId ?? null };
if (u.standIn === true) lo.standIn = true;
if (diy) lo.diy = diy;
if (pot != null) lo.potential = pot;
const prev = map.get(u.chessId);
if (!prev) map.set(u.chessId, lo);
else if (prev.skillIndex !== lo.skillIndex || prev.moduleId !== lo.moduleId || !!prev.standIn !== !!lo.standIn
|| (prev.potential ?? null) !== (lo.potential ?? null)
|| JSON.stringify(prev.diy ?? null) !== JSON.stringify(lo.diy ?? null)) conflicts.add(u.chessId);
}
}