Files
erqiwang_youle/client/js/gameabc-framework/ui/SpriteCopyUtils.js
T

651 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================================
* 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;
}