fix boss tank cross brick
This commit is contained in:
@@ -200,7 +200,10 @@ const BuffSelectScene = {
|
||||
const GameScene = require('./GameScene');
|
||||
sm.register(SCENE.GAME, GameScene);
|
||||
}
|
||||
sm.switchTo(SCENE.GAME, this._gameParams);
|
||||
// ★ DEBUG: Force level 20 (Boss Battle) for quick verification of Boss tank gap-fix
|
||||
// const params = Object.assign({}, this._gameParams, { level: 20 });
|
||||
const params = Object.assign({}, this._gameParams);
|
||||
sm.switchTo(SCENE.GAME, params);
|
||||
},
|
||||
|
||||
handleTouch(eventType, e) {
|
||||
|
||||
+795
-138
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ const ROOM_STATE = {
|
||||
// Room Scene
|
||||
// ============================================================
|
||||
const RoomScene = {
|
||||
_state: ROOM_STATE.IDLE,
|
||||
_state: ROOM_STATE.CREATING,
|
||||
_roomCode: '',
|
||||
_errorMsg: '',
|
||||
_countdown: 3,
|
||||
@@ -152,7 +152,7 @@ const RoomScene = {
|
||||
}));
|
||||
|
||||
unsubs.push(nm.on('disconnected', () => {
|
||||
if (this._state !== ROOM_STATE.IDLE) {
|
||||
if (this._state !== ROOM_STATE.CREATING) {
|
||||
this._errorMsg = t('common.disconnected');
|
||||
this._state = ROOM_STATE.ERROR;
|
||||
}
|
||||
@@ -198,7 +198,7 @@ const RoomScene = {
|
||||
if (shareManager) {
|
||||
shareManager.setShareContent({
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
imageUrl: 'js/ui/images/1v1.png',
|
||||
query: `roomId=${this._roomCode}`,
|
||||
});
|
||||
}
|
||||
@@ -214,7 +214,7 @@ const RoomScene = {
|
||||
|
||||
const shareData = {
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
imageUrl: 'js/ui/images/1v1.png',
|
||||
query: `roomId=${this._roomCode}`,
|
||||
};
|
||||
|
||||
@@ -240,7 +240,7 @@ const RoomScene = {
|
||||
|
||||
/**
|
||||
* 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"
|
||||
* This is the primary action — automatically creates a room and triggers invite share
|
||||
* and we handle everything in one go.
|
||||
* MUST be called within a touch event for WeChat policy compliance.
|
||||
* @private
|
||||
@@ -280,7 +280,7 @@ const RoomScene = {
|
||||
|
||||
const shareData = {
|
||||
title: t('room.shareTitle'),
|
||||
imageUrl: '',
|
||||
imageUrl: 'js/ui/images/1v1.png',
|
||||
query: `roomId=${roomId}`,
|
||||
};
|
||||
|
||||
@@ -505,7 +505,7 @@ const RoomScene = {
|
||||
|
||||
switch (this._state) {
|
||||
case ROOM_STATE.ERROR:
|
||||
this._state = ROOM_STATE.IDLE;
|
||||
this._state = ROOM_STATE.CREATING;
|
||||
this._errorMsg = '';
|
||||
break;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Team2v2RoomScene.js
|
||||
* 2v2 Brawl team room — delegates to the shared TeamRoomSceneFactory.
|
||||
*/
|
||||
|
||||
const { createTeamRoomScene } = require('./TeamRoomSceneFactory');
|
||||
|
||||
const Team2v2RoomScene = createTeamRoomScene({
|
||||
teamSize: 2,
|
||||
battleMode: '2v2',
|
||||
i18nPrefix: 'team2v2Room',
|
||||
logTag: 'Team2v2Room',
|
||||
shareImageUrl: 'js/ui/images/2v2.png',
|
||||
});
|
||||
|
||||
module.exports = Team2v2RoomScene;
|
||||
@@ -1039,15 +1039,15 @@ const TeamGameScene = {
|
||||
|
||||
// Validate pushed positions against terrain; revert if stuck in wall
|
||||
if (this._mapManager) {
|
||||
const leftA = tankA.x - tankA.halfSize;
|
||||
const topA = tankA.y - tankA.halfSize;
|
||||
if (this._mapManager.rectCollidesWithTerrain(leftA, topA, tankA.size, tankA.size)) {
|
||||
const leftA = tankA.x - tankA.colliderHalfSize;
|
||||
const topA = tankA.y - tankA.colliderHalfSize;
|
||||
if (this._mapManager.rectCollidesWithTerrain(leftA, topA, tankA.colliderSize, tankA.colliderSize)) {
|
||||
tankA.x = origAX;
|
||||
tankA.y = origAY;
|
||||
}
|
||||
const leftB = tankB.x - tankB.halfSize;
|
||||
const topB = tankB.y - tankB.halfSize;
|
||||
if (this._mapManager.rectCollidesWithTerrain(leftB, topB, tankB.size, tankB.size)) {
|
||||
const leftB = tankB.x - tankB.colliderHalfSize;
|
||||
const topB = tankB.y - tankB.colliderHalfSize;
|
||||
if (this._mapManager.rectCollidesWithTerrain(leftB, topB, tankB.colliderSize, tankB.colliderSize)) {
|
||||
tankB.x = origBX;
|
||||
tankB.y = origBY;
|
||||
}
|
||||
|
||||
+10
-1011
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,947 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user