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:
1 parent
bc50cac01f
commit
221e6b8dd2
134 files changed
+4590
-1201
No files matched your search
+35
-12
@@ -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 ----------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user