初始化仓库:友乐/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,174 @@
// ============================================================================
// 精灵手势识别器 (gameabc-framework / system, ES5标准)
// 在 SpriteEventController 之上做高层手势识别——游戏中立,可被任意子游戏复用
// ============================================================================
/**
* SpriteGestureRecognizer —— 通用精灵手势识别器
*
* 职责:
* 把底层的「按下/移动/松开」原始事件,识别为高层手势并以语义回调通知:
* - press :按下(即时反馈,如元素站起)——双击的第二击不再触发 press
* - doubleTap :在 doubleTapInterval 内对【同一精灵】快速按下两次
* - swipe :沿指定方向移动超过阈值(不依赖松开,越阈即触发)
* - tap :一次「点击」(小位移按下→松开,且未被 swipe/doubleTap 消费)
*
* 中立原则:
* 本识别器只做手势分类,不含任何业务语义(选中/出牌/牌面等均由子游戏在回调里处理)。
* 阈值参数由调用方注入(doubleTapInterval / swipe.threshold / minMove),框架只给默认值。
*
* 依赖:SpriteEventController(须先加载)。
*
* 用法:
* var handle = SpriteGestureRecognizer.attach(spriteIds, {
* onPress: function (e) {}, // e: { spriteId, x, y, ... }
* onTap: function (e) {}, // e: 透传 mouseUp 事件(含 upSpriteId/downPos/upPos/isClick)
* onDoubleTap: function (e) {}, // e: 透传 mouseDown 事件
* onSwipe: function (e) {} // e: 透传 mouseMove 事件 + e.direction
* }, {
* doubleTapInterval: 300, // 双击间隔(ms)
* swipe: { direction: 'up', threshold: 50 }, // 滑动方向与阈值(像素,正值)
* minMove: 10 // 判定为点击的最大位移(像素)
* });
* // handle.resetDoubleTap() 清双击计时(防跨局误判)
* // handle.resetDrag() 取消进行中的拖动追踪
* // handle.detach() 注销该组精灵的手势识别
*
* 严格 ES5。
*/
var SpriteGestureRecognizer = {
/**
* 为一组精灵附加手势识别。
* @param {Array<number>} spriteIds - 精灵 ID 列表
* @param {Object} callbacks - { onPress, onTap, onDoubleTap, onSwipe }(均可选)
* @param {Object} [options] - { doubleTapInterval, swipe:{direction,threshold}, minMove }
* @returns {Object|null} handle: { detach, resetDoubleTap, resetDrag };参数非法返回 null
*/
attach: function (spriteIds, callbacks, options) {
if (typeof SpriteEventController === 'undefined') {
console.error('❌ SpriteGestureRecognizer: SpriteEventController 未加载');
return null;
}
if (!spriteIds || typeof spriteIds.length !== 'number') {
console.error('❌ SpriteGestureRecognizer: spriteIds 必须为数组');
return null;
}
callbacks = callbacks || {};
options = options || {};
var doubleTapInterval = (options.doubleTapInterval != null) ? options.doubleTapInterval : 300;
var minMove = (options.minMove != null) ? options.minMove : 10;
var swipe = options.swipe || null; // { direction, threshold }
// 本组精灵独立的手势状态(闭包隔离,支持多处 attach 互不干扰)
var state = {
dragActive: false,
lastTapTime: 0,
lastTapTarget: null
};
function _resetDrag() {
state.dragActive = false;
}
// ---- 越阈方向判定(仅在配置了 swipe 时启用)----
function _swipeCrossed(offset) {
if (!swipe || !swipe.direction) { return false; }
var th = (swipe.threshold != null) ? swipe.threshold : 50;
switch (swipe.direction) {
case 'up': return offset.y < -th;
case 'down': return offset.y > th;
case 'left': return offset.x < -th;
case 'right': return offset.x > th;
default: return false;
}
}
// ---- 原始事件处理(注册到 SpriteEventController)----
function _onDown(e) {
var now = Date.now();
var isDouble = state.lastTapTime > 0 &&
e.spriteId === state.lastTapTarget &&
(now - state.lastTapTime) < doubleTapInterval;
if (isDouble) {
// 消费双击:清计时,避免三连击;不再触发 press(与原手牌交互一致)
state.lastTapTime = 0;
state.lastTapTarget = null;
if (callbacks.onDoubleTap) { callbacks.onDoubleTap(e); }
return;
}
// 记录本次按下,开始拖动追踪
state.lastTapTime = now;
state.lastTapTarget = e.spriteId;
state.dragActive = true;
if (callbacks.onPress) { callbacks.onPress(e); }
}
function _onMove(e) {
if (!state.dragActive) { return; }
if (!e.offset) { return; }
if (_swipeCrossed(e.offset)) {
// 越阈即触发 swipe,并结束拖动追踪(后续 move/up 不再产生手势)
_resetDrag();
var se = e;
se.direction = swipe.direction;
if (callbacks.onSwipe) { callbacks.onSwipe(se); }
}
}
function _onUp(e) {
var wasDragging = state.dragActive;
_resetDrag();
if (!wasDragging) { return; } // 未在追踪(如双击第二击/已被 swipe 消费)→ 非 tap
if (!e.isClick) { return; } // 按下与松开不在同一精灵 → 非 tap
// 位移过大(非点击的拖动)→ 非 tap
if (e.upPos && e.downPos && Math.abs(e.upPos.y - e.downPos.y) > minMove) { return; }
if (callbacks.onTap) { callbacks.onTap(e); }
}
// 注册到底层事件控制器
var ids = [];
for (var i = 0; i < spriteIds.length; i++) {
var sid = spriteIds[i];
if (sid == null) { continue; }
SpriteEventController.registerMouseDown(sid, _onDown);
SpriteEventController.registerMouseMove(sid, _onMove);
SpriteEventController.registerMouseUp(sid, _onUp);
ids.push(sid);
}
return {
/** 注销该组精灵的全部手势事件 */
detach: function () {
for (var j = 0; j < ids.length; j++) {
SpriteEventController.unregister(ids[j], 'mouseDown');
SpriteEventController.unregister(ids[j], 'mouseMove');
SpriteEventController.unregister(ids[j], 'mouseUp');
}
_resetDrag();
},
/** 清空双击计时(游戏开始/重置时调用,防跨局误判) */
resetDoubleTap: function () {
state.lastTapTime = 0;
state.lastTapTarget = null;
},
/** 取消进行中的拖动追踪 */
resetDrag: function () {
_resetDrag();
}
};
}
};
// 全局暴露(浏览器/平台运行时)
if (typeof window !== 'undefined') {
window.SpriteGestureRecognizer = SpriteGestureRecognizer;
}
// Node 测试环境导出
if (typeof module !== 'undefined' && module.exports) {
module.exports = SpriteGestureRecognizer;
}