Files
ark-stronghold-protocol/public/js/ui/underframe.js
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

103 lines
6.2 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.
// Underframe — the selection diamond of a tapped own piece (research 09 §1.2 "Tap a deployed unit", §5, §6.5;
// official `act2autochess_panel_character_menu`: `util_btn` icon_sell + price "+1", funcId autochessSale; the
// destroy variant `_underFramePanelBtnParamDestroyPos` with icon_destory).
//
// board operator → 撤退 (upper-left: back to the bench) + 出售 +N (upper-right)
// board summon → 撤退
// bench operator → 出售 +N
// item / Art → 销毁
// The diamond frames the piece's tile; only its buttons take pointer events, so the unit under it can still be
// dragged (moving it — or dropping it back on its own tile to re-orient it with the wheel). The game screen shows the
// unit's range tiles (rotated to its facing) and the detail card beside it while it is selected (on the side away
// from the unit, `underframeRect` / gameLogic `panelSide`), and the underframe sits above every HUD panel, so its
// buttons are never covered.
//
// Button look (user playtest #2 item 7): the official sprites icon_sell / icon_destory are WHITE octagon plates with
// the glyph cut out — tinted in the client. Drawn as is they read as a plain white block, so each button is a coloured
// octagon plate: the sprite is used as a CSS mask over the plate colour (出售 amber like every money action, 销毁 /
// 撤退 red) on a dark backing that shows through the glyph cut-out. Without the local sprites the same plate carries
// the built-in glyph.
import { html, HexBadge } from './components.js';
import { GIcon } from './gameComponents.js';
import { useTileScreen } from './facingWheel.js';
import { localAsset } from '../data.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
/** Runner glyph of 撤退 (a figure leaving through a door; original shape). */
function RetreatGlyph() {
return html`<svg class="uframe__glyph" viewBox="0 0 24 24" aria-hidden="true">
<path d="M13.5 3.2a2 2 0 1 1 0 4 2 2 0 0 1 0-4zM9.6 8.3l3.7-.6 2.2 3.3 2.7 1-.6 1.8-3.4-1.2-.9-1.3-.8 3.2 2.6 2.6V22h-2v-4.7l-2.4-2.3-.9 3.6-4.4-1 .5-1.9 2.4.5 1.6-6.6-1 .3-1.4 2.8-1.8-.9 1.9-3.6z" />
<path d="M2 5h5v2H4v10h3v2H2z" opacity=".7" />
</svg>`;
}
/**
* A tinted octagon plate: the official sprite (white plate, glyph cut out) as a mask over the tone colour, or the
* built-in glyph on the same plate when the local sprite is not installed.
* @param {{ sprite: string, glyph: string, tone: 'sell'|'destroy' }} props
*/
export function PlateIcon({ sprite, glyph, tone }) {
const url = localAsset('ui/battle', sprite);
return html`<span class=${cx('uframe__plate', `uframe__plate--${tone}`, url ? 'has-mask' : 'is-glyph')}
style=${url ? `--uf-mask:url("${url}")` : ''} data-sprite=${url ? sprite : null} aria-hidden="true">
${url ? null : html`<${GIcon} name=${glyph} class="uframe__pglyph" />`}
</span>`;
}
/**
* Client-px rect the underframe of a tile covers: the diamond plus its buttons (plates, labels, the +N price) — the
* geometry of the css below (.uframe__btn--retreat / --sell: .56rem plates at 25 % / 75 % across, 25 % down, shifted
* −80 % / −20 % horizontally and −90 % vertically). Used for the detail panel placement.
* @param {{ x: number, y: number, s: number }|null} g view.tileScreen(row, col)
* @param {number} [rem] root font size (px)
* @returns {{ left: number, right: number, top: number, bottom: number }|null}
*/
export function underframeRect(g, rem = 100) {
if (!g || !Number.isFinite(g.x) || !Number.isFinite(g.y)) return null;
const s = g.s > 0 ? g.s : 64;
const half = s * 1.05;
const P = rem * 0.56; // plate
const H = P + rem * 0.26; // plate + label
const q = half / 2; // 25 % / 75 % of the diamond box, from its centre
const btnTop = g.y - q - 0.9 * H;
const left = Math.min(g.x - half, g.x - q - 0.8 * P);
const right = Math.max(g.x + half, g.x + q - 0.2 * P + P + rem * 0.14);
return { left, right, top: Math.min(g.y - half, btnTop - rem * 0.08), bottom: g.y + half };
}
/**
* @param {{ view: any, uid?: number|null, row: number, col: number, actions: { retreat: boolean, sell: number|null, destroy: boolean },
* name?: string, busy?: boolean, onRetreat?: () => void, onSell?: () => void, onDestroy?: () => void }} props
*/
export function Underframe({ view, uid = null, row, col, actions, name = '', busy = false, onRetreat, onSell, onDestroy }) {
const g = useTileScreen(view, row, col);
if (!g || !actions) return null;
const s = g.s > 0 ? g.s : 64;
const half = s * 1.05;
const stop = (e) => e.stopPropagation();
return html`<div class="uframe" data-uid=${uid} style=${`left:${g.x}px;top:${g.y}px;width:${half * 2}px;height:${half * 2}px`} role="group"
aria-label=${`${name || '单位'} 操作`}>
<svg class="uframe__dia" viewBox="-110 -110 220 220" aria-hidden="true">
<path class="uframe__outer" d="M0 -100 L100 0 L0 100 L-100 0 Z" />
<path class="uframe__corner" d="M-100 0 L-86 -14 M-100 0 L-86 14 M100 0 L86 -14 M100 0 L86 14 M0 -100 L-14 -86 M0 -100 L14 -86 M0 100 L-14 86 M0 100 L14 86" />
</svg>
${actions.retreat ? html`<button type="button" class="uframe__btn uframe__btn--retreat" disabled=${busy} onPointerDown=${stop}
onClick=${(e) => { stop(e); onRetreat?.(); }} title="撤退至整备区" aria-label="撤退">
<${RetreatGlyph} /><span class="uframe__label">撤退</span>
</button>` : null}
${actions.sell != null ? html`<button type="button" class="uframe__btn uframe__btn--sell" disabled=${busy} onPointerDown=${stop}
onClick=${(e) => { stop(e); onSell?.(); }} title=${`出售(+${actions.sell} 资金)`} aria-label=${`出售,获得 ${actions.sell} 资金`}>
<${PlateIcon} sprite="icon_sell" glyph="sell" tone="sell" />
<span class="uframe__label">出售</span>
<${HexBadge} value=${`+${actions.sell}`} tone="gold" size="sm" class="uframe__price" />
</button>` : null}
${actions.destroy ? html`<button type="button" class=${cx('uframe__btn', 'uframe__btn--destroy')} disabled=${busy} onPointerDown=${stop}
onClick=${(e) => { stop(e); onDestroy?.(); }} title="销毁道具" aria-label="销毁">
<${PlateIcon} sprite="icon_destory" glyph="trash" tone="destroy" />
<span class="uframe__label">销毁</span>
</button>` : null}
</div>`;
}