feat: use wx.createUserInfoButton to get weixin's avarta

This commit is contained in:
jakciehan
2026-05-14 22:41:32 +08:00
parent c4bd390478
commit 9359139186
13 changed files with 1181 additions and 184 deletions
+195 -35
View File
@@ -70,6 +70,7 @@ const TeamRoomScene = {
_cancelMatchBtnRect: null,
_slotRects: [],
_kickBtnRects: [],
_avatarImages: {},
enter(params) {
this._state = TEAM_STATE.MODE_SELECT;
@@ -82,10 +83,13 @@ const TeamRoomScene = {
this._networkManager = GameGlobal.networkManager;
this._myPlayerId = this._networkManager ? this._networkManager.playerId : 'player_' + Date.now();
this._isLeader = false;
this._avatarImages = {};
this._buildLayout();
// Setup network events BEFORE auto-join so listeners are ready
this._setupNetworkEvents();
// Listen for profile updates (avatar/nickname granted mid-session)
this._setupProfileListener();
// If entering with a teamId (from invite card), auto-join
if (params && params.teamId) {
@@ -112,6 +116,7 @@ const TeamRoomScene = {
exit() {
this._cleanupNetworkEvents();
this._cleanupProfileListener();
// Reset share content when leaving team room
const shareManager = GameGlobal.shareManager;
if (shareManager) {
@@ -209,6 +214,24 @@ const TeamRoomScene = {
const unsubs = [];
unsubs.push(nm.on(NET_MSG.TEAM_STATE, (data) => {
// Invalidate avatar cache for members whose avatarUrl changed
// so that _loadAvatar will reload the new image.
if (data.teamA) {
for (const m of data.teamA) {
if (m.avatarUrl && this._avatarImages[m.playerId] === null) {
// null = previously failed to load; the new URL may work
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;
@@ -271,6 +294,43 @@ const TeamRoomScene = {
this._unsubscribers = [];
},
/**
* Listen for profile:updated events. When the avatar URL is granted
* (e.g. after fetchSilent completes or user taps UserInfoButton),
* we need to trigger a network message so the server propagates
* the new avatarUrl to other team members.
* @private
*/
_setupProfileListener() {
this._cleanupProfileListener();
const bus = (typeof GameGlobal !== 'undefined') ? GameGlobal.eventBus : null;
if (!bus || typeof bus.on !== 'function') return;
this._profileUnsub = bus.on('profile:updated', (data) => {
console.log('[TeamRoom] profile:updated received, avatarUrl:', data && data.avatarUrl ? 'present' : 'empty');
// If we are in a team room and have a network connection, send a
// lightweight ping so the server picks up the updated avatarUrl
// from the next message's top-level field.
if (this._teamData && this._networkManager && this._networkManager.connected) {
this._networkManager.send(NET_MSG.PING);
console.log('[TeamRoom] Sent PING to sync updated profile to server');
}
// Also invalidate cached avatar for self so it reloads
if (this._myPlayerId && this._avatarImages[this._myPlayerId] !== undefined) {
delete this._avatarImages[this._myPlayerId];
}
});
},
_cleanupProfileListener() {
if (this._profileUnsub) {
this._profileUnsub();
this._profileUnsub = null;
}
},
update(dt) {
this._animTimer += dt;
@@ -392,40 +452,18 @@ const TeamRoomScene = {
ctx.stroke();
if (member) {
// Avatar placeholder (circle)
const avatarR = Math.min(rect.w, rect.h) * 0.22;
// Avatar centered in the slot
const avatarR = Math.min(rect.w, rect.h) * 0.32;
const avatarCX = rect.x + rect.w / 2;
const avatarCY = rect.y + rect.h * 0.3;
const avatarCY = rect.y + rect.h * 0.42;
this._drawAvatar(ctx, member, avatarCX, avatarCY, avatarR);
ctx.fillStyle = member.isLeader ? '#FFD700' : '#4a90d9';
ctx.beginPath();
ctx.arc(avatarCX, avatarCY, avatarR, 0, Math.PI * 2);
ctx.fill();
// Player icon
ctx.fillStyle = '#FFFFFF';
ctx.font = `${avatarR}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🎖', avatarCX, avatarCY);
// Leader badge
if (member.isLeader) {
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 9px Arial';
ctx.fillText(t('teamRoom.leader'), avatarCX, avatarCY + avatarR + 10);
}
// Player name (truncated)
ctx.fillStyle = '#FFFFFF';
ctx.font = '10px Arial';
const name = this._getDisplayName(member);
ctx.fillText(name, avatarCX, rect.y + rect.h * 0.7);
// Ready state
// Ready state (below avatar)
if (!member.isLeader) {
ctx.fillStyle = member.ready ? '#00FF00' : '#FF6347';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(member.ready ? t('teamRoom.ready') : t('teamRoom.notReady'), avatarCX, rect.y + rect.h * 0.88);
}
@@ -477,19 +515,34 @@ const TeamRoomScene = {
const member = members[i];
ctx.fillStyle = member ? '#1e3a5f' : '#0d1b2a';
ctx.strokeStyle = '#0f3460';
ctx.lineWidth = 1;
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) {
ctx.fillStyle = '#FFFFFF';
ctx.font = '10px Arial';
// Avatar centered in the slot
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);
// Ready state (below avatar)
if (!member.isLeader) {
ctx.fillStyle = member.ready ? '#00FF00' : '#FF6347';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(member.ready ? t('teamRoom.ready') : t('teamRoom.notReady'), avatarCX, rect.y + rect.h * 0.88);
}
} else {
// Empty slot
ctx.fillStyle = '#555555';
ctx.font = '24px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const name = this._getDisplayName(member);
ctx.fillText(name, rect.x + rect.w / 2, rect.y + rect.h / 2);
ctx.fillText('+', rect.x + rect.w / 2, rect.y + rect.h / 2);
}
}
@@ -575,6 +628,113 @@ const TeamRoomScene = {
return tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h;
},
/**
* Draw an avatar for a team member: WeChat image if loaded, else colored placeholder.
* For the local player, also checks PlayerProfile.avatarUrl as a fallback
* source (it may be set before the server broadcasts the update).
* @private
*/
_drawAvatar(ctx, member, cx, cy, r) {
const img = this._avatarImages[member.playerId];
if (img) {
// Circular clip for the avatar image
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();
// Border ring
ctx.strokeStyle = member.isLeader ? '#FFD700' : '#4a90d9';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, r + 1, 0, Math.PI * 2);
ctx.stroke();
} else {
// For the local player, check if PlayerProfile has an avatarUrl that
// hasn't been loaded into _avatarImages yet (async timing gap).
if (member.playerId === this._myPlayerId) {
const profile = (typeof GameGlobal !== 'undefined') ? GameGlobal.playerProfile : null;
if (profile && profile.avatarUrl && !this._avatarImages[member.playerId]) {
// Profile has an avatarUrl but we haven't loaded it yet — trigger load
this._loadAvatar(member);
// Don't draw the placeholder; next frame will render the image.
// However, to avoid a flash, still draw placeholder this frame.
}
}
// Placeholder: colored circle with a simple person silhouette
const bgColor = member.isLeader ? '#FFD700' : '#4a90d9';
ctx.fillStyle = bgColor;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
// Draw a simple person silhouette (head + shoulders)
ctx.fillStyle = 'rgba(255,255,255,0.7)';
// Head
ctx.beginPath();
ctx.arc(cx, cy - r * 0.2, r * 0.3, 0, Math.PI * 2);
ctx.fill();
// Shoulders
ctx.beginPath();
ctx.ellipse(cx, cy + r * 0.55, r * 0.55, r * 0.35, 0, Math.PI, 0);
ctx.fill();
// Border ring for placeholder
ctx.strokeStyle = bgColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, r + 1, 0, Math.PI * 2);
ctx.stroke();
// Trigger async load
this._loadAvatar(member);
}
},
/**
* Asynchronously load a WeChat avatar image for the given member.
* Caches the Image object so subsequent frames render the real avatar.
* For the local player, also checks PlayerProfile as a fallback source
* for the avatarUrl (it may have been granted after the team state was
* last broadcast from the server).
* @private
*/
_loadAvatar(member) {
// For self, prefer the latest PlayerProfile avatarUrl (it updates
// asynchronously via fetchSilent / UserInfoButton, possibly after
// the server last broadcast the team state).
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;
// Mark as loading (null) to prevent duplicate loads
this._avatarImages[member.playerId] = null;
try {
const img = wx.createImage();
img.onload = () => {
this._avatarImages[member.playerId] = img;
console.log(`[TeamRoom] Avatar loaded for ${member.playerId}, url=${avatarUrl.substring(0, 60)}...`);
};
img.onerror = (err) => {
// Keep null so we don't retry endlessly
console.warn(`[TeamRoom] Failed to load avatar for ${member.playerId}, url=${avatarUrl.substring(0, 60)}..., err:`, err);
};
img.src = avatarUrl;
} catch (e) {
console.warn('[TeamRoom] wx.createImage not available:', e);
}
},
/**
* Compute a display name for a team member entry.
* Uses real WeChat nickname when available, otherwise a stable fallback.