feedback7 R22-LOADOUT: the battle takes each operator's potential and 练度 from its spec entry; golden: unchanged

A PlayerBattleInput operator entry may carry `potential` (潜能 1–6) and `cultivate` (练度 0–3, the 自持有 bonus) —
the player's 干员调配 settings (the owner's decision of 2026-10-08). Specs without them field what 0.2.1 fielded, so
no golden scenario moves here; the match states both next.

- shared/loadoutRecord.js: a resolved loadout carries `potential`; loadoutRecord composes the record at it first
  (shared/potential.js atPotential), then the skill / module as before.
- server/sim/simdata.js: getChess(id, { …, potential }), getDiy(slot, pick, potential) (an owned 自选 pick's form at it;
  a prototype has none — `diyProto` on its record, shared/diy.js), getToken / getDiyToken compose the owner variant at
  the owner's potential (talents; a 自选 summon's deploy limit / holding / count); the per-battle loadout view keeps it
  per unit; DataSource.cultivateMul reads the effects.json CHAR_MAP record's char_attribute_mul.
- battle/players.js: `potential` composes the def (never a stand-in's); `cultivate` sets unit.cultMul — ×ATK / ×DEF /
  ×max HP in the unit's Πmul (units.js), a multiplier of its own, never summed with the 直接乘算 percentages (PRTS
  盟约记录 「…与自持有干员的属性加成独立」); a stand-in, a prototype 自选 pick and a summon take none [ASSUMED for summons].
- spec.js keeps both only when well-formed (never on a stand-in or a summon); content/index.js passes the potential to
  the loadout sync; the kits that read a summon variant raw (content/tokens.js ownVariant, summoner.js tokenStat,
  多萝西, 麦克尼斯特) compose it at the owner's potential; UnitInfo carries `potential` (below 6) / `cultivate`;
  unitStatsEntry counts the 练度 in the unit's own numbers (no green delta for it on the card).
- Docs: SIM.md §2 (Πmul), §9 (UnitInfo), §12.
- Tests: test/sim/potential-battle.test.js (new, 6): a unit at each potential (刺玫 413 / 17 / 70 s and 8 % at 潜能1;
  the elite on 不装备), the four 练度 tiers and their product with a +20 % ATK bonus, HP at deployment and the card's
  numbers, stand-in / prototype take neither and an owned 自选 pick both, Mon3tr / 望's 棋子 / “小自在” at the owner's
  potential, the spec round trip (kept, dropped, the JSON spec gives the same result digest), two players with the same
  chess at different settings on one field; test/sim/loadout.test.js expects the resolved loadout's new fields.
This commit is contained in:
sganggs committed 2026-10-08 20:38:08 +08:00
1 parent 7e09c63da8
commit ec0a2fc669
16 files changed
+324 -62

No files matched your search

+4 -1
View File
@@ -28,6 +28,7 @@
import { genericKit, genericTalents } from './generic.js';
import { withUnitLoadouts } from '../simdata.js';
import { isPotential } from '../../../shared/potential.js';
// Content files are loaded with guarded dynamic imports: a module that fails to load (syntax error, throwing
// top-level code, missing file) is logged and replaced by an empty module instead of breaking the server.
@@ -136,7 +137,8 @@ function inputEntry(unit) {
* The loadout `{ skillIndex, moduleId }` a unit's PlayerBattleInput entry gives; an entry without loadout fields means
* the DEFAULT loadout (`{}`), never "whatever the data view maps this chess id to" (another player's choice in a
* multi-player field). `standIn: true` rides along (补位: the stand-in def whatever the skill / module fields say), as
* does `diy` (自选: the pick of a DIY slot's piece). Null when the unit has no input entry.
* do `diy` (自选: the pick of a DIY slot's piece) and `potential` (0.2.2: 1–6, never a stand-in's). Null when the unit
* has no input entry.
*/
function inputLoadout(unit) {
const x = inputEntry(unit);
@@ -144,6 +146,7 @@ function inputLoadout(unit) {
const extra = {
...(x.standIn === true ? { standIn: true } : null),
...(x.diy && typeof x.diy === 'object' ? { diy: x.diy } : null),
...(x.standIn !== true && isPotential(x.potential) ? { potential: x.potential } : null),
};
if (x.skillIndex == null && x.moduleId == null) return extra;
return { skillIndex: x.skillIndex ?? null, moduleId: x.moduleId ?? null, ...extra };
+2 -1
View File
@@ -58,6 +58,7 @@ import { num, talentBb, moduleBb, traitBb, up, toggleBuff, summonTileFree } from
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
import { bodyOnTile } from '../../../body.js';
import { COLS } from '../../../constants.js';
import { atPotential } from '../../../../../shared/potential.js';
const S1 = 'skchr_doroth_1';
const S2 = 'skchr_doroth_2';
@@ -112,7 +113,7 @@ export default {
/** The token's deploy limit for her loadout (variant stats, the module's when it carries one); her max stock too. */
function limitOf(battle, unit) {
const raw = battle.data.rawToken?.(RESONATOR);
const v = raw?.variants?.[unit.def?.tokenOwner];
const v = atPotential(raw?.variants?.[unit.def?.tokenOwner], unit.def?.loadout?.potential);
const mod = unit.def?.loadout?.moduleId;
const n = num(v?.byModule?.[mod]?.stats?.deployLimit, num(v?.stats?.deployLimit, num(raw?.deployLimit, 0)));
if (n > 0) return n;
+2 -1
View File
@@ -59,6 +59,7 @@ import { num, skillRec, batMod, up } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { dirVec } from '../../../dir.js';
import { hypot } from '../../../detmath.js';
import { atPotential } from '../../../../../shared/potential.js';
const S1 = 'skchr_mcnist_1';
const S2 = 'skchr_mcnist_2';
@@ -284,7 +285,7 @@ export default {
},
talents: [
{ install(battle, unit) { // 结构性原理: her pieces run the token's kit (set up before the battle starts)
const raw = battle.data.rawToken?.(MCGRAF)?.variants?.[unit.def?.tokenOwner] ?? null;
const raw = atPotential(battle.data.rawToken?.(MCGRAF)?.variants?.[unit.def?.tokenOwner] ?? null, unit.def?.loadout?.potential);
for (const t of battle.allyUnits) {
if (!isGrafOf(t, unit) || t.alive || t.deployed) continue;
// its life: the base variant's (the SO-A stage-3 skill@duration −1 is 集成战略-only)
+5 -3
View File
@@ -27,6 +27,7 @@
// its owner), else the engine's removal reason.
import { num, up } from './tier1.js';
import { atPotential } from '../../../../../shared/potential.js';
/** Seconds between two tries of a piece waiting to come back (its owner off the field, timer, holding, DP, tile). */
export const DECK_RETRY = 0.25;
@@ -73,8 +74,9 @@ export function summonTriggerArea(battle, owner, skillId, area) {
}
/**
* A field of a summon's token record at its owner's form and module (data/backups.json `tokens[id].variants[<charId>@
* <status>]`, `byModule[uniEquipId]`): the stats the normalised def drops (`deployLimit`, `deckStack`).
* A field of a summon's token record at its owner's form, module and potential (data/backups.json `tokens[id].variants[<charId>@
* <status>]`, `byModule[uniEquipId]`; shared/potential.js atPotential): the stats the normalised def drops (`deployLimit`,
* `deckStack`).
* @param {object} battle
* @param {object} owner the summoner unit
* @param {string} tokenId
@@ -82,7 +84,7 @@ export function summonTriggerArea(battle, owner, skillId, area) {
*/
export function tokenStat(battle, owner, tokenId, key) {
const raw = battle.data?.rawToken?.(tokenId) ?? null;
const v = raw?.variants?.[owner?.def?.tokenOwner] ?? null;
const v = atPotential(raw?.variants?.[owner?.def?.tokenOwner] ?? null, owner?.def?.loadout?.potential);
const mod = owner?.def?.loadout?.diy?.uniEquipId ?? owner?.def?.loadout?.moduleId ?? null;
const st = (mod && v?.byModule?.[mod]?.stats) || v?.stats || raw?.stats || null;
return st && Number.isFinite(st[key]) ? st[key] : (Number.isFinite(raw?.[key]) ? raw[key] : null);
+3 -1
View File
@@ -93,6 +93,7 @@ import { bardRegen } from '../professions.js';
import { normDir, localOrder } from '../dir.js';
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
import { hypot } from '../detmath.js';
import { atPotential } from '../../../shared/potential.js';
const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : (typeof v === 'string' && v.trim() !== '' && Number.isFinite(+v) ? +v : d));
const GRID_3X3 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
@@ -157,7 +158,8 @@ function ownVariant(raw, owner) {
const oid = owner?.def?.tokenOwner ?? owner?.defId;
if (!vs || typeof vs !== 'object' || !oid) return null;
const v = vs[oid] ?? vs[String(oid).replace(/_b$/, '_a')] ?? null;
return v ? withLoadout(v, owner.def?.loadout) : null;
// at the owner's potential (0.2.2: a 自选 summon's deploy limit / count, the talents — shared/potential.js)
return v ? withLoadout(atPotential(v, owner.def?.loadout?.potential), owner.def?.loadout) : null;
}
/**