feat: optimize pvp invite

This commit is contained in:
jakciehan
2026-05-18 07:39:03 +08:00
parent 7d17325be6
commit c3a4aa8f15
8 changed files with 358 additions and 245 deletions
+4 -4
View File
@@ -29,10 +29,10 @@
"js/managers/BuffManager.js",
"js/managers/BattlePassManager.js",
"js/managers/ResourceManager.js",
"js/managers/SkinManager.js",
"js/managers/SpawnManager.js",
"js/managers/StaminaManager.js",
"js/managers/StorageManager.js"
"js/managers/StorageManager.js",
"js/managers/SkinManager.js"
]
},
"obfuscation": {
@@ -68,10 +68,10 @@
"js/managers/BuffManager.js",
"js/managers/BattlePassManager.js",
"js/managers/ResourceManager.js",
"js/managers/SkinManager.js",
"js/managers/SpawnManager.js",
"js/managers/StaminaManager.js",
"js/managers/StorageManager.js"
"js/managers/StorageManager.js",
"js/managers/SkinManager.js"
]
}
]
+78 -3
View File
@@ -175,18 +175,25 @@ wx.onShow((res) => {
console.log(`[game.js] onShow fired: scene=${res && res.scene}, query=${JSON.stringify(res && res.query)}, shareTicket=${res && res.shareTicket}`);
// Check for teamId from invite card (3v3 mode)
// Check for teamId from invite card (3v3 mode) or roomId from invite card (1v1 mode)
const teamId = _extractTeamId(res && res.query);
const roomId = _extractRoomId(res && res.query);
if (teamId) {
_handleInviteTeamId(teamId);
} else if (roomId) {
_handleInviteRoomId(roomId);
} else {
// Fallback: also check launch options in case onShow query is empty on cold start
try {
const launchOptions = wx.getLaunchOptionsSync();
const fallbackTeamId = _extractTeamId(launchOptions && launchOptions.query);
const fallbackRoomId = _extractRoomId(launchOptions && launchOptions.query);
if (fallbackTeamId) {
console.log(`[game.js] onShow query empty, but found teamId in launchOptions: ${fallbackTeamId}`);
_handleInviteTeamId(fallbackTeamId);
} else if (fallbackRoomId) {
console.log(`[game.js] onShow query empty, but found roomId in launchOptions: ${fallbackRoomId}`);
_handleInviteRoomId(fallbackRoomId);
}
} catch (e) {}
}
@@ -235,6 +242,71 @@ function _extractTeamId(query) {
return null;
}
/**
* Extract roomId from query parameter (1v1 invite card).
* Similar to _extractTeamId, but looks for roomId key.
* @param {object|string|undefined} query
* @returns {string|null}
*/
function _extractRoomId(query) {
if (!query) return null;
// Case 1: query is already an object with roomId property
if (typeof query === 'object' && query.roomId) {
return query.roomId;
}
// Case 2: query is a string like 'roomId=R12345' or 'roomId=R12345&foo=bar'
if (typeof query === 'string') {
const match = query.match(/roomId=([^&]+)/);
if (match) return match[1];
}
// Case 3: query is an object but roomId might be nested in a raw string field
if (typeof query === 'object') {
const keys = Object.keys(query);
for (const key of keys) {
const combined = key + (query[key] ? '=' + query[key] : '');
const match = combined.match(/roomId=([^&]+)/);
if (match) return match[1];
}
}
return null;
}
/**
* Handle roomId from 1v1 invite card.
* Navigates to RoomScene (auto-join) if possible, otherwise stores as pending.
* @param {string} roomId
*/
function _handleInviteRoomId(roomId) {
if (!roomId) return;
// Avoid duplicate processing if already pending the same roomId
if (GameGlobal._pendingRoomId === roomId) {
console.log(`[game.js] roomId ${roomId} already pending, skipping duplicate`);
return;
}
console.log(`[game.js] Received roomId from 1v1 invite: ${roomId}, currentScene: ${sceneManager._currentName}`);
// If already past loading, navigate directly to PVP room scene
if (sceneManager._currentScene && sceneManager._currentName !== SCENE.LOADING) {
console.log(`[game.js] Navigating directly to RoomScene with roomId: ${roomId}`);
if (!sceneManager._scenes.has(SCENE.PVP_ROOM)) {
const RoomScene = require('./js/scenes/RoomScene');
sceneManager.register(SCENE.PVP_ROOM, RoomScene);
}
sceneManager.switchTo(SCENE.PVP_ROOM, { roomId });
GameGlobal._pendingRoomId = null;
} else {
// Still loading — store pending roomId for auto-navigation after load
console.log(`[game.js] Still loading, storing pendingRoomId: ${roomId}`);
GameGlobal._pendingRoomId = roomId;
}
}
/**
* Handle teamId from invite card (shared between onShow and cold launch).
* Navigates to TeamRoomScene if possible, otherwise stores as pending.
@@ -267,15 +339,18 @@ function _handleInviteTeamId(teamId) {
}
}
// Check for teamId from cold launch (user opened game via invite card)
// Check for teamId / roomId from cold launch (user opened game via invite card)
try {
const launchOptions = wx.getLaunchOptionsSync();
console.log(`[game.js] Cold launch options: scene=${launchOptions.scene}, query=${JSON.stringify(launchOptions.query)}, referrerInfo=${JSON.stringify(launchOptions.referrerInfo)}`);
const launchTeamId = _extractTeamId(launchOptions && launchOptions.query);
const launchRoomId = _extractRoomId(launchOptions && launchOptions.query);
if (launchTeamId) {
_handleInviteTeamId(launchTeamId);
} else if (launchRoomId) {
_handleInviteRoomId(launchRoomId);
} else {
console.log('[game.js] No teamId found in cold launch options');
console.log('[game.js] No teamId/roomId found in cold launch options');
}
} catch (e) {
console.error('[game.js] getLaunchOptionsSync failed:', e);
+3 -5
View File
@@ -37,14 +37,12 @@ module.exports = {
// Room Scene (PVP)
// ============================================================
'room.title': 'PVP Battle',
'room.idleHint': 'Create a room or join with a code',
'room.create': 'Create Room',
'room.join': 'Join Room',
'room.connecting': 'Connecting{dots}',
'room.roomCode': 'Room Code:',
'room.waiting': 'Waiting for opponent{dots}',
'room.shareHint': 'Share the room code with your friend',
'room.inputCode': 'Enter Room Code:',
'room.inviteFriend': '📨 Invite Friend',
'room.shareTitle': 'Come play 1v1 Tank Battle with me!',
'room.shareHint': 'Or share the room code with your friend',
'room.opponentFound': 'Opponent found!',
'room.starting': 'Game starting...',
'room.tapBack': 'Tap anywhere to go back',
+3 -5
View File
@@ -37,14 +37,12 @@ module.exports = {
// Room Scene (PVP)
// ============================================================
'room.title': '双人对战',
'room.idleHint': '创建房间或输入房间号加入',
'room.create': '创建房间',
'room.join': '加入房间',
'room.connecting': '连接中{dots}',
'room.roomCode': '房间号:',
'room.waiting': '等待对手加入{dots}',
'room.shareHint': '将房间号分享给好友',
'room.inputCode': '输入房间号:',
'room.inviteFriend': '📨 邀请好友',
'room.shareTitle': '来和我1v1坦克大战吧!',
'room.shareHint': '或者将房间号分享给好友',
'room.opponentFound': '对手已找到!',
'room.starting': '即将开始...',
'room.tapBack': '点击任意位置返回',
+15
View File
@@ -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() {
+191 -164
View File
@@ -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,64 +504,37 @@ 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) {
this._errorMsg = t('common.cannotConnect');
this._state = ROOM_STATE.ERROR;
return;
}
try {
if (!nm.connected) {
const ok = await nm.connect(this._serverUrl);
if (!ok) {
@@ -523,7 +544,13 @@ const RoomScene = {
}
}
nm.joinRoom(this._inputCode);
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() {
+1 -1
View File
@@ -49,7 +49,7 @@
"disableSWC": true
},
"compileType": "game",
"libVersion": "2.25.0",
"libVersion": "3.15.1",
"appid": "wx3527fe2fd49db523",
"projectname": "tankwar",
"condition": {},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"libVersion": "3.15.1",
"projectname": "tankwar",
"projectname": "tankwar_proj",
"condition": {
"game": {
"list": [