Files
ark-stronghold-protocol/public/js/screens/cultivation.js
T
sganggs dc336f24a1 feedback7 R22-LOADOUT: 干员调配's roster is one list — a row per operator with the quick skill / module choices and the 潜能 / 练度 selects in aligned columns — and the detail opens with 潜能与练度; golden: unchanged
The owner's decisions of 2026-10-08: 「D12 [PR #301] 可以做,具体布局怎么优化你来决定,他那样确实方便,但是三个竖排有点乱影响
观感」 and 「D7D8调配干员里自己设置吧,默认满潜满加成」. The quick choices are PR #301's idea (by @farridge); the layout is new:
one list instead of three columns of cards.

- screens/loadout.js: RosterHead + RosterRow — every row the same four columns under a sticky head (干员 | 技能 |
  模组 精锐 | 潜能 · 练度): portrait / name / bonds (selects, opens the detail), three skill slots (an empty one for a
  two-skill chess; the label says slot, name, SP, duration — quickSkillTags), the elite's modules (不装备 first), the
  潜能 / 练度 selects; a quick choice goes through setChoice / setEntries like the detail and keeps a phone on the list.
  The detail's 普通 / 精锐 skill toggle is one preview for the skills, the 特质 and the rows. The detail opens with the
  潜能与练度 section; 恢复默认 / 全部恢复默认 reset the settings too (全部: the roster's operators — a 自选 pick's stay
  with the 自选编队 tab); 导入 reports a settings-only import; ←/→ stay inside a focused row's choices.
- screens/cultivation.js (new): CultivationSelects (two compact selects; quiet at the defaults, mint when changed) and
  CultivationSection (潜能 1–6 and the four official tiers — effects.json names and effect texts — the rule in one line,
  the stand-in note for a chess marked not owned).
- screens/diy.js: an owned 自选 pick's slot card carries its operator's selects; a prototype says it has neither.
- css: the head and the rows share one grid (`--cols`), 32 px quick targets (40 px on a coarse pointer); the detail
  slides over the list below 1000 px (was 760 px); below 700 px the row's bonds give way to the name. Headless check at
  1920 × 1080, 1366 × 768, 844 × 390 and 667 × 375: no sideways overflow, the head's columns equal every row's.
- The 局内数值 caption says 含潜能与练度 when the numbers carry both (effects.json loaded); the overlay's note says the
  skills, modules, 潜能 and 练度 can be set (the operator's in-match level cannot).
- i18n: 21 strings in the four packs (the tier names, short tier labels, the notes, the reset confirm, the import toast).
- Docs: design/match.md (the list, the 潜能与练度 section, 导出's `ops`), ARCHITECTURE.md (screens/cultivation.js).
- Tests: test/ui/loadout-rows.test.js (new, 7): a row's pick / skill slots / modules / aria-pressed / callbacks and the
  shared 精锐 preview, its selects (values, options, a change, the changed mark, a stand-in), the head, the detail's
  section (tiers from effects.json, the effect text, the stand-in note), a 自选 owned pick vs a prototype, 局内数值 at
  潜能1 / 未精英化 and at the defaults with its caption, the css grid and breakpoints. e2e (not run here; their steps
  replayed with a headless page): the row pick selector, the phone test (one list, 40 px targets, the detail slides
  over, back to the list), the 局内数值 expectations ×1.1, a new 潜能 / 练度 flow (row select → stored, 部署费用 17 / 攻击
  454; the detail's 未精英化 → 413; the row follows; 恢复默认).
2026-10-08 21:38:26 +08:00

95 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 潜能 / 练度 controls of the 干员调配 overlay (0.2.2, the owner's decision of 2026-10-08: 「调配干员里自己设置吧,默认满潜
// 满加成」): per operator (by charId — the normal, elite and 自选 forms share them), 潜能 1–6 and the 自持有 练度 tier
// (effects.json CHAR_MAP aceffect_char_1…4: 未精英化 / 精英阶段1 / 精英阶段2 / 精英阶段2-60级 — their official names and
// texts, localized by the data overlay), default 潜能 6 / 精英阶段2-60级. Official: 调度手册 「卫戍协议中干员潜能由自身已
// 持有干员潜能决定」 and 「提升已持有的干员,将为卫戍协议中同名的干员提供加成」 (an operator the player does not own fights
// at 潜能 1 with no bonus: set by hand for a 特许 one; a NORMAL one not owned is its stand-in, which has neither).
// CultivationSelects: two compact selects (a roster row, a 自选 slot card); CultivationSection: the detail's segmented
// controls with the tier's effect. Hook-free views (the tests draw them). The settings live in ui/loadoutSync.js
// (`ops`, synced with room.loadout); the model is ui/loadoutModel.js opsOf / setOps.
import { html, MicroLabel } from '../ui/components.js';
import { data } from '../data.js';
import { opsOf } from '../ui/loadoutModel.js';
import { CULTIVATE_EFFECTS, POTENTIAL_MIN, POTENTIAL_MAX } from '../../../shared/potential.js';
import { t, N_ } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
const POTENTIALS = Array.from({ length: POTENTIAL_MAX - POTENTIAL_MIN + 1 }, (_, i) => POTENTIAL_MIN + i);
const TIERS = CULTIVATE_EFFECTS.map((_, i) => i);
/** Names while effects.json is not loaded (msgids; the data's own names win). */
const FALLBACK_NAMES = Object.freeze([N_('未精英化'), N_('精英阶段1'), N_('精英阶段2'), N_('精英阶段2-60级')]);
/** The short names a row's select shows (the full official name is its title and the detail's). */
const SHORT_NAMES = Object.freeze([N_('未精英化'), N_('精英1'), N_('精英2'), N_('精英2 Lv.60')]);
/** The official name of 练度 tier `n` (effects.json `name`, localized), else its fallback. */
export function cultivateName(n) {
return data.lookup('effects', CULTIVATE_EFFECTS[n])?.name || t(FALLBACK_NAMES[n] || '');
}
/** The official effect text of 练度 tier `n` (effects.json `desc`: 「攻击力、防御力和最大生命值+10%」 …), or ''. */
export function cultivateDesc(n) {
return data.lookup('effects', CULTIVATE_EFFECTS[n])?.desc || '';
}
/**
* Two compact selects — 潜能 and 练度 — of operator `charId` under the stored settings `ops` (a roster row, a 自选 slot).
* @param {{ charId: string, ops: Record<string, any>, onSet: (charId: string, patch: { potential?: number, cultivate?: number }) => void,
* note?: string|null }} props `note`: why the settings do not apply now (a 补位 stand-in), shown as the tooltip
*/
export function CultivationSelects({ charId, ops, onSet, note = null }) {
if (!charId) return null;
const cur = opsOf(ops, charId);
const set = (patch) => onSet(charId, patch);
// a setting off the default stands out (mint); the default stays quiet — it is the same on every row
return html`<span class=${cx('lo-cult', cur.changed && 'is-changed', note && 'is-moot')} data-char=${charId} title=${note || undefined}>
<span class=${cx('lo-select', 'lo-cult__sel', 'lo-cult__pot', cur.potential !== 6 && 'is-off')}>
<select value=${String(cur.potential)} aria-label=${t('潜能')} data-cult="potential"
onChange=${(e) => set({ potential: Number(e.currentTarget.value) })}>
${POTENTIALS.map((n) => html`<option key=${n} value=${String(n)}>${t('潜能 {n}', { n })}</option>`)}
</select>
</span>
<span class=${cx('lo-select', 'lo-cult__sel', 'lo-cult__tier', cur.cultivate !== TIERS.length - 1 && 'is-off')} title=${note ? undefined : cultivateName(cur.cultivate)}>
<select value=${String(cur.cultivate)} aria-label=${t('练度')} data-cult="cultivate"
onChange=${(e) => set({ cultivate: Number(e.currentTarget.value) })}>
${TIERS.map((n) => html`<option key=${n} value=${String(n)}>${t(SHORT_NAMES[n])}</option>`)}
</select>
</span>
</span>`;
}
/**
* The detail's 潜能 / 练度 section: 潜能 1–6 and the four 练度 tiers as segmented buttons, the tier's official effect,
* the rule in one line, and — for a chess the player marked not owned — that its stand-in has neither.
* @param {{ charId: string, ops: Record<string, any>, onSet: (charId: string, patch: object) => void, standIn?: boolean }} props
*/
export function CultivationSection({ charId, ops, onSet, standIn = false }) {
if (!charId) return null;
const cur = opsOf(ops, charId);
const set = (patch) => onSet(charId, patch);
return html`<section class=${cx('lo-sec', 'lo-sec--cult', standIn && 'is-moot')} aria-label=${t('潜能与练度')} data-char=${charId}>
<header class="lo-sec__head">
<h3>${t('潜能与练度')}<${MicroLabel}>POTENTIAL<//></h3>
${cur.changed ? html`<span class="lo-badge lo-badge--changed">${t('已调整')}</span>` : html`<span class="lo-badge lo-badge--plain">${t('满潜能 · 精英2 Lv.60')}</span>`}
</header>
<div class="lo-cult__row">
<span class="lo-cult__k">${t('潜能')}</span>
<div class="lo-seg lo-seg--pot" role="radiogroup" aria-label=${t('潜能')}>
${POTENTIALS.map((n) => html`<button key=${n} type="button" role="radio" aria-checked=${cur.potential === n ? 'true' : 'false'}
class=${cx(cur.potential === n && 'is-on')} data-potential=${n} onClick=${() => set({ potential: n })}><span class="num">${n}</span></button>`)}
</div>
</div>
<div class="lo-cult__row">
<span class="lo-cult__k">${t('练度')}</span>
<div class="lo-seg lo-seg--tier" role="radiogroup" aria-label=${t('练度')}>
${TIERS.map((n) => html`<button key=${n} type="button" role="radio" aria-checked=${cur.cultivate === n ? 'true' : 'false'}
class=${cx(cur.cultivate === n && 'is-on')} data-cultivate=${n} onClick=${() => set({ cultivate: n })}>${cultivateName(n)}</button>`)}
</div>
</div>
<p class="lo-cult__eff" data-cultivate=${cur.cultivate}><b>${cultivateName(cur.cultivate)}</b> <span>${cultivateDesc(cur.cultivate)}</span></p>
<p class="lo-cult__note">${standIn
? t('干员持有中标记为未持有:替补干员没有潜能与练度;这里的设置在改回「持有」后生效')
: t('潜能取你自己的潜能;练度是自持有加成(已持有的同名干员的养成,攻击 / 防御 / 生命单独乘算)。未持有的特许干员按潜能1、未精英化。')}</p>
</section>`;
}