feat: optimize pvp invite
This commit is contained in:
@@ -134,6 +134,21 @@ const MenuScene = {
|
||||
sm.switchTo(SCENE.TEAM_ROOM, { teamId });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (GameGlobal._pendingRoomId) {
|
||||
const roomId = GameGlobal._pendingRoomId;
|
||||
GameGlobal._pendingRoomId = null;
|
||||
console.log(`[MenuScene] Found pendingRoomId: ${roomId}, will auto-navigate to RoomScene`);
|
||||
setTimeout(() => {
|
||||
console.log(`[MenuScene] Auto-navigating to RoomScene with roomId: ${roomId}`);
|
||||
const sm = GameGlobal.sceneManager;
|
||||
if (!sm._scenes.has(SCENE.PVP_ROOM)) {
|
||||
const RoomScene = require('./RoomScene');
|
||||
sm.register(SCENE.PVP_ROOM, RoomScene);
|
||||
}
|
||||
sm.switchTo(SCENE.PVP_ROOM, { roomId });
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
exit() {
|
||||
|
||||
+198
-171
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* RoomScene.js
|
||||
* Room creation/joining UI for PVP online multiplayer mode.
|
||||
* Allows players to create a room or join an existing one by room code.
|
||||
* PVP 1v1 online multiplayer scene.
|
||||
* Entering this scene automatically creates a room and triggers invite share.
|
||||
* From an invite card, auto-joins the specified room.
|
||||
*/
|
||||
|
||||
const {
|
||||
@@ -26,11 +27,9 @@ const CENTER_X = SCREEN_WIDTH / 2;
|
||||
// Room Scene States
|
||||
// ============================================================
|
||||
const ROOM_STATE = {
|
||||
IDLE: 'idle', // Initial state: show create/join buttons
|
||||
CREATING: 'creating', // Connecting and creating room
|
||||
CREATING: 'creating', // Connecting and creating room (also initial state)
|
||||
WAITING: 'waiting', // Room created, waiting for opponent
|
||||
JOINING: 'joining', // Joining a room
|
||||
INPUT_CODE: 'input', // Entering room code
|
||||
COUNTDOWN: 'countdown', // Both players ready, counting down
|
||||
ERROR: 'error', // Error state
|
||||
};
|
||||
@@ -41,7 +40,6 @@ const ROOM_STATE = {
|
||||
const RoomScene = {
|
||||
_state: ROOM_STATE.IDLE,
|
||||
_roomCode: '',
|
||||
_inputCode: '',
|
||||
_errorMsg: '',
|
||||
_countdown: 3,
|
||||
_countdownTimer: 0,
|
||||
@@ -53,17 +51,10 @@ const RoomScene = {
|
||||
_serverUrl: SERVER_URL,
|
||||
|
||||
// Button rects (calculated in enter)
|
||||
_createBtnRect: null,
|
||||
_joinBtnRect: null,
|
||||
_backBtnRect: null,
|
||||
_confirmBtnRect: null,
|
||||
_numpadRects: [],
|
||||
_deleteBtnRect: null,
|
||||
_inviteBtnRect: null,
|
||||
|
||||
enter() {
|
||||
this._state = ROOM_STATE.IDLE;
|
||||
this._roomCode = '';
|
||||
this._inputCode = '';
|
||||
enter(params) {
|
||||
this._errorMsg = '';
|
||||
this._countdown = 3;
|
||||
this._countdownTimer = 0;
|
||||
@@ -71,20 +62,6 @@ const RoomScene = {
|
||||
this._pendingStartData = null;
|
||||
this._networkManager = GameGlobal.networkManager;
|
||||
|
||||
// Calculate button positions
|
||||
const btnY = SCREEN_HEIGHT * 0.4;
|
||||
this._createBtnRect = {
|
||||
x: CENTER_X - BTN_WIDTH / 2,
|
||||
y: btnY,
|
||||
w: BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
};
|
||||
this._joinBtnRect = {
|
||||
x: CENTER_X - BTN_WIDTH / 2,
|
||||
y: btnY + BTN_HEIGHT + BTN_GAP,
|
||||
w: BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
};
|
||||
this._backBtnRect = {
|
||||
x: 10,
|
||||
y: 10,
|
||||
@@ -92,47 +69,37 @@ const RoomScene = {
|
||||
h: 30,
|
||||
};
|
||||
|
||||
// Numpad for room code input (3x4 grid: 1-9, 0, delete, confirm)
|
||||
this._buildNumpad();
|
||||
|
||||
// Confirm button for code input
|
||||
this._confirmBtnRect = {
|
||||
x: CENTER_X - BTN_WIDTH / 2,
|
||||
y: SCREEN_HEIGHT * 0.75,
|
||||
w: BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
// Invite friend button (shown in WAITING state)
|
||||
const inviteBtnW = Math.min(SCREEN_WIDTH * 0.5, 240);
|
||||
const inviteBtnH = Math.min(40, SCREEN_HEIGHT * 0.08);
|
||||
this._inviteBtnRect = {
|
||||
x: CENTER_X - inviteBtnW / 2,
|
||||
y: SCREEN_HEIGHT * 0.73,
|
||||
w: inviteBtnW,
|
||||
h: inviteBtnH,
|
||||
};
|
||||
|
||||
// Setup network event listeners
|
||||
this._setupNetworkEvents();
|
||||
|
||||
// Decide initial flow based on entry params
|
||||
if (params && params.roomId) {
|
||||
// From invite card — auto-join the room
|
||||
this._autoJoinRoom(params.roomId);
|
||||
} else {
|
||||
// Direct entry from menu — auto-create room + share (one-step invite)
|
||||
this._state = ROOM_STATE.CREATING;
|
||||
this._roomCode = '';
|
||||
this._handleInviteAndCreate();
|
||||
}
|
||||
},
|
||||
|
||||
exit() {
|
||||
this._cleanupNetworkEvents();
|
||||
},
|
||||
|
||||
_buildNumpad() {
|
||||
const padWidth = Math.min(SCREEN_WIDTH * 0.6, 200);
|
||||
const padHeight = Math.min(SCREEN_HEIGHT * 0.35, 180);
|
||||
const startX = CENTER_X - padWidth / 2;
|
||||
const startY = SCREEN_HEIGHT * 0.42;
|
||||
const cellW = padWidth / 3;
|
||||
const cellH = padHeight / 4;
|
||||
|
||||
this._numpadRects = [];
|
||||
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, null, 0, 'del'];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const col = i % 3;
|
||||
const row = Math.floor(i / 3);
|
||||
if (nums[i] !== null) {
|
||||
this._numpadRects.push({
|
||||
x: startX + col * cellW,
|
||||
y: startY + row * cellH,
|
||||
w: cellW - 4,
|
||||
h: cellH - 4,
|
||||
value: nums[i],
|
||||
});
|
||||
}
|
||||
// Reset share content when leaving room
|
||||
const shareManager = GameGlobal.shareManager;
|
||||
if (shareManager) {
|
||||
shareManager.resetShareContent();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,6 +113,8 @@ const RoomScene = {
|
||||
unsubs.push(nm.on(NET_MSG.ROOM_CREATED, (data) => {
|
||||
this._roomCode = data.roomId || data.roomCode || '';
|
||||
this._state = ROOM_STATE.WAITING;
|
||||
// Update share content so right-corner menu also carries the roomId
|
||||
this._updateShareContent();
|
||||
}));
|
||||
|
||||
unsubs.push(nm.on(NET_MSG.ROOM_JOINED, (data) => {
|
||||
@@ -218,6 +187,134 @@ const RoomScene = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update share content so the right-corner ··· menu always carries
|
||||
* the current roomId for 1v1 invite.
|
||||
* @private
|
||||
*/
|
||||
_updateShareContent() {
|
||||
if (!this._roomCode) return;
|
||||
const shareManager = GameGlobal.shareManager;
|
||||
if (shareManager) {
|
||||
shareManager.setShareContent({
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
query: `roomId=${this._roomCode}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle "Invite Friend" button tap — trigger WeChat share.
|
||||
* MUST be called within a touch event for WeChat policy compliance.
|
||||
* @private
|
||||
*/
|
||||
_handleInvite() {
|
||||
if (!this._roomCode) return;
|
||||
|
||||
const shareData = {
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
query: `roomId=${this._roomCode}`,
|
||||
};
|
||||
|
||||
console.log(`[RoomScene] Sharing 1v1 invite with query: roomId=${this._roomCode}`);
|
||||
|
||||
const shareManager = GameGlobal.shareManager;
|
||||
if (shareManager) {
|
||||
shareManager.triggerShare(shareData);
|
||||
} else {
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && wx.showToast) {
|
||||
wx.showToast({
|
||||
title: '请点击右上角 ··· 转发给好友',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[RoomScene] Share not available, roomId:', this._roomCode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* One-step invite: auto-create room then immediately share the invite card.
|
||||
* This is the primary action on the IDLE screen — user clicks "Invite Friend"
|
||||
* and we handle everything in one go.
|
||||
* MUST be called within a touch event for WeChat policy compliance.
|
||||
* @private
|
||||
*/
|
||||
async _handleInviteAndCreate() {
|
||||
const nm = this._networkManager;
|
||||
|
||||
if (!nm) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
// If already connected and in a room (WAITING state), just re-share
|
||||
if (this._state === ROOM_STATE.WAITING && this._roomCode) {
|
||||
this._handleInvite();
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect if needed
|
||||
if (!nm.connected) {
|
||||
this._state = ROOM_STATE.CREATING;
|
||||
const ok = await nm.connect(this._serverUrl);
|
||||
if (!ok) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create room — the ROOM_CREATED event handler will set state to WAITING
|
||||
// and store the roomCode. We need to share AFTER the room is created,
|
||||
// so we listen for the event once.
|
||||
const shareOnCreated = (data) => {
|
||||
const roomId = data.roomId || data.roomCode || '';
|
||||
if (!roomId) return;
|
||||
|
||||
const shareData = {
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
query: `roomId=${roomId}`,
|
||||
};
|
||||
|
||||
console.log(`[RoomScene] Auto-sharing 1v1 invite after room created: roomId=${roomId}`);
|
||||
|
||||
const shareManager = GameGlobal.shareManager;
|
||||
if (shareManager) {
|
||||
shareManager.triggerShare(shareData);
|
||||
} else {
|
||||
try {
|
||||
if (typeof wx !== 'undefined' && wx.showToast) {
|
||||
wx.showToast({
|
||||
title: '请点击右上角 ··· 转发给好友',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[RoomScene] Share not available after room created');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe one-time for auto-share after room creation
|
||||
const unsub = nm.on(NET_MSG.ROOM_CREATED, (data) => {
|
||||
// Unsubscribe immediately (one-time listener)
|
||||
unsub();
|
||||
shareOnCreated(data);
|
||||
});
|
||||
|
||||
// Now create the room
|
||||
nm.createRoom();
|
||||
},
|
||||
|
||||
_startGame(data) {
|
||||
const sm = GameGlobal.sceneManager;
|
||||
if (!sm._scenes.has(SCENE.TEAM_GAME)) {
|
||||
@@ -281,9 +378,6 @@ const RoomScene = {
|
||||
|
||||
// Render based on state
|
||||
switch (this._state) {
|
||||
case ROOM_STATE.IDLE:
|
||||
this._renderIdle(ctx);
|
||||
break;
|
||||
case ROOM_STATE.CREATING:
|
||||
case ROOM_STATE.JOINING:
|
||||
this._renderConnecting(ctx);
|
||||
@@ -291,9 +385,6 @@ const RoomScene = {
|
||||
case ROOM_STATE.WAITING:
|
||||
this._renderWaiting(ctx);
|
||||
break;
|
||||
case ROOM_STATE.INPUT_CODE:
|
||||
this._renderInputCode(ctx);
|
||||
break;
|
||||
case ROOM_STATE.COUNTDOWN:
|
||||
this._renderCountdown(ctx);
|
||||
break;
|
||||
@@ -303,16 +394,6 @@ const RoomScene = {
|
||||
}
|
||||
},
|
||||
|
||||
_renderIdle(ctx) {
|
||||
ctx.fillStyle = '#AAAAAA';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t('room.idleHint'), CENTER_X, SCREEN_HEIGHT * 0.28);
|
||||
|
||||
this._drawButton(ctx, this._createBtnRect, t('room.create'));
|
||||
this._drawButton(ctx, this._joinBtnRect, t('room.join'));
|
||||
},
|
||||
|
||||
_renderConnecting(ctx) {
|
||||
const dots = '.'.repeat(Math.floor(this._animTimer * 3) % 4);
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
@@ -336,48 +417,15 @@ const RoomScene = {
|
||||
const dots = '.'.repeat(Math.floor(this._animTimer * 2) % 4);
|
||||
ctx.fillStyle = '#AAAAAA';
|
||||
ctx.font = '16px Arial';
|
||||
ctx.fillText(t('room.waiting', { dots }), CENTER_X, SCREEN_HEIGHT * 0.55);
|
||||
ctx.fillText(t('room.waiting', { dots }), CENTER_X, SCREEN_HEIGHT * 0.52);
|
||||
|
||||
// Hint
|
||||
// Invite friend button (primary action)
|
||||
this._drawButton(ctx, this._inviteBtnRect, t('room.inviteFriend'), false, 16, '#e94560');
|
||||
|
||||
// Hint text below the button
|
||||
ctx.fillStyle = '#666666';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(t('room.shareHint'), CENTER_X, SCREEN_HEIGHT * 0.65);
|
||||
},
|
||||
|
||||
_renderInputCode(ctx) {
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t('room.inputCode'), CENTER_X, SCREEN_HEIGHT * 0.25);
|
||||
|
||||
// Code display box
|
||||
const boxW = Math.min(SCREEN_WIDTH * 0.5, 180);
|
||||
const boxH = 40;
|
||||
const boxX = CENTER_X - boxW / 2;
|
||||
const boxY = SCREEN_HEIGHT * 0.30;
|
||||
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.strokeStyle = COLORS.MENU_TITLE;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillRect(boxX, boxY, boxW, boxH);
|
||||
ctx.strokeRect(boxX, boxY, boxW, boxH);
|
||||
|
||||
// Input text
|
||||
ctx.fillStyle = COLORS.MENU_TITLE;
|
||||
ctx.font = 'bold 24px Arial';
|
||||
const displayCode = this._inputCode + (Math.floor(this._animTimer * 2) % 2 === 0 ? '|' : '');
|
||||
ctx.fillText(displayCode, CENTER_X, boxY + boxH / 2);
|
||||
|
||||
// Numpad
|
||||
for (const btn of this._numpadRects) {
|
||||
const label = btn.value === 'del' ? '⌫' : String(btn.value);
|
||||
this._drawButton(ctx, btn, label, false, 16);
|
||||
}
|
||||
|
||||
// Confirm button
|
||||
if (this._inputCode.length >= 4) {
|
||||
this._drawButton(ctx, this._confirmBtnRect, t('common.joinBtn'), false, 16);
|
||||
}
|
||||
ctx.fillText(t('room.shareHint'), CENTER_X, SCREEN_HEIGHT * 0.84);
|
||||
},
|
||||
|
||||
_renderCountdown(ctx) {
|
||||
@@ -406,11 +454,11 @@ const RoomScene = {
|
||||
ctx.fillText(t('room.tapBack'), CENTER_X, SCREEN_HEIGHT * 0.55);
|
||||
},
|
||||
|
||||
_drawButton(ctx, rect, label, pressed, fontSize) {
|
||||
_drawButton(ctx, rect, label, pressed, fontSize, bgColor) {
|
||||
if (!rect) return;
|
||||
const fs = fontSize || 16;
|
||||
|
||||
ctx.fillStyle = pressed ? '#0f3460' : COLORS.MENU_BTN;
|
||||
ctx.fillStyle = bgColor || (pressed ? '#0f3460' : COLORS.MENU_BTN);
|
||||
ctx.strokeStyle = COLORS.MENU_BTN_BORDER;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
@@ -456,74 +504,53 @@ const RoomScene = {
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case ROOM_STATE.IDLE:
|
||||
if (this._hitTest(tx, ty, this._createBtnRect)) {
|
||||
this._handleCreateRoom();
|
||||
} else if (this._hitTest(tx, ty, this._joinBtnRect)) {
|
||||
this._state = ROOM_STATE.INPUT_CODE;
|
||||
this._inputCode = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ROOM_STATE.INPUT_CODE:
|
||||
// Check numpad
|
||||
for (const btn of this._numpadRects) {
|
||||
if (this._hitTest(tx, ty, btn)) {
|
||||
if (btn.value === 'del') {
|
||||
this._inputCode = this._inputCode.slice(0, -1);
|
||||
} else if (this._inputCode.length < 6) {
|
||||
this._inputCode += String(btn.value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check confirm
|
||||
if (this._inputCode.length >= 4 && this._hitTest(tx, ty, this._confirmBtnRect)) {
|
||||
this._handleJoinRoom();
|
||||
}
|
||||
break;
|
||||
|
||||
case ROOM_STATE.ERROR:
|
||||
this._state = ROOM_STATE.IDLE;
|
||||
this._errorMsg = '';
|
||||
break;
|
||||
|
||||
case ROOM_STATE.WAITING:
|
||||
// Invite friend button
|
||||
if (this._hitTest(tx, ty, this._inviteBtnRect)) {
|
||||
this._handleInvite();
|
||||
}
|
||||
// Allow going back while waiting
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async _handleCreateRoom() {
|
||||
this._state = ROOM_STATE.CREATING;
|
||||
const nm = this._networkManager;
|
||||
|
||||
if (!nm.connected) {
|
||||
const ok = await nm.connect(this._serverUrl);
|
||||
if (!ok) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nm.createRoom();
|
||||
},
|
||||
|
||||
async _handleJoinRoom() {
|
||||
/**
|
||||
* Auto-join a 1v1 room when entering from an invite card.
|
||||
* @param {string} roomId - Room ID from the invite card query parameter.
|
||||
*/
|
||||
async _autoJoinRoom(roomId) {
|
||||
this._state = ROOM_STATE.JOINING;
|
||||
this._errorMsg = '';
|
||||
const nm = this._networkManager;
|
||||
|
||||
if (!nm.connected) {
|
||||
const ok = await nm.connect(this._serverUrl);
|
||||
if (!ok) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
if (!nm) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
nm.joinRoom(this._inputCode);
|
||||
try {
|
||||
if (!nm.connected) {
|
||||
const ok = await nm.connect(this._serverUrl);
|
||||
if (!ok) {
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[RoomScene] Auto-joining 1v1 room ${roomId}`);
|
||||
nm.joinRoom(roomId);
|
||||
} catch (e) {
|
||||
console.error('[RoomScene] Auto-join failed:', e);
|
||||
this._errorMsg = t('common.cannotConnect');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
}
|
||||
},
|
||||
|
||||
_goBack() {
|
||||
|
||||
Reference in New Issue
Block a user