初始化仓库:友乐/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,650 @@
/**
* ============================================================================
* SpriteCopyUtils.js - 精灵复制底层工具
* ============================================================================
*
* 封装 gameabc 框架的精灵复制核心功能,提供更易用的 API。
* 这是一个底层工具类,DynamicSpriteList 基于此构建。
*
* 核心概念:
* - 父精灵 (Parent Sprite): 容器精灵,复制出的子精灵将附着其上
* - 模板精灵 (Template Sprite): 提供视觉样式的精灵模板
* - 子精灵 (Child Sprite): 运行时动态复制创建的精灵
* - Tag: 子精灵的唯一标识符,用于删除和点击识别
*
* ============================================================================
* @guide 开发指南 - SpriteCopyUtils 使用规范
* ============================================================================
*
* 【创建动态精灵(模板复制)】
* // 在编辑器中预先放置一个模板精灵(通常隐藏),用作样式来源
* // 运行时从该模板复制,附着到容器精灵上
*
* var parentId = sprites.LIST_CONTAINER; // 容器精灵ID(编辑器放置)
* var templateId = sprites.ROW_TEMPLATE; // 模板精灵ID(编辑器放置,通常隐藏)
* var tag = rowIndex; // 行索引作为tag(在父精灵内唯一)
*
* var spriteId = SpriteCopyUtils.create(parentId, templateId, 0, rowIndex * rowHeight, tag);
* // spriteId 是字符串格式:"parentIdaddtag",如 "2836add0"
*
* // 创建后可通过 SpriteManager 设置属性(spriteId 是字符串,SpriteManager 自动处理)
* SpriteManager.setFrame(spriteId, card.code - 1);
* SpriteManager.setText(spriteId, String(score));
* SpriteManager.show(spriteId);
*
* 【删除动态精灵】
* SpriteCopyUtils.remove(parentId, tag); // 删除单个
* SpriteCopyUtils.removeRange(parentId, 0, 10); // 删除tag 0-9
*
* 【hit-test 点击检测(在鼠标事件中使用)】
* // 在 SpriteEventController 的 mouseUp 回调中:
* var clickedTag = SpriteCopyUtils.hitTest(parentId, event.offsetX, event.offsetY);
* if (clickedTag !== SpriteCopyUtils.HIT_NONE) {
* // clickedTag 是被点击子精灵的 tag
* var rowIndex = clickedTag;
* self._onRowClick(rowIndex);
* }
*
* 【典型使用场景:听牌提示列表(TingHintView 模式)】
* // 1. 编辑器中放置:容器精灵 + 行背景模板 + 牌图模板 + 分数模板
* // 2. 运行时:为每条听牌提示创建一行
* for (var i = 0; i < tingHints.length; i++) {
* var hint = tingHints[i];
* var yOffset = i * ROW_HEIGHT;
*
* // 复制行背景
* var bgId = SpriteCopyUtils.create(containerId, bgTemplate, 0, yOffset, i);
*
* // 复制牌图
* var cardId = SpriteCopyUtils.create(containerId, cardTemplate, CARD_X, yOffset, i + 1000);
* SpriteManager.setFrame(cardId, hint.code - 1);
*
* // 复制分数标签
* var scoreId = SpriteCopyUtils.create(containerId, scoreTemplate, SCORE_X, yOffset, i + 2000);
* SpriteManager.setText(scoreId, String(hint.score));
* }
* // 3. 清理:游戏结束或弹窗关闭时
* SpriteCopyUtils.removeRange(containerId, 0, tingHints.length);
* SpriteCopyUtils.removeRange(containerId, 1000, 1000 + tingHints.length);
* SpriteCopyUtils.removeRange(containerId, 2000, 2000 + tingHints.length);
*
* 【注意事项】
* - tag 在同一个 parentId 下必须唯一;不同列使用不同的 tag 偏移(如+1000, +2000)
* - 复制精灵的 ID 格式 "parentIdaddtag" 是字符串,SpriteManager 已支持此格式
* - 如果使用 DynamicSpriteList,则无需手动调用 SpriteCopyUtils,列表内部已封装
*
* @author JinXian Team
* @version 1.0.0
* @since 2025-12-13
*/
'use strict';
/**
* 精灵复制工具类
* @namespace
*/
var SpriteCopyUtils = {};
// ============================================================================
// 精灵创建与删除
// ============================================================================
/**
* 从模板复制创建精灵
*
* @param {number} parentId - 父精灵ID(容器),子精灵将附着在此精灵上
* @param {number} templateId - 模板精灵ID,复制其视觉属性
* @param {number} x - 相对于父精灵的X坐标
* @param {number} y - 相对于父精灵的Y坐标
* @param {number} tag - 子精灵标识符(在父精灵内唯一)
* @returns {string} 复合精灵ID,格式为 "parentId + 'add' + tag"
*
* @example
* // 在父精灵100上,基于模板200,在位置(50,30)创建子精灵,tag为1
* var spriteId = SpriteCopyUtils.create(100, 200, 50, 30, 1);
*
* // 设置子精灵的文字内容
* set_self(spriteId, 7, '你好世界', 0, 0);
*
* // 设置子精灵的帧
* set_self(spriteId, 43, 2, 0, 0);
*/
SpriteCopyUtils.create = function(parentId, templateId, x, y, tag) {
return ifast_addtospritefromspritecopy(parentId, templateId, x, y, tag);
};
/**
* 删除复制创建的子精灵
*
* @param {number} parentId - 父精灵ID
* @param {number} tag - 要删除的子精灵标识符
*
* @example
* // 删除父精灵100上tag为1的子精灵
* SpriteCopyUtils.remove(100, 1);
*/
SpriteCopyUtils.remove = function(parentId, tag) {
ifast_dllpritefromspritecopy(parentId, tag);
};
/**
* 批量删除复制创建的子精灵
*
* @param {number} parentId - 父精灵ID
* @param {number} startTag - 起始tag(包含)
* @param {number} endTag - 结束tag(不包含)
*
* @example
* // 删除父精灵100上tag从1到10的所有子精灵
* SpriteCopyUtils.removeRange(100, 1, 11);
*/
SpriteCopyUtils.removeRange = function(parentId, startTag, endTag) {
for (var tag = startTag; tag < endTag; tag++) {
ifast_dllpritefromspritecopy(parentId, tag);
}
};
/**
* 批量删除指定tag数组的子精灵
*
* @param {number} parentId - 父精灵ID
* @param {Array<number>} tags - tag数组
*
* @example
* // 删除父精灵100上tag为1, 3, 5, 7的子精灵
* SpriteCopyUtils.removeByTags(100, [1, 3, 5, 7]);
*/
SpriteCopyUtils.removeByTags = function(parentId, tags) {
for (var i = 0; i < tags.length; i++) {
ifast_dllpritefromspritecopy(parentId, tags[i]);
}
};
// ============================================================================
// 点击检测
// ============================================================================
/**
* 检测点击了父精灵上的哪个子精灵
*
* @param {number} parentId - 父精灵ID
* @param {number} x - 点击位置X坐标(屏幕坐标)
* @param {number} y - 点击位置Y坐标(屏幕坐标)
* @returns {number} 被点击的子精灵tag,未命中返回 -99999999
*
* @example
* // 在mouseup事件中检测点击
* var clickedTag = SpriteCopyUtils.hitTest(100, upx, upy);
* if (clickedTag !== -99999999) {
* console.log('点击了tag为', clickedTag, '的子精灵');
* }
*/
SpriteCopyUtils.hitTest = function(parentId, x, y) {
return ifast_check_add(parentId, x, y);
};
/**
* 检测点击是否命中任何子精灵
*
* @param {number} parentId - 父精灵ID
* @param {number} x - 点击位置X坐标
* @param {number} y - 点击位置Y坐标
* @returns {boolean} 是否命中
*
* @example
* if (SpriteCopyUtils.isHit(100, upx, upy)) {
* console.log('点击到了子精灵');
* }
*/
SpriteCopyUtils.isHit = function(parentId, x, y) {
return ifast_check_add(parentId, x, y) !== -99999999;
};
/**
* 检测点击的tag是否在指定范围内
*
* @param {number} parentId - 父精灵ID
* @param {number} x - 点击位置X坐标
* @param {number} y - 点击位置Y坐标
* @param {number} startTag - 起始tag(包含)
* @param {number} endTag - 结束tag(不包含)
* @returns {number} 如果在范围内返回tag,否则返回 -1
*
* @example
* // 检测是否点击了按钮区域(tag 1000-1999)
* var btnTag = SpriteCopyUtils.hitTestInRange(100, upx, upy, 1000, 2000);
* if (btnTag !== -1) {
* var buttonIndex = btnTag - 1000;
* console.log('点击了第', buttonIndex, '个按钮');
* }
*/
SpriteCopyUtils.hitTestInRange = function(parentId, x, y, startTag, endTag) {
var tag = ifast_check_add(parentId, x, y);
if (tag >= startTag && tag < endTag) {
return tag;
}
return -1;
};
// ============================================================================
// 精灵ID工具
// ============================================================================
/**
* 生成复合精灵ID
*
* @param {number} parentId - 父精灵ID
* @param {number} tag - 子精灵tag
* @returns {string} 复合精灵ID
*
* @example
* var spriteId = SpriteCopyUtils.getSpriteId(100, 5);
* // 返回 "100add5"
* set_self(spriteId, 7, '文字内容', 0, 0);
*/
SpriteCopyUtils.getSpriteId = function(parentId, tag) {
return parentId + 'add' + tag;
};
/**
* 从复合精灵ID解析父精灵ID和tag
*
* @param {string} spriteId - 复合精灵ID
* @returns {Object|null} 包含parentId和tag的对象,解析失败返回null
*
* @example
* var info = SpriteCopyUtils.parseSpriteId('100add5');
* // 返回 { parentId: 100, tag: 5 }
*/
SpriteCopyUtils.parseSpriteId = function(spriteId) {
if (typeof spriteId !== 'string') {
return null;
}
var parts = spriteId.split('add');
if (parts.length !== 2) {
return null;
}
var parentId = parseInt(parts[0], 10);
var tag = parseInt(parts[1], 10);
if (isNaN(parentId) || isNaN(tag)) {
return null;
}
return {
parentId: parentId,
tag: tag
};
};
// ============================================================================
// 批量创建工具
// ============================================================================
/**
* 批量创建子精灵(网格布局)
*
* @param {Object} options - 配置选项
* @param {number} options.parentId - 父精灵ID
* @param {number} options.templateId - 模板精灵ID
* @param {number} options.count - 创建数量
* @param {number} options.startTag - 起始tag
* @param {number} options.startX - 起始X坐标
* @param {number} options.startY - 起始Y坐标
* @param {number} options.spacingX - X方向间距
* @param {number} options.spacingY - Y方向间距
* @param {number} [options.columns=0] - 每行列数,0表示不换行
* @returns {Array<string>} 创建的精灵ID数组
*
* @example
* // 创建3x3的网格(9个精灵)
* var sprites = SpriteCopyUtils.createGrid({
* parentId: 100,
* templateId: 200,
* count: 9,
* startTag: 1,
* startX: 50,
* startY: 50,
* spacingX: 80,
* spacingY: 80,
* columns: 3
* });
*/
SpriteCopyUtils.createGrid = function(options) {
var result = [];
var columns = options.columns || 0;
for (var i = 0; i < options.count; i++) {
var col = columns > 0 ? (i % columns) : i;
var row = columns > 0 ? Math.floor(i / columns) : 0;
var x = options.startX + col * options.spacingX;
var y = options.startY + row * options.spacingY;
var tag = options.startTag + i;
var spriteId = ifast_addtospritefromspritecopy(
options.parentId,
options.templateId,
x,
y,
tag
);
result.push(spriteId);
}
return result;
};
/**
* 批量创建子精灵(垂直列表布局)
*
* @param {Object} options - 配置选项
* @param {number} options.parentId - 父精灵ID
* @param {number} options.templateId - 模板精灵ID
* @param {number} options.count - 创建数量
* @param {number} options.startTag - 起始tag
* @param {number} options.x - X坐标
* @param {number} options.startY - 起始Y坐标
* @param {number} options.rowHeight - 行高
* @returns {Array<string>} 创建的精灵ID数组
*
* @example
* // 创建5行的垂直列表
* var sprites = SpriteCopyUtils.createVerticalList({
* parentId: 100,
* templateId: 200,
* count: 5,
* startTag: 1,
* x: 50,
* startY: 100,
* rowHeight: 60
* });
*/
SpriteCopyUtils.createVerticalList = function(options) {
return SpriteCopyUtils.createGrid({
parentId: options.parentId,
templateId: options.templateId,
count: options.count,
startTag: options.startTag,
startX: options.x,
startY: options.startY,
spacingX: 0,
spacingY: options.rowHeight,
columns: 1
});
};
/**
* 批量创建子精灵(水平列表布局)
*
* @param {Object} options - 配置选项
* @param {number} options.parentId - 父精灵ID
* @param {number} options.templateId - 模板精灵ID
* @param {number} options.count - 创建数量
* @param {number} options.startTag - 起始tag
* @param {number} options.startX - 起始X坐标
* @param {number} options.y - Y坐标
* @param {number} options.colWidth - 列宽
* @returns {Array<string>} 创建的精灵ID数组
*
* @example
* // 创建一行5个精灵
* var sprites = SpriteCopyUtils.createHorizontalList({
* parentId: 100,
* templateId: 200,
* count: 5,
* startTag: 1,
* startX: 50,
* y: 100,
* colWidth: 80
* });
*/
SpriteCopyUtils.createHorizontalList = function(options) {
return SpriteCopyUtils.createGrid({
parentId: options.parentId,
templateId: options.templateId,
count: options.count,
startTag: options.startTag,
startX: options.startX,
startY: options.y,
spacingX: options.colWidth,
spacingY: 0,
columns: 0
});
};
// ============================================================================
// 属性设置工具
// ============================================================================
/**
* 批量设置子精灵的文字内容
*
* @param {number} parentId - 父精灵ID
* @param {number} startTag - 起始tag
* @param {Array<string>} texts - 文字内容数组
*
* @example
* // 设置tag 1-5的文字内容
* SpriteCopyUtils.setTexts(100, 1, ['第1行', '第2行', '第3行', '第4行', '第5行']);
*/
SpriteCopyUtils.setTexts = function(parentId, startTag, texts) {
for (var i = 0; i < texts.length; i++) {
var spriteId = parentId + 'add' + (startTag + i);
set_self(spriteId, 7, texts[i], 0, 0);
}
};
/**
* 批量设置子精灵的帧
*
* @param {number} parentId - 父精灵ID
* @param {number} startTag - 起始tag
* @param {Array<number>} frames - 帧号数组
*
* @example
* // 设置tag 1-5的帧
* SpriteCopyUtils.setFrames(100, 1, [1, 2, 3, 4, 5]);
*/
SpriteCopyUtils.setFrames = function(parentId, startTag, frames) {
for (var i = 0; i < frames.length; i++) {
var spriteId = parentId + 'add' + (startTag + i);
set_self(spriteId, 43, frames[i], 0, 0);
}
};
/**
* 批量设置子精灵的可见性
*
* @param {number} parentId - 父精灵ID
* @param {number} startTag - 起始tag
* @param {number} count - 精灵数量
* @param {boolean} visible - 是否可见
*
* @example
* // 隐藏tag 1-10的所有精灵
* SpriteCopyUtils.setVisibility(100, 1, 10, false);
*/
SpriteCopyUtils.setVisibility = function(parentId, startTag, count, visible) {
for (var i = 0; i < count; i++) {
var spriteId = parentId + 'add' + (startTag + i);
set_self(spriteId, 37, visible ? 1 : 0, 0, 0);
}
};
// ============================================================================
// Tag 管理器
// ============================================================================
/**
* 创建 Tag 管理器
* 用于管理多种类型精灵的 tag 分配
*
* @param {Object} ranges - tag 范围配置
* @returns {Object} Tag 管理器实例
*
* @example
* // 创建tag管理器
* var tagManager = SpriteCopyUtils.createTagManager({
* background: { start: 1, size: 100 }, // 1-100
* title: { start: 101, size: 100 }, // 101-200
* button: { start: 201, size: 100 } // 201-300
* });
*
* // 获取下一个tag
* var bgTag = tagManager.next('background'); // 返回 1
* var bgTag2 = tagManager.next('background'); // 返回 2
*
* // 重置计数器
* tagManager.reset();
*
* // 获取某类型的tag范围
* var range = tagManager.getRange('button'); // { start: 201, end: 301 }
*/
SpriteCopyUtils.createTagManager = function(ranges) {
var counters = {};
var rangeConfig = {};
// 初始化
var types = Object.keys(ranges);
for (var i = 0; i < types.length; i++) {
var type = types[i];
var config = ranges[type];
rangeConfig[type] = {
start: config.start,
end: config.start + config.size
};
counters[type] = config.start;
}
return {
/**
* 获取下一个可用的tag
* @param {string} type - 类型名称
* @returns {number} tag值
*/
next: function(type) {
if (!counters.hasOwnProperty(type)) {
throw new Error('TagManager: unknown type ' + type);
}
return counters[type]++;
},
/**
* 获取当前计数器值(不递增)
* @param {string} type - 类型名称
* @returns {number} 当前计数器值
*/
current: function(type) {
return counters[type];
},
/**
* 重置所有计数器
*/
reset: function() {
for (var t = 0; t < types.length; t++) {
counters[types[t]] = rangeConfig[types[t]].start;
}
},
/**
* 重置指定类型的计数器
* @param {string} type - 类型名称
*/
resetType: function(type) {
if (rangeConfig.hasOwnProperty(type)) {
counters[type] = rangeConfig[type].start;
}
},
/**
* 获取某类型的tag范围
* @param {string} type - 类型名称
* @returns {Object} { start, end }
*/
getRange: function(type) {
return rangeConfig[type] ? {
start: rangeConfig[type].start,
end: rangeConfig[type].end
} : null;
},
/**
* 获取某类型已分配的tag数量
* @param {string} type - 类型名称
* @returns {number} 已分配数量
*/
getAllocatedCount: function(type) {
return counters[type] - rangeConfig[type].start;
},
/**
* 检测tag属于哪个类型
* @param {number} tag - tag值
* @returns {string|null} 类型名称,不属于任何类型返回null
*/
getTypeByTag: function(tag) {
for (var t = 0; t < types.length; t++) {
var type = types[t];
var range = rangeConfig[type];
if (tag >= range.start && tag < range.end) {
return type;
}
}
return null;
},
/**
* 计算tag在其类型中的索引
* @param {number} tag - tag值
* @returns {number} 索引,不属于任何类型返回-1
*/
getIndexByTag: function(tag) {
for (var t = 0; t < types.length; t++) {
var type = types[t];
var range = rangeConfig[type];
if (tag >= range.start && tag < range.end) {
return tag - range.start;
}
}
return -1;
}
};
};
// ============================================================================
// 精灵ID工具
// ============================================================================
/**
* 根据容器ID和tag生成复制精灵的实际ID
*
* 对于通过 SpriteCopyUtils.create() 复制创建的精灵,
* 其实际ID遵循格式:容器精灵ID + 'add' + tag
*
* @param {number} containerId - 容器精灵ID
* @param {number} tag - 子精灵标识符
* @returns {string} 精灵ID,格式为 "containerIdaddtag"
*
* @example
* // 容器ID为100,tag为20
* var spriteId = SpriteCopyUtils.getSpriteId(100, 20);
* // 返回: "100add20"
*
* // 可用于获取精灵尺寸、位置等
* var size = SpriteManager.getSize(spriteId);
* var pos = SpriteManager.getPosition(spriteId);
*/
SpriteCopyUtils.getSpriteId = function(containerId, tag) {
return containerId + 'add' + tag;
};
// ============================================================================
// 导出
// ============================================================================
if (typeof module !== 'undefined' && module.exports) {
module.exports = SpriteCopyUtils;
} else if (typeof window !== 'undefined') {
window.SpriteCopyUtils = SpriteCopyUtils;
}