/** * TeamRoomSceneFactory.js * Factory function that creates a team room scene object. * Shared by 2v2 and 3v3 team rooms — eliminates code duplication. * * Usage: * const Team2v2RoomScene = createTeamRoomScene({ * teamSize: 2, * battleMode: '2v2', * i18nPrefix: 'team2v2Room', * logTag: 'Team2v2Room', * }); */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, NET_MSG, SERVER_URL, } = require('../base/GameGlobal'); const { t } = require('../i18n/I18n'); // ============================================================ // Layout Constants (shared across all team room variants) // ============================================================ const BTN_WIDTH = Math.min(SCREEN_WIDTH * 0.35, 180); const BTN_HEIGHT = Math.min(36, SCREEN_HEIGHT * 0.07); const BTN_GAP = 10; const CENTER_X = SCREEN_WIDTH / 2; const SLOT_WIDTH = Math.min(SCREEN_WIDTH * 0.15, 80); const SLOT_HEIGHT = Math.min(SCREEN_HEIGHT * 0.18, 90); const SLOT_GAP = 8; // ============================================================ // Team Room States (shared) // ============================================================ const TEAM_STATE = { MODE_SELECT: 'mode_select', JOINING: 'joining', FORMING: 'forming', MATCHING: 'matching', COUNTDOWN: 'countdown', ERROR: 'error', }; // ============================================================ // i18n helper // ============================================================ function tp(prefix, key, params) { const fullKey = `${prefix}.${key}`; return params ? t(fullKey, params) : t(fullKey); } // ============================================================ // Factory // ============================================================ function createTeamRoomScene(config) { const { teamSize, battleMode, i18nPrefix, logTag, shareImageUrl = '', } = config; return { _state: TEAM_STATE.MODE_SELECT, _teamData: null, _errorMsg: '', _animTimer: 0, _matchTimer: 0, _countdown: 3, _countdownTimer: 0, _networkManager: null, _unsubscribers: [], _isLeader: false, _myPlayerId: null, _serverUrl: SERVER_URL, // Button rects _createTeamBtnRect: null, _soloMatchBtnRect: null, _backBtnRect: null, _inviteBtnRect: null, _matchBtnRect: null, _readyBtnRect: null, _disbandBtnRect: null, _leaveBtnRect: null, _cancelMatchBtnRect: null, _slotRects: [], _kickBtnRects: [], _avatarImages: {}, // ---- Lifecycle ---- enter(params) { this._state = TEAM_STATE.MODE_SELECT; this._teamData = null; this._errorMsg = ''; this._animTimer = 0; this._matchTimer = 0; this._countdown = 3; this._countdownTimer = 0; this._networkManager = GameGlobal.networkManager; this._myPlayerId = this._networkManager ? this._networkManager.playerId : 'player_' + Date.now(); this._isLeader = false; this._avatarImages = {}; this._buildLayout(); this._setupNetworkEvents(); this._setupProfileListener(); if (params && params.teamId) { this._autoJoinTeam(params.teamId); } }, exit() { this._cleanupNetworkEvents(); this._cleanupProfileListener(); const shareManager = GameGlobal.shareManager; if (shareManager) { shareManager.resetShareContent(); } }, update(dt) { this._animTimer += dt; if (this._state === TEAM_STATE.MATCHING) { this._matchTimer += dt; } if (this._state === TEAM_STATE.COUNTDOWN) { this._countdownTimer += dt; if (this._countdownTimer >= 1) { this._countdownTimer -= 1; this._countdown--; } } }, // ---- Layout ---- _buildLayout() { const modeY = SCREEN_HEIGHT * 0.4; this._createTeamBtnRect = { x: CENTER_X - BTN_WIDTH / 2, y: modeY, w: BTN_WIDTH, h: BTN_HEIGHT, }; this._soloMatchBtnRect = { x: CENTER_X - BTN_WIDTH / 2, y: modeY + BTN_HEIGHT + BTN_GAP, w: BTN_WIDTH, h: BTN_HEIGHT, }; this._backBtnRect = { x: 10, y: 10, w: 60, h: 30 }; const totalSlotsWidth = teamSize * SLOT_WIDTH + (teamSize - 1) * SLOT_GAP; const slotsStartX = CENTER_X - totalSlotsWidth / 2; const slotsY = SCREEN_HEIGHT * 0.25; this._slotRects = []; this._kickBtnRects = []; for (let i = 0; i < teamSize; i++) { const x = slotsStartX + i * (SLOT_WIDTH + SLOT_GAP); this._slotRects.push({ x, y: slotsY, w: SLOT_WIDTH, h: SLOT_HEIGHT }); this._kickBtnRects.push({ x: x + SLOT_WIDTH - 18, y: slotsY + 2, w: 16, h: 16 }); } const actionY = SCREEN_HEIGHT * 0.58; const smallBtnW = BTN_WIDTH * 0.8; this._inviteBtnRect = { x: CENTER_X - smallBtnW - BTN_GAP / 2, y: actionY, w: smallBtnW, h: BTN_HEIGHT, }; this._matchBtnRect = { x: CENTER_X + BTN_GAP / 2, y: actionY, w: smallBtnW, h: BTN_HEIGHT, }; this._readyBtnRect = { x: CENTER_X - smallBtnW / 2, y: actionY, w: smallBtnW, h: BTN_HEIGHT, }; this._disbandBtnRect = { x: CENTER_X - smallBtnW - BTN_GAP / 2, y: actionY + BTN_HEIGHT + BTN_GAP, w: smallBtnW, h: BTN_HEIGHT, }; this._leaveBtnRect = { x: CENTER_X - smallBtnW / 2, y: actionY + BTN_HEIGHT + BTN_GAP, w: smallBtnW, h: BTN_HEIGHT, }; this._cancelMatchBtnRect = { x: CENTER_X - BTN_WIDTH / 2, y: SCREEN_HEIGHT * 0.7, w: BTN_WIDTH, h: BTN_HEIGHT, }; }, // ---- Share ---- _updateShareContent() { if (!this._teamData || !this._teamData.teamId) return; const shareManager = GameGlobal.shareManager; if (shareManager) { shareManager.setShareContent({ title: tp(i18nPrefix, 'shareTitle'), imageUrl: shareImageUrl, query: `teamId=${this._teamData.teamId}&mode=${battleMode}`, }); } }, // ---- Network Events ---- _setupNetworkEvents() { this._cleanupNetworkEvents(); const nm = this._networkManager; if (!nm) return; const unsubs = []; unsubs.push(nm.on(NET_MSG.TEAM_STATE, (data) => { if (data.teamA) { for (const m of data.teamA) { if (m.avatarUrl && this._avatarImages[m.playerId] === null) { delete this._avatarImages[m.playerId]; } } } if (data.teamB) { for (const m of data.teamB) { if (m.avatarUrl && this._avatarImages[m.playerId] === null) { delete this._avatarImages[m.playerId]; } } } this._teamData = data; this._isLeader = data.leaderId === this._myPlayerId; if (data.state === 'forming') { this._state = TEAM_STATE.FORMING; } else if (data.state === 'matching') { this._state = TEAM_STATE.MATCHING; } this._updateShareContent(); })); unsubs.push(nm.on(NET_MSG.TEAM_DISBAND, (data) => { this._teamData = null; this._state = TEAM_STATE.MODE_SELECT; if (data.reason === 'kicked') { this._errorMsg = t('common.kicked'); this._state = TEAM_STATE.ERROR; } })); unsubs.push(nm.on(NET_MSG.MATCH_FOUND, () => { this._state = TEAM_STATE.COUNTDOWN; this._countdown = 3; this._countdownTimer = 0; })); unsubs.push(nm.on(NET_MSG.TEAM_GAME_START, (data) => { this._startTeamGame(data); })); unsubs.push(nm.on(NET_MSG.ROOM_ERROR, (data) => { this._errorMsg = data.message || 'Unknown error'; if (this._state !== TEAM_STATE.COUNTDOWN) { this._state = TEAM_STATE.ERROR; } })); unsubs.push(nm.on('error', () => { this._errorMsg = t('common.connectFailed'); this._state = TEAM_STATE.ERROR; })); unsubs.push(nm.on('disconnected', () => { if (this._state !== TEAM_STATE.MODE_SELECT) { this._errorMsg = t('common.disconnected'); this._state = TEAM_STATE.ERROR; } })); this._unsubscribers = unsubs; }, _cleanupNetworkEvents() { for (const unsub of this._unsubscribers) { if (typeof unsub === 'function') unsub(); } this._unsubscribers = []; }, // ---- Profile Listener ---- _setupProfileListener() { this._cleanupProfileListener(); const bus = (typeof GameGlobal !== 'undefined') ? GameGlobal.eventBus : null; if (!bus || typeof bus.on !== 'function') return; this._profileUnsub = bus.on('profile:updated', () => { if (this._teamData && this._networkManager && this._networkManager.connected) { this._networkManager.send(NET_MSG.PING); } if (this._myPlayerId && this._avatarImages[this._myPlayerId] !== undefined) { delete this._avatarImages[this._myPlayerId]; } }); }, _cleanupProfileListener() { if (this._profileUnsub) { this._profileUnsub(); this._profileUnsub = null; } }, // ---- Game Start ---- _startTeamGame(data) { const sm = GameGlobal.sceneManager; if (!sm._scenes.has(SCENE.TEAM_GAME)) { const TeamGameScene = require('./TeamGameScene'); sm.register(SCENE.TEAM_GAME, TeamGameScene); } sm.switchTo(SCENE.TEAM_GAME, { teamId: this._teamData ? this._teamData.teamId : null, mapId: data.mapId, teamA: data.teamA, teamB: data.teamB, teamABaseHp: data.teamABaseHp, teamBBaseHp: data.teamBBaseHp, battleMode: data.battleMode || battleMode, roomId: data.roomId || '', myPlayerId: this._myPlayerId, }); }, // ---- Render ---- render(ctx) { ctx.fillStyle = COLORS.MENU_BG; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); const gradient = ctx.createLinearGradient(0, 0, SCREEN_WIDTH, 0); gradient.addColorStop(0, '#0f3460'); gradient.addColorStop(0.5, '#e94560'); gradient.addColorStop(1, '#0f3460'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, SCREEN_WIDTH, 4); this._drawButton(ctx, this._backBtnRect, t('common.back'), false, 12); ctx.fillStyle = COLORS.MENU_TITLE; ctx.font = 'bold 22px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(tp(i18nPrefix, 'title'), CENTER_X, SCREEN_HEIGHT * 0.08); switch (this._state) { case TEAM_STATE.MODE_SELECT: this._renderModeSelect(ctx); break; case TEAM_STATE.JOINING: this._renderJoining(ctx); break; case TEAM_STATE.FORMING: this._renderForming(ctx); break; case TEAM_STATE.MATCHING: this._renderMatching(ctx); break; case TEAM_STATE.COUNTDOWN: this._renderCountdown(ctx); break; case TEAM_STATE.ERROR: this._renderError(ctx); break; } }, _renderJoining(ctx) { const dots = '.'.repeat(Math.floor(this._animTimer * 3) % 4); ctx.fillStyle = '#FFFFFF'; ctx.font = '18px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(tp(i18nPrefix, 'joining') + dots, CENTER_X, SCREEN_HEIGHT * 0.45); }, _renderModeSelect(ctx) { ctx.fillStyle = '#AAAAAA'; ctx.font = '14px Arial'; ctx.textAlign = 'center'; ctx.fillText(tp(i18nPrefix, 'chooseMode'), CENTER_X, SCREEN_HEIGHT * 0.28); this._drawButton(ctx, this._createTeamBtnRect, tp(i18nPrefix, 'createTeam')); this._drawButton(ctx, this._soloMatchBtnRect, tp(i18nPrefix, 'soloMatch')); }, _renderForming(ctx) { if (!this._teamData) return; ctx.fillStyle = '#AAAAAA'; ctx.font = '12px Arial'; ctx.textAlign = 'center'; ctx.fillText(tp(i18nPrefix, 'teamId', { id: this._teamData.teamId }), CENTER_X, SCREEN_HEIGHT * 0.16); const members = this._teamData.teamA || []; for (let i = 0; i < teamSize; i++) { const rect = this._slotRects[i]; const member = members[i]; ctx.fillStyle = member ? '#1e3a5f' : '#0d1b2a'; ctx.strokeStyle = member ? (member.isLeader ? '#FFD700' : '#0f3460') : '#333333'; ctx.lineWidth = member && member.isLeader ? 3 : 1; this._drawRoundRect(ctx, rect.x, rect.y, rect.w, rect.h, 8); ctx.fill(); ctx.stroke(); if (member) { const avatarR = Math.min(rect.w, rect.h) * 0.32; const avatarCX = rect.x + rect.w / 2; const avatarCY = rect.y + rect.h * 0.42; this._drawAvatar(ctx, member, avatarCX, avatarCY, avatarR); if (!member.isLeader) { ctx.fillStyle = member.ready ? '#00FF00' : '#FF6347'; ctx.font = 'bold 10px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( member.ready ? tp(i18nPrefix, 'ready') : tp(i18nPrefix, 'notReady'), avatarCX, rect.y + rect.h * 0.88, ); } if (this._isLeader && !member.isLeader && member.playerId !== this._myPlayerId) { const kickRect = this._kickBtnRects[i]; ctx.fillStyle = '#FF4444'; ctx.font = 'bold 12px Arial'; ctx.fillText('✕', kickRect.x + kickRect.w / 2, kickRect.y + kickRect.h / 2); } } else { ctx.fillStyle = '#555555'; ctx.font = '24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('+', rect.x + rect.w / 2, rect.y + rect.h / 2); ctx.fillStyle = '#666666'; ctx.font = '10px Arial'; ctx.fillText(tp(i18nPrefix, 'emptySlot'), rect.x + rect.w / 2, rect.y + rect.h * 0.78); } } if (this._isLeader) { this._drawButton(ctx, this._inviteBtnRect, tp(i18nPrefix, 'invite')); const allReady = members.length > 0 && members.every(m => m.ready || m.isLeader); this._drawButton(ctx, this._matchBtnRect, tp(i18nPrefix, 'startMatch'), false, 14, allReady ? null : '#555555'); this._drawButton(ctx, this._disbandBtnRect, tp(i18nPrefix, 'disband'), false, 12, '#8B0000'); } else { const myMember = members.find(m => m.playerId === this._myPlayerId); const readyLabel = myMember && myMember.ready ? tp(i18nPrefix, 'cancelReady') : tp(i18nPrefix, 'readyBtn'); this._drawButton(ctx, this._readyBtnRect, readyLabel); this._drawButton(ctx, this._leaveBtnRect, tp(i18nPrefix, 'leaveTeam'), false, 12, '#8B0000'); } }, _renderMatching(ctx) { if (!this._teamData) return; const members = this._teamData.teamA || []; for (let i = 0; i < teamSize; i++) { const rect = this._slotRects[i]; const member = members[i]; ctx.fillStyle = member ? '#1e3a5f' : '#0d1b2a'; ctx.strokeStyle = member ? (member.isLeader ? '#FFD700' : '#0f3460') : '#333333'; ctx.lineWidth = member && member.isLeader ? 3 : 1; this._drawRoundRect(ctx, rect.x, rect.y, rect.w, rect.h, 8); ctx.fill(); ctx.stroke(); if (member) { const avatarR = Math.min(rect.w, rect.h) * 0.32; const avatarCX = rect.x + rect.w / 2; const avatarCY = rect.y + rect.h * 0.42; this._drawAvatar(ctx, member, avatarCX, avatarCY, avatarR); if (!member.isLeader) { ctx.fillStyle = member.ready ? '#00FF00' : '#FF6347'; ctx.font = 'bold 10px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText( member.ready ? tp(i18nPrefix, 'ready') : tp(i18nPrefix, 'notReady'), avatarCX, rect.y + rect.h * 0.88, ); } } else { ctx.fillStyle = '#555555'; ctx.font = '24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('+', rect.x + rect.w / 2, rect.y + rect.h / 2); } } const elapsed = Math.floor(this._matchTimer); const dots = '.'.repeat(Math.floor(this._animTimer * 3) % 4); ctx.fillStyle = '#FFFFFF'; ctx.font = '18px Arial'; ctx.textAlign = 'center'; ctx.fillText(tp(i18nPrefix, 'matching', { dots }), CENTER_X, SCREEN_HEIGHT * 0.55); ctx.fillStyle = '#AAAAAA'; ctx.font = '14px Arial'; ctx.fillText(tp(i18nPrefix, 'waitTime', { seconds: elapsed }), CENTER_X, SCREEN_HEIGHT * 0.62); if (this._isLeader) { this._drawButton(ctx, this._cancelMatchBtnRect, tp(i18nPrefix, 'cancelMatch')); } }, _renderCountdown(ctx) { ctx.fillStyle = '#00FF00'; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.fillText(tp(i18nPrefix, 'matchFound'), CENTER_X, SCREEN_HEIGHT * 0.35); ctx.fillStyle = COLORS.MENU_TITLE; ctx.font = 'bold 64px Arial'; ctx.fillText(String(Math.max(1, this._countdown)), CENTER_X, SCREEN_HEIGHT * 0.52); ctx.fillStyle = '#AAAAAA'; ctx.font = '14px Arial'; ctx.fillText(tp(i18nPrefix, 'enterBattle'), CENTER_X, SCREEN_HEIGHT * 0.65); }, _renderError(ctx) { ctx.fillStyle = '#FF4444'; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.fillText(this._errorMsg, CENTER_X, SCREEN_HEIGHT * 0.45); ctx.fillStyle = '#AAAAAA'; ctx.font = '14px Arial'; ctx.fillText(tp(i18nPrefix, 'tapBack'), CENTER_X, SCREEN_HEIGHT * 0.55); }, // ---- Drawing Helpers ---- _drawButton(ctx, rect, label, pressed, fontSize, bgColor) { if (!rect) return; const fs = fontSize || 14; ctx.fillStyle = bgColor || (pressed ? '#0f3460' : COLORS.MENU_BTN); ctx.strokeStyle = COLORS.MENU_BTN_BORDER; ctx.lineWidth = 2; this._drawRoundRect(ctx, rect.x, rect.y, rect.w, rect.h, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = pressed ? COLORS.MENU_TITLE : COLORS.MENU_BTN_TEXT; ctx.font = `bold ${fs}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, rect.x + rect.w / 2, rect.y + rect.h / 2); }, _drawRoundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.arcTo(x + w, y, x + w, y + r, r); ctx.lineTo(x + w, y + h - r); ctx.arcTo(x + w, y + h, x + w - r, y + h, r); ctx.lineTo(x + r, y + h); ctx.arcTo(x, y + h, x, y + h - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); }, _hitTest(tx, ty, rect) { if (!rect) return false; return tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h; }, _drawAvatar(ctx, member, cx, cy, r) { const img = this._avatarImages[member.playerId]; if (img) { ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(img, cx - r, cy - r, r * 2, r * 2); ctx.restore(); ctx.strokeStyle = member.isLeader ? '#FFD700' : '#4a90d9'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r + 1, 0, Math.PI * 2); ctx.stroke(); } else { if (member.playerId === this._myPlayerId) { const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null; if (profile && profile.avatarUrl && !this._avatarImages[member.playerId]) { this._loadAvatar(member); } } const bgColor = member.isLeader ? '#FFD700' : '#4a90d9'; ctx.fillStyle = bgColor; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.beginPath(); ctx.arc(cx, cy - r * 0.2, r * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(cx, cy + r * 0.55, r * 0.55, r * 0.35, 0, Math.PI, 0); ctx.fill(); ctx.strokeStyle = bgColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, r + 1, 0, Math.PI * 2); ctx.stroke(); this._loadAvatar(member); } }, _loadAvatar(member) { let avatarUrl = member.avatarUrl; if (member.playerId === this._myPlayerId) { const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null; if (profile && profile.avatarUrl) { avatarUrl = profile.avatarUrl; } } if (!avatarUrl || this._avatarImages[member.playerId] !== undefined) return; this._avatarImages[member.playerId] = null; try { const img = wx.createImage(); img.onload = () => { this._avatarImages[member.playerId] = img; }; img.onerror = () => { // Keep null so we don't retry endlessly }; img.src = avatarUrl; } catch (e) { // wx.createImage not available } }, _getDisplayName(member) { if (!member) return ''; const profile = GameGlobal.playerProfile; let raw = member.nickname || ''; if (!raw) { if (profile && typeof profile.getDisplayName === 'function') { raw = profile.getDisplayName(member.playerId); } else { raw = member.playerId || ''; } } if (profile && typeof profile.truncate === 'function') { return profile.truncate(raw, 4); } return raw.length > 8 ? raw.substring(0, 8) + '..' : raw; }, // ---- Touch Handling ---- handleTouch(eventType, e) { if (eventType !== 'touchstart') return; const touch = e.touches[0]; const tx = touch.clientX; const ty = touch.clientY; if (this._hitTest(tx, ty, this._backBtnRect)) { this._goBack(); return; } switch (this._state) { case TEAM_STATE.MODE_SELECT: if (this._hitTest(tx, ty, this._createTeamBtnRect)) { this._handleCreateTeam(); } else if (this._hitTest(tx, ty, this._soloMatchBtnRect)) { this._handleSoloMatch(); } break; case TEAM_STATE.FORMING: this._handleFormingTouch(tx, ty); break; case TEAM_STATE.MATCHING: if (this._isLeader && this._hitTest(tx, ty, this._cancelMatchBtnRect)) { this._handleCancelMatch(); } break; case TEAM_STATE.ERROR: this._state = TEAM_STATE.MODE_SELECT; this._errorMsg = ''; break; } }, _handleFormingTouch(tx, ty) { if (!this._teamData) return; if (this._isLeader) { if (this._hitTest(tx, ty, this._inviteBtnRect)) { this._handleInvite(); return; } if (this._hitTest(tx, ty, this._matchBtnRect)) { this._handleStartMatch(); return; } if (this._hitTest(tx, ty, this._disbandBtnRect)) { this._handleDisband(); return; } const members = this._teamData.teamA || []; for (let i = 0; i < members.length; i++) { const member = members[i]; if (member && !member.isLeader && member.playerId !== this._myPlayerId) { if (this._hitTest(tx, ty, this._kickBtnRects[i])) { this._handleKick(member.playerId); return; } } } } else { if (this._hitTest(tx, ty, this._readyBtnRect)) { this._handleReady(); return; } if (this._hitTest(tx, ty, this._leaveBtnRect)) { this._handleLeave(); return; } } }, // ---- Action Handlers ---- async _handleCreateTeam() { const nm = this._networkManager; if (!nm) return; if (!nm.connected) { const ok = await nm.connect(this._serverUrl); if (!ok) { this._errorMsg = t('common.cannotConnect'); this._state = TEAM_STATE.ERROR; return; } } this._myPlayerId = nm.playerId; nm.createTeam(battleMode); }, async _handleSoloMatch() { const nm = this._networkManager; if (!nm) return; if (!nm.connected) { const ok = await nm.connect(this._serverUrl); if (!ok) { this._errorMsg = t('common.cannotConnect'); this._state = TEAM_STATE.ERROR; return; } } this._myPlayerId = nm.playerId; this._matchTimer = 0; nm.soloMatch(battleMode); }, async _autoJoinTeam(teamId) { const nm = this._networkManager; if (!nm) { this._errorMsg = t('common.cannotConnect'); this._state = TEAM_STATE.ERROR; return; } this._state = TEAM_STATE.JOINING; this._errorMsg = ''; try { if (!nm.connected) { const ok = await nm.connect(this._serverUrl); if (!ok) { this._errorMsg = t('common.cannotConnect'); this._state = TEAM_STATE.ERROR; return; } } this._myPlayerId = nm.playerId; console.log(`[${logTag}] Auto-joining team ${teamId} as ${this._myPlayerId}`); nm.send(NET_MSG.JOIN_TEAM, { teamId }); } catch (e) { console.error(`[${logTag}] Auto-join failed:`, e); this._errorMsg = t('common.cannotConnect'); this._state = TEAM_STATE.ERROR; } }, _handleInvite() { if (!this._teamData) return; const teamId = this._teamData.teamId; const shareData = { title: tp(i18nPrefix, 'shareTitle'), imageUrl: shareImageUrl, query: `teamId=${teamId}&mode=${battleMode}`, }; console.log(`[${logTag}] Sharing invite with query: teamId=${teamId}&mode=${battleMode}`); const shareManager = GameGlobal.shareManager; if (shareManager) { shareManager.triggerShare(shareData); } else { try { wx.showToast({ title: '请点击右上角 ··· 转发给好友', icon: 'none', duration: 2500, }); } catch (e) { console.log(`[${logTag}] Share not available, teamId:`, teamId); } } }, _handleStartMatch() { const nm = this._networkManager; if (!nm || !this._teamData) return; const members = this._teamData.teamA || []; const allReady = members.length > 0 && members.every(m => m.ready || m.isLeader); if (!allReady) return; this._matchTimer = 0; nm.send(NET_MSG.MATCH_START, {}); }, _handleCancelMatch() { const nm = this._networkManager; if (!nm) return; nm.send(NET_MSG.MATCH_CANCEL, {}); }, _handleReady() { const nm = this._networkManager; if (!nm || !this._teamData) return; const myMember = (this._teamData.teamA || []).find(m => m.playerId === this._myPlayerId); nm.send(NET_MSG.TEAM_READY, { ready: myMember ? !myMember.ready : true }); }, _handleKick(playerId) { const nm = this._networkManager; if (!nm) return; nm.send(NET_MSG.TEAM_KICK, { playerId }); }, _handleDisband() { const nm = this._networkManager; if (!nm) return; nm.send(NET_MSG.TEAM_DISBAND, {}); }, _handleLeave() { const nm = this._networkManager; if (!nm) return; nm.send(NET_MSG.LEAVE_TEAM, {}); this._teamData = null; this._state = TEAM_STATE.MODE_SELECT; }, _goBack() { if (this._teamData) { const nm = this._networkManager; if (nm) { if (this._state === TEAM_STATE.MATCHING && this._isLeader) { nm.send(NET_MSG.MATCH_CANCEL, {}); } if (this._isLeader) { nm.send(NET_MSG.TEAM_DISBAND, {}); } else { nm.send(NET_MSG.LEAVE_TEAM, {}); } } } const sm = GameGlobal.sceneManager; sm.switchTo(SCENE.MENU); }, }; } module.exports = { createTeamRoomScene, TEAM_STATE };