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:
1 parent
7e09c63da8
commit
ec0a2fc669
16 files changed
+324
-62
No files matched your search
+11
-4
@@ -1,7 +1,7 @@
|
||||
// server/sim/units.js — Unit model (operators, tokens, enemies, devices) and stat aggregation (DESIGN §5.2).
|
||||
//
|
||||
// Aggregation (recomputed lazily whenever buffs change — `unit.markDirty()`):
|
||||
// ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul
|
||||
// ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul (Πmul includes the unit's 练度 `cultMul`, 0.2.2)
|
||||
// res = clamp((base + Σflat) × Πmul, 0, 100)
|
||||
// aspd = clamp(base + Σaspd, 20, 600) (base is 100 for almost everyone; floor 20 = PRTS 数值范围)
|
||||
// interval = bat × (1 + ΣbatPct) × 100 / aspd (ΣbatPct floored at −0.9)
|
||||
@@ -16,6 +16,8 @@ import { DIR_VEC, normDir } from './dir.js';
|
||||
|
||||
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
|
||||
const EMPTY = Object.freeze({});
|
||||
/** No 练度 multiplier (a unit without one: enemies, summons, stand-ins, a raw spec unit). */
|
||||
const NO_MUL = Object.freeze({ atk: 1, def: 1, hp: 1 });
|
||||
/** Aggregates can overflow (huge stacked *Mul mods → Infinity) — fall back to `d` so no stat is ever non-finite. */
|
||||
const fin = (v, d) => (Number.isFinite(v) ? v : d);
|
||||
|
||||
@@ -97,6 +99,10 @@ export class Unit {
|
||||
this.persist = { redeployMul: 1, freeRedeploys: 0 };
|
||||
this.isBoss = false;
|
||||
this.bossPool = null;
|
||||
// 练度 (自持有, 0.2.2; battle/players.js): the owned operator's tier (0–3, effects.json aceffect_char_1…4) and its
|
||||
// char_attribute_mul — ATK / DEF / max HP, a multiplier of its own (never summed with the 直接乘算 percentages)
|
||||
this.cultivate = null;
|
||||
this.cultMul = null;
|
||||
}
|
||||
|
||||
markDirty() { this._dirty = true; }
|
||||
@@ -123,12 +129,13 @@ export class Unit {
|
||||
const a = (k) => add[k] ?? 0;
|
||||
const m = (k) => mul[k] ?? 1;
|
||||
const b = this.base;
|
||||
const cm = this.cultMul || NO_MUL;
|
||||
const bHp = fin(b.maxHp, 1) > 0 ? fin(b.maxHp, 1) : 1;
|
||||
const maxHp = Math.max(1, fin((bHp + a('hpFlat')) * Math.max(0, 1 + a('hpPct')) * m('hpMul'), bHp));
|
||||
const maxHp = Math.max(1, fin((bHp + a('hpFlat')) * Math.max(0, 1 + a('hpPct')) * m('hpMul') * cm.hp, bHp));
|
||||
// PRTS 游戏数据基础 属性基本公式 A_f = F_t[(A + D_p)(1 + D_t) + F_p]: `atkFinal` is the 最终加算 (FINAL_ADDITION) —
|
||||
// added after the percentages, inside the Πmul (阿戈尔's devoured base ATK, DESIGN §24.7)
|
||||
const atk = Math.max(0, fin(((b.atk + a('atkFlat')) * Math.max(0, 1 + a('atkPct')) + a('atkFinal')) * m('atkMul'), fin(b.atk, 0)));
|
||||
const def = Math.max(0, fin((b.def + a('defFlat')) * Math.max(0, 1 + a('defPct')) * m('defMul'), fin(b.def, 0)));
|
||||
const atk = Math.max(0, fin(((b.atk + a('atkFlat')) * Math.max(0, 1 + a('atkPct')) + a('atkFinal')) * m('atkMul') * cm.atk, fin(b.atk, 0)));
|
||||
const def = Math.max(0, fin((b.def + a('defFlat')) * Math.max(0, 1 + a('defPct')) * m('defMul') * cm.def, fin(b.def, 0)));
|
||||
const res = clamp(fin((b.res + a('resFlat')) * m('resMul'), fin(b.res, 0)), 0, 100);
|
||||
const aspd = clamp(fin(b.aspd + a('aspd'), 100), ASPD_MIN, ASPD_MAX);
|
||||
const bBat = fin(b.bat, 1) > 0 ? fin(b.bat, 1) : 1;
|
||||
|
||||
Reference in new issue
Block a user