Playtest #3 fixes: lost models, picking, live LP, temp overflow, 蕾缪安 S3, 回环射手, FX, detail card, spinners

- #1 lost operator models: the Spine store no longer hands out a skeleton whose unload is in flight
  (PIXI.Assets.unload is async); eviction runs 1 s after a release and the empty-scene budget only after 3 s
- #7 picking: one shared rule (render/pick.js, pixel probe where bodies overlap) for prep/battle clicks,
  hover, the enemy pen and equipment drops; the drag ghost stands on its drop tile; hover probes capped
  at one per frame
- #2 the top-bar LP drops live as the own battle's enemies enter the blue gate (−N, 联防中), teammates'
  pending LP in the team panel
- #3 temp overflow pieces that arrive after a prep deadline survive through the next prep (tempDue);
  temp-row warning and ready-button reason
- #4 蕾缪安 S3: locks every 0.5 s, then one shell every 0.3 s in lock order (PRTS notes), bombardShell fx
- #5 回环射手: real boomerang (out 15, back 3.75 tiles/s, attack only when every boomerang is caught)
- #6 visible projectiles, muzzle flashes, slashes, skill bursts and auras at unchanged frame cost
- #8 特质 right under the detail card header, with its official type icon
- #9 no spinning busy cursors / spinners next to texts; game data warmed before the match
- docs, recordings and tests updated (2414 unit tests, all browser suites green)
This commit is contained in:
sganggs committed 2026-09-29 23:42:55 +08:00
1 parent 01a44cd288
commit 0aba602ecf
72 files changed
+5132 -571

No files matched your search

+13 -8
View File
@@ -71,14 +71,19 @@
.btn__icon { width: 1.15em; height: 1.15em; }
.btn__label { position: relative; }
.btn__spin {
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin .7s linear infinite;
/* a request in flight (Button `loading`): a thin bar sweeps along the bottom edge, shown only after 250 ms (quick
replies show nothing). Deliberately no spinning ring / wait cursor: next to text those read as "the text is still
loading from the server" (user playtest #3 item 9). */
.btn__busy {
position: absolute; left: 0; right: 0; bottom: 0; height: 2px; overflow: hidden; pointer-events: none;
opacity: 0; animation: btn-busy-in 1ms linear 250ms forwards;
}
.btn__busy::before {
content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
background: var(--btn-bd-hover, var(--mint-400)); animation: btn-busy 1.1s var(--ease-in-out) infinite;
}
@keyframes btn-busy-in { to { opacity: 1; } }
@keyframes btn-busy { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
@keyframes spin { to { transform: rotate(360deg); } }
/* filled variants carry a hatch strip on the right end (in-game "准备就绪" look) */
@@ -156,7 +161,7 @@
background-position: right center;
background-repeat: no-repeat;
}
.btn.is-loading { cursor: progress; --btn-fg: var(--text-lo); }
.btn.is-loading { cursor: default; --btn-fg: var(--text-lo); }
.btn--primary.is-loading { --btn-bg: var(--mint-700); --btn-fg: var(--text-on-mint); }
.btn--sm { height: .36rem; min-width: .36rem; padding: 0 .16rem; font-size: .15rem; gap: .07rem; }
+5 -1
View File
@@ -122,8 +122,12 @@ html:not(.sp-in-match) .toast-host { margin-top: var(--sa-t); }
}
/* Short landscape phones (≤ 460 CSS px tall, 1rem = 40 px): the lobby's two columns are compressed so the
创建同盟 button and the join panel stay on screen without scrolling. */
创建同盟 button and the join panel stay on screen without scrolling. The detail card's header is shorter (no
romanised name, a shorter portrait) so the operator's own effect (特质) under it stays in view even in the short card
docked above the shop bar (user playtest #3 item 8). */
@media (max-height: 460px) {
.dhead__art { height: 1.66rem; }
.dhead__en { display: none; }
.lobby-body { padding-top: .2rem; padding-bottom: .2rem; gap: .36rem; }
.mode-cards { margin-bottom: .2rem; }
.mode-card { min-height: 0; gap: .12rem; padding: .22rem .26rem .2rem; }
+2 -2
View File
@@ -41,7 +41,7 @@
}
.ewheel__btn.has-sprite:hover:not(:disabled), .ewheel__btn.has-sprite.is-on { color: #fff; filter: brightness(1.18); border: 0; }
.ewheel__btn.has-sprite:disabled { opacity: 1; color: rgba(242, 242, 242, .45); }
.ewheel__btn.is-cooling { cursor: wait; }
.ewheel__btn.is-cooling { cursor: default; }
.ewheel__label { white-space: nowrap; }
/* ---- panel: one page per theme ----------------------------------------------------------------------------- */
@@ -84,7 +84,7 @@
.ewheel__panel.has-cell .ewheel__item:hover:not(:disabled) { background-color: transparent; filter: brightness(1.35); }
.ewheel__item:active:not(:disabled) { transform: scale(.9); }
.ewheel__item:focus-visible { outline: 2px solid var(--mint-500); outline-offset: 1px; }
.ewheel__item:disabled { opacity: .45; cursor: wait; }
.ewheel__item:disabled { opacity: .45; cursor: default; }
/* pager: ‹ dots › in the strip under the cells */
.ewheel__dots { position: absolute; left: .15rem; width: 2.82rem; top: 1.99rem; height: .12rem; display: flex; align-items: center; justify-content: center; gap: .1rem; }
+52 -9
View File
@@ -102,10 +102,14 @@
.dtrait { display: flex; gap: .08rem; align-items: flex-start; font-size: .14rem; line-height: 1.6; color: var(--text-md); padding: .08rem .1rem; background: rgba(255, 255, 255, .03); border-left: 2px solid var(--line-3); }
.dtrait .icon { width: .16rem; height: .16rem; margin-top: .03rem; color: var(--text-lo); flex: none; }
.dstats-wrap { display: flex; gap: .12rem; align-items: stretch; }
.dstats { flex: 1; display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); }
.dstat { display: flex; align-items: baseline; justify-content: space-between; gap: .06rem; padding: .05rem .1rem; background: #0f1311; }
.dstat__k { font-size: .13rem; color: var(--text-lo); white-space: nowrap; }
.dstat__v { font-size: .18rem; color: #fff; }
/* compact 4 × 2 grid (label over value): the skill stays close under the 特质 block (user playtest #3 item 8) */
/* (enemy / token cards: the grid is a direct child of the column-flex scroller — it must not grow to fill the card) */
.dstats { flex: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); }
.dstats-wrap > .dstats { flex: 1; }
.dstat { display: flex; flex-direction: column; justify-content: center; gap: .01rem; min-width: 0; padding: .04rem .07rem; background: #0f1311; }
.dstat__k { font-size: .12rem; color: var(--text-lo); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dstat__row { display: flex; align-items: baseline; min-width: 0; white-space: nowrap; }
.dstat__v { font-size: .17rem; line-height: 1.15; color: #fff; }
.dstat small { font-size: .11rem; color: var(--text-dim); margin-left: .04rem; }
.drange { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .06rem; padding: .06rem .1rem; background: #0f1311; border: 1px solid var(--line); min-width: 1.1rem; }
.rgrid { --rg: .14rem; display: grid; gap: 2px; }
@@ -134,9 +138,16 @@
.dsp__num .icon { width: .14rem; height: .14rem; color: var(--gold); }
.dtalent { display: flex; flex-direction: column; gap: .02rem; }
.dtalent b { font-size: .15rem; color: var(--text-hi); }
.dgarrison { display: flex; flex-direction: column; gap: .06rem; }
.dgarrison__type { display: inline-flex; align-self: flex-start; align-items: center; gap: .05rem; font-size: .13rem; color: var(--gold-2); padding: .01rem .08rem .01rem .04rem; border: 1px solid rgba(242, 189, 62, .4); }
.dgarrison__icon { width: .2rem; height: .2rem; object-fit: contain; }
/* the operator's own effect (特质, user playtest #3 item 8): compact, right under the header, gold accent */
.dgarrison {
display: flex; flex-direction: column; gap: .04rem; padding: .07rem .1rem .08rem; flex: none;
background: linear-gradient(90deg, rgba(242, 189, 62, .1), rgba(242, 189, 62, .02) 70%); border-left: 3px solid var(--gold-2);
}
.dgarrison__head { display: flex; align-items: center; gap: .08rem; }
.dgarrison__k { font-size: .14rem; font-weight: 900; letter-spacing: .12em; color: var(--gold-2); }
.dgarrison__type { display: inline-flex; align-items: center; gap: .04rem; font-size: .12rem; line-height: 1.5; color: var(--gold-2); padding: 0 .07rem 0 .03rem; border: 1px solid rgba(242, 189, 62, .4); }
.dgarrison__icon { width: .18rem; height: .18rem; object-fit: contain; }
.dgarrison__text { font-size: .15rem; line-height: 1.55; color: var(--text-hi); }
.dbonds { display: flex; flex-wrap: wrap; gap: .08rem; }
.dbond { display: inline-flex; align-items: center; gap: .05rem; padding: .03rem .1rem .03rem .05rem; font-size: .15rem; background: rgba(78, 216, 175, .08); border: 1px solid var(--mint-800); color: var(--mint-400); }
/* the unit's bonds in the header's right column, right under name / class (user playtest #2 item 9): icon, name,
@@ -351,7 +362,14 @@
background: rgba(8, 11, 10, .92); border: 1px solid var(--mint-500); border-radius: .3rem; font-size: .13rem; color: var(--mint-400); font-weight: 700; }
.spcard__taker .pavatar { --pa: .4rem; border-radius: 50%; overflow: hidden; border: 2px solid var(--mint-400); }
.spcard.is-taken .spcard__taker { filter: none; }
.spcard__spin { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0, 0, 0, .4); }
/* the card being picked (g.choice in flight): mint frame + a 选择中 strip with the sweeping busy bar of the buttons
(components.css btn-busy) — no spinner over the card's text (user playtest #3 item 9) */
.spcard.is-busy { border-color: var(--mint-400); box-shadow: inset 0 0 0 1px var(--mint-400); }
.spcard__busy {
position: absolute; left: 0; right: 0; bottom: 0; padding: .05rem 0 .08rem; overflow: hidden; pointer-events: none;
font-size: .14rem; font-weight: 700; letter-spacing: .2em; color: var(--mint-400); background: rgba(8, 11, 10, .85);
}
.spcard__busy::after { content: ''; position: absolute; left: 0; bottom: 0; width: 40%; height: 2px; background: var(--mint-400); animation: btn-busy 1.1s var(--ease-in-out) infinite; }
/* Grid image boxes: definite 100% tracks so `height: 100%` images resolve against the box (an auto track would
let non-square art overflow its frame). */
@@ -390,6 +408,31 @@
}
.fwheel__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* ---- the temp overflow row's notice (ui/underframe.js TempRowNotice, user playtest #3 item 3): a dashed red frame
around 临时整备区 and a label at its end. Over the field, under every HUD panel (.gm__hud z 3), never takes the
pointer (the pieces under it stay draggable). ---- */
.tempnote { position: fixed; inset: 0; z-index: 2; pointer-events: none; }
.tempnote * { pointer-events: none; }
.tempnote__frame { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.tempnote__frame polygon {
fill: rgba(255, 60, 40, .1); stroke: #ff5a47; stroke-width: 2.2px; stroke-dasharray: 9 5; stroke-linejoin: round;
vector-effect: non-scaling-stroke; filter: drop-shadow(0 0 .05rem rgba(255, 60, 40, .7)); animation: tempnote-pulse 1.6s ease-in-out infinite;
}
@keyframes tempnote-pulse { 50% { opacity: .55; } }
.tempnote__label {
position: absolute; transform: translate(-100%, -50%); display: flex; flex-direction: column; gap: .02rem; max-width: 2.9rem;
padding: .06rem .12rem .07rem .1rem; background: rgba(40, 6, 6, .9); border: 1px solid rgba(255, 90, 71, .75); border-left: 3px solid #ff5a47;
box-shadow: 0 .04rem .16rem rgba(0, 0, 0, .6); color: #ffdcd6; animation: tempnote-in var(--t-med) var(--ease-out);
}
@keyframes tempnote-in { from { opacity: 0; } }
.tempnote__label.is-right { transform: translate(0, -50%); border-left-width: 1px; border-right: 3px solid #ff5a47; }
.tempnote__title { display: flex; align-items: center; gap: .05rem; font-size: .15rem; font-weight: 700; color: #fff; white-space: nowrap; }
.tempnote__title .icon { width: .17rem; height: .17rem; color: #ff6a55; flex: none; }
.tempnote__title .num { color: #ff8a78; font-size: .17rem; }
.tempnote__rule { font-size: .12rem; line-height: 1.45; color: rgba(255, 220, 214, .88); }
@media (prefers-reduced-motion: reduce) { .tempnote__frame polygon, .tempnote__label { animation: none; } }
.sp-reduced-motion .tempnote__frame polygon, .sp-reduced-motion .tempnote__label { animation: none; }
/* ---- underframe: the selection diamond of a tapped own piece (ui/underframe.js): 撤退 / 出售 +N / 销毁 ----
It sits above every HUD panel (detail card z 9, bond popup, drawer, shop bar): only its buttons take pointer events,
so they are never covered (user playtest #2 item 8); overlays that must win (机变, modals, the drag ghost) are higher. */
@@ -403,7 +446,7 @@
padding: 0; background: none; border: 0; color: #fff; cursor: pointer; filter: drop-shadow(0 .03rem .06rem rgba(0, 0, 0, .8));
animation: uframe-in 200ms var(--ease-out);
}
.uframe__btn:disabled { opacity: .5; cursor: wait; }
.uframe__btn:disabled { opacity: .5; cursor: default; }
.uframe__btn--retreat { left: 25%; top: 25%; transform: translate(-80%, -90%); }
.uframe__btn--sell, .uframe__btn--destroy { left: 75%; top: 25%; transform: translate(-20%, -90%); }
/* the octagon plate every button stands on (the official util_btn plates are octagons) */
+31 -4
View File
@@ -133,6 +133,23 @@
.lp--danger { color: var(--red-premium); }
.lp--danger .lp__icon { filter: hue-rotate(-140deg) saturate(2.4) brightness(.95); }
.lp--team { color: var(--mint-glow); }
/* the running battle's pending loss (LpTower `pending`, user playtest #3 item 2): the number turns red, a −N plate
pops again on every new leak (keyed), 联防中 while teammates may still save part of it */
.lp.is-pending .lp__val { color: var(--red-premium); text-shadow: 0 0 .08rem rgba(255, 60, 60, .45); }
.lp__pend {
align-self: center; margin-left: .02em; padding: .06em .24em; font-size: .56em; font-weight: 700; line-height: 1.1;
color: #fff; background: #d8262a; box-shadow: 0 0 .1rem rgba(255, 50, 50, .55);
clip-path: polygon(.12em 0, 100% 0, 100% calc(100% - .12em), calc(100% - .12em) 100%, 0 100%, 0 .12em);
animation: lp-pend 520ms var(--ease-out);
}
.lp__note {
align-self: center; padding: .08em .3em; font-size: .4em; font-weight: 700; letter-spacing: .08em; line-height: 1.2;
color: #1a0800; background: var(--orange);
}
.lp--sm .lp__note { font-size: .6em; }
@keyframes lp-pend { 0% { opacity: 0; transform: scale(1.9); } 45% { opacity: 1; transform: scale(.92); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .lp__pend { animation: none; } }
.sp-reduced-motion .lp__pend { animation: none; }
/* enemy preview button: amber [🔍▶▶] → the pen (research 09 §2); grey [🔍] in the pen view and outside 休整期 */
.enemybtn {
@@ -159,6 +176,13 @@
.readywrap { display: flex; flex-direction: column; align-items: flex-end; gap: .05rem; }
.readywrap__count { font-size: .14rem; color: var(--text-lo); }
.readywrap__count b { color: var(--mint-400); font-size: .17rem; }
/* why 准备就绪 is refused (the temp overflow row holds pieces, user playtest #3 item 3): shown, not only on hover */
.readywrap__why {
display: inline-flex; align-items: center; gap: .05rem; padding: .02rem .1rem .02rem .06rem; font-size: .14rem; font-weight: 700;
color: #ffdcd6; background: rgba(40, 6, 6, .88); border: 1px solid rgba(255, 90, 71, .7); border-right: 3px solid #ff5a47; white-space: nowrap;
}
.readywrap__why .icon { width: .16rem; height: .16rem; color: #ff6a55; flex: none; }
.readywrap__why b { color: #ff8a78; font-size: .16rem; }
.readybtn {
display: flex; align-items: center; gap: .12rem; height: .56rem; padding: 0 .16rem 0 .12rem; min-width: 2.3rem;
background: rgba(10, 14, 12, .9); border: 2px solid var(--mint-500); color: var(--text-hi);
@@ -264,6 +288,9 @@
/* ---- effects ---------------------------------------------------------------------------------------------- */
.gm__effects { position: absolute; right: .3rem; top: 2.4rem; }
/* 准备就绪 refused (the temp row holds pieces): its reason shows under the button and pushes the ready count down —
the effects column follows, or the count runs into it on phones */
.gm.has-readywhy .gm__effects { top: calc(2.4rem + max(.3rem, 16px)); }
.effects { display: flex; flex-direction: column; align-items: flex-end; gap: .08rem; }
.effect {
position: relative; width: .5rem; height: .5rem; display: grid; place-items: center;
@@ -310,7 +337,7 @@
transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast), color var(--t-fast);
}
.ewheel__item:hover:not(:disabled) { background: var(--mint-a10); border-color: var(--mint-700); color: var(--mint-400); transform: translateY(-.02rem); }
.ewheel__item:disabled { opacity: .45; cursor: wait; }
.ewheel__item:disabled { opacity: .45; cursor: default; }
.ewheel__art { width: .6rem; height: .6rem; display: grid; place-items: center; }
.ewheel__art:empty { display: none; }
.ewheel__art .eart { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 0 .06rem rgba(23, 249, 183, .25)); }
@@ -383,10 +410,10 @@
background: rgba(8, 11, 10, .9); border: 1px solid var(--line-2); border-radius: .3rem; white-space: nowrap;
animation: pop-in var(--t-slow) var(--ease-out);
}
.chud__msg .icon { width: .22rem; height: .22rem; color: var(--gold); animation: hg-flip 2.4s var(--ease-in-out) infinite; }
@keyframes hg-flip { 0%, 40% { transform: rotate(0); } 50%, 90% { transform: rotate(180deg); } 100% { transform: rotate(360deg); } }
/* a still hourglass: a rotating one next to the text read as a loading spinner (user playtest #3 item 9) */
.chud__msg .icon { width: .22rem; height: .22rem; color: var(--gold); }
.chud__msg--dead { border-color: rgba(255, 84, 84, .5); color: var(--red-premium); }
.chud__msg--dead .icon { color: var(--red-premium); animation: none; }
.chud__msg--dead .icon { color: var(--red-premium); }
.vswitch { display: flex; align-items: center; gap: .1rem; height: .5rem; padding: 0 .06rem; background: rgba(8, 11, 10, .9); border: 1px solid var(--line-2); }
.vswitch__arrow { width: .4rem; height: .4rem; display: grid; place-items: center; background: none; border: 0; color: var(--mint-500); }
.vswitch__arrow .icon { width: .3rem; height: .3rem; }
+1 -1
View File
@@ -257,7 +257,7 @@
.dpick__opt .icon { width: .2rem; height: .2rem; color: var(--d-color); opacity: .55; transition: opacity var(--t-fast); }
.dpick__opt:hover:not(:disabled) { color: var(--text-hi); background: rgba(255, 255, 255, .03); }
.dpick__opt:hover:not(:disabled) .icon { opacity: 1; }
.dpick__opt:disabled { cursor: progress; }
.dpick__opt:disabled { cursor: default; }
.dpick__opt.is-active {
color: var(--d-color);
background: transparent; background: color-mix(in srgb, var(--d-color) 17%, transparent);