初始化仓库:友乐/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,830 @@
/**
* ============================================================================
* DynamicSpriteList.js - 动态精灵列表工具
* ============================================================================
*
* 基于 gameabc 框架的精灵复制功能,提供动态列表创建、管理和交互的完整解决方案。
*
* 核心功能:
* - 动态创建/删除精灵列表
* - 自动滑动和边界回弹
* - 点击检测和事件分发
* - 裁剪区域管理
*
* 适用场景:
* - 小局结算列表
* - 大局结算列表
* - 听牌提示列表
* - 战绩列表
* - 排行榜
*
* 使用示例:
* ```javascript
* // 1. 创建列表实例
* var myList = new DynamicSpriteList({
* containerId: 100, // 父精灵ID(容器)
* clipArea: { x: 50, y: 100, width: 600, height: 400 },
* rowHeight: 80,
* templates: {
* background: { spriteId: 101 },
* title: { spriteId: 102, offset: { x: 20, y: 10 } },
* button: { spriteId: 103, offset: { x: 500, y: 20 } }
* }
* });
*
* // 2. 设置数据并渲染
* myList.setData([
* { title: '第1行', buttonText: '查看' },
* { title: '第2行', buttonText: '查看' }
* ]);
*
* // 3. 监听点击事件
* myList.onClick = function(type, index, data) {
* console.log('点击了', type, '第', index, '行');
* };
*
* // 4. 在事件处理函数中调用
* // mousedown: myList.handleMouseDown(spid, x, y);
* // mousemove: myList.handleMouseMove(spid, offsetY);
* // mouseup: myList.handleMouseUp(spid, x, y);
* // drawbegin: myList.handleDrawBegin(spid);
*
* // 5. 销毁
* myList.destroy();
* ```
*
* ============================================================================
* @guide 开发指南 - DynamicSpriteList 完整使用规范
* ============================================================================
*
* 【准备工作:在编辑器中预置精灵】
* 1. 在编辑器中放置一个容器精灵(作为 containerId)
* 2. 在容器精灵旁边放置各列模板精灵(通常隐藏):
* - 行背景模板(每行底色/分隔线)
* - 数据列模板(牌图、分数、数量等,每种一个)
* 3. 记录这些模板精灵的 ID,用于配置 templates
*
* 【初始化列表(听牌提示列表示例)】
* var tingList = new DynamicSpriteList({
* containerId: sprites.LIST_CONTAINER, // 容器精灵
* clipArea: {
* x: 0, y: 0, // 相对容器左上角
* width: 640, height: 480 // 可见区域尺寸
* },
* rowHeight: 80, // 每行高度(像素)
* templates: {
* // 每列都需要一个 spriteId(模板)
* rowBg: { spriteId: sprites.ROW_TEMPLATE },
* card: { spriteId: sprites.CARD_TEMPLATE, offset: { x: 20, y: 10 } },
* score: { spriteId: sprites.SCORE_TEMPLATE, offset: { x: 120, y: 10 } },
* jingBadge: { spriteId: sprites.JING_BADGE, offset: { x: 90, y: 5 } },
* remainBadge: { spriteId: sprites.REMAIN_BADGE, offset: { x: 200, y: 5 } },
* rowSeparator: { spriteId: sprites.ROW_SEPARATOR }
* },
* scrollSensitivity: 5, // 移动5px以上才触发滚动
* bounceSpeed: 1 // 回弹速度
* });
*
* 【设置数据(触发渲染)】
* // 数据数组中每个元素对应一行
* // setData 会清除旧行、重新创建新行
* tingList.setData(tingHints); // tingHints = [{ code, score, remain, isJing }, ...]
*
* 【监听点击(在 setData 之前或之后设置均可)】
* tingList.onClick = function(type, rowIndex, rowData) {
* // type: 模板键名,如 'card'、'score'
* // rowIndex: 第几行(0-based)
* // rowData: 该行对应的数据对象
* if (type === 'card') {
* self._onCardClick(rowData);
* }
* };
*
* 【接入 SpriteEventController 事件(必须接入,否则列表不响应交互)】
* var self = this;
* // 在 init 中注册容器精灵的鼠标事件
* SpriteEventController.registerMouseDown(sprites.LIST_CONTAINER, function(event) {
* tingList.handleMouseDown(event.spid, event.offsetX, event.offsetY);
* });
* SpriteEventController.registerMouseMove(sprites.LIST_CONTAINER, function(event) {
* tingList.handleMouseMove(event.spid, event.offsetY);
* });
* SpriteEventController.registerMouseUp(sprites.LIST_CONTAINER, function(event) {
* tingList.handleMouseUp(event.spid, event.offsetX, event.offsetY);
* });
* SpriteEventController.registerDrawBegin(sprites.LIST_CONTAINER, function(event) {
* tingList.handleDrawBegin(event.spid);
* });
*
* 【销毁(组件关闭/destroy时必须调用)】
* tingList.destroy(); // 清除所有复制精灵,解除事件绑定
*
* 【注意事项】
* - destroy() 必须在组件 onDestroy/hide 时调用,否则复制精灵会残留
* - 每次 setData 会完全重建行,不要频繁调用(建议数据变化时才调用)
* - templates 中的 spriteId 是编辑器中预置的精灵ID,不是复制出来的ID
* - 如果列表不需要滚动,设置 enableScroll: false 可提升性能
*
* @author JinXian Team
* @version 1.0.0
* @since 2025-12-13
*/
'use strict';
// ============================================================================
// DynamicSpriteList 类定义
// ============================================================================
/**
* 动态精灵列表构造函数
*
* @constructor
* @param {Object} config - 列表配置对象
* @param {number} config.containerId - 父精灵ID(容器精灵,所有复制精灵将附着在此精灵上)
* @param {Object} config.clipArea - 裁剪区域配置(可见区域)
* @param {number} config.clipArea.x - 裁剪区域左上角X坐标
* @param {number} config.clipArea.y - 裁剪区域左上角Y坐标
* @param {number} config.clipArea.width - 裁剪区域宽度
* @param {number} config.clipArea.height - 裁剪区域高度
* @param {number} config.rowHeight - 每行高度(像素)
* @param {Object} config.templates - 模板精灵配置
* @param {number} [config.scrollSensitivity=2] - 滑动灵敏度(移动多少像素触发滑动)
* @param {number} [config.bounceSpeed=1] - 回弹动画速度
* @param {boolean} [config.enableScroll=true] - 是否启用滚动
* @param {boolean} [config.enableClick=true] - 是否启用点击
*
* @example
* // 模板配置示例
* templates: {
* // 行背景 - 必须有
* background: {
* spriteId: 101, // 模板精灵ID
* offset: { x: 0, y: 0 } // 相对行起点的偏移(可选,默认0,0)
* },
* // 文字模板
* title: {
* spriteId: 102,
* offset: { x: 20, y: 15 },
* textProperty: 'title' // 对应数据中的属性名
* },
* // 按钮模板
* button: {
* spriteId: 103,
* offset: { x: 500, y: 20 },
* clickable: true // 标记为可点击
* }
* }
*/
function DynamicSpriteList(config) {
// ========================================================================
// 参数验证
// ========================================================================
if (!config) {
throw new Error('DynamicSpriteList: config is required');
}
if (typeof config.containerId !== 'number') {
throw new Error('DynamicSpriteList: containerId must be a number');
}
if (!config.clipArea) {
throw new Error('DynamicSpriteList: clipArea is required');
}
if (typeof config.rowHeight !== 'number' || config.rowHeight <= 0) {
throw new Error('DynamicSpriteList: rowHeight must be a positive number');
}
var hasTemplates = config.templates && typeof config.templates === 'object';
var hasRenderRow = typeof config.renderRow === 'function';
if (!hasTemplates && !hasRenderRow) {
throw new Error('DynamicSpriteList: templates or renderRow is required');
}
// ========================================================================
// 实例属性初始化
// ========================================================================
/**
* 容器精灵ID
* @type {number}
*/
this.containerId = config.containerId;
/**
* 裁剪区域配置
* @type {Object}
*/
this.clipArea = {
x: config.clipArea.x || 0,
y: config.clipArea.y || 0,
width: config.clipArea.width || 0,
height: config.clipArea.height || 0
};
/**
* 每行高度
* @type {number}
*/
this.rowHeight = config.rowHeight;
/**
* 模板精灵配置
* @type {Object}
*/
this.templates = config.templates || null;
this.renderRow = config.renderRow || null;
/**
* 滑动灵敏度
* @type {number}
*/
this.scrollSensitivity = config.scrollSensitivity || 2;
/**
* 回弹动画速度
* @type {number}
*/
this.bounceSpeed = config.bounceSpeed || 1;
/**
* 是否启用滚动
* @type {boolean}
*/
this.enableScroll = config.enableScroll !== false;
/**
* 是否启用点击
* @type {boolean}
*/
this.enableClick = config.enableClick !== false;
// ========================================================================
// 运行时状态
// ========================================================================
/**
* 当前数据
* @type {Array}
* @private
*/
this._data = [];
/**
* 是否正在滑动
* @type {boolean}
* @private
*/
this._isSliding = false;
/**
* 是否已渲染
* @type {boolean}
* @private
*/
this._isRendered = false;
/**
* 已创建的精灵记录 (用于删除)
* @type {Array}
* @private
*/
this._createdSprites = [];
this._tagIndex = {};
this._nextTag = 1;
// ========================================================================
// 事件回调
// ========================================================================
/**
* 点击事件回调
* @type {Function|null}
* @param {string} templateName - 点击的模板类型名称
* @param {number} rowIndex - 行索引(从0开始)
* @param {Object} rowData - 该行的数据对象
*
* @example
* list.onClick = function(templateName, rowIndex, rowData) {
* if (templateName === 'button') {
* console.log('点击了第', rowIndex, '行的按钮');
* console.log('数据:', rowData);
* }
* };
*/
this.onClick = null;
/**
* 滚动事件回调
* @type {Function|null}
* @param {number} scrollY - 当前滚动位置
* @param {number} maxScrollY - 最大滚动位置
*
* @example
* list.onScroll = function(scrollY, maxScrollY) {
* var progress = scrollY / maxScrollY;
* console.log('滚动进度:', progress);
* };
*/
this.onScroll = null;
}
// ============================================================================
// 公共方法
// ============================================================================
/**
* 设置数据并渲染列表
*
* @param {Array} data - 数据数组,每个元素对应一行
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*
* @example
* list.setData([
* { title: '战绩1', score: '+100', time: '12:30' },
* { title: '战绩2', score: '-50', time: '12:25' },
* { title: '战绩3', score: '+200', time: '12:20' }
* ]);
*/
DynamicSpriteList.prototype.setData = function(data) {
if (!Array.isArray(data)) {
console.error('DynamicSpriteList.setData: data must be an array');
return this;
}
// 先清除旧内容
this.clear();
// 保存数据
this._data = data;
// 渲染列表
this._render();
return this;
};
/**
* 获取当前数据
*
* @returns {Array} 当前数据数组的副本
*/
DynamicSpriteList.prototype.getData = function() {
return this._data.slice();
};
/**
* 获取数据行数
*
* @returns {number} 数据行数
*/
DynamicSpriteList.prototype.getRowCount = function() {
return this._data.length;
};
/**
* 显示列表
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.show = function() {
set_self(this.containerId, 37, 1, 0, 0);
return this;
};
/**
* 隐藏列表
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.hide = function() {
set_self(this.containerId, 37, 0, 0, 0);
return this;
};
/**
* 清除所有动态创建的精灵
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.clear = function() {
for (var i = 0; i < this._createdSprites.length; i++) {
ifast_dllpritefromspritecopy(this.containerId, this._createdSprites[i].tag);
}
this._createdSprites = [];
this._tagIndex = {};
this._nextTag = 1;
this._isRendered = false;
this._isSliding = false;
return this;
};
/**
* 销毁列表实例
* 释放所有资源,清除所有精灵
*/
DynamicSpriteList.prototype.destroy = function() {
this.clear();
this.hide();
this._data = [];
this.onClick = null;
this.onScroll = null;
};
/**
* 滚动到指定位置
*
* @param {number} y - 目标Y坐标
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollTo = function(y, animated) {
var currentY = get_self(this.containerId, 19, 0, 0, 0);
var targetY = this.clipArea.y - y;
// 边界限制
var contentHeight = this._data.length * this.rowHeight;
var minY = this.clipArea.y + this.clipArea.height - contentHeight;
if (targetY < minY) {
targetY = minY;
}
if (targetY > this.clipArea.y) {
targetY = this.clipArea.y;
}
if (animated !== false) {
play_ani(1, this.containerId, 19, currentY, targetY, 0,
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
} else {
set_self(this.containerId, 19, targetY, 0, 0, 0);
}
return this;
};
/**
* 滚动到顶部
*
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToTop = function(animated) {
return this.scrollTo(0, animated);
};
/**
* 滚动到底部
*
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToBottom = function(animated) {
var contentHeight = this._data.length * this.rowHeight;
var maxScroll = contentHeight - this.clipArea.height;
if (maxScroll < 0) {
maxScroll = 0;
}
return this.scrollTo(maxScroll, animated);
};
/**
* 滚动到指定行
*
* @param {number} rowIndex - 行索引(从0开始)
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToRow = function(rowIndex, animated) {
var y = rowIndex * this.rowHeight;
return this.scrollTo(y, animated);
};
// ============================================================================
// 事件处理方法 - 需要在对应的事件回调中调用
// ============================================================================
/**
* 处理鼠标/触摸按下事件
* 需要在 utlmousedown 回调中调用
*
* @param {number} spid - 按下的精灵ID
* @param {number} x - 按下位置X坐标
* @param {number} y - 按下位置Y坐标
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* utlmousedown: function(gameid, spid, downx, downy) {
* if (myList.handleMouseDown(spid, downx, downy)) {
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseDown = function(spid, x, y) {
if (spid === this.containerId) {
this._isSliding = false;
return true;
}
return false;
};
/**
* 处理鼠标/触摸移动事件
* 需要在 utlmousemove 回调中调用
*
* @param {number} spid - 移动的精灵ID
* @param {number} offsetY - Y方向移动偏移量
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* utlmousemove: function(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey) {
* if (myList.handleMouseMove(spid, offmovey)) {
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetY) {
if (!this.enableScroll) {
return false;
}
if (spid === this.containerId) {
if (Math.abs(offsetY) > this.scrollSensitivity) {
// 增量移动容器
set_self(this.containerId, 19, offsetY, 1, 0, 0);
this._isSliding = true;
// 触发滚动回调
if (typeof this.onScroll === 'function') {
var currentY = get_self(this.containerId, 19, 0, 0, 0);
var scrollY = this.clipArea.y - currentY;
var contentHeight = this._data.length * this.rowHeight;
var maxScrollY = contentHeight - this.clipArea.height;
this.onScroll(scrollY, maxScrollY > 0 ? maxScrollY : 0);
}
}
return true;
}
return false;
};
/**
* 处理鼠标/触摸松开事件
* 需要在 mouseup 回调中调用
*
* @param {number} spidDown - 按下时的精灵ID
* @param {number} spidUp - 松开时的精灵ID
* @param {number} upX - 松开位置X坐标
* @param {number} upY - 松开位置Y坐标
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* mouseup: function(gameid, spid_down, downx, downy, spid_up, upx, upy, timelong) {
* if (myList.handleMouseUp(spid_down, spid_up, upx, upy)) {
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseUp = function(spidDown, spidUp, upX, upY) {
if (spidDown !== this.containerId) {
return false;
}
// 处理边界回弹
this._handleBounce();
// 处理点击(非滑动状态)
if (!this._isSliding && this.enableClick && spidDown === spidUp) {
this._handleClick(upX, upY);
}
return true;
};
/**
* 处理绘制开始事件
* 需要在 utlgamemydrawbegin 回调中调用,用于设置裁剪区域
*
* @param {number} spid - 正在绘制的精灵ID
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* utlgamemydrawbegin: function(gameid, spid, times, timelong) {
* if (myList.handleDrawBegin(spid)) {
* return; // 已设置裁剪区域
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleDrawBegin = function(spid) {
if (spid === this.containerId) {
set_clip(0, 0,
this.clipArea.x,
this.clipArea.y,
this.clipArea.width,
this.clipArea.height
);
return true;
}
return false;
};
// ============================================================================
// 私有方法
// ============================================================================
/**
* 内部创建精灵并登记到 _tagIndex / _createdSprites
* @param {string} name - 精灵名称(模板键名或自定义名)
* @param {number} templateSpriteId - 模板精灵 ID
* @param {number} x - X 坐标
* @param {number} y - Y 坐标(已叠加 rowBaseY)
* @param {number} rowIndex - 所在行索引
* @param {boolean} clickable - 是否可点击
* @returns {*} 创建出的精灵 ID
* @private
*/
DynamicSpriteList.prototype._addSpriteInternal = function (name, templateSpriteId, x, y, rowIndex, clickable) {
var tag = this._nextTag++;
var spriteId = ifast_addtospritefromspritecopy(this.containerId, templateSpriteId, x, y, tag);
var rec = { tag: tag, spriteId: spriteId, rowIndex: rowIndex, name: name, clickable: (clickable !== false) };
this._createdSprites.push(rec);
this._tagIndex[tag] = rec;
return spriteId;
};
/**
* 渲染列表
* @private
*/
DynamicSpriteList.prototype._render = function() {
set_self(this.containerId, 18, this.clipArea.x, 0, 0);
set_self(this.containerId, 19, this.clipArea.y, 0, 0);
var contentHeight = this._data.length * this.rowHeight;
set_self(this.containerId, 21, contentHeight, 0, 0);
if (this.templates) {
var tnames = Object.keys(this.templates);
if (tnames.length > 0) {
var w = get_self(this.templates[tnames[0]].spriteId, 20, 0, 0, 0);
set_self(this.containerId, 20, w, 0, 0);
}
} else {
// renderRow 模式:容器宽度取裁剪区宽,保证整行可视宽都在容器 hit 区(点击/拖动检测)内。
// 否则容器保持编辑器预置窄宽度,超出部分点击/拖动报的 spid≠containerId 而失效(对账 bak set_self(fSpid,20,bg宽))。
set_self(this.containerId, 20, this.clipArea.width, 0, 0);
}
for (var rowIndex = 0; rowIndex < this._data.length; rowIndex++) {
var rowData = this._data[rowIndex];
var rowY = rowIndex * this.rowHeight;
if (this.renderRow) {
this.renderRow(this._makeRowContext(rowIndex, rowData, rowY));
} else {
var names = Object.keys(this.templates);
for (var i = 0; i < names.length; i++) {
var name = names[i];
var t = this.templates[name];
var ox = (t.offset && t.offset.x) || 0;
var oy = (t.offset && t.offset.y) || 0;
var sid = this._addSpriteInternal(name, t.spriteId, ox, rowY + oy, rowIndex, t.clickable !== false);
if (t.textProperty && rowData[t.textProperty] !== undefined) {
set_self(sid, 7, rowData[t.textProperty], 0, 0);
}
if (t.frameProperty && rowData[t.frameProperty] !== undefined) {
set_self(sid, 43, rowData[t.frameProperty], 0, 0);
}
}
}
}
this._isRendered = true;
};
/**
* 创建行上下文对象(传给 renderRow 回调)
* @param {number} rowIndex - 行索引
* @param {Object} rowData - 行数据
* @param {number} rowBaseY - 行基准 Y 坐标
* @returns {Object} 行上下文
* @private
*/
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBaseY) {
var self = this;
return {
rowIndex: rowIndex,
rowData: rowData,
rowBaseY: rowBaseY,
containerId: this.containerId,
addSprite: function (name, templateSpriteId, x, y) {
return self._addSpriteInternal(name, templateSpriteId, x, rowBaseY + y, rowIndex, true);
}
};
};
/**
* 处理边界回弹
* @private
*/
DynamicSpriteList.prototype._handleBounce = function() {
var currentY = get_self(this.containerId, 19, 0, 0, 0);
var contentHeight = get_self(this.containerId, 21, 0, 0, 0);
var clipY = this.clipArea.y;
var clipH = this.clipArea.height;
var targetY = currentY;
var needBounce = false;
if (contentHeight <= clipH) {
// 内容不足一屏,回弹到顶部
if (currentY !== clipY) {
targetY = clipY;
needBounce = true;
}
} else {
if (currentY > clipY) {
// 超出顶部
targetY = clipY;
needBounce = true;
} else {
var minY = clipY + clipH - contentHeight;
if (currentY < minY) {
// 超出底部
targetY = minY;
needBounce = true;
}
}
}
if (needBounce) {
play_ani(1, this.containerId, 19, currentY, targetY, 0,
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
}
};
/**
* 处理点击事件
* @param {number} x - 点击X坐标
* @param {number} y - 点击Y坐标
* @private
*/
DynamicSpriteList.prototype._handleClick = function(x, y) {
var clickedTag = ifast_check_add(this.containerId, x, y);
if (clickedTag === -99999999) { return; }
var rec = this._tagIndex[clickedTag];
if (!rec || rec.clickable === false) { return; }
if (typeof this.onClick === 'function' && rec.rowIndex < this._data.length) {
this.onClick(rec.name, rec.rowIndex, this._data[rec.rowIndex]);
}
};
// ============================================================================
// 静态工具方法
// ============================================================================
/**
* 创建简单列表的快捷方法
*
* @static
* @param {Object} options - 配置选项
* @param {number} options.containerId - 容器精灵ID
* @param {number} options.itemTemplateId - 列表项模板精灵ID
* @param {Object} options.clipArea - 裁剪区域
* @param {number} options.rowHeight - 行高
* @returns {DynamicSpriteList} 列表实例
*
* @example
* var simpleList = DynamicSpriteList.createSimple({
* containerId: 100,
* itemTemplateId: 101,
* clipArea: { x: 50, y: 100, width: 600, height: 400 },
* rowHeight: 60
* });
*/
DynamicSpriteList.createSimple = function(options) {
return new DynamicSpriteList({
containerId: options.containerId,
clipArea: options.clipArea,
rowHeight: options.rowHeight,
templates: {
item: {
spriteId: options.itemTemplateId,
offset: { x: 0, y: 0 }
}
}
});
};
// ============================================================================
// 导出
// ============================================================================
// 兼容多种模块系统
if (typeof module !== 'undefined' && module.exports) {
module.exports = DynamicSpriteList;
} else if (typeof window !== 'undefined') {
window.DynamicSpriteList = DynamicSpriteList;
}