first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* EventBus.js
* Simple publish/subscribe event system for decoupled communication
* between game systems.
*/
class EventBus {
constructor() {
/** @type {Map<string, Array<{fn: Function, once: boolean}>>} */
this._listeners = new Map();
}
/**
* Subscribe to an event.
* @param {string} event
* @param {Function} fn
* @returns {Function} Unsubscribe function.
*/
on(event, fn) {
if (!this._listeners.has(event)) {
this._listeners.set(event, []);
}
const entry = { fn, once: false };
this._listeners.get(event).push(entry);
return () => this.off(event, fn);
}
/**
* Subscribe to an event, but only fire once.
* @param {string} event
* @param {Function} fn
*/
once(event, fn) {
if (!this._listeners.has(event)) {
this._listeners.set(event, []);
}
this._listeners.get(event).push({ fn, once: true });
}
/**
* Unsubscribe from an event.
* @param {string} event
* @param {Function} fn
*/
off(event, fn) {
const list = this._listeners.get(event);
if (!list) return;
const idx = list.findIndex((entry) => entry.fn === fn);
if (idx !== -1) list.splice(idx, 1);
}
/**
* Emit an event with optional data.
* @param {string} event
* @param {*} [data]
*/
emit(event, data) {
const list = this._listeners.get(event);
if (!list || list.length === 0) return;
// Iterate in reverse so we can safely remove "once" entries
for (let i = list.length - 1; i >= 0; i--) {
const entry = list[i];
entry.fn(data);
if (entry.once) {
list.splice(i, 1);
}
}
}
/**
* Remove all listeners for a specific event, or all events.
* @param {string} [event]
*/
clear(event) {
if (event) {
this._listeners.delete(event);
} else {
this._listeners.clear();
}
}
}
module.exports = EventBus;
+350
View File
@@ -0,0 +1,350 @@
/**
* GameGlobal.js
* Global constants, enums, and configuration for Tank War mini game.
*/
// ============================================================
// Screen & Canvas
// ============================================================
const systemInfo = wx.getSystemInfoSync();
const SCREEN_WIDTH = systemInfo.windowWidth;
const SCREEN_HEIGHT = systemInfo.windowHeight;
const DEVICE_PIXEL_RATIO = systemInfo.pixelRatio || 1;
// ============================================================
// Map Grid
// ============================================================
// TILE_SIZE is determined by screen height so the map fills vertically
const GRID_ROWS = 13;
const GRID_COLS = 21;
const TILE_SIZE = SCREEN_HEIGHT / GRID_ROWS;
const MAP_WIDTH = TILE_SIZE * GRID_COLS;
const MAP_HEIGHT = TILE_SIZE * GRID_ROWS;
// Center map on screen — controls (joystick & fire button) overlay on the map
const MAP_OFFSET_X = Math.floor((SCREEN_WIDTH - MAP_WIDTH) / 2);
const MAP_OFFSET_Y = Math.floor((SCREEN_HEIGHT - MAP_HEIGHT) / 2);
// ============================================================
// Terrain Types
// ============================================================
const TERRAIN = {
EMPTY: 0,
BRICK: 1,
STEEL: 2,
RIVER: 3,
FOREST: 4,
BASE: 5,
BASE_WALL: 6, // brick wall around base
};
// ============================================================
// Direction
// ============================================================
const DIRECTION = {
UP: 0,
DOWN: 1,
LEFT: 2,
RIGHT: 3,
};
// Direction vectors (dx, dy)
const DIR_VECTORS = {
[DIRECTION.UP]: { dx: 0, dy: -1 },
[DIRECTION.DOWN]: { dx: 0, dy: 1 },
[DIRECTION.LEFT]: { dx: -1, dy: 0 },
[DIRECTION.RIGHT]: { dx: 1, dy: 0 },
};
// ============================================================
// Tank Types
// ============================================================
const TANK_TYPE = {
PLAYER: 'player',
ENEMY_NORMAL: 'enemy_normal',
ENEMY_FAST: 'enemy_fast',
ENEMY_ARMOR: 'enemy_armor',
ENEMY_BOSS: 'enemy_boss',
};
// Tank configuration table
const TANK_CONFIG = {
[TANK_TYPE.PLAYER]: {
speed: 2,
hp: 1,
color: '#FFD700', // gold
size: TILE_SIZE * 0.85,
},
[TANK_TYPE.ENEMY_NORMAL]: {
speed: 1.5,
hp: 1,
color: '#AAAAAA', // gray
size: TILE_SIZE * 0.85,
score: 100,
},
[TANK_TYPE.ENEMY_FAST]: {
speed: 3,
hp: 1,
color: '#FF6347', // tomato
size: TILE_SIZE * 0.85,
score: 200,
},
[TANK_TYPE.ENEMY_ARMOR]: {
speed: 1,
hp: 3,
color: '#228B22', // forest green
size: TILE_SIZE * 0.85,
score: 300,
},
[TANK_TYPE.ENEMY_BOSS]: {
speed: 1.2,
hp: 6,
color: '#8B0000', // dark red
size: TILE_SIZE * 1.2,
score: 500,
},
};
// ============================================================
// Bullet
// ============================================================
const BULLET_SPEED = 5;
const BULLET_SIZE = 6;
// ============================================================
// Fire Level
// ============================================================
const FIRE_LEVEL = {
LV1: 1, // single shot, 1 bullet on screen
LV2: 2, // rapid fire, 2 bullets on screen
LV3: 3, // rapid fire + steel break, 2 bullets on screen
};
const MAX_BULLETS_BY_LEVEL = {
[FIRE_LEVEL.LV1]: 1,
[FIRE_LEVEL.LV2]: 2,
[FIRE_LEVEL.LV3]: 2,
};
// ============================================================
// Power-Up Types
// ============================================================
const POWERUP_TYPE = {
STAR: 'star',
CLOCK: 'clock',
BOMB: 'bomb',
HELMET: 'helmet',
SHOVEL: 'shovel',
TANK: 'tank',
};
const POWERUP_DURATION = 15000; // ms, how long a power-up stays on map
// ============================================================
// Game Settings
// ============================================================
const DEFAULT_LIVES = 3;
const ENEMIES_PER_LEVEL = 20;
const MAX_ENEMIES_ON_SCREEN = 4;
const ENEMY_SPAWN_INTERVAL = 3000; // ms
const FREEZE_DURATION = 10000; // ms
const SHIELD_DURATION = 15000; // ms
const SHOVEL_DURATION = 20000; // ms
const INVINCIBLE_BLINK_INTERVAL = 100; // ms
// ============================================================
// Scene Names
// ============================================================
const SCENE = {
LOADING: 'loading',
MENU: 'menu',
GAME: 'game',
RESULT: 'result',
RANKING: 'ranking',
SETTINGS: 'settings',
SHOP: 'shop',
BUFF_SELECT: 'buff_select',
PVP_ROOM: 'pvp_room',
PVP_GAME: 'pvp_game',
PVP_RESULT: 'pvp_result',
TEAM_ROOM: 'team_room',
TEAM_GAME: 'team_game',
TEAM_RESULT: 'team_result',
};
// ============================================================
// Game Modes
// ============================================================
const GAME_MODE = {
CLASSIC: 'classic',
ENDLESS: 'endless',
PVP: 'pvp',
TEAM_3V3: 'team_3v3',
};
// ============================================================
// PVP Settings
// ============================================================
const PVP_ROUND_TIME = 180; // seconds per round (legacy, unused in base-destruction mode)
const PVP_RESPAWN_DELAY = 3000; // ms before respawn
const PVP_MAX_LIVES = 5; // legacy, unused in base-destruction mode
const PVP_WIN_KILLS = 5; // legacy, unused in base-destruction mode
const PVP_BASE_HP = 5; // base hit points for 1v1 PVP mode
// ============================================================
// Server Configuration
// ============================================================
// const SERVER_URL = 'ws://192.168.1.103:3000'; // local testing server URL, replace with actual server URL in production
const SERVER_URL = 'wss://www.igeek.site/games/wx/tankwar';
// ============================================================
// 3v3 Team Settings
// ============================================================
const TEAM_SIZE = 3;
const TEAM_RESPAWN_DELAY = 3000; // ms before respawn
const TEAM_BASE_HP = 10; // base hit points for 3v3 mode
const TEAM_RECONNECT_TIMEOUT = 60000; // ms, 60s to reconnect
const TEAM_MATCH_TIMEOUT = 60000; // ms, 60s matchmaking timeout
// ============================================================
// Battle Configuration (X vs X configurable)
// ============================================================
const BATTLE_CONFIG = {
'1v1': {
teamSize: 1,
baseHp: PVP_BASE_HP,
respawnDelay: PVP_RESPAWN_DELAY,
fillWithBots: false,
mapPool: 'pvp',
},
'3v3': {
teamSize: 3,
baseHp: TEAM_BASE_HP,
respawnDelay: TEAM_RESPAWN_DELAY,
fillWithBots: true,
mapPool: 'team',
},
};
// ============================================================
// Network Message Types
// ============================================================
const NET_MSG = {
// Room
CREATE_ROOM: 'create_room',
JOIN_ROOM: 'join_room',
ROOM_CREATED: 'room_created',
ROOM_JOINED: 'room_joined',
ROOM_ERROR: 'room_error',
OPPONENT_JOINED: 'opponent_joined',
OPPONENT_LEFT: 'opponent_left',
GAME_START: 'game_start',
// Gameplay
PLAYER_INPUT: 'player_input',
PLAYER_STATE: 'player_state',
BULLET_FIRE: 'bullet_fire',
BULLET_HIT: 'bullet_hit',
PLAYER_HIT: 'player_hit',
PLAYER_KILLED: 'player_killed',
GAME_OVER: 'game_over',
// Sync
PING: 'ping',
PONG: 'pong',
SYNC_STATE: 'sync_state',
// Team (3v3)
CREATE_TEAM: 'create_team',
JOIN_TEAM: 'join_team',
LEAVE_TEAM: 'leave_team',
TEAM_READY: 'team_ready',
TEAM_KICK: 'team_kick',
TEAM_DISBAND: 'team_disband',
TEAM_STATE: 'team_state',
MATCH_START: 'match_start',
MATCH_CANCEL: 'match_cancel',
MATCH_FOUND: 'match_found',
MATCH_TIMEOUT: 'match_timeout',
BASE_HIT: 'base_hit',
BASE_DESTROYED: 'base_destroyed',
PLAYER_RESPAWN: 'player_respawn',
TEAM_GAME_START: 'team_game_start',
TEAM_GAME_OVER: 'team_game_over',
RECONNECT: 'reconnect',
RECONNECT_OK: 'reconnect_ok',
PLAYER_DISCONNECT: 'player_disconnect',
BOT_TAKEOVER: 'bot_takeover',
SOLO_MATCH: 'solo_match',
REMATCH: 'rematch',
REMATCH_READY: 'rematch_ready',
};
// ============================================================
// Colors
// ============================================================
const COLORS = {
BG: '#000000',
BRICK: '#B5651D',
STEEL: '#808080',
RIVER: '#4169E1',
FOREST: '#006400',
BASE: '#FFD700',
BASE_WALL: '#B5651D',
HUD_TEXT: '#FFFFFF',
MENU_BG: '#1a1a2e',
MENU_TITLE: '#FFD700',
MENU_BTN: '#16213e',
MENU_BTN_TEXT: '#FFFFFF',
MENU_BTN_BORDER: '#0f3460',
};
// ============================================================
// Export
// ============================================================
module.exports = {
SCREEN_WIDTH,
SCREEN_HEIGHT,
DEVICE_PIXEL_RATIO,
GRID_COLS,
GRID_ROWS,
TILE_SIZE,
MAP_WIDTH,
MAP_HEIGHT,
MAP_OFFSET_X,
MAP_OFFSET_Y,
TERRAIN,
DIRECTION,
DIR_VECTORS,
TANK_TYPE,
TANK_CONFIG,
BULLET_SPEED,
BULLET_SIZE,
FIRE_LEVEL,
MAX_BULLETS_BY_LEVEL,
POWERUP_TYPE,
POWERUP_DURATION,
DEFAULT_LIVES,
ENEMIES_PER_LEVEL,
MAX_ENEMIES_ON_SCREEN,
ENEMY_SPAWN_INTERVAL,
FREEZE_DURATION,
SHIELD_DURATION,
SHOVEL_DURATION,
INVINCIBLE_BLINK_INTERVAL,
SCENE,
GAME_MODE,
PVP_ROUND_TIME,
PVP_RESPAWN_DELAY,
PVP_MAX_LIVES,
PVP_WIN_KILLS,
PVP_BASE_HP,
TEAM_SIZE,
TEAM_RESPAWN_DELAY,
TEAM_BASE_HP,
TEAM_RECONNECT_TIMEOUT,
TEAM_MATCH_TIMEOUT,
BATTLE_CONFIG,
NET_MSG,
COLORS,
SERVER_URL,
};
+83
View File
@@ -0,0 +1,83 @@
/**
* ObjectPool.js
* Generic object pool for reusing frequently created/destroyed objects
* (bullets, explosions, etc.) to avoid GC pressure in WeChat mini game.
*/
class ObjectPool {
/**
* @param {Function} createFn - Factory function that returns a new object instance.
* @param {Function} [resetFn] - Optional function to reset an object before reuse.
* @param {number} [initialSize=0] - Number of objects to pre-allocate.
*/
constructor(createFn, resetFn, initialSize = 0) {
this._createFn = createFn;
this._resetFn = resetFn || null;
this._pool = [];
this._activeCount = 0;
// Pre-allocate
for (let i = 0; i < initialSize; i++) {
this._pool.push(this._createFn());
}
}
/**
* Get an object from the pool. Creates a new one if pool is empty.
* @returns {*} A reusable object instance.
*/
get() {
let obj;
if (this._pool.length > 0) {
obj = this._pool.pop();
} else {
obj = this._createFn();
}
if (this._resetFn) {
this._resetFn(obj);
}
this._activeCount++;
return obj;
}
/**
* Return an object back to the pool for future reuse.
* @param {*} obj - The object to recycle.
*/
put(obj) {
if (obj) {
this._pool.push(obj);
this._activeCount = Math.max(0, this._activeCount - 1);
}
}
/**
* Pre-allocate a number of objects into the pool.
* @param {number} count
*/
preAllocate(count) {
for (let i = 0; i < count; i++) {
this._pool.push(this._createFn());
}
}
/**
* Clear the pool entirely.
*/
clear() {
this._pool.length = 0;
this._activeCount = 0;
}
/** Number of objects currently in the pool (idle). */
get size() {
return this._pool.length;
}
/** Number of objects currently in use. */
get activeCount() {
return this._activeCount;
}
}
module.exports = ObjectPool;