/** * game.js * Entry point for Tank War WeChat mini game. * Initializes canvas, sets up the game loop, and manages scene lifecycle. */ const SceneManager = require('./js/managers/SceneManager'); const ResourceManager = require('./js/managers/ResourceManager'); const StorageManager = require('./js/managers/StorageManager'); const AudioManager = require('./js/managers/AudioManager'); const NetworkManager = require('./js/managers/NetworkManager'); const AdManager = require('./js/managers/AdManager'); const ShareManager = require('./js/managers/ShareManager'); const CurrencyManager = require('./js/managers/CurrencyManager'); const PaymentManager = require('./js/managers/PaymentManager'); const ComplianceManager = require('./js/managers/ComplianceManager'); const BuffManager = require('./js/managers/BuffManager'); const EventBus = require('./js/base/EventBus'); const { SCREEN_WIDTH, SCREEN_HEIGHT, DEVICE_PIXEL_RATIO, COLORS, SCENE, } = require('./js/base/GameGlobal'); // ============================================================ // Canvas Setup // ============================================================ const canvas = wx.createCanvas(); const ctx = canvas.getContext('2d'); // Handle high-DPI screens canvas.width = SCREEN_WIDTH * DEVICE_PIXEL_RATIO; canvas.height = SCREEN_HEIGHT * DEVICE_PIXEL_RATIO; ctx.scale(DEVICE_PIXEL_RATIO, DEVICE_PIXEL_RATIO); // ============================================================ // Core Singletons // ============================================================ const sceneManager = new SceneManager(); const resourceManager = new ResourceManager(); const storageManager = new StorageManager(); const audioManager = new AudioManager(); const networkManager = new NetworkManager(); const eventBus = new EventBus(); // Expose globally so all modules can access GameGlobal.canvas = canvas; GameGlobal.ctx = ctx; GameGlobal.sceneManager = sceneManager; GameGlobal.resourceManager = resourceManager; GameGlobal.storageManager = storageManager; GameGlobal.audioManager = audioManager; GameGlobal.networkManager = networkManager; GameGlobal.eventBus = eventBus; // Initialize ad and share managers (depend on storageManager being set) const adManager = new AdManager(); const shareManager = new ShareManager(); const currencyManager = new CurrencyManager(); const paymentManager = new PaymentManager(); const complianceManager = new ComplianceManager(); const buffManager = new BuffManager(); GameGlobal.adManager = adManager; GameGlobal.shareManager = shareManager; GameGlobal.currencyManager = currencyManager; GameGlobal.paymentManager = paymentManager; GameGlobal.complianceManager = complianceManager; GameGlobal.buffManager = buffManager; // ============================================================ // Game State // ============================================================ let isPaused = false; let lastTimestamp = 0; // ============================================================ // Touch Event Forwarding // ============================================================ wx.onTouchStart((e) => { sceneManager.handleTouch('touchstart', e); }); wx.onTouchMove((e) => { sceneManager.handleTouch('touchmove', e); }); wx.onTouchEnd((e) => { sceneManager.handleTouch('touchend', e); }); // ============================================================ // Lifecycle: pause / resume on background switch // ============================================================ wx.onHide(() => { isPaused = true; audioManager.pauseAll(); eventBus.emit('game:pause'); }); wx.onShow((res) => { isPaused = false; lastTimestamp = 0; // reset so dt doesn't spike audioManager.resumeAll(); eventBus.emit('game:resume'); 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) const teamId = _extractTeamId(res && res.query); if (teamId) { _handleInviteTeamId(teamId); } 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); if (fallbackTeamId) { console.log(`[game.js] onShow query empty, but found teamId in launchOptions: ${fallbackTeamId}`); _handleInviteTeamId(fallbackTeamId); } } catch (e) {} } }); /** * Extract teamId from query parameter. * WeChat may provide query as an Object ({teamId: 'xxx'}) or as a raw * query-string ('teamId=xxx'). This helper handles both formats. * @param {object|string|undefined} query * @returns {string|null} */ function _extractTeamId(query) { if (!query) return null; // Log the raw query for debugging try { console.log(`[game.js] _extractTeamId raw query: ${JSON.stringify(query)}, type: ${typeof query}`); } catch (e) { console.log(`[game.js] _extractTeamId query type: ${typeof query}`); } // Case 1: query is already an object with teamId property if (typeof query === 'object' && query.teamId) { return query.teamId; } // Case 2: query is a string like 'teamId=T12345' or 'teamId=T12345&foo=bar' if (typeof query === 'string') { const match = query.match(/teamId=([^&]+)/); if (match) return match[1]; } // Case 3: query is an object but teamId might be nested in a raw string field // Some WeChat versions put the whole query string in a single property if (typeof query === 'object') { const keys = Object.keys(query); // If the object has a single key that looks like a query string for (const key of keys) { const combined = key + (query[key] ? '=' + query[key] : ''); const match = combined.match(/teamId=([^&]+)/); if (match) return match[1]; } } return null; } /** * Handle teamId from invite card (shared between onShow and cold launch). * Navigates to TeamRoomScene if possible, otherwise stores as pending. * @param {string} teamId */ function _handleInviteTeamId(teamId) { if (!teamId) return; // Avoid duplicate processing if already pending the same teamId if (GameGlobal._pendingTeamId === teamId) { console.log(`[game.js] teamId ${teamId} already pending, skipping duplicate`); return; } console.log(`[game.js] Received teamId from invite: ${teamId}, currentScene: ${sceneManager._currentName}`); // If already past loading, navigate directly to team room if (sceneManager._currentScene && sceneManager._currentName !== SCENE.LOADING) { console.log(`[game.js] Navigating directly to TeamRoomScene with teamId: ${teamId}`); if (!sceneManager._scenes.has(SCENE.TEAM_ROOM)) { const TeamRoomScene = require('./js/scenes/TeamRoomScene'); sceneManager.register(SCENE.TEAM_ROOM, TeamRoomScene); } sceneManager.switchTo(SCENE.TEAM_ROOM, { teamId }); GameGlobal._pendingTeamId = null; } else { // Still loading — store pending teamId for auto-navigation after load console.log(`[game.js] Still loading, storing pendingTeamId: ${teamId}`); GameGlobal._pendingTeamId = teamId; } } // Check for teamId 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); if (launchTeamId) { _handleInviteTeamId(launchTeamId); } else { console.log('[game.js] No teamId found in cold launch options'); } } catch (e) { console.error('[game.js] getLaunchOptionsSync failed:', e); } // ============================================================ // Game Loop // ============================================================ function gameLoop(timestamp) { requestAnimationFrame(gameLoop); if (isPaused) return; // Calculate delta time in seconds, cap at 100ms to avoid spiral if (lastTimestamp === 0) lastTimestamp = timestamp; let dt = (timestamp - lastTimestamp) / 1000; if (dt > 0.1) dt = 0.1; lastTimestamp = timestamp; // Clear screen ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); ctx.fillStyle = COLORS.BG; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Update & render current scene sceneManager.update(dt); sceneManager.render(ctx); } // ============================================================ // Loading Scene (inline, minimal) // ============================================================ const LoadingScene = { _progress: 0, enter() { this._progress = 0; this._startLoading(); }, exit() {}, async _startLoading() { // Initialize audio system (programmatic synthesis, no files needed) audioManager.init(); // Define all image assets to preload // For now we use procedural drawing, so asset list is empty. // Assets can be added later as the game grows. const assets = []; if (assets.length === 0) { // No assets to load, go directly to menu this._progress = 1; // Use setTimeout to allow at least one render frame of loading screen setTimeout(() => { const MenuScene = require('./js/scenes/MenuScene'); sceneManager.register(SCENE.MENU, MenuScene); // Register other scenes lazily as they are created sceneManager.switchTo(SCENE.MENU); }, 300); return; } await resourceManager.loadImages(assets, (loaded, total) => { this._progress = loaded / total; }); const MenuScene = require('./js/scenes/MenuScene'); sceneManager.register(SCENE.MENU, MenuScene); sceneManager.switchTo(SCENE.MENU); }, update(dt) {}, render(ctx) { // Draw loading screen const cx = SCREEN_WIDTH / 2; const cy = SCREEN_HEIGHT / 2; // Title ctx.fillStyle = COLORS.MENU_TITLE; ctx.font = 'bold 28px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('坦克探险', cx, cy - 60); // Progress bar background const barW = SCREEN_WIDTH * 0.6; const barH = 12; const barX = cx - barW / 2; const barY = cy; ctx.fillStyle = '#333333'; ctx.fillRect(barX, barY, barW, barH); // Progress bar fill ctx.fillStyle = COLORS.MENU_TITLE; ctx.fillRect(barX, barY, barW * this._progress, barH); // Loading text ctx.fillStyle = COLORS.HUD_TEXT; ctx.font = '14px Arial'; ctx.fillText(`加载中... ${Math.floor(this._progress * 100)}%`, cx, cy + 30); }, handleTouch() {}, }; // ============================================================ // Bootstrap // ============================================================ sceneManager.register(SCENE.LOADING, LoadingScene); sceneManager.switchTo(SCENE.LOADING); // Start the game loop requestAnimationFrame(gameLoop);