'use strict'
// ******************************** 统计信息 ********************************
const Stats = new class {
// 是否在本地客户端上运行
isOnClient = !!window.process
// 获取调试状态
debug = !!window.process?.argv.includes('--debug-mode')
// 获取应用外壳
shell = window.process ? 'electron' : 'web'
// 获取设备类型
get deviceType() {
return /ipad|iphone|android/i.test(navigator.userAgent) ? 'mobile' : 'pc'
}
/**
* 判断是不是Mac平台
* @returns {boolean}
*/
isMacOS() {
if (navigator.userAgentData) {
return navigator.userAgentData.platform === 'macOS'
}
if (navigator.platform) {
return navigator.platform.indexOf('Mac') === 0
}
}
}
// ******************************** 对象静态属性 ********************************
/** 对象静态属性 - 空对象 */
Object.empty = {}
// ******************************** 数组静态属性 ********************************
/** 数组静态属性 - 空数组 */
Array.empty = []
/**
* 数组静态方法 - 比较数组值是否相等
* @param {Array} a 数组A
* @param {Array} b 数组B
* @returns {boolean} 数组值是否相等
*/
Array.isEqual = function (a, b) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
// ******************************** 数组方法 ********************************
// 数组方法 - 添加
Object.defineProperty(
Array.prototype, 'append', {
enumerable: false,
value: function (value) {
if (this.indexOf(value) === -1) {
this.push(value)
return true
}
return false
}
}
)
// 数组方法 - 移除
Object.defineProperty(
Array.prototype, 'remove', {
enumerable: false,
value: function (value) {
const index = this.indexOf(value)
if (index !== -1) {
this.splice(index, 1)
return true
}
return false
}
}
)
// 数组方法 - 替换
Object.defineProperty(
Array.prototype, 'replace', {
enumerable: false,
value: function (a, b) {
const index = this.indexOf(a)
if (index !== -1) {
this[index] = b
return true
}
return false
}
}
)
// 数组方法 - 设置
Object.defineProperty(
Array.prototype, 'set', {
enumerable: false,
value: function (array) {
const length = Math.min(this.length, array.length)
for (let i = 0; i < length; i++) {
this[i] = array[i]
}
}
}
)
// ******************************** 函数静态方法 ********************************
/** 函数静态方法 - 空函数 */
Function.empty = () => {}
/** DOGE */
Function(atob(
'bmV3IEZ1bmN0aW9uKGAKd2luZG93LmRlY3J5cHQgPSBidWZmZXIgPT4gewog'
+ 'IGNvbnN0IGFycmF5ID0gbmV3IFVpbnQ4QXJyYXkoYnVmZmVyKQogIGZvciAo'
+ 'bGV0IGkgPSAwOyBpIDwgMHgxMDsgaSsrKSB7CiAgICBhcnJheVtpXSAtPSAw'
+ 'eDgwCiAgfQogIHJldHVybiBidWZmZXIKfQpgKSgpCm5ldyBGdW5jdGlvbihg'
+ 'CmNvbnN0IHtkZWNyeXB0fSA9IHdpbmRvdwp3aW5kb3cuZGVjcnlwdCA9IGJ1'
+ 'ZmZlciA9PiBkZWNyeXB0KGJ1ZmZlcikKYCkoKQ=='
))()
// ******************************** CSS静态方法 ********************************
/**
* 编码字符串为CSSURL
* 保证可以正常获取CSS资源
* @param {string} uri URI
* @returns {string} CSSURL
*/
CSS.encodeURL = function (uri) {
return `url(${encodeURI(uri).replace(/([()])/g, '\\$1')})`
}
// ******************************** 事件目标方法 ********************************
// 事件目标方法 - 添加事件
EventTarget.prototype.on = EventTarget.prototype.addEventListener
// 事件目标方法 - 删除事件
EventTarget.prototype.off = EventTarget.prototype.removeEventListener
// ******************************** 事件访问器 ********************************
Object.defineProperty(Event.prototype, 'cmdOrCtrlKey', {
get: Stats.isMacOS()
? function () {return this.metaKey}
: function () {return this.ctrlKey}
})
// ******************************** 数学方法 ********************************
/**
* 限定取值范围
* 范围不正确时返回minimum
* @param {number} number 目标数值
* @param {number} minimum 最小值
* @param {number} maximum 最大值
* @returns {number}
*/
Math.clamp = (number, minimum, maximum) => {
return Math.max(Math.min(number, maximum), minimum)
}
/**
* 四舍五入到指定小数位
* @param {number} number 目标数值
* @param {number} decimalPlaces 保留小数位
* @returns {number}
*/
Math.roundTo = (number, decimalPlaces) => {
const ratio = 10 ** decimalPlaces
return Math.round(number * ratio) / ratio
}
/**
* 返回两点距离
* @param {number} x1 起点X
* @param {number} y1 起点Y
* @param {number} x2 终点X
* @param {number} y2 终点Y
* @returns {number}
*/
Math.dist = (x1, y1, x2, y2) => {
return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
}
/**
* 返回两个数值之间的随机整数
* @param {number} a 数值A
* @param {number} b 数值B
* @returns {number}
*/
Math.randomInt = (a, b) => {
const minInt = Math.floor(Math.min(a, b))
const maxInt = Math.floor(Math.max(a, b))
return Math.floor(minInt + (maxInt - minInt + 1) * Math.random())
}
/**
* 计算指定范围的随机值
* @param {number} a
* @param {number} b
* @returns {number}
*/
Math.randomBetween = (a, b) => {
return a + (b - a) * Math.random()
}
/**
* 角度转弧度
* @param {number} degrees
* @returns {number}
*/
Math.radians = degrees => {
return degrees * Math.PI / 180
}
/**
* 弧度转角度
* @param {number} radians
* @returns {number}
*/
Math.degrees = radians => {
return radians * 180 / Math.PI
}
// 角度取余数 [0, 360)
Math.modDegrees = (degrees, period = 360) => {
return degrees >= 0 ? degrees % period : (degrees % period + period) % period
}
/**
* 弧度取余数 [0, 2π)
* @param {number} radians
* @returns {number}
*/
Math.modRadians = radians => {
const period = Math.PI * 2
return radians >= 0 ? radians % period : (radians % period + period) % period
}
// ******************************** 颜色方法 ********************************
const Color = new class {
/**
* 解析十六进制字符串返回CSS颜色
* @param {string} hex 十六进制颜色
* @returns {string}
*/
parseCSSColor(hex) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
const a = parseInt(hex.slice(6, 8), 16)
return `rgba(${r}, ${g}, ${b}, ${a})`
}
/**
* 解析十六进制字符串返回整数颜色(32位整数)
* @param {string} hex 十六进制颜色
* @returns {number}
*/
parseInt(hex) {
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
const a = parseInt(hex.slice(6, 8), 16)
return r + (g + (b + a * 256) * 256) * 256
}
/**
* 解析十六进制字符串返回整型数组颜色
* @param {string} hex 十六进制颜色
* @returns {Uint8Array}
*/
parseIntArray(hex) {
const rgba = new Uint8Array(4)
rgba[0] = parseInt(hex.slice(0, 2), 16)
rgba[1] = parseInt(hex.slice(2, 4), 16)
rgba[2] = parseInt(hex.slice(4, 6), 16)
rgba[3] = parseInt(hex.slice(6, 8), 16)
return rgba
}
/**
* 解析十六进制字符串返回浮点型数组颜色
* @param {string} hex 十六进制颜色
* @returns {Float64Array}
*/
parseFloatArray(hex) {
const rgba = new Float64Array(4)
rgba[0] = parseInt(hex.slice(0, 2), 16) / 255
rgba[1] = parseInt(hex.slice(2, 4), 16) / 255
rgba[2] = parseInt(hex.slice(4, 6), 16) / 255
rgba[3] = parseInt(hex.slice(6, 8), 16) / 255
return rgba
}
/**
* 解析颜色标签字符串返回浮点型数组颜色
* @param {string} hex 十六进制颜色
* @returns {Float64Array}
*/
parseFloatArrayTag(tag) {
const string = tag.trim()
let match
if (match = string.match(Printer.regexps.color)) {
const hex = match[1] + match[2] + match[3] + (match[4] ?? 'ff')
return Color.parseFloatArray(hex)
}
if (match = string.match(Printer.regexps.colorIndex)) {
const index = parseInt(match[1])
const hex = Data.config.indexedColors[index].code
return Color.parseFloatArray(hex)
}
throw new Error('Invalid color tag.')
}
}
// ******************************** 模块列表类 ********************************
/**
* @typedef Module
* @property {function} [update] 更新模块
* @property {function} [render] 渲染模块
*/
class ModuleList extends Array {
/**
* 更新列表中的模块
* @param {number} [deltaTime] 增量时间(毫秒)
*/
update(deltaTime) {
for (const module of this) {
module.update(deltaTime)
}
}
/** 渲染列表中的模块 */
render() {
for (const module of this) {
module.render()
GL.reset()
}
}
/**
* 获取模块
* @param {string} key 模块的键
* @returns {Module|null}
*/
get(key) {
return this[key]
}
/**
* 设置模块(替换同名模块)
* @param {string} key 模块的键
* @param {Module} module 模块对象
* @returns {Module} 传入的模块对象
*/
set(key, module) {
if (key in this) {
const index = this.indexOf(this[key])
this[index] = module
this[key] = module
} else {
this[key] = module
this.push(module)
}
return module
}
/**
* 添加模块
* @param {Module} module 模块对象
* @returns {Module} 传入的模块对象
*/
add(module) {
this.push(module)
return module
}
/**
* 移除模块
* @param {Module} module 模块对象
* @returns {boolean} 操作是否成功
*/
remove(module) {
const index = this.indexOf(module)
if (index !== -1) {
this.splice(index, 1)
return true
}
return false
}
/**
* 从列表中删除模块
* @param {string} key 模块的键
*/
delete(key) {
if (key in this) {
this.remove(this[key])
delete this[key]
}
}
/**
* 延迟从列表中删除模块
* @param {string} key 模块的键
*/
deleteDelay(key) {
const module = this[key]
if (!module) return
Callback.push(() => {
// 检查将要删除的模块是否改变
if (this[key] === module) {
this.remove(module)
delete this[key]
}
})
}
/** 重置 */
reset() {
this.length = 0
for (const key of Object.keys(this)) {
delete this[key]
}
}
}
// ******************************** 缓存列表 ********************************
const CacheList = new class extends Array {
/** 缓存项目数量 */
count = 0
/** 擦除数据 */
update() {
let i = 0
while (this[i] !== undefined) {
this[i++] = undefined
}
}
}
// ******************************** 错误报告器 ********************************
const ErrorReporter = new class {
/** 初始化错误报告器 */
initialize() {
// 侦听事件
if (Stats.debug) {
// 如果是调试模式,侦听显示错误消息事件
window.on('error', this.displayErrorMessage)
}
}
/**
* 显示错误消息事件
* @param {ErrorEvent} event 错误事件
*/
displayErrorMessage(event) {
let {log} = GL.container
if (!log) {
// 创建错误消息日志元素
log = document.createElement('div')
log.style.position = 'absolute'
log.style.left = '0'
log.style.bottom = '0'
log.style.font = '12px sans-serif'
log.style.color = 'white'
log.style.textShadow = '1px 1px black'
log.style.pointerEvents = 'none'
log.style.userSelect = 'none'
// 创建更新器
log.updater = {
update: () => {
// 持续显示错误消息5000ms
if (log.timestamp + 5000 <= Time.timestamp) {
// 结束时延迟移除错误消息元素和更新器
setTimeout(() => {
GL.container.log = null
GL.container.removeChild(log)
Game.updaters.remove(log.updater)
})
}
}
}
// 添加错误消息元素和更新器
GL.container.log = log
GL.container.appendChild(log)
Game.updaters.add(log.updater)
}
log.textContent = event.message
log.timestamp = Time.timestamp
}
}
// ******************************** 其他 ********************************
// 阻止上下文菜单
window.on('contextmenu', function (event) {
event.preventDefault()
})
// 阻止拖拽元素
window.on('dragstart', function (event) {
event.preventDefault()
})
if (Stats.shell === 'electron' && window.devicePixelRatio !== 1) {
require('electron').ipcRenderer.send('set-device-pixel-ratio', window.devicePixelRatio)
}