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
@@ -40,7 +40,7 @@ import { abilityRows } from './abilityLines.js';
|
||||
import { data } from '../data.js';
|
||||
import { attackRangeGrid } from '../../../shared/loadoutRecord.js';
|
||||
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
|
||||
import { moduleBadge } from './loadoutModel.js';
|
||||
import { moduleBadge, fullTraitText } from './loadoutModel.js';
|
||||
import { t, tc, N_ } from '../../../shared/i18n.js';
|
||||
import { audio } from '../audio.js';
|
||||
|
||||
@@ -277,11 +277,11 @@ export function BondChips({ bondIds, bonds = [], onBond = null, off = null, gran
|
||||
*/
|
||||
export function traitText(c, golden, lo) {
|
||||
const t = (lo?.record || c).trait || {};
|
||||
const base = t.descRaw || t.desc || '';
|
||||
if (!golden) return base;
|
||||
// the record's own module line, also for a record cloned for another skill or module (a 不装备 record carries none) —
|
||||
// until 0.1.2 a clone fell back to the class trait (Grok review of GitHub #64; the in-match card had it too)
|
||||
return t.moduleDescRaw || base;
|
||||
if (!golden) return t.descRaw || t.desc || '';
|
||||
// the class trait (or the module's rewrite of it), then the record's own module line — also for a record cloned for
|
||||
// another skill or module (a 不装备 record carries none). Until 0.1.2 a clone fell back to the class trait (Grok review
|
||||
// of GitHub #64); until 0.2.0 the module line replaced the class trait (community report of 2026-10-06, item 16.2).
|
||||
return fullTraitText(t);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,7 +67,8 @@ export const actions = {
|
||||
choice: (idx) => act('g.choice', { idx }),
|
||||
ready: (ready) => act('g.ready', { ready }, { sfx: ready ? 'ready' : 'back' }),
|
||||
emote: (id) => act('g.emote', { id }, { quiet: true }),
|
||||
watch: (fieldId) => act('g.watch', { fieldId }, { sfx: 'tab' }),
|
||||
// `playerId`: the player tapped in the team panel (a shared field shows two) — what an eliminated viewer follows
|
||||
watch: (fieldId, playerId = null) => act('g.watch', typeof playerId === 'string' && playerId ? { fieldId, playerId } : { fieldId }, { sfx: 'tab' }),
|
||||
autoplay: (on) => act('g.autoplay', { on }),
|
||||
// solo battles only (ui/matchStatus.js pauseAvailable): m.public.paused follows
|
||||
pause: (on) => act('g.pause', { on: !!on }, { sfx: on ? 'click' : 'confirm' }),
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
export { clamp, tileKey } from './gameLogic/shared.js';
|
||||
export { phaseMode, isCombatPhase, showDeadPill, isBossPhase } from './gameLogic/phases.js';
|
||||
export { prepCamera, prepCameraFor, foldCamera, deployFieldOf, fieldTile, boardTileOf, ownerBandId } from './gameLogic/camera.js';
|
||||
export { prepCamera, prepCameraFor, foldCamera, deployFieldOf, fieldTile, boardTileOf, ownerBandId, teamFrameIds } from './gameLogic/camera.js';
|
||||
export { phaseBanner, prepCapsuleLabel, countdownState, phaseTotalSeconds } from './gameLogic/phases.js';
|
||||
export { STATUS_META } from './gameLogic/format.js';
|
||||
export { sortedPlayers } from './gameLogic/shared.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// ui/gameLogic/camera.js — prep camera, boss-field tile mapping, owner band. Re-exported from ../gameLogic.js.
|
||||
|
||||
import { GEO } from '../../../../shared/constants.js';
|
||||
import { GEO, PHASE } from '../../../../shared/constants.js';
|
||||
import { BOSS_ROW_SHIFT, MAX_COL } from '../../render/prepfield.js';
|
||||
import { int, isObj } from './shared.js';
|
||||
|
||||
@@ -26,6 +26,37 @@ export function prepCamera(pub, myId) {
|
||||
return { kind: 'bossPrep', opts: { side: idx % 2 === 1 ? 'R' : 'L' } };
|
||||
}
|
||||
|
||||
const BOSS_PREP_PHASES = new Set([PHASE.ROUND_START, PHASE.SP_DRAFT, PHASE.PREP]);
|
||||
|
||||
/**
|
||||
* The players framed in green on the team panel (community report of 2026-10-06, item 51: 「同组两个玩家屏幕左边的头像也像原版
|
||||
* 那样用绿框框起来」 — the official co-op frame, ui/battle `bg_team_border`): in a boss round (最终攻势 / 隐秘核心) the viewer
|
||||
* and its pair partner — from the round's start (回合开始 / 机变 / 休整期: the pairing `prepCamera` reads, the players still
|
||||
* in by seat, two by two — server/match/finalAssault.js pairPlayers) through the fight (the boss field listing the
|
||||
* viewer). Empty for a lone player, an eliminated player or a spectator seat (no pair of its own) and in every other
|
||||
* round. PR #191 framed the partner alone, in gold (not merged).
|
||||
* @param {any} pub m.public @param {string|null} myId
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function teamFrameIds(pub, myId) {
|
||||
const none = new Set();
|
||||
if (!pub || !myId) return none;
|
||||
const players = Array.isArray(pub.players) ? pub.players.filter(isObj) : [];
|
||||
const me = players.find((p) => p.playerId === myId);
|
||||
if (!me || me.alive === false) return none;
|
||||
const r = pub.round;
|
||||
const bossRound = Number.isInteger(r) && r > 0 && (r === pub.bossRound || r === pub.hiddenRound);
|
||||
if (pub.phase === PHASE.FINAL_ASSAULT || pub.phase === PHASE.HIDDEN_CORE) {
|
||||
const f = (Array.isArray(pub.fields) ? pub.fields : []).find((x) => isObj(x) && (x.kind === 'boss' || x.kind === 'hidden') && Array.isArray(x.players) && x.players.includes(myId));
|
||||
return f && f.players.length > 1 ? new Set(f.players) : none;
|
||||
}
|
||||
if (!bossRound || !BOSS_PREP_PHASES.has(pub.phase)) return none;
|
||||
const alive = players.filter((p) => p.alive !== false).sort((a, b) => int(a.seat, 99) - int(b.seat, 99)).map((p) => p.playerId);
|
||||
const idx = alive.indexOf(myId);
|
||||
const pair = alive.slice(idx - (idx % 2), idx - (idx % 2) + 2);
|
||||
return pair.length > 1 ? new Set(pair) : none;
|
||||
}
|
||||
|
||||
/**
|
||||
* The own prep board's camera request with the shop bar's state (public issue #5 "准备阶段,收起商店界面时,界面并不会进行
|
||||
* 缩放"): `prepCamera` plus `shop: !folded` — a folded shop (收起) takes the official shop-collapsed camera
|
||||
|
||||
@@ -325,6 +325,29 @@ export function moduleBadge(rec, id = null) {
|
||||
return m ? m[1] : t.slice(-1) || '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* The two lines of a trait record (data `trait` / `traitBase`, ModuleRecord `traitOverride`, DATA.md §2): `base` = the
|
||||
* 特性 the unit fights with — the class trait, or the module's own wording where the module rewrites it (official
|
||||
* `overrideDescripton`) — and `added` = the module's extra line (official `additionalDescription`), or null. The extra
|
||||
* line comes after the class trait, never instead of it: PRTS flags it 「特性追加」 on every such module, and the sim keeps
|
||||
* the class trait with the module equipped (community report of 2026-10-06, item 16.2: until 0.2.0 the 干员调配 module
|
||||
* card, its 局内数值 and the detail card showed the extra line alone on 114 of the 164 modules the screen offers).
|
||||
* @param {any} trait
|
||||
* @returns {{ base: string, added: string|null }}
|
||||
*/
|
||||
export function traitLines(trait) {
|
||||
if (!isObj(trait)) return { base: '', added: null };
|
||||
const base = String(trait.descRaw || trait.desc || '');
|
||||
const added = trait.moduleDescRaw || trait.moduleDesc || null;
|
||||
return { base, added: added ? String(added) : null };
|
||||
}
|
||||
|
||||
/** The whole 特性 text of a trait record: its base line, then the module's extra line (`\n` between; RichText breaks it). */
|
||||
export function fullTraitText(trait) {
|
||||
const { base, added } = traitLines(trait);
|
||||
return base && added ? `${base}\n${added}` : base || added || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Module stat bonus as display rows (non-zero entries only).
|
||||
* @param {Record<string, number> | null | undefined} attr
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Left team panel (research 06 §11.1, research 09 §3.1): one row per seat — avatar (band icon once picked), name, LP
|
||||
// tower, status glyph (… acting / ✓ ready / ⌛ deciding / ⚔ combat / door left / ✕ dead), AI badge, "you" marker, the
|
||||
// field being watched (eye badge), and emote bubbles.
|
||||
// field being watched (eye badge), and emote bubbles. In a boss round the viewer's pair is framed in green from the
|
||||
// round's start (gameLogic teamFrameIds; community report of 2026-10-06, item 51).
|
||||
// Observing (client-side combat, `observe` prop — the official flow): tapping a teammate's avatar expands a mint
|
||||
// "前往查看" button under the row (when that teammate can be observed now; otherwise the reason is toasted through
|
||||
// onWatch); while observing, the own row shows a "返回战场" button. Without `observe` (server-run combat) a click
|
||||
@@ -19,7 +20,7 @@ import { PHASE } from '../../../shared/constants.js';
|
||||
import { html, Icon, Tooltip } from './components.js';
|
||||
import { PlayerAvatar, LpTower, GIcon, LocalSprite } from './gameComponents.js';
|
||||
import { EmoteBubble } from './emotes.js';
|
||||
import { STATUS_META, sortedPlayers } from './gameLogic.js';
|
||||
import { STATUS_META, sortedPlayers, teamFrameIds } from './gameLogic.js';
|
||||
import { MissTag, uniteRemaining } from './hud.js';
|
||||
import { localAsset } from '../data.js';
|
||||
import { t } from '../../../shared/i18n.js';
|
||||
@@ -79,6 +80,10 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
|
||||
useEffect(() => { setOpenPid(null); }, [phaseKey, watching, observe?.observing]);
|
||||
const players = sortedPlayers(pub);
|
||||
if (!players.length) return null;
|
||||
// a boss round: the viewer's pair framed in green from the round's start (item 51 — the official bg_team_border,
|
||||
// tinted like the official green; a plain green ring without the local art)
|
||||
const team = teamFrameIds(pub, myId);
|
||||
const frameArt = team.size ? localAsset('ui/battle', 'bg_team_border') : null;
|
||||
const click = (p, self) => {
|
||||
if (!observe) { onWatch(p); return; }
|
||||
if (self) { if (observe.observing) observe.onBack(); setOpenPid(null); return; }
|
||||
@@ -98,9 +103,11 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
|
||||
const back = !!observe && self && observe.observing;
|
||||
const title = observe ? (self ? (observe.observing ? t('返回战场') : t('你自己')) : t('查看 {name} 的战场', { name: p.name })) : (self ? t('查看自己的阵地') : t('查看 {name} 的阵地', { name: p.name }));
|
||||
const lp = rowLp(p, pub, self ? selfLive : null, { uniteLocal, cap });
|
||||
return html`<div key=${p.playerId} class=${cx('team__row', self && 'is-self', watched && 'is-watched', p.alive === false && 'is-dead', open && 'is-open')}>
|
||||
<button type="button" class="team__btn" onClick=${() => click(p, self)} title=${title} aria-expanded=${observe && !self ? String(open) : undefined}>
|
||||
const inTeam = team.has(p.playerId);
|
||||
return html`<div key=${p.playerId} class=${cx('team__row', self && 'is-self', inTeam && 'is-team', watched && 'is-watched', p.alive === false && 'is-dead', open && 'is-open')}>
|
||||
<button type="button" class="team__btn" onClick=${() => click(p, self)} title=${inTeam && !self ? `${title} · ${t('与你在同一战场')}` : title} aria-expanded=${observe && !self ? String(open) : undefined}>
|
||||
<${PlayerAvatar} player=${p} self=${self} />
|
||||
${inTeam ? html`<span class=${cx('team__frame', !frameArt && 'team__frame--plain')} style=${frameArt ? `--frame:url("${frameArt}")` : undefined} aria-hidden="true"></span>` : null}
|
||||
<span class="team__seat num">P${(p.seat ?? 0) + 1}</span>
|
||||
${p.isBot ? html`<span class="team__ai">AI</span>` : null}
|
||||
${self ? html`<span class="team__you"><${Icon} name="user" /></span>` : null}
|
||||
|
||||
Reference in new issue
Block a user