// Equip-replace dialog (research 04 §"Replacement" / 09 §1.2 UseEquipUp { charChessInstId, equipChessInstId, // isChangeEquip, unloadInstId }; PRTS 帮助 "达到上限强行佩戴会改为替换装备"): an item dropped on an operator whose two // equipment slots are used opens this dialog. It shows the operator, the incoming item and the two equipped items // (icon, tier, name, effect); the player picks the one to replace — it is DESTROYED — and confirms: g.equip // {itemUid, targetUid, replaceUid}. 取消 / ✕ / Esc / the backdrop change nothing (no intent is sent). // Official look from the local ui/battle sprites (equip_replace_bg, _opiton_bg, _opition_select_01/02_bg, _arrow_line / // _arrow_head, _avatart_bg / _frame, _round, _close, _comfirm_icon, _replace_icon), CSS look-alikes without them // (css/screens/game-panels.css .eqr). // // replaceRequest(ctx, intent, getChess, getItem) → { targetUid, targetId, targetName, targetRec, item: {uid,id,name}, // options } | null (pure; the data the dialog shows — null = no dialog; // `getChess` = what the own pieces show: a 补位 piece's stand-in record) // replaceIntent(intent, replaceUid) → { t: 'g.equip', fields: { itemUid, targetUid, replaceUid } } // import { useEffect, useState } from '../../vendor/hooks.module.js'; import { html, Modal, Button, TierChip, MicroLabel } from './components.js'; import { UnitThumb, RichText, GIcon } from './gameComponents.js'; import { localAsset } from '../data.js'; import { t, tParts } from '../../../shared/i18n.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); const isObj = (v) => !!v && typeof v === 'object'; /** * What the replace dialog shows for a g.equip drop intent that needs a replacement (`intent.confirmReplace`). * @param {{ pieces: Map }} ctx placementContext * @param {{ t: string, fields: { itemUid: number, targetUid: number }, confirmReplace?: boolean }|null} intent gameLogic dropIntent * @param {(id: string) => any} [getChess] * @param {(id: string) => any} [getItem] */ export function replaceRequest(ctx, intent, getChess = () => null, getItem = () => null) { if (!intent || intent.t !== 'g.equip' || !intent.confirmReplace || !isObj(intent.fields)) return null; const target = ctx?.pieces?.get(intent.fields.targetUid)?.piece; const item = ctx?.pieces?.get(intent.fields.itemUid)?.piece; if (!isObj(target) || target.kind !== 'chess' || !isObj(item) || item.kind !== 'item') return null; const equipped = (Array.isArray(target.items) ? target.items : []).filter((it) => isObj(it) && Number.isInteger(it.uid)); if (equipped.length < 2) return null; const rec = (id) => getItem(id) || null; const shown = getChess(target.id) || null; return { targetUid: target.uid, targetId: target.id, targetName: shown?.name || t('该干员'), // the record the avatar draws (a 补位 stand-in's or a 自选 operator's composed record; the data's for any other chess) targetRec: shown && (shown.standInFor || shown.diyFor) ? shown : null, golden: !!target.golden, item: { uid: item.uid, id: item.id, name: rec(item.id)?.name || t('装备') }, options: equipped.map((it) => ({ uid: it.uid, id: it.id, name: rec(it.id)?.name || it.id })), }; } /** * The g.equip intent that replaces `replaceUid` (one of the target's equipped items, or the intent is unchanged). * @param {{ t: string, fields: { itemUid: number, targetUid: number } }} intent * @param {number} replaceUid */ export function replaceIntent(intent, replaceUid) { const f = intent?.fields || {}; const fields = { itemUid: f.itemUid, targetUid: f.targetUid }; if (Number.isInteger(replaceUid)) fields.replaceUid = replaceUid; return { t: 'g.equip', fields }; } /** One official sprite as a CSS custom property (empty when the local-client art is not installed). */ const spriteVar = (name, sprite) => { const url = localAsset('ui/battle', sprite); return url ? `${name}:url("${url}");` : ''; }; /** An item card of the dialog (the incoming item on top, the two equipped options below). */ function ItemCard({ id, item, rec, selected = false, option = false, onPick }) { const tier = rec?.tier; const body = html` <${UnitThumb} kind="item" id=${id} size="md" showTier=${false} /> ${tier ? html`<${TierChip} tier=${tier} golden=${!!rec?.isGolden} size="sm" />` : null} ${rec?.isGolden ? html`${t('进阶')}` : null} ${rec?.name || item?.name || id} <${RichText} as="p" class="eqr__desc" text=${rec?.descRaw || rec?.desc || ''} />`; if (!option) return html`
${body}
`; return html``; } /** * The dialog. * @param {{ request: ReturnType, getItem?: (id: string) => any, busy?: boolean, * onConfirm: (replaceUid: number) => void, onCancel: () => void }} props */ export function EquipReplaceDialog({ request, getItem = () => null, busy = false, onConfirm, onCancel }) { const [sel, setSel] = useState(null); const key = request ? `${request.targetUid}:${request.item.uid}` : ''; useEffect(() => { setSel(null); }, [key]); if (!request) return null; const chosen = request.options.find((o) => o.uid === sel) || null; const style = [ spriteVar('--eqr-bg', 'equip_replace_bg'), spriteVar('--eqr-opt', 'equip_replace_opiton_bg'), spriteVar('--eqr-sel', 'equip_replace_opition_select_02_bg'), spriteVar('--eqr-selbar', 'equip_replace_opition_select_01_bg'), spriteVar('--eqr-round', 'equip_replace_round'), spriteVar('--eqr-ava', 'equip_replace_avatart_bg'), spriteVar('--eqr-avaf', 'equip_replace_avatart_frame'), spriteVar('--eqr-aline', 'equip_replace_arrow_line'), spriteVar('--eqr-ahead', 'equip_replace_arrow_head'), ].join(''); const confirmIcon = localAsset('ui/battle', 'equip_replace_comfirm_icon'); const swapIcon = localAsset('ui/battle', 'equip_replace_replace_icon'); // A focused button owns Enter through its native click, including Cancel and the item options. return html`
{ if (e.key === 'Enter' && e.target?.tagName !== 'BUTTON' && chosen && !busy) { e.preventDefault(); onConfirm(chosen.uid); } }}> <${Modal} open=${true} tone="red" class="eqr" width="min(7.6rem, 96vw)" micro="EQUIPMENT · REPLACE" title=${t('替换装备')} onClose=${onCancel} actions=${html` <${Button} variant="secondary" icon="close" class="eqr__cancel" onClick=${onCancel}>${t('取消')} <${Button} variant="danger" class="eqr__ok" disabled=${!chosen} loading=${busy} data-autofocus onClick=${() => chosen && onConfirm(chosen.uid)}> ${confirmIcon ? html`` : null}${t('确认替换')}`}>

${tParts('「{name}」的装备栏已满(2/2)。选择一件装备进行替换,{warn}。', { name: html`${request.targetName}`, warn: html`${t('被替换的装备将被销毁')}` })}

<${UnitThumb} kind="chess" id=${request.targetId} golden=${request.golden} size="md" rec=${request.targetRec || null} /> ${request.targetName}
<${MicroLabel} tone="mint">${t('新装备')} <${ItemCard} id=${request.item.id} item=${request.item} rec=${getItem(request.item.id)} />
${request.options.map((o) => html`<${ItemCard} key=${o.uid} id=${o.id} item=${o} rec=${getItem(o.id)} option=${true} selected=${o.uid === sel} onPick=${(uid) => setSel((s) => (s === uid ? null : uid))} />`)}

${chosen ? html`${tParts('将销毁「{old}」并配发「{item}」', { old: html`${chosen.name}`, item: html`${request.item.name}` })}` : t('点击选择要替换的装备')}

`; }