// 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. // 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 // watches that player's field at once. // Live LP (user playtest #3 item 2): during a normal round's battle each row's tower shows lp − the loss that player's // leaks so far will cost (red, −N): the own row the top bar's live value (`self`, ui/hud.js liveLp), a teammate's row // m.public players[].pendingLp (server/match/Match.js, ~1 Hz). import { useEffect, useState } from '../../vendor/hooks.module.js'; 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 { localAsset } from '../data.js'; const cx = (...p) => p.flat().filter(Boolean).join(' '); /** Official status glyphs (ui/battle) per m.public players[].status; CSS glyph fallback. */ const STATUS_SPRITE = { ready: 'icon_ready', deciding: 'icon_waiting', done: 'icon_complete', dead: 'icon_dead' }; /** * A row's LP tower: the settled LP and the pending loss of the round's battle (0 outside COMBAT / UNITE). The own row * takes the top bar's live value when there is one (its m.private lp: the same number as the top bar). * @param {any} p m.public players[] entry @param {any} pub m.public * @param {{ lp?: number|null, pending: number, unite: boolean } | null} [self] the own live value (only for the own row) * @returns {{ lp: number|null, pending: number, unite: boolean }} */ export function rowLp(p, pub, self = null) { const pubLp = Number.isFinite(p?.lp) ? p.lp : null; if ((pub?.phase !== PHASE.COMBAT && pub?.phase !== PHASE.UNITE) || p?.alive === false) return { lp: pubLp, pending: 0, unite: false }; const lp = self && Number.isFinite(self.lp) ? self.lp : pubLp; if (lp == null) return { lp, pending: 0, unite: false }; const raw = self ? self.pending : p.pendingLp; const pending = Math.min(lp, Math.max(0, Math.trunc(Number(raw) || 0))); return { lp, pending, unite: pending > 0 && (self ? !!self.unite : pub.phase === PHASE.UNITE) }; } /** * @param {{ pub:any, myId:string, watching:string|null, bubbles: Map, onWatch:(p:any)=>void, * compact?: boolean, teamLp?: number|null, self?: { lp?: number|null, pending: number, unite: boolean } | null, * observe?: null | { canObserve: (p:any) => { fieldId?: string, reason?: string|null, back?: boolean }, observing: boolean, onBack: () => void } }} props */ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = false, observe = null, self: selfLive = null }) { const [openPid, setOpenPid] = useState(null); const phaseKey = `${pub?.phase}:${pub?.round}`; useEffect(() => { setOpenPid(null); }, [phaseKey, watching, observe?.observing]); const players = sortedPlayers(pub); if (!players.length) return null; const click = (p, self) => { if (!observe) { onWatch(p); return; } if (self) { if (observe.observing) observe.onBack(); setOpenPid(null); return; } const t = observe.canObserve(p) || {}; if (!t.fieldId) { setOpenPid(null); onWatch(p); return; } // the game screen toasts the reason setOpenPid((cur) => (cur === p.playerId ? null : p.playerId)); }; return html``; }