feat: use wx.createUserInfoButton to get weixin's avarta
This commit is contained in:
+129
-6
@@ -97,6 +97,24 @@ const MenuScene = {
|
||||
enter() {
|
||||
this._pressedIndex = -1;
|
||||
this._tankAnim = 0;
|
||||
this._avatarImg = null;
|
||||
|
||||
// Load avatar image if profile has one
|
||||
const profile = GameGlobal.playerProfile;
|
||||
if (profile && profile.avatarUrl) {
|
||||
this._loadAvatarImage(profile.avatarUrl);
|
||||
}
|
||||
|
||||
// Listen for profile updates (avatar may arrive after initial render)
|
||||
this._profileHandler = (data) => {
|
||||
if (data && data.avatarUrl && !this._avatarImg) {
|
||||
this._loadAvatarImage(data.avatarUrl);
|
||||
}
|
||||
};
|
||||
const bus = GameGlobal.eventBus;
|
||||
if (bus && typeof bus.on === 'function') {
|
||||
bus.on('profile:updated', this._profileHandler);
|
||||
}
|
||||
|
||||
// Kick off nickname acquisition as early as possible so that later
|
||||
// network messages (CREATE_TEAM, SOLO_MATCH, ...) can carry it.
|
||||
@@ -120,6 +138,18 @@ const MenuScene = {
|
||||
|
||||
exit() {
|
||||
this._pressedIndex = -1;
|
||||
// Destroy UserInfoButton overlay when leaving the menu
|
||||
const profile = GameGlobal.playerProfile;
|
||||
if (profile && typeof profile.destroyUserInfoButton === 'function') {
|
||||
profile.destroyUserInfoButton();
|
||||
}
|
||||
// Remove profile update listener
|
||||
const bus = GameGlobal.eventBus;
|
||||
if (bus && typeof bus.off === 'function' && this._profileHandler) {
|
||||
bus.off('profile:updated', this._profileHandler);
|
||||
}
|
||||
this._profileHandler = null;
|
||||
this._avatarImg = null;
|
||||
},
|
||||
|
||||
update(dt) {
|
||||
@@ -151,6 +181,55 @@ const MenuScene = {
|
||||
ctx.fillStyle = accentGrad;
|
||||
ctx.fillRect(0, 0, SCREEN_WIDTH, 3);
|
||||
|
||||
// ---- Player Avatar & Nickname (top-left) ----
|
||||
const profile = GameGlobal.playerProfile;
|
||||
const avatarSize = 28;
|
||||
const avatarX = 10;
|
||||
const avatarY = 10;
|
||||
const avatarR = avatarSize / 2;
|
||||
|
||||
// Avatar circle background
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(avatarX + avatarR, avatarY + avatarR, avatarR, 0, Math.PI * 2);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(30,48,84,0.7)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,215,0,0.4)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Avatar image or default icon
|
||||
if (profile && profile.avatarUrl && this._avatarImg && this._avatarImg.complete) {
|
||||
ctx.clip();
|
||||
ctx.drawImage(this._avatarImg, avatarX, avatarY, avatarSize, avatarSize);
|
||||
} else {
|
||||
// Default user icon (simple silhouette)
|
||||
ctx.fillStyle = 'rgba(255,215,0,0.5)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(avatarX + avatarR, avatarY + avatarR - 2, avatarR * 0.35, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(avatarX + avatarR, avatarY + avatarR + avatarR * 0.55, avatarR * 0.55, avatarR * 0.3, 0, Math.PI, 0);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Nickname
|
||||
const displayName = profile ? profile.getDisplayName() : 'Tanker';
|
||||
ctx.font = 'bold 11px Arial';
|
||||
ctx.fillStyle = MC.GOLD;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(displayName, avatarX + avatarSize + 6, avatarY + avatarR - 5);
|
||||
|
||||
// Hint text (only if not yet granted)
|
||||
if (profile && !profile.granted) {
|
||||
ctx.font = '9px Arial';
|
||||
ctx.fillStyle = MC.SUBTITLE;
|
||||
ctx.fillText(t('menu.tapToAuth') || 'Tap to authorize', avatarX + avatarSize + 6, avatarY + avatarR + 8);
|
||||
}
|
||||
|
||||
// ---- Gold Balance (top-right pill) ----
|
||||
const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0;
|
||||
const goldText = `🪙 ${gold}`;
|
||||
@@ -469,19 +548,63 @@ const MenuScene = {
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Kick off profile acquisition on menu enter. Since WeChat 2022-10 there
|
||||
* is NO silent way to get the real nickname — we draw a canvas button and
|
||||
* call `wx.getUserProfile` directly from its touchend handler.
|
||||
* Load avatar image from URL for Canvas rendering.
|
||||
* @param {string} url
|
||||
* @private
|
||||
*/
|
||||
_loadAvatarImage(url) {
|
||||
if (!url || this._avatarImg) return;
|
||||
try {
|
||||
const img = wx.createImage();
|
||||
img.onload = () => {
|
||||
this._avatarImg = img;
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.warn('[MenuScene] Failed to load avatar image');
|
||||
};
|
||||
img.src = url;
|
||||
} catch (e) {
|
||||
console.warn('[MenuScene] _loadAvatarImage error:', e && e.message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Kick off profile acquisition on menu enter.
|
||||
*
|
||||
* Since WeChat 2022-10, `wx.getUserInfo` returns "scope unauthorized" —
|
||||
* the ONLY way to get the real nickname + avatar is `wx.createUserInfoButton`.
|
||||
* We create a transparent overlay button that covers the avatar area in the
|
||||
* top-left corner. When the user taps their avatar, WeChat returns the real
|
||||
* profile data.
|
||||
* @private
|
||||
*/
|
||||
_initPlayerProfile() {
|
||||
const profile = GameGlobal.playerProfile;
|
||||
if (!profile) return;
|
||||
|
||||
// Best-effort placeholder fetch (used only to pre-fill _nickname with
|
||||
// "微信用户" on older devices; does not mark granted).
|
||||
// If already granted (from a previous session's cache), no button needed
|
||||
if (profile.granted) return;
|
||||
|
||||
// Layout must match the avatar area drawn in render():
|
||||
// avatarX=10, avatarY=10, avatarSize=28 + nickname text area
|
||||
const avatarLayout = {
|
||||
x: 8,
|
||||
y: 8,
|
||||
width: 120,
|
||||
height: 32,
|
||||
};
|
||||
|
||||
// Create the UserInfoButton — visible style covering avatar + nickname area
|
||||
if (typeof profile.fetchSilent === 'function') {
|
||||
profile.fetchSilent().catch(() => { /* ignore */ });
|
||||
profile.fetchSilent(avatarLayout).then((ok) => {
|
||||
console.log('[MenuScene] fetchSilent completed, granted=', ok);
|
||||
if (ok) {
|
||||
// Button was tapped and profile was granted — destroy it
|
||||
profile.destroyUserInfoButton();
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.warn('[MenuScene] fetchSilent error:', e);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -85,10 +85,6 @@ const SettingsScene = {
|
||||
this._renderToggle(ctx, cx, cy, row);
|
||||
}
|
||||
|
||||
// Profile entry button (below the last toggle row)
|
||||
const profileY = firstCenterY + rows.length * step;
|
||||
this._renderProfileButton(ctx, cx, profileY);
|
||||
|
||||
// Back button
|
||||
this._renderBackButton(ctx, cx, backCenterY);
|
||||
},
|
||||
@@ -138,34 +134,6 @@ const SettingsScene = {
|
||||
ctx.fill();
|
||||
},
|
||||
|
||||
_renderProfileButton(ctx, cx, y) {
|
||||
const w = SCREEN_WIDTH * 0.7;
|
||||
const h = 50;
|
||||
const x = cx - w / 2;
|
||||
|
||||
this._buttons['profile'] = { x, y: y - h / 2, w, h };
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#1e1e3a';
|
||||
ctx.fillRect(x, y - h / 2, w, h);
|
||||
ctx.strokeStyle = '#333366';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y - h / 2, w, h);
|
||||
|
||||
// Icon and label
|
||||
ctx.fillStyle = COLORS.HUD_TEXT;
|
||||
ctx.font = '16px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`👤 ${t('settings.profile')}`, x + 15, y);
|
||||
|
||||
// Arrow indicator
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.font = '14px Arial';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText('›', x + w - 15, y);
|
||||
},
|
||||
|
||||
_renderBackButton(ctx, cx, y) {
|
||||
const w = SCREEN_WIDTH * 0.4;
|
||||
const h = 42;
|
||||
@@ -197,13 +165,6 @@ const SettingsScene = {
|
||||
if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) {
|
||||
if (key === 'back') {
|
||||
GameGlobal.sceneManager.switchTo(SCENE.MENU);
|
||||
} else if (key === 'profile') {
|
||||
const sm = GameGlobal.sceneManager;
|
||||
if (!sm._scenes.has(SCENE.PROFILE)) {
|
||||
const ProfileScene = require('./ProfileScene');
|
||||
sm.register(SCENE.PROFILE, ProfileScene);
|
||||
}
|
||||
sm.switchTo(SCENE.PROFILE);
|
||||
} else if (this._settings.hasOwnProperty(key)) {
|
||||
this._settings[key] = !this._settings[key];
|
||||
// Notify audio system
|
||||
|
||||
+195
-35
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user