feedback5: merge CB5 — community reports (match / UI): module 特性追加 shown after the base trait (text only), a shop upgrade fills its new slot at once, spectators follow the player they picked through every phase (g.watch carries the playerId — PR #189's idea), the boss shows in boss-round prep scouting, the final-boss prep shows the pair together with green avatar frames (PR #191's idea, official colour); §25.19 after the hotkeys' §25.15; golden: the 18 bot matches (the upgrade's new card), battles unchanged
This commit is contained in:
commit
37558f0d5e
40 files changed
+1815
-607
No files matched your search
+42
-19
@@ -106,7 +106,7 @@ import { ResultScreen } from './result.js';
|
||||
import { net } from '../net.js';
|
||||
import { store, useStore, shallowEqual, serverNow, isSpectating } from '../store.js';
|
||||
import { battleRunner } from '../battle/runner.js';
|
||||
import { isClientCombat, observeTarget, teammateProgress, cameraLayers, layerCamera, sidesOf, resumedWatch } from '../battle/observe.js';
|
||||
import { isClientCombat, observeTarget, teammateProgress, cameraLayers, layerCamera, sidesOf, resumedWatch, followedScout } from '../battle/observe.js';
|
||||
import { screenStrip, playerBonds, playerLayer, detailBondOwner, toggleBond, popupView } from '../ui/watchBonds.js';
|
||||
import { data, getMode } from '../data.js';
|
||||
import { audio, resultSpeaker, resultVoiceSlot } from '../audio.js';
|
||||
@@ -251,7 +251,7 @@ function MatchScreen() {
|
||||
getChess: (id) => { const c = gd.chess(id); return ownDiyRecord(c, priv, { chess: data.get('chess'), backups: data.get('backups') }) || c; },
|
||||
getToken: gd.token, getItem: gd.item, getEffect: gd.effect, backups: gd.backups,
|
||||
}), [priv, pub?.stageId, editable, gd.ready, deployField]);
|
||||
live.current = { pub, priv, field, editable, placeCtx, watching, watchWho, home, myId, detail, drawer, bondOpen, emoteOpen, settingsOpen, exitOpen, drag, facing, sel, selBusy, pen, collapsedNow: collapsed, localDone: false, canPause: false, paused };
|
||||
live.current = { pub, priv, field, editable, placeCtx, watching, watchWho, home, myId, alive, spectator, detail, drawer, bondOpen, emoteOpen, settingsOpen, exitOpen, drag, facing, sel, selBusy, pen, collapsedNow: collapsed, localDone: false, canPause: false, paused };
|
||||
|
||||
// ---- camera: every request goes through setCam, which remembers it for the pen's way back -----------------------
|
||||
// the own prep board: the normal board, or — in the prep of a boss round — the player's half of the boss field
|
||||
@@ -400,7 +400,10 @@ function MatchScreen() {
|
||||
const pf = (Array.isArray(pub?.fields) ? pub.fields : []).find((f) => f && f.fieldId === field.fieldId);
|
||||
const members = Array.isArray(pf?.players) ? pf.players : Array.isArray(field.players) ? field.players : [];
|
||||
const sides = field.sides && typeof field.sides === 'object' ? field.sides : null;
|
||||
const side = sides && sides[myId] ? sides[myId] : members.length > 1 && members.indexOf(myId) === 1 ? 'R' : 'L';
|
||||
// a scouted board of a boss round's prep (Match.prepFieldMeta: the boss-field rows, `side` = the scouted player's
|
||||
// half) is framed by the boss-field prep camera of that half — the leader standing at its spawn (item 55)
|
||||
const side = field.prep && (field.side === 'L' || field.side === 'R') ? field.side
|
||||
: sides && sides[myId] ? sides[myId] : members.length > 1 && members.indexOf(myId) === 1 ? 'R' : 'L';
|
||||
// local simulation (client-side combat) feeds a frame per animation frame: no network jitter buffer
|
||||
view.raw?.setLocalFeed?.({ on: !!field.local, speed: field.speed });
|
||||
setLayer('ALL');
|
||||
@@ -582,8 +585,15 @@ function MatchScreen() {
|
||||
else if (phase === PHASE.FINAL_ASSAULT) audio.sfx(solo ? 'bossRoundSingle' : 'bossRoundTeam');
|
||||
else if (phase === PHASE.HIDDEN_CORE) audio.sfx('bossRoundSecret');
|
||||
else if (phase === PHASE.SP_DRAFT) audio.sfx('draft');
|
||||
setWatching(null); // the server resets every watcher to its own field on phase changes
|
||||
setWatchWho(null);
|
||||
// the server resets every watcher to its own field on phase changes — but an eliminated player / spectator seat
|
||||
// keeps the prep board it follows from 回合开始 through 机变 and 休整期 (Match._followScout re-points it only at the
|
||||
// round start): resetting here would fly the camera home and back at every prep phase (item 56)
|
||||
const keepScout = (phase === PHASE.SP_DRAFT || phase === PHASE.PREP) && (!alive || spectator)
|
||||
&& typeof live.current.watching === 'string' && live.current.watching.startsWith('n:');
|
||||
if (!keepScout) {
|
||||
setWatching(null);
|
||||
setWatchWho(null);
|
||||
}
|
||||
// the pen is a 休整期 view: leaving prep returns the camera (the next setCam would, too)
|
||||
if (phase !== PHASE.PREP && penRef.current.on) togglePenRef.current(false);
|
||||
// a battle unit's panel (live HP of a unit of the fight that just ended) never outlives its battle
|
||||
@@ -699,7 +709,7 @@ function MatchScreen() {
|
||||
const who = playerId ? { fieldId: fid, playerId } : null;
|
||||
setWatching(fid);
|
||||
setWatchWho(who);
|
||||
const ok = await actions.watch(fid);
|
||||
const ok = await actions.watch(fid, playerId);
|
||||
if (!ok) {
|
||||
setWatching((w) => (w === fid ? prev : w));
|
||||
setWatchWho((w) => (w === who ? prevWho : w));
|
||||
@@ -713,8 +723,11 @@ function MatchScreen() {
|
||||
if (L.watching && L.watching !== L.home) {
|
||||
const target = isCombatPhase(L.pub?.phase) ? L.home : ownFieldId(L.myId);
|
||||
// client-side combat without an own field to go back to (a 联防 leaker, an eliminated player): the screen keeps
|
||||
// the field it shows — g.watch of a field that does not exist would only be refused (an error toast)
|
||||
const exists = !isClientCombat(L.pub) || !isCombatPhase(L.pub?.phase) || (Array.isArray(L.pub?.fields) && L.pub.fields.some((f) => f && f.fieldId === target));
|
||||
// the field it shows — g.watch of a field that does not exist would only be refused (an error toast); an
|
||||
// eliminated player / spectator seat has no prep board of its own either
|
||||
const combatNow = isCombatPhase(L.pub?.phase);
|
||||
const exists = combatNow ? (!isClientCombat(L.pub) || (Array.isArray(L.pub?.fields) && L.pub.fields.some((f) => f && f.fieldId === target)))
|
||||
: (L.alive && !L.spectator);
|
||||
if (exists) actions.watch(target);
|
||||
}
|
||||
setWatching(null);
|
||||
@@ -733,7 +746,9 @@ function MatchScreen() {
|
||||
}
|
||||
const self = p.playerId === L.myId;
|
||||
if (self) {
|
||||
if (L.watching && L.watching !== L.home) actions.watch(isCombatPhase(L.pub?.phase) ? L.home : ownFieldId(L.myId));
|
||||
// (an eliminated player / spectator seat has no prep board of its own to ask for: item 56)
|
||||
const combatNow = isCombatPhase(L.pub?.phase);
|
||||
if (L.watching && L.watching !== L.home && (combatNow || (L.alive && !L.spectator))) actions.watch(combatNow ? L.home : ownFieldId(L.myId));
|
||||
setWatching(null);
|
||||
setWatchWho(null);
|
||||
return;
|
||||
@@ -745,17 +760,25 @@ function MatchScreen() {
|
||||
|
||||
const watchField = useCallback((fid) => { requestWatch(fid); }, []);
|
||||
|
||||
// a spectator seat has no board of its own: in 休整期 / 机变 / round start it is shown the first player still in (as a
|
||||
// tap on that row would — g.watch 'n:<pid>', the read-only board), once per phase; a row switches to another player
|
||||
const scoutedRef = useRef(null);
|
||||
// an eliminated player or a spectator seat follows a player through every phase reset (community report of
|
||||
// 2026-10-06, item 56; the idea of PR #189): the server pushes that player's prep board (Match._followScout) — the one
|
||||
// it last watched, else the first player still in — and the screen adopts it like a 前往查看 tap, once per phase and
|
||||
// board (a 返回战场 this phase is not overridden; a row switches to another player, who is followed from then on).
|
||||
// Until 0.2.0 every round's reset dropped an eliminated player onto its own empty board (a spectator seat asked for
|
||||
// the first player itself).
|
||||
const followRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (!spectator || watching || !pub || scoutedRef.current === phaseKey) return;
|
||||
if (phase !== PHASE.PREP && phase !== PHASE.SP_DRAFT && phase !== PHASE.ROUND_START) return;
|
||||
const first = players.find((p) => p.alive !== false && p.status !== 'left');
|
||||
if (!first) return;
|
||||
scoutedRef.current = phaseKey;
|
||||
requestWatch(ownFieldId(first.playerId), first.playerId);
|
||||
}, [spectator, phaseKey, watching]);
|
||||
const fid = followedScout({ field, watching, alive, spectator, myId });
|
||||
if (!fid || !pub) return;
|
||||
const key = `${phaseKey}:${fid}`;
|
||||
if (followRef.current === key) return;
|
||||
followRef.current = key;
|
||||
// the phase reset's render (watching null) ran the own-prep branch with this scout meta in `field` and marked it
|
||||
// stale: un-mark it, or the enter effect refuses it and the board stays blank until the player moves (PR #189)
|
||||
if (staleFieldRef.current === field) staleFieldRef.current = null;
|
||||
setWatching(fid);
|
||||
setWatchWho({ fieldId: fid, playerId: fid.slice(2) });
|
||||
}, [field, pub, watching, alive, spectator, phaseKey, myId]);
|
||||
|
||||
// ---- view events (drag & drop, clicks) ----------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
|
||||
Reference in new issue
Block a user