/** * ============================================================================ * 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} 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} 创建的精灵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} 创建的精灵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} 创建的精灵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} 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} 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; }