0.2.5-dev: community round 30 and follow-ups

- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently).
- Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision).
- 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified).
- Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage.
- Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue.
- Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia).
- golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
This commit is contained in:
sganggs committed 2026-10-12 14:07:08 +08:00
1 parent bc50cac01f
commit 221e6b8dd2
134 files changed
+4590 -1201

No files matched your search

+35 -12
View File
@@ -534,24 +534,27 @@ export function DialogHost() {
// ---- Tooltip -----------------------------------------------------------------------------------
let tipState = null; // { id, content, rect, placement }
let tipState = null; // { id, content, aside, rect, placement }
let tipSeq = 0;
const tipListeners = new Set();
const setTip = (s) => { tipState = s; for (const fn of [...tipListeners]) fn(); };
/**
* Hover/focus tooltip around its children. Content renders in <TooltipLayer/> (fixed, viewport-clamped).
* @param {{ text: any, placement?: 'top'|'bottom', delay?: number, block?: boolean, class?: string, children?: any }} props
* `aside`: a second section in the same clamped, scrollable box; stacked on narrow screens.
* @param {{ text: any, aside?: any, placement?: 'top'|'bottom', delay?: number, block?: boolean, class?: string, children?: any }} props
*/
const TOUCH_TIP_MS = 450;
export function Tooltip({ text, placement = 'top', delay = 120, block = false, class: cls, children }) {
export function Tooltip({ text, aside = null, placement = 'top', delay = 120, block = false, class: cls, children }) {
const ref = useRef(null);
const idRef = useRef(0);
const timer = useRef(null);
const textRef = useRef(text);
const asideRef = useRef(aside);
const pressed = useRef(false); // pointer pressed inside: the focus that follows is not a keyboard focus
textRef.current = text;
asideRef.current = aside;
if (!idRef.current) idRef.current = ++tipSeq;
const show = () => {
clearTimeout(timer.current);
@@ -560,7 +563,8 @@ export function Tooltip({ text, placement = 'top', delay = 120, block = false, c
const el = ref.current;
const content = textRef.current; // the text of the render current when the delay ends (a click may change it)
if (!el || !el.isConnected || content == null || content === '') return;
setTip({ id: idRef.current, content, rect: el.getBoundingClientRect(), placement });
setTip({ id: idRef.current, content, aside: asideRef.current, rect: el.getBoundingClientRect(), placement,
hold: () => clearTimeout(timer.current), hide });
}, delay);
};
const hide = () => {
@@ -570,7 +574,7 @@ export function Tooltip({ text, placement = 'top', delay = 120, block = false, c
// A click hides the tooltip until the pointer leaves: the focus it gives the button must not bring it back
// (with the text of the click's closure) over whatever the click opened. Keyboard focus still shows it.
// Touch: a tap never shows it (the compatibility mouseenter that follows a tap is ignored); a long press
// (≥ 450 ms) does, and it goes away shortly after the finger lifts.
// (≥ 450 ms) does. Ordinary tips expire after release; compound lists stay open for scrolling until dismissed.
const touchAt = useRef(0);
const longTimer = useRef(null);
const onPointerDown = (e) => {
@@ -586,18 +590,24 @@ export function Tooltip({ text, placement = 'top', delay = 120, block = false, c
if (!e || e.pointerType === 'mouse') return;
touchAt.current = Date.now();
clearTimeout(longTimer.current);
if (tipState?.id === idRef.current) { clearTimeout(timer.current); timer.current = setTimeout(hide, 1600); }
if (tipState?.id === idRef.current && !asideRef.current) { clearTimeout(timer.current); timer.current = setTimeout(hide, 1600); }
};
const onMouseEnter = () => { if (Date.now() - touchAt.current > 900) show(); };
const onFocusIn = () => { if (!pressed.current && Date.now() - touchAt.current > 900) show(); };
const onLeave = () => { pressed.current = false; if (Date.now() - touchAt.current > 900) hide(); };
const onLeave = () => {
pressed.current = false;
if (Date.now() - touchAt.current > 900) {
if (asideRef.current) { clearTimeout(timer.current); timer.current = setTimeout(hide, 250); }
else hide();
}
};
useEffect(() => () => { clearTimeout(longTimer.current); hide(); }, []);
// Keep the visible tooltip's content fresh when `text` changes (and hide it when text goes away).
useEffect(() => {
if (tipState?.id !== idRef.current) return;
if (text == null || text === '') setTip(null);
else setTip({ ...tipState, content: text });
}, [text]);
else setTip({ ...tipState, content: text, aside: asideRef.current });
}, [text, aside]);
return html`<span ref=${ref} class=${cx('tt-anchor', block && 'tt-anchor--block', cls)}
onMouseEnter=${onMouseEnter} onMouseLeave=${onLeave} onFocusIn=${onFocusIn} onFocusOut=${onLeave} onPointerDown=${onPointerDown}
onPointerUp=${onPointerEnd} onPointerCancel=${onPointerEnd} onContextMenu=${(e) => { if (Date.now() - touchAt.current < 900) e.preventDefault(); }}>
@@ -612,7 +622,12 @@ export function TooltipLayer() {
const [pos, setPos] = useState(null);
useEffect(() => {
tipListeners.add(force);
return () => tipListeners.delete(force);
const outside = (e) => { if (tipState?.aside && !e.target?.closest?.('.tooltip')) tipState.hide?.(); };
const escape = (e) => { if (e.key === 'Escape') tipState?.hide?.(); };
document.addEventListener('pointerdown', outside);
document.addEventListener('keydown', escape);
window.addEventListener('resize', force);
return () => { tipListeners.delete(force); document.removeEventListener('pointerdown', outside); document.removeEventListener('keydown', escape); window.removeEventListener('resize', force); };
}, []);
useLayoutEffect(() => {
const el = boxRef.current;
@@ -628,13 +643,21 @@ export function TooltipLayer() {
if (place === 'bottom' && top + b.height > vh - m) { place = 'top'; top = Math.max(m, r.top - b.height - m); }
let left = r.left + r.width / 2 - b.width / 2;
left = Math.max(m, Math.min(vw - b.width - m, left));
top = Math.max(m, Math.min(vh - b.height - m, top));
const next = { top: Math.round(top), left: Math.round(left), place, id: tipState.id };
if (!pos || pos.top !== next.top || pos.left !== next.left || pos.id !== next.id || pos.place !== next.place) setPos(next);
});
if (!tipState) return null;
const ready = pos && pos.id === tipState.id;
return html`<div ref=${boxRef} class=${cx('tooltip', ready && 'is-shown', ready && `tooltip--${pos.place}`)} role="tooltip"
style=${ready ? `top:${pos.top}px;left:${pos.left}px` : 'top:-9999px;left:-9999px'}>${tipState.content}</div>`;
const hasAside = tipState.aside != null && tipState.aside !== '';
const off = 'top:-9999px;left:-9999px';
return html`<div ref=${boxRef} class=${cx('tooltip', hasAside && 'tooltip--compound', ready && 'is-shown', ready && `tooltip--${pos.place}`)} role="tooltip"
tabIndex=${hasAside ? 0 : undefined} onMouseEnter=${() => tipState?.hold?.()} onMouseLeave=${() => tipState?.hide?.()}
onFocusIn=${() => tipState?.hold?.()} onPointerDown=${() => tipState?.hold?.()}
onFocusOut=${(e) => { if (!e.currentTarget.contains(e.relatedTarget)) tipState?.hide?.(); }}
style=${ready ? `top:${pos.top}px;left:${pos.left}px` : off}>
${hasAside ? html`<div class="tooltip__main">${tipState.content}</div><div class="tooltip--aside">${tipState.aside}</div>` : tipState.content}
</div>`;
}
// ---- ProgressBar, Tabs, Spinner ----------------------------------------------------------------