feedback7: merge fb7-g-small — PR #368 (AI teammates draft last, default off), #374 (double-click a teammate's avatar in prep), #321 (copy local diagnostics; whole-token / exact-value redaction) with the review's fixes

- Golden unchanged (union OK); broad node --test 5292 (5291 pass, 0 fail, 1 skipped); lint 0 errors.
This commit is contained in:
sganggs committed 2026-10-08 20:23:26 +08:00
commit fc2e57ebff
28 files changed
+1256 -35

No files matched your search

+35 -3
View File
@@ -1,6 +1,6 @@
// Room screen (同盟等待室): 4 seat cards (avatar frame, name, ready state, AI badge, host crown),
// host controls (difficulty picker, add/remove AI in co-op, start), invite code with copy code /
// copy link, ready toggle and leave.
// host controls (difficulty picker, add/remove AI and the 「AI 队友最后选择」 switch in co-op, start), invite code with
// copy code / copy link, ready toggle and leave.
//
// Start rule (server/lobby.js): room.start needs every *other* human connected and ready; the
// host's start counts as the host's ready. So 开始模拟 is enabled exactly then and sends room.start
@@ -69,6 +69,19 @@ export function roomFacts(room, myId) {
};
}
/**
* The co-op room option 「AI 队友最后选择」 (GitHub #338; room.setAiPicksLast, room.state.aiPicksLast): in the strategy and
* 机变 drafts every human picks before the AI teammates. null in a solo room (no AI teammates); otherwise its state (off
* when the server sends none) and whether this player may switch it — the host only, the others see it read-only.
* @param {any} room room.state payload
* @param {any} myId
* @returns {{ on: boolean, editable: boolean } | null}
*/
export function aiLastOption(room, myId) {
if (!room || room.mode === 'solo') return null;
return { on: room.aiPicksLast === true, editable: room.hostId != null && room.hostId === myId };
}
/** Invite link for a room code (current page URL with ?room=CODE). */
export function inviteLink(code) {
const loc = globalThis.location;
@@ -189,6 +202,21 @@ function DifficultyPicker({ room, isHost, busy, onPick }) {
</div>`;
}
/** The 「AI 队友最后选择」 switch (co-op): the host toggles it, everybody else sees its state. */
function AiLastToggle({ option, busy, onToggle }) {
if (!option) return null;
return html`<div class="ailast">
<${Tooltip} text=${t('策略与机变轮选时,所有博士先于 AI 队友选择')}>
<button type="button" role="switch" aria-checked=${option.on ? 'true' : 'false'}
class=${`dpick__opt ailast__opt${option.on ? ' is-active' : ''}`} disabled=${!option.editable || !!busy}
onClick=${() => option.editable && onToggle(!option.on)}>
<${Icon} name="check" class=${option.on ? 'is-on' : ''} />${t('AI 队友最后选择')}
</button>
<//>
${option.editable ? null : html`<span class="t-dim">${t('由创建者设置')}</span>`}
</div>`;
}
/** Room screen component. */
export function RoomScreen() {
const room = useStore((s) => s.room);
@@ -229,6 +257,7 @@ export function RoomScreen() {
if (ok) run(`kick${seat}`, () => net.request('room.kick', { seat, playerId }));
};
const setDifficulty = (difficulty) => run('diff', () => net.request('room.setDifficulty', { difficulty }));
const setAiLast = (on) => run('ailast', () => net.request('room.setAiPicksLast', { on }));
// spectator seats: the host frees one; a spectator takes a free player seat with room.join of this room
const removeSpectator = (playerId) => run(`rs${playerId}`, () => net.request('room.removeSpectator', { playerId }));
const sit = () => run('sit', () => net.request('room.join', { code: room.code }));
@@ -307,7 +336,10 @@ export function RoomScreen() {
<footer class="room-bar">
<div class="room-bar__left">
<span class="room-bar__label">${t('模拟难度')}<${MicroLabel}>DIFFICULTY<//></span>
<${DifficultyPicker} room=${room} isHost=${facts.isHost} busy=${busy} onPick=${setDifficulty} />
<div class="room-bar__opts">
<${DifficultyPicker} room=${room} isHost=${facts.isHost} busy=${busy} onPick=${setDifficulty} />
<${AiLastToggle} option=${aiLastOption(room, me.playerId)} busy=${busy} onToggle=${setAiLast} />
</div>
</div>
<div class="room-bar__center">
<div class="ready-count" hidden=${!coop}>