d263c7bf48
- GameGlobal.js: keep upstream SERVER_URL with /ws suffix - en.js/zh.js: merge both settings.nickname and settings.profile keys - SettingsScene.js: keep both nickname row and profile button - server/index.js: merge express app + content security proxy with noServer WebSocket mode and path validation - Add .gitignore for node_modules and .codebuddy
356 lines
12 KiB
JavaScript
356 lines
12 KiB
JavaScript
/**
|
|
* game.js
|
|
* Entry point for Tank War WeChat mini game.
|
|
* Initializes canvas, sets up the game loop, and manages scene lifecycle.
|
|
*/
|
|
|
|
// Ensure timer globals exist (some WeChat base library versions may not
|
|
// inject them early enough, causing WAGame.js internal error-reporting to
|
|
// crash with "Can't find variable: setTimeout").
|
|
if (typeof setTimeout === 'undefined') {
|
|
GameGlobal.setTimeout = GameGlobal.setTimeout || function (fn, ms) {
|
|
// Fallback: execute synchronously when real timer is unavailable
|
|
fn();
|
|
};
|
|
GameGlobal.setInterval = GameGlobal.setInterval || function () {};
|
|
GameGlobal.clearTimeout = GameGlobal.clearTimeout || function () {};
|
|
GameGlobal.clearInterval = GameGlobal.clearInterval || function () {};
|
|
}
|
|
|
|
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 ContentSecurityManager = require('./js/managers/ContentSecurityManager');
|
|
const BuffManager = require('./js/managers/BuffManager');
|
|
const SkinManager = require('./js/managers/SkinManager');
|
|
const PlayerProfile = require('./js/managers/PlayerProfile');
|
|
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 contentSecurityManager = new ContentSecurityManager();
|
|
const buffManager = new BuffManager();
|
|
const skinManager = new SkinManager();
|
|
const playerProfile = new PlayerProfile();
|
|
GameGlobal.adManager = adManager;
|
|
GameGlobal.shareManager = shareManager;
|
|
GameGlobal.currencyManager = currencyManager;
|
|
GameGlobal.paymentManager = paymentManager;
|
|
GameGlobal.complianceManager = complianceManager;
|
|
GameGlobal.contentSecurityManager = contentSecurityManager;
|
|
GameGlobal.buffManager = buffManager;
|
|
GameGlobal.skinManager = skinManager;
|
|
GameGlobal.playerProfile = playerProfile;
|
|
|
|
// ============================================================
|
|
// 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();
|
|
|
|
// Initialize content security manager (local word list + remote sync)
|
|
// Derive HTTP base URL from WebSocket server URL for content security API calls
|
|
const wsUrl = networkManager._serverUrl || '';
|
|
const httpServerUrl = wsUrl.replace(/^wss?/, 'https');
|
|
contentSecurityManager.init({
|
|
serverUrl: httpServerUrl,
|
|
});
|
|
|
|
// 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);
|