Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit
- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item / 机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call) - official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11 - a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight, promotion effect - 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪 - DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
This commit is contained in:
1 parent
6464e4b46a
commit
620f88c0d8
81 files changed
+4194
-447
No files matched your search
@@ -44,6 +44,10 @@
|
||||
// User playtest #4: the detail card shows live stats (item 7) — in battle the local sim's unit (battle/runner.js
|
||||
// unitStats), in prep an own board unit's start-of-battle stats (g.unitStats → m.unitStats); 机变 cards take two taps
|
||||
// (item 2, ui/choiceOverlay.js).
|
||||
// Merges (user playtest #6 follow-up, PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"): a shop /
|
||||
// reward card armed for a purchase that completes a merge lights, in gold, the board tile its elite will take (the
|
||||
// deployed copy that deploys first, gameLogic.mergeTarget — none when no copy is deployed: the elite goes to the hand);
|
||||
// the elite then appears there with the promotion cue (render/app.js setPrep → fx.promote).
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.js';
|
||||
import { PHASE, GEO } from '../../../shared/constants.js';
|
||||
@@ -76,7 +80,7 @@ import {
|
||||
phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent,
|
||||
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers,
|
||||
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
|
||||
previewEnemyKey, prepCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout,
|
||||
previewEnemyKey, prepCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout, mergeTarget,
|
||||
} from '../ui/gameLogic.js';
|
||||
import { toast } from '../ui/toasts.js';
|
||||
import { BriefingScreen } from './briefing.js';
|
||||
@@ -94,6 +98,8 @@ const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||
const HUD_HZ_MS = 200;
|
||||
/** Range tiles of the selected unit (its own highlight group: the wheel's 'facing' group may be up at the same time). */
|
||||
const SEL_RANGE = Object.freeze({ group: 'selRange', color: 0xff9c33, fill: 0.3, line: 0.95 });
|
||||
/** The tile an armed merge-completing card's elite will take (its own group; gold like the promotion cue, render/fx.js). */
|
||||
const MERGE_HL = Object.freeze({ group: 'mergeTile', color: 0xffd45a, fill: 0.34, line: 1 });
|
||||
const STATE_EV = new Set(['spawn', 'die', 'deploy', 'status', 'skill']);
|
||||
|
||||
/** Router for the in-match screens. */
|
||||
@@ -192,6 +198,7 @@ function MatchScreen() {
|
||||
const [camKind, setCamKind] = useState('prep'); // kind of the last camera request (data-camera)
|
||||
const [replace, setReplace] = useState(null); // equip-replace dialog: { request, resolve }
|
||||
const [pauseBusy, setPauseBusy] = useState(false);
|
||||
const [armedCard, setArmedCard] = useState(null); // the shop bar's armed card { kind, id } (merge tile cue)
|
||||
const cc = isClientCombat(pub);
|
||||
const battleState = useStore((s) => s.match.battle, shallowEqual); // local battle runner (client-side combat)
|
||||
|
||||
@@ -518,6 +525,14 @@ function MatchScreen() {
|
||||
// promotion (merge reward offered) and bond activation cues
|
||||
const hasOffer = !!priv?.shop?.rewardOffer;
|
||||
useEffect(() => { if (hasOffer) audio.sfx('merge'); }, [hasOffer]);
|
||||
// a shop / reward card armed for a purchase that completes a merge lights the tile its elite will take (the deployed
|
||||
// copy that deploys first — PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置", gameLogic.mergeTarget)
|
||||
const mergeAt = armedCard?.kind === 'chess' && editable && showPrep ? mergeTarget(priv, armedCard.id, gd.chess) : null;
|
||||
const mergeAtKey = mergeAt ? `${mergeAt.row},${mergeAt.col}` : '';
|
||||
useEffect(() => {
|
||||
if (!view) return;
|
||||
try { view.highlightTiles(mergeAt ? [[mergeAt.row, mergeAt.col]] : [], MERGE_HL); } catch { /* cosmetic */ }
|
||||
}, [view, mergeAtKey]);
|
||||
const activeBonds = (priv?.bonds || []).filter((b) => b && b.active).length;
|
||||
const prevActive = useRef(activeBonds);
|
||||
useEffect(() => {
|
||||
@@ -842,6 +857,9 @@ function MatchScreen() {
|
||||
// panel anyway (css z-index), this keeps it visible too
|
||||
const ufTile = selEntry && editable && showPrep ? pieceTile(selEntry) : null;
|
||||
const ufGeo = useTileScreen(view, ufTile ? ufTile.row : null, ufTile ? ufTile.col : null);
|
||||
// the gold tile of an armed merge card (mergeAt): the detail card the first tap opened docks away from it (QA 6b — at
|
||||
// 1920×1080 the left card covered half of a target tile in the leftmost legal column)
|
||||
const mergeGeo = useTileScreen(view, mergeAt ? mergeAt.row : null, mergeAt ? mergeAt.col : null);
|
||||
const retreatSel = useCallback(async () => {
|
||||
const L = live.current;
|
||||
const uid = L.sel?.uid;
|
||||
@@ -1003,9 +1021,16 @@ function MatchScreen() {
|
||||
return { rem, vp };
|
||||
};
|
||||
const dSide = (() => {
|
||||
if (!ufShown) return 'left';
|
||||
const { rem, vp } = panelFrame();
|
||||
return panelSide(underframeRect(ufGeo, rem), panelSlots(vp, rem, { shopOpen }));
|
||||
if (ufShown) {
|
||||
const { rem, vp } = panelFrame();
|
||||
return panelSide(underframeRect(ufGeo, rem), panelSlots(vp, rem, { shopOpen }));
|
||||
}
|
||||
if (mergeGeo && resolved) {
|
||||
const { rem, vp } = panelFrame();
|
||||
const half = (mergeGeo.s > 0 ? mergeGeo.s : 64) * 1.05; // the tile diamond's box (underframeRect's)
|
||||
return panelSide({ left: mergeGeo.x - half, right: mergeGeo.x + half, top: mergeGeo.y - half, bottom: mergeGeo.y + half }, panelSlots(vp, rem, { shopOpen }));
|
||||
}
|
||||
return 'left';
|
||||
})();
|
||||
// the bond popup: next to the card, never over the selected unit's underframe when a place is free (bondPopupPlace)
|
||||
const bpPlace = (() => {
|
||||
@@ -1062,11 +1087,11 @@ function MatchScreen() {
|
||||
${showShop ? html`<${ShopBar} priv=${priv} editable=${editable} collapsed=${collapsed} onCollapse=${setCollapsed}
|
||||
barRef=${barRef}
|
||||
onBuy=${buy} onLevel=${() => actions.levelUp()} onRefresh=${() => actions.refresh()} onFreeze=${() => actions.freeze()}
|
||||
onDetail=${(id, kind) => setDetail({ kind: kind === 'item' ? 'item' : 'chess', id })}
|
||||
onDetail=${(id, kind, hint) => setDetail({ kind: kind === 'item' ? 'item' : 'chess', id, hint: hint || null })}
|
||||
onDetailClose=${() => setDetail((d) => (d?.kind === 'chess' || d?.kind === 'item' ? null : d))}
|
||||
onRefuse=${(reason) => { toast(reason, 'warn'); audio.sfx('error', { volume: 0.5 }); }}
|
||||
reward=${phase === PHASE.PREP && !rewardMin ? priv?.shop?.rewardOffer || null : null}
|
||||
onReward=${(i) => actions.reward(i)} onRewardLater=${() => setRewardMin(true)} />` : null}
|
||||
onReward=${(i) => actions.reward(i)} onRewardLater=${() => setRewardMin(true)} onArm=${setArmedCard} />` : null}
|
||||
|
||||
${phase === PHASE.PREP && priv?.shop?.rewardOffer ? html`<${RewardOverlay} priv=${priv} minimized=${rewardMin || collapsed}
|
||||
onMinimize=${(m) => { setRewardMin(m); if (!m) setCollapsed(false); }} />` : null}
|
||||
|
||||
Reference in new issue
Block a user