初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:13:26 +08:00
co-authored by Claude Opus 5
commit 594820d393
655 changed files with 310861 additions and 0 deletions
@@ -0,0 +1,234 @@
/**
* 事件总线 (独立模块)
* 职责: 提供模块间的事件通信机制,解耦模块依赖
* 遵循原则: 发布-订阅模式,支持事件监听和触发
*
* ============================================================================
* @guide 开发指南 - EventBus 使用规范
* ============================================================================
*
* 【事件命名规范】
* 使用 "模块:动作" 两段式命名,所有名称定义在 EventBus.Events 常量中:
* 'game:started' 游戏开始
* 'game:finished' 游戏结束
* 'player:discarded' 玩家出牌
* 说明:框架只内置通用事件(game:/player:/room:/ui:/network: 等);具体玩法的
* 专属事件(如发牌/摸牌/吃碰杠)由子游戏自行追加到 EventBus.Events,框架不内置。
* ⚠️ 不要硬编码字符串,统一用 EventBus.Events.XXX 引用
*
* 【订阅事件(在组件 init 中注册)】
* // 在 BaseComponent.init 或 Object.create 的 init 方法中:
* var self = this;
* var unsubscribe = EventBus.on(EventBus.Events.GAME_STARTED, function(data) {
* self._onGameStarted(data);
* }, self);
* // 保存引用用于 destroy 时清理
* this.eventListeners['game:started'] = unsubscribe;
*
* 【发布事件】
* // 在控制器或业务逻辑中发布:
* EventBus.emit(EventBus.Events.GAME_STARTED, { playerCount: 4 });
* EventBus.emit('player:discarded', { seat: 0, card: cardData });
*
* 【一次性事件(触发后自动取消)】
* EventBus.once(EventBus.Events.GAME_FINISHED, function(data) {
* self._showSettleUI(data);
* });
*
* 【在 destroy 中清理(防内存泄漏)】
* // 如果用 BaseComponent 的 addEventListener,destroy 时自动清理
* // 如果手动保存了 unsubscribe 函数:
* if (this.eventListeners['game:started']) {
* this.eventListeners['game:started'](); // 调用取消函数
* }
* // 或者 BaseComponent.destroy() 会自动处理 this.eventListeners 中的所有项
*
* 【推荐:通过 BaseComponent.addEventListener 代替手动管理】
* // BaseComponent 子类中:
* this.addEventListener(EventBus.Events.PLAYER_DISCARDED, function(data) {
* self._onPlayerDiscarded(data);
* });
* // destroy() 时 BaseComponent 会自动调用所有 off
*/
var EventBus = {
/**
* 私有数据存储
* @private
*/
_listeners: {}, // 事件监听器列表 {eventName: [callbacks]}
/**
* 订阅事件
* @param {String} eventName - 事件名称
* @param {Function} callback - 回调函数
* @param {Object} context - 回调函数的上下文(this)
* @return {Function} 取消订阅函数
*/
on: function(eventName, callback, context) {
if (!eventName || typeof callback !== 'function') {
console.error('❌ EventBus.on: 无效的参数');
return null;
}
if (!this._listeners[eventName]) {
this._listeners[eventName] = [];
}
var listener = {
callback: callback,
context: context || null
};
this._listeners[eventName].push(listener);
console.log('✅ EventBus: 订阅事件', eventName);
// 返回取消订阅函数
var self = this;
return function() {
self.off(eventName, callback);
};
},
/**
* 订阅一次性事件(触发后自动取消订阅)
* @param {String} eventName - 事件名称
* @param {Function} callback - 回调函数
* @param {Object} context - 回调函数的上下文
* @return {Function} 取消订阅函数
*/
once: function(eventName, callback, context) {
var self = this;
var onceCallback = function() {
callback.apply(context || null, arguments);
self.off(eventName, onceCallback);
};
return this.on(eventName, onceCallback, context);
},
/**
* 取消订阅事件
* @param {String} eventName - 事件名称
* @param {Function} callback - 要取消的回调函数(可选,不传则取消所有)
* @return {Boolean} 是否取消成功
*/
off: function(eventName, callback) {
if (!this._listeners[eventName]) {
return false;
}
// 如果没有指定callback,取消该事件的所有监听器
if (!callback) {
delete this._listeners[eventName];
console.log('✅ EventBus: 取消所有', eventName, '监听器');
return true;
}
// 取消指定的callback
var listeners = this._listeners[eventName];
for (var i = listeners.length - 1; i >= 0; i--) {
if (listeners[i].callback === callback) {
listeners.splice(i, 1);
console.log('✅ EventBus: 取消', eventName, '监听器');
return true;
}
}
return false;
},
/**
* 触发事件
* @param {String} eventName - 事件名称
* @param {*} data - 事件数据(可选)
* @return {Number} 触发的监听器数量
*/
emit: function(eventName, data) {
if (!this._listeners[eventName]) {
return 0;
}
var listeners = this._listeners[eventName].slice(); // 复制数组,避免迭代中修改
var count = 0;
console.log('📡 EventBus: 触发事件', eventName, data);
for (var i = 0; i < listeners.length; i++) {
try {
var listener = listeners[i];
listener.callback.call(listener.context, data);
count++;
} catch (error) {
console.error('❌ EventBus: 事件回调异常', eventName, error);
}
}
return count;
},
/**
* 检查事件是否有监听器
* @param {String} eventName - 事件名称
* @return {Boolean} 是否有监听器
*/
hasListener: function(eventName) {
return !!(this._listeners[eventName] && this._listeners[eventName].length > 0);
},
/**
* 获取事件的监听器数量
* @param {String} eventName - 事件名称
* @return {Number} 监听器数量
*/
getListenerCount: function(eventName) {
return this._listeners[eventName] ? this._listeners[eventName].length : 0;
},
/**
* 获取所有事件名称
* @return {Array} 事件名称数组
*/
getEventNames: function() {
var names = [];
for (var name in this._listeners) {
if (this._listeners.hasOwnProperty(name)) {
names.push(name);
}
}
return names;
},
/**
* 清空所有事件监听器
*/
clear: function() {
this._listeners = {};
console.log('✅ EventBus: 已清空所有监听器');
},
/**
* 清空指定事件的所有监听器
* @param {String} eventName - 事件名称
*/
clearEvent: function(eventName) {
if (this._listeners[eventName]) {
delete this._listeners[eventName];
console.log('✅ EventBus: 已清空', eventName, '的所有监听器');
}
}
};
/**
* 事件名称常量容器(框架不预置任何具体事件名)
*
* 框架保持「游戏中立」:只提供发布-订阅机制与一个空的 EventBus.Events 容器,
* 不预定义任何事件名称常量。各子游戏在自身事件常量文件(如
* game/constants/GameEvents.js)中向 EventBus.Events 注册自己用到的【全部】事件,
* 业务侧统一通过 EventBus.Events.XXX 引用。
*
* 命名规范:两段式 "模块:动作"。例如通用语义 'game:started' / 'player:discarded' /
* 'room:updated' / 'ui:sceneChanged' / 'network:connected';玩法专属自定前缀(如
* 'mahjong:tilesDealt')。统一用常量引用,不在业务中硬编码字符串。
*/
EventBus.Events = {};