Files
tankwar_proj/game.js
T
2026-04-10 22:59:39 +08:00

326 lines
11 KiB
JavaScript

/**
* 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);