'use strict'
// ******************************** 玩家队伍管理器 ********************************
const Party = new class {
/** 玩家角色对象
* @type {Actor|null}
*/ player = null
/** 玩家队伍成员列表
* @type {Array<Actor>}
*/ members = []
/** 玩家队伍共享库存ID
* @type {string}
*/ inventoryId = ''
/** 队伍版本(随着队伍成员的添加和移除发生变化)
* @type {number}
*/ version = 0
/** 初始化队伍管理器 */
initialize() {
this.createPlayer()
this.createMembers()
this.shareInventory()
}
/** 重置队伍角色 */
reset() {
this.player = null
this.members = []
this.inventoryId = ''
this.createPlayer()
this.createMembers()
this.shareInventory()
}
/** 创建玩家角色 */
createPlayer() {
const {playerTeam, playerActor} = Data.config.actor
const actor = ActorManager.create(playerActor)
if (actor) {
actor.setTeam(playerTeam)
this.setPlayer(actor)
}
}
/** 创建玩家队伍成员 */
createMembers() {
const {playerTeam, partyMembers} = Data.config.actor
for (const actorId of partyMembers) {
const actor = ActorManager.get(actorId) ?? ActorManager.create(actorId)
if (actor) {
actor.setTeam(playerTeam)
this.addMember(actor)
}
}
}
/**
* 设置玩家角色
* @param {GlobalActor} actor 玩家角色
*/
setPlayer(actor) {
if (actor instanceof GlobalActor && !actor.destroyed) {
this.player = actor
}
}
/**
* 添加玩家队伍成员
* @param {GlobalActor} actor 队伍成员
*/
addMember(actor) {
if (actor instanceof GlobalActor && !actor.destroyed) {
if (this.members.append(actor)) {
if (this.inventoryId) {
const inventoryActor = ActorManager.get(this.inventoryId)
if (inventoryActor instanceof GlobalActor) {
actor.useInventory(inventoryActor.inventory)
}
}
this.version++
}
}
}
/**
* 移除玩家队伍成员
* @param {GlobalActor} actor 队伍成员
*/
removeMember(actor) {
if (actor instanceof GlobalActor) {
if (this.members.remove(actor)) {
if (actor.inventory.actor !== actor &&
actor.inventory.actor.data.id === this.inventoryId) {
actor.restoreInventory()
}
this.version++
}
}
}
/** 共享玩家角色库存 */
shareInventory() {
if (Data.config.actor.partyInventory === 'shared') {
if (this.player instanceof GlobalActor) {
this.inventoryId = this.player.data.id
for (const actor of this.members) {
actor.useInventory(this.player.inventory)
}
}
}
}
/** 保存队伍角色数据 */
saveData() {
return {
player: this.player?.data.id ?? '',
members: this.members.map(a => a.data.id),
}
}
/**
* 加载队伍角色数据
* @param {Object} party
*/
loadData(party) {
const {player, members} = party
this.player = ActorManager.get(player) ?? null
this.members = []
for (const member of members) {
const actor = ActorManager.get(member)
if (actor) this.addMember(actor)
}
}
}
// ******************************** 势力队伍管理器 ********************************
const Team = new class {
/** 队伍列表
* @type {Array<string>}
*/ list
/** 队伍的键(ID)列表
* @type {Array<string>}
*/ keys
/** 队伍的键(ID)列表
* @type {Array<string>}
*/ map
/** 默认队伍ID
* @type {string}
*/ defaultId
/** 队伍关系数组(0:敌对, 1:友好)
* @type {Uint8Array}
*/ relationMap = new Uint8Array(65536)
/** 队伍碰撞开关表(0:关闭, 1:开启)
* @type {Uint8Array}
*/ collisionMap = new Uint8Array(65536)
/** 初始化 */
initialize() {
// 给队伍设置索引
const map = {}
const teams = Data.teams.list
const keys = teams.map(team => team.id)
this.list = teams
this.keys = keys
this.map = map
const data = this.unpackTeamData(keys, Data.teams)
const length = teams.length
for (let i = 0; i < length; i++) {
const team = teams[i]
const key = keys[i]
team.index = i
team.relations = data.relationsMap[key]
team.collisions = data.collisionsMap[key]
map[team.id] = team
for (let j = 0; j < length; j++) {
this.relationMap[i | j << 8] = team.relations[keys[j]]
this.collisionMap[i | j << 8] = team.collisions[keys[j]]
}
}
this.defaultId = keys[0]
}
/**
* 解包角色队伍数据
* @param {string[]} keys 队伍的ID列表
* @param {string} code 队伍的关系代码
* @returns {Object}
*/
unpackTeamData(keys, data) {
const relationsMap = {}
const collisionsMap = {}
const length = keys.length
// 解码已压缩的队伍关系数据
const sRelations = Codec.decodeTeamData(data.relations, length)
const sCollisions = Codec.decodeTeamData(data.collisions, length)
const a = length * 2
// 构建完整的队伍关系数据结构
for (let i = 0; i < length; i++) {
const dRelations = {}
const dCollisions = {}
for (let j = 0; j < i; j++) {
const ri = (a - j + 1) / 2 * j - j + i
dRelations[keys[j]] = sRelations[ri]
dCollisions[keys[j]] = sCollisions[ri]
}
const b = (a - i + 1) / 2 * i - i
for (let j = i; j < length; j++) {
const ri = b + j
dRelations[keys[j]] = sRelations[ri]
dCollisions[keys[j]] = sCollisions[ri]
}
relationsMap[keys[i]] = dRelations
collisionsMap[keys[i]] = dCollisions
}
return {relationsMap, collisionsMap}
}
/**
* 通过ID获取队伍
* @param {string} teamId 队伍ID
* @returns {Object} 队伍
*/
get(teamId) {
return this.map[teamId]
}
/**
* 通过队伍索引获取队伍关系
* @param {number} teamIndex1 队伍索引1
* @param {number} teamIndex2 队伍索引2
* @returns {number} 队伍关系(0:敌对, 1:友好)
*/
getRelationByIndexes(teamIndex1, teamIndex2) {
return this.relationMap[teamIndex1 | teamIndex2 << 8]
}
/**
* 判断敌对关系
* @param {string} teamId1 队伍ID1
* @param {string} teamId2 队伍ID2
* @returns {boolean} 是否为敌对关系
*/
isEnemy(teamId1, teamId2) {
return this.map[teamId1]?.relations[teamId2] === 0
}
/**
* 判断友好关系
* @param {string} teamId1 队伍ID1
* @param {string} teamId2 队伍ID2
* @returns {boolean} 是否为友好关系
*/
isFriendly(teamId1, teamId2) {
return this.map[teamId1]?.relations[teamId2] === 1
}
/**
* 改变角色队伍的关系
* @param {string} teamId1 队伍ID1
* @param {string} teamId2 队伍ID2
* @param {number} relation 队伍1和队伍2的关系(0:敌对, 1:友好)
*/
changeRelation(teamId1, teamId2, relation) {
const team1 = this.get(teamId1)
const team2 = this.get(teamId2)
if (team1 && team2) {
team1.relations[teamId2] = relation
team2.relations[teamId1] = relation
this.relationMap[team1.index | team2.index << 8] = relation
this.relationMap[team2.index | team1.index << 8] = relation
}
}
/** 保存队伍关系数据 */
saveData() {
const keys = this.keys
const teams = this.list
const length = teams.length
const dRelations = GL.arrays[0].uint8
const dCollisions = GL.arrays[1].uint8
let ri = 0
// 压缩队伍关系数据
for (let i = 0; i < length; i++) {
const team = teams[i]
const sRelations = team.relations
const sCollisions = team.collisions
for (let j = i; j < length; j++, ri++) {
dRelations[ri] = sRelations[keys[j]]
dCollisions[ri] = sCollisions[keys[j]]
}
}
// 编码已压缩的队伍关系数据
return {
keys: keys,
relations: Codec.encodeTeamData(new Uint8Array(dRelations.buffer, 0, ri)),
collisions: Codec.encodeTeamData(new Uint8Array(dCollisions.buffer, 0, ri)),
}
}
/**
* 加载队伍关系数据
* @param {Object} team
*/
loadData(team) {
const sKeys = team.keys
const data = this.unpackTeamData(sKeys, team)
const dKeys = this.keys
const teams = this.list
const length = teams.length
// 将加载的队伍关系数据合并到现有的数据中
// 丢弃项目编辑所造成的无效数据
for (let i = 0; i < length; i++) {
const key = dKeys[i]
const sRelations = data.relationsMap[key]
const sCollisions = data.collisionsMap[key]
if (!sRelations || !sCollisions) continue
const team = teams[i]
const dRelations = team.relations
const dCollisions = team.collisions
for (let j = 0; j < length; j++) {
const key = dKeys[j]
const relation = sRelations[key]
if (relation !== undefined) {
dRelations[key] = relation
this.relationMap[i | j << 8] = relation
}
const collision = sCollisions[key]
if (collision !== undefined) {
dCollisions[key] = collision
this.collisionMap[i | j << 8] = collision
}
}
}
}
}
// ******************************** 全局角色管理器 ********************************
const ActorManager = new class {
/** 全局角色列表
* @type {Array<Actor>}
*/ list = []
/** 全局角色ID映射表(ID->实例)
* @type {Object}
*/ idMap = {}
/** 重置全局角色 */
reset() {
this.clearGlobalActors()
}
/** 清除所有全局角色 */
clearGlobalActors() {
this.idMap = {}
// 遍历所有全局角色
for (const actor of this.list) {
for (const context of Scene.contexts) {
// 从所有场景角色列表中移除该角色
if (context?.actors.includes(actor)) {
context.actors.remove(actor)
}
}
actor.destroy()
}
this.list = []
}
/**
* 创建全局角色
* @param {string} actorId 角色文件ID
* @param {Object} [savedData] 角色存档数据
* @returns {GlobalActor|null}
*/
create(actorId, savedData) {
const data = Data.actors[actorId]
if (!this.idMap[actorId] && data) {
// 如果角色ID未被占用,则创建角色
const actor = new GlobalActor(data, savedData)
this.idMap[actorId] = actor
this.list.push(actor)
return actor
}
return null
}
/**
* 删除全局角色
* @param {string} actorId 角色文件ID
*/
delete(actorId) {
const actor = this.idMap[actorId]
if (actor) {
for (const context of Scene.contexts) {
// 从所有场景角色列表中移除该角色
if (context?.actors.includes(actor)) {
context.actors.remove(actor)
}
}
delete this.idMap[actorId]
actor.destroy()
this.list.remove(actor)
Party.removeMember(actor)
if (Party.player === actor) {
Party.player = null
}
}
}
/**
* 获取全局角色
* @param {string} actorId 角色文件ID
* @returns {GlobalActor|undefined}
*/
get(actorId) {
return this.idMap[actorId]
}
/** 保存全局角色列表数据 */
saveData() {
const actors = this.list
const length = actors.length
const data = new Array(length)
for (let i = 0; i < length; i++) {
data[i] = actors[i].saveData()
}
return data
}
/**
* 加载全局角色列表数据
* @param {Object} actors
*/
loadData(actors) {
this.clearGlobalActors()
// 恢复全局角色列表
for (const savedData of actors) {
this.create(
savedData.fileId,
savedData,
)
}
}
}
// ******************************** 角色类 ********************************
class Actor {
/** 角色对象可见性
* @type {boolean}
*/ visible
/** 角色对象实体ID
* @type {string}
*/ entityId
/** 角色预设数据ID
* @type {string}
*/ presetId
/** 角色独立变量ID
* @type {string}
*/ selfVarId
/** 角色对象名称
* @type {string}
*/ name
/** 角色头像ID
* @type {string}
*/ portrait
/** 角色头像矩形裁剪区域
* @type {Array<number>}
*/ clip
/** 角色文件数据
* @type {Object}
*/ data
/** 角色的场景分区ID
* @type {number}
*/ cellId
/** 角色的场景网格ID
* @type {number}
*/ gridId
/** 角色队伍ID
* @type {string}
*/ teamId
/** 角色队伍索引
* @type {number}
*/ teamIndex
/** 角色的激活状态
* @type {boolean}
*/ active
/** 角色是否已销毁
* @type {boolean}
*/ destroyed
/** 角色的通行区域
* @type {number}
*/ passage
/** 角色的渲染优先级
* @type {number}
*/ priority
/** 角色的场景位置X
* @type {number}
*/ x
/** 角色的场景位置Y
* @type {number}
*/ y
/** 角色的整数位置X
* @type {number}
*/ intX
/** 角色的整数位置Y
* @type {number}
*/ intY
/** 角色的缩放系数
* @type {number}
*/ scale
/** 角色的角度
* @type {number}
*/ angle
/** 是否固定角度
* @type {boolean}
*/ angleFixed
/** 受击时间戳
* @type {number}
*/ hitTimestamp
/** 角色碰撞器组件
* @type {ActorCollider}
*/ collider
/** 角色导航器组件
* @type {ActorNavigator}
*/ navigator
/** 角色动画播放器组件
* @type {Animation}
*/ animation
/** 角色动画精灵图表
* @type {Object}
*/ sprites
/** 角色更新器列表
* @type {ModuleList}
*/ updaters
/** 角色属性映射表
* @type {Object}
*/ attributes
/** 角色动画控制器组件
* @type {AnimationController}
*/ animationController
/** 角色动画管理器
* @type {Array<Animation>}
*/ animationManager
/** 角色技能管理器
* @type {SkillManager}
*/ skillManager
/** 角色状态管理器
* @type {StateManager}
*/ stateManager
/** 角色装备管理器
* @type {EquipmentManager}
*/ equipmentManager
/** 角色公共冷却管理器
* @type {CooldownManager}
*/ cooldownManager
/** 角色快捷栏管理器
* @type {ShortcutManager}
*/ shortcutManager
/** 角色目标对象管理器
* @type {TargetManager}
*/ targetManager
/** 角色库存管理器
* @type {Inventory}
*/ inventory
/** 角色事件类型映射表
* @type {Object}
*/ events
/** 角色脚本管理器
* @type {Script}
*/ script
/** 角色的父级对象
* @type {SceneActorList|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
/**
* 场景角色对象
* @param {ActorFile} data 角色文件数据
* @param {Object} [savedData] 角色存档数据
* @param {string} [presetId] 角色预设数据ID
*/
constructor(data, savedData, presetId = '') {
this.visible = true
this.entityId = ''
this.presetId = presetId
this.selfVarId = ''
this.name = ''
this.portrait = data.portrait
this.clip = [...data.clip]
this.data = data
this.cellId = -1
this.gridId = -1
this.teamId = Team.defaultId
this.teamIndex = 0
this.active = true
this.destroyed = false
this.priority = data.priority
this.x = 0
this.y = 0
this.scale = data.scale
this.angle = 0
this.angleFixed = false
this.hitTimestamp = -100000000
this.parent = null
this.started = false
// 角色组件
this.collider = new ActorCollider(this)
this.navigator = new ActorNavigator(this)
this.animation = null
this.sprites = null
this.updaters = new ModuleList()
this.attributes = null
this.animationController = new AnimationController(this)
this.animationManager = new AnimationManager(this)
this.skillManager = new SkillManager(this)
this.stateManager = new StateManager(this)
this.equipmentManager = new EquipmentManager(this)
this.cooldownManager = new CooldownManager(this)
this.shortcutManager = new ShortcutManager(this)
this.targetManager = new TargetManager(this)
this.inventory = new Inventory(this)
this.events = data.events
this.script = Script.create(this, data.scripts)
Actor.latest = this
if (savedData) {
// 加载存档数据
this.visible = savedData.visible
this.entityId = savedData.entityId
this.presetId = savedData.presetId
this.selfVarId = savedData.selfVarId
this.name = savedData.name
this.active = savedData.active
this.passage = savedData.passage
this.priority = savedData.priority
this.portrait = savedData.portrait
this.clip = savedData.clip
this.scale = savedData.scale
this.angle = savedData.angle
this.sprites = savedData.sprites
this.setTeam(savedData.teamId)
this.setPosition(savedData.x, savedData.y)
this.collider.weight = savedData.weight
this.navigator.movementSpeed = savedData.movementSpeed
this.navigator.movementFactor = savedData.movementFactor
this.attributes = savedData.attributes
this.animationController.loadData(savedData.motions)
this.animationManager.loadData(savedData.animations)
this.animation = this.animationManager.get('actor') ?? null
this.animation?.setSpriteImages(savedData.sprites)
this.animationController.bindAnimation(this.animation)
this.skillManager.loadData(savedData.skills)
this.stateManager.loadData(savedData.states)
this.equipmentManager.loadData(savedData.equipments)
this.cooldownManager.loadData(savedData.cooldowns)
this.shortcutManager.loadData(savedData.shortcuts)
this.inventory.loadData(savedData.inventory)
EntityManager.add(this)
} else {
// 初始化
EntityManager.add(this)
this.setPassage(data.passage)
this.setAnimation(data.animationId)
this.loadSprites()
this.loadAttributes()
this.loadSkills()
this.loadEquipments()
this.loadInventory()
this.emit('create')
}
// 定义临时属性
Actor.defineTempAttributes(this.attributes)
}
/** 加载初始动画精灵哈希表 */
loadSprites() {
const map = {}
const sprites = this.data.sprites
const length = sprites.length
// 使用精灵数组生成哈希表
for (let i = 0; i < length; i++) {
const sprite = sprites[i]
map[sprite.id] = sprite.image
}
this.sprites = map
// 角色精灵图像优先于默认动画精灵图像
this.animation?.setSpriteImages(map)
}
/** 加载初始角色属性 */
loadAttributes() {
Attribute.loadEntries(
this.attributes = {},
this.data.attributes,
)
}
/** 加载初始角色技能 */
loadSkills() {
const {skillManager} = this
const dataMap = Data.skills
const skills = this.data.skills
const length = skills.length
// 创建初始技能并设置快捷键
for (let i = 0; i < length; i++) {
const skill = skills[i]
const data = dataMap[skill.id]
const key = Enum.get(skill.key)
if (data !== undefined) {
const skill = new Skill(data)
skillManager.add(skill)
if (key) {
this.shortcutManager.set(key.value, skill)
}
}
}
}
/** 加载初始角色装备 */
loadEquipments() {
const {equipmentManager} = this
const dataMap = Data.equipments
const equipments = this.data.equipments
const length = equipments.length
// 创建初始装备并设置快捷键
for (let i = 0; i < length; i++) {
const equipment = equipments[i]
const data = dataMap[equipment.id]
const slot = Enum.get(equipment.slot)
if (data !== undefined && slot !== undefined) {
equipmentManager.set(slot.value, new Equipment(data))
}
}
}
/** 加载初始角色库存 */
loadInventory() {
const inventory = this.inventory
const list = this.data.inventory
const length = list.length
// 创建初始物品和装备,避免触发获得事件
for (let i = 0; i < length; i++) {
const goods = list[i]
switch (goods.type) {
case 'item': {
const data = Data.items[goods.id]
if (data) {
const item = new Item(data)
inventory.insert(item)
item.increase(goods.quantity)
}
continue
}
case 'equipment': {
const data = Data.equipments[goods.id]
if (data) {
inventory.insert(new Equipment(data))
}
continue
}
case 'money':
inventory.money += goods.money
continue
}
}
}
/**
* 角色朝指定角度位移一段距离
* @param {number} angle 位移角度(弧度)
* @param {number} distance 位移距离(单位:图块)
* @param {string} [easingId] 过渡曲线ID
* @param {number} [duration] 持续时间(毫秒)
* @param {string} [key] 位移更新器的键(指定以避免冲突)
*/
translate(angle, distance, easingId, duration, key = 'translate') {
const distX = distance * Math.cos(angle)
const distY = distance * Math.sin(angle)
if (duration > 0) {
// 创建过渡更新器,使用set方法:
// 如果已有同名更新器,则替换
let elapsed = 0
let lastTime = 0
const easing = Easing.get(easingId)
this.updaters.set(key, {
protected: true,
update: deltaTime => {
// 更新中不断设置角色位置
elapsed += deltaTime
const time = easing.map(elapsed / duration)
const increase = time - lastTime
const x = distX * increase
const y = distY * increase
this.move(x, y)
lastTime = time
// 过渡结束,延迟删除更新器
if (elapsed >= duration) {
this.updaters.deleteDelay(key)
}
}
})
} else {
// 立即执行
this.updaters.deleteDelay(key)
const x = this.x + distX
const y = this.y + distY
this.setPosition(x, y)
}
}
/**
* 设置角色的缩放系数
* @param {number} scale 角色缩放系数
* @param {string} easingId 过渡曲线ID
* @param {number} [duration] 持续时间(毫秒)
*/
setScale(scale, easingId, duration) {
if (duration > 0) {
// 创建过渡更新器,使用set方法:
// 如果已有同名更新器,则替换
let elapsed = 0
const start = this.scale
const easing = Easing.get(easingId)
this.updaters.set('scale', {
protected: true,
update: deltaTime => {
// 更新中不断设置角色角度
elapsed += deltaTime
const time = easing.map(elapsed / duration)
this.scale = start * (1 - time) + scale * time
this.animationManager.setGlobalScale(this.scale)
// 过渡结束,延迟删除更新器
if (elapsed >= duration) {
this.updaters.deleteDelay('scale')
}
}
})
} else {
// 立即执行
this.updaters.deleteDelay('scale')
this.scale = scale
this.animationManager.setGlobalScale(this.scale)
}
}
/**
* 设置角色的角度
* @param {number} angle 角色角度(弧度)
* @param {string} [easingId] 过度曲线ID
* @param {number} [duration] 持续时间(毫秒)
*/
setAngle(angle, easingId, duration) {
if (duration > 0) {
this.rotate(angle - this.angle, easingId, duration)
} else {
// 立即执行
this.updaters.deleteDelay('rotate')
this.updateAngle(angle)
}
}
/**
* 角色旋转指定的角度
* @param {number} angle 旋转角度(弧度)
* @param {string} [easingId] 过渡曲线ID
* @param {number} [duration] 持续时间(毫秒)
* @param {string} [key] 旋转更新器的键(指定以避免冲突)
*/
rotate(angle, easingId, duration, key = 'rotate') {
if (duration > 0) {
// 创建过渡更新器,使用set方法:
// 如果已有同名更新器,则替换
let elapsed = 0
let lastTime = 0
const easing = Easing.get(easingId)
this.updaters.set(key, {
protected: true,
update: deltaTime => {
// 更新中不断设置角色角度
elapsed += deltaTime
const time = easing.map(elapsed / duration)
this.updateAngle(this.angle + angle * (time - lastTime))
lastTime = time
// 过渡结束,延迟删除更新器
if (elapsed >= duration) {
this.updaters.deleteDelay(key)
}
}
})
} else {
// 立即执行
this.updaters.deleteDelay(key)
this.updateAngle(this.angle + angle)
}
}
/**
* 设置角色动画
* @param {string} animationId 动画文件ID
*/
setAnimation(animationId) {
const data = Data.animations[animationId]
if (data) {
// 如果动画ID有效,创建新的动画播放器
const animation = new Animation(data)
animation.rotatable = this.data.rotatable
animation.syncAngle = true
this.animationManager.set('actor', animation)
this.animation = animation
} else if (this.animation) {
// 否则销毁上一个动画播放器
this.animationManager.delete('actor')
this.animation = null
}
// 绑定到动画控制器
this.animationController.bindAnimation(this.animation)
}
/**
* 设置角色的精灵图
* @param {string} spriteId 精灵图ID
* @param {string} imageId 图像文件ID
*/
setSprite(spriteId, imageId) {
// 修改角色精灵表中的键值
this.sprites[spriteId] = imageId
// 如果角色动画已经加载了同名纹理,则删除
this.animation?.deleteTexture(spriteId)
}
/**
* 设置角色的队伍
* @param {string} teamId 队伍ID
*/
setTeam(teamId) {
const team = Team.get(teamId)
if (team !== undefined) {
this.teamId = teamId
this.teamIndex = team.index
}
}
/**
* 设置通行区域
* @param {string} passage 通行区域
*/
setPassage(passage) {
this.passage = Actor.passageMap[passage]
}
/**
* 移动角色
* @param {number} x 位移X
* @param {number} y 位移Y
*/
move(x, y) {
this.x += x
this.y += y
// 设置碰撞器为已经移动状态
this.collider.moved = true
}
/**
* 设置角色在场景中的位置
* @param {number} x 场景网格X
* @param {number} y 场景网格Y
*/
setPosition(x, y) {
this.x = x
this.y = y
this.updateGridPosition()
// 设置碰撞器为已经移动状态
this.collider.moved = true
}
/**
* 更新角色在场景中的网格位置
*/
updateGridPosition() {
this.intX = Math.floor(this.x)
this.intY = Math.floor(this.y)
}
/**
* 更新受击时间戳
*/
updateHitTimestamp() {
this.hitTimestamp = Time.elapsed
}
/**
* 设置角色的激活状态
* @param {boolean} active 如果禁用,角色将不再更新事件和脚本
*/
setActive(active) {
if (this.active !== active) {
this.active = active
// 如果是未激活状态,重置目标列表
if (!active) {
this.targetManager.reset()
}
}
}
/**
* 判断角色是否处于激活状态(并且已出场)
* @returns {boolean}
*/
isActive() {
return this.active && this.parent !== null
}
/**
* 更新角色的角度,并计算动画动作方向
* @param {number} angle 弧度
*/
updateAngle(angle) {
if (this.angleFixed) return
angle = Math.modRadians(angle)
// 当新的角度与当前角度不同时,计算动画方向
// 允许存在一点角度误差,避免频繁计算动画方向
if (Math.abs(this.angle - angle) >= 0.0001) {
this.angle = angle
this.animationManager.setAngle(angle)
}
}
/**
* 更新角色的模块
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
// 更新导航器
this.navigator.update(deltaTime)
// 更新动画组件
this.animationManager.update(deltaTime)
// 更新模块列表
if (this.active) {
this.updaters.update(deltaTime)
} else {
// 如果角色未激活,仅执行受保护的更新器
for (const updater of this.updaters) {
if (updater.protected) {
updater.update(deltaTime)
}
}
}
}
/**
* 使用指定全局角色的库存
* @param {Inventory} inventory 全局角色的库存
*/
useInventory(inventory) {
if (this.inventory !== inventory) {
if (!this.savedInventory) {
this.savedInventory = this.inventory
}
this.inventory = inventory
}
}
/**
* 恢复角色的库存引用
*/
restoreInventory() {
if (this.savedInventory) {
this.inventory = this.savedInventory
delete this.savedInventory
}
}
/**
* 调用角色事件
* @param {string} type 角色事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.triggerActor = this
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用角色事件和脚本
* @param {string} type 角色事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁角色 */
destroy() {
if (!this.destroyed) {
EntityManager.remove(this)
this.parent?.remove(this)
this.destroyed = true
this.active = false
this.targetManager.reset()
this.animationManager.destroy()
this.emit('destroy')
}
}
/** 保存角色数据 */
saveData() {
return {
visible: this.visible,
entityId: this.entityId,
presetId: this.presetId,
selfVarId: this.selfVarId,
fileId: this.data.id,
teamId: this.teamId,
active: this.active,
passage: this.passage,
priority: this.priority,
name: this.name,
x: this.x,
y: this.y,
scale: this.scale,
angle: this.angle,
portrait: this.portrait,
clip: this.clip,
sprites: this.sprites,
weight: this.collider.weight,
motions: this.animationController.saveData(),
movementSpeed: this.navigator.movementSpeed,
movementFactor: this.navigator.movementFactor,
attributes: this.attributes,
animations: this.animationManager.saveData(),
skills: this.skillManager.saveData(),
states: this.stateManager.saveData(),
equipments: this.equipmentManager.saveData(),
cooldowns: this.cooldownManager.saveData(),
shortcuts: this.shortcutManager.saveData(),
inventory: this.inventory.saveData(this),
}
}
/** 最新创建的角色
* @type {Actor|undefined}
*/ static latest
/** 定义临时属性映射表方法
* @type {Function}
*/ static defineTempAttributes
/** 通行区域映射表 */
static passageMap = {
land: 0,
water: 1,
unrestricted: -1,
}
/** 初始化角色相关的数据 */
static initialize() {
// 创建角色临时属性的描述器
let hasAttributes = false
const properties = {}
for (const entry of Data.config.actor.tempAttributes) {
const attr = Attribute.get(entry.key)
if (!attr) continue
let value = entry.value
if (attr.type === 'enum') {
const enumstr = Enum.get(value)
if (!enumstr) continue
value = enumstr.value
}
hasAttributes = true
properties[attr.key] = {
configurable: true,
writable: true,
value: value,
}
}
// 创建定义角色临时属性方法
if (hasAttributes) {
this.defineTempAttributes = attributes => {
Object.defineProperties(attributes, properties)
}
} else {
this.defineTempAttributes = Function.empty
}
}
// 角色检查器集合
static inspectors = new class {
// 检查器 - 判断敌对角色
'enemy' = (a, b) => {
return Team.relationMap[a.teamIndex | b.teamIndex << 8] === 0 && a !== b
}
// 检查器 - 判断友好角色
'friend' = (a, b) => {
return Team.relationMap[a.teamIndex | b.teamIndex << 8] === 1
}
// 检查器 - 判断小队角色
'team' = (a, b) => {
return a.teamId === b.teamId
}
// 检查器 - 判断小队角色除自己以外
'team-except-self' = (a, b) => {
return a !== b && a.teamId === b.teamId
}
// 检查器 - 判断任意角色除自己以外
'any-except-self' = (a, b) => {
return a !== b
}
// 检查器 - 判断任意角色
'any' = (a, b) => {
return true
}
}
}
// ******************************** 全局角色类 ********************************
class GlobalActor extends Actor {
/**
* 转移到场景中的指定位置
* @param {number} x 场景坐标X
* @param {number} y 场景坐标Y
*/
transferToScene(x, y) {
if (Scene.binding && !this.destroyed) {
this.parent?.remove(this)
this.setPosition(x, y)
this.targetManager.reset()
Scene.actors.append(this)
}
}
/** 销毁全局角色 */
destroy() {
if (ActorManager.get(this.data.id) === this) {
// 如果角色还存在于管理器中,释放资源
this.parent?.remove(this)
this.navigator.stopMoving()
this.targetManager.reset()
this.animationManager.release()
} else {
super.destroy()
}
}
}
// ******************************** 角色动画管理器类 ********************************
class AnimationManager {
constructor(actor) {
this.actor = actor
this.scale = actor.scale
this.existParticles = false
this.list = []
this.keyMap = {}
}
/**
* 获取动画播放器
* @param {string} key 动画键
* @returns {Animation|undefined}
*/
get(key) {
return this.keyMap[key]
}
/**
* 设置动画播放器
* @param {string} key 动画键
* @param {Animation} animation 动画播放器
*/
set(key, animation) {
if (key && this.keyMap[key] !== animation) {
animation.key = key
animation.parent = this
animation.setPosition(this.actor)
// 设置原始缩放系数
if (animation.rawScale === undefined) {
animation.rawScale = animation.scale
animation.scale *= this.scale
}
// 设置原始偏移Y
if (animation.rawOffsetY === undefined) {
animation.rawOffsetY = animation.offsetY
animation.offsetY *= this.scale
}
// 如果存在旧的动画替换它(销毁)
const oldAnim = this.keyMap[key]
if (oldAnim instanceof Animation) {
// 继承一部分数据
animation.rawScale = oldAnim.rawScale
animation.scale = oldAnim.scale
animation.speed = oldAnim.speed
animation.opacity = oldAnim.opacity
animation.setMotion(oldAnim.motionName)
animation.setAngle(oldAnim.angle)
oldAnim.destroy()
this.list.replace(oldAnim, animation)
this.keyMap[key] = animation
} else {
this.list.push(animation)
this.keyMap[key] = animation
}
this.sort()
}
}
/**
* 删除动画播放器
* @param {string} key 动画键
*/
delete(key) {
const animation = this.keyMap[key]
if (animation) {
animation.destroy()
this.list.remove(animation)
delete this.keyMap[key]
}
}
/**
* 播放动作(结束时恢复动作)
* @param {string} key 动画键
* @param {string} motionName 动作名称
* @returns {Animation|undefined}
*/
playMotion(key, motionName) {
const animation = this.get(key)
if (animation?.setMotion(motionName)) {
animation.playing = true
// 重新播放动画
const callback = () => {
if (animation.playing) {
// 播放结束后设置回默认动作
animation.playing = false
if (animation.setMotion(animation.defaultMotion)) {
animation.restart()
}
} else {
animation.onFinish(callback)
}
}
animation.restart()
animation.onFinish(callback)
// 返回动画对象
return animation
}
return undefined
}
/**
* 停止播放动画动作
* @param {string} key 动画键
*/
stopMotion(key) {
this.get(key)?.finish()
}
/**
* 设置全局缩放系数
* @param {string} key 动画键
* @param {number} scale 缩放系数
*/
setGlobalScale(scale) {
this.scale = scale
for (const animation of this.list) {
animation.scale = animation.rawScale * scale
animation.offsetY = animation.rawOffsetY * scale
}
}
/**
* 设置动画缩放系数
* @param {string} key 动画键
* @param {number} scale 缩放系数
*/
setScale(key, scale) {
const animation = this.keyMap[key]
if (animation) {
animation.rawScale = scale
animation.scale = scale * this.scale
}
}
/**
* 设置动画角度
* @param {number} angle 角度(弧度)
*/
setAngle(angle) {
for (const animation of this.list) {
if (animation.syncAngle) {
if (animation.playing) {
animation.playing = false
animation.setAngle(angle)
animation.playing = true
} else {
animation.setAngle(angle)
}
}
}
}
/**
* 设置动画优先级
* @param {string} key 动画键
* @param {number} priority 排序优先级
*/
setPriority(key, priority) {
const animation = this.keyMap[key]
if (animation) {
animation.priority = priority
this.sort()
}
}
/**
* 设置动画垂直偏移距离
* @param {string} key 动画键
* @param {number} offsetY 垂直偏移
*/
setOffsetY(key, offsetY) {
const animation = this.keyMap[key]
if (animation) {
animation.rawOffsetY = offsetY
animation.offsetY = offsetY * this.scale
}
}
/**
* 设置角色的精灵图
* @param {string} key 动画键
* @param {string} spriteId 精灵图ID
* @param {string} imageId 图像文件ID
*/
setSprite(key, spriteId, imageId) {
const animation = this.keyMap[key]
if (animation && spriteId) {
// 创建优先精灵图像映射表
if (!animation.priorityImages) {
animation.priorityImages = {}
animation.setSpriteImages(animation.priorityImages)
}
// 修改角色精灵表中的键值
animation.priorityImages[spriteId] = imageId
// 删除精灵图像 - 暂时保留
// if (animation.priorityImages) {
// // 删除优先精灵图像
// delete animation.priorityImages[spriteId]
// // 如果优先精灵图像映射表为空,删除它并恢复默认图像映射表
// if (Object.keys(animation.priorityImages).length === 0) {
// animation.restoreSpriteImages()
// delete animation.priorityImages
// }
// }
// 如果角色动画已经加载了同名纹理,则删除
animation?.deleteTexture(spriteId)
}
}
/** 排序动画组件 */
sort() {
this.list.sort(AnimationManager.sorter)
}
/**
* 更新角色动画播放进度
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
this.existParticles = false
for (const animation of this.list) {
animation.update(deltaTime)
if (animation.existParticles) {
this.existParticles = true
}
}
}
/**
* 绘制角色动画
*/
draw() {
for (const animation of this.list) {
animation.draw()
}
}
/**
* 激活管理器中的动画
* @param {number} x 动画的场景X
* @param {number} y 动画的场景Y
*/
activate(drawX, drawY, lightX, lightY) {
for (const animation of this.list) {
animation.activate(drawX, drawY, lightX, lightY)
}
}
/** 释放所有动画组件显存 */
release() {
for (const animation of this.list) {
animation.release()
}
}
/** 销毁所有动画组件 */
destroy() {
for (const animation of this.list) {
// 完成动画结束回调并销毁动画
animation.finish()
animation.destroy()
}
}
/** 保存动画组件列表数据 */
saveData() {
const length = this.list.length
const animations = new Array(length)
for (let i = 0; i < length; i++) {
const animation = this.list[i]
// 编码为json时忽略undefined
animations[i] = {
id: animation.data.id,
key: animation.key,
rotatable: animation.rotatable,
syncAngle: animation.syncAngle,
angle: animation.angle,
scale: animation.rawScale,
speed: animation.speed,
opacity: animation.opacity,
priority: animation.priority,
offsetY: animation.rawOffsetY,
motion: animation.defaultMotion ?? undefined,
images: animation.priorityImages ?? undefined,
}
}
return animations
}
/**
* 加载动画组件列表数据
* @param {Object[]} animations
*/
loadData(animations) {
this.scale = this.actor.scale
for (const savedData of animations) {
const data = Data.animations[savedData.id]
if (data) {
const animation = new Animation(data)
animation.key = savedData.key
animation.playing = false
animation.rotatable = savedData.rotatable
animation.syncAngle = savedData.syncAngle
animation.rawScale = savedData.scale
animation.scale = savedData.scale * this.scale
animation.speed = savedData.speed
animation.opacity = savedData.opacity
animation.priority = savedData.priority
animation.rawOffsetY = savedData.offsetY
animation.offsetY = savedData.offsetY * this.scale
animation.parent = this
animation.setPosition(this.actor)
animation.setAngle(savedData.angle)
if (savedData.motion) {
animation.defaultMotion = savedData.motion
animation.setMotion(savedData.motion)
}
if (savedData.images) {
animation.priorityImages = savedData.images
animation.setSpriteImages(savedData.images)
}
this.list.push(animation)
this.keyMap[animation.key] = animation
}
}
}
/** 动画组件排序器函数 */
static sorter = (a, b) => a.priority - b.priority
}
// ******************************** 技能管理器类 ********************************
class SkillManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 技能ID映射表
* @type {Object}
*/ idMap
/** 技能冷却列表
* @type {CooldownList}
*/ cooldownList
/** 技能管理器版本(随着技能添加和移除发生变化)
* @type {number}
*/ version
/**
* 角色技能管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.idMap = {}
this.cooldownList = new SkillCooldownList(actor)
this.version = 0
}
/**
* 获取角色技能
* @param {string} id 技能文件ID
* @returns {Skill|undefined}
*/
get(id) {
return this.idMap[id]
}
/**
* 添加角色技能
* @param {Skill} skill 技能实例
*/
add(skill) {
const {id} = skill
const {idMap} = this
// 如果不存在该技能,则添加,并触发技能添加事件
if (!idMap[id]) {
idMap[id] = skill
this.version++
skill.parent = this
skill.emit('skilladd')
}
}
/**
* 移除角色技能
* @param {Skill} skill 技能实例
*/
remove(skill) {
const {id} = skill
const {idMap} = this
// 如果存在该技能,则移除,并触发技能移除事件
if (idMap[id] === skill) {
delete idMap[id]
this.version++
skill.emit('skillremove')
skill.parent = null
}
}
/**
* 删除角色技能
* @param {string} id 技能文件ID
*/
delete(id) {
// 从管理器中移除指定ID的技能
const skill = this.idMap[id]
if (skill) this.remove(skill)
}
/** 自动排序技能列表 */
sort() {
const idMap = {}
// 使用idMap创建技能列表,并通过文件名排序
const list = Object.values(this.idMap).sort(
(a, b) => a.data.filename.localeCompare(b.data.filename)
)
// 遍历技能列表,重构idMap
const length = list.length
for (let i = 0; i < length; i++) {
const skill = list[i]
idMap[skill.id] = skill
}
this.idMap = idMap
this.version++
}
/** 保存技能列表数据 */
saveData() {
const skills = Object.values(this.idMap)
const length = skills.length
for (let i = 0; i < length; i++) {
skills[i] = skills[i].saveData()
}
return skills
}
/**
* 加载技能列表数据
* @param {Object[]} skills
*/
loadData(skills) {
const dataMap = Data.skills
const {idMap, cooldownList} = this
for (const savedData of skills) {
const id = savedData.id
const data = dataMap[id]
if (data) {
// 重新创建技能实例
const skill = new Skill(data, savedData)
idMap[id] = skill
skill.parent = this
// 如果技能正在冷却中
// 添加到技能冷却列表
if (skill.cooldown !== 0) {
cooldownList.append(skill)
}
}
}
}
}
// ******************************** 技能冷却列表类 ********************************
class SkillCooldownList extends Array {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/**
* 角色技能冷却列表
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
super()
this.actor = actor
}
/**
* 添加角色技能
* @param {Skill} skill 技能实例
*/
append(skill) {
// 如果列表为空,延迟将本列表添加到角色的更新器列表中
if (this.length === 0) {
Callback.push(() => {
this.actor.updaters.add(this)
})
}
super.append(skill)
}
/**
* 更新列表中的技能冷却时间
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
let i = this.length
// 逆序遍历冷却中的技能
while (--i >= 0) {
// 如果冷却结束,则将技能从列表中移除
if ((this[i].cooldown -= deltaTime) <= 0) {
this[i].cooldown = 0
this[i].duration = 0
this.splice(i, 1)
// 如果列表为空,延迟将本列表从角色的更新器列表中移除
if (this.length === 0) {
Callback.push(() => {
this.actor.updaters.remove(this)
})
}
}
}
}
}
// ******************************** 技能类 ********************************
class Skill {
/** 技能文件ID
* @type {string}
*/ id
/** 技能文件数据
* @type {Object}
*/ data
/** 技能图标文件ID
* @type {string}
*/ icon
/** 技能图标矩形裁剪区域
* @type {Array<number>}
*/ clip
/** 技能当前冷却时间
* @type {number}
*/ cooldown
/** 技能持续冷却时间
* @type {number}
*/ duration
/** 技能属性映射表
* @type {Object}
*/ attributes
/** 技能事件映射表
* @type {Object}
*/ events
/** 技能脚本管理器
* @type {Script}
*/ script
/** 技能管理器
* @type {SkillManager|null}
*/ parent
/**
* 角色技能对象
* @param {SkillFile} data 技能文件数据
* @param {Object} [savedData] 技能存档数据
*/
constructor(data, savedData) {
this.id = data.id
this.data = data
this.icon = data.icon
this.clip = data.clip
this.cooldown = 0
this.duration = 0
this.attributes = null
this.events = data.events
this.script = Script.create(this, data.scripts)
this.parent = null
Skill.latest = this
if (savedData) {
// 加载存档数据
this.cooldown = savedData.cooldown
this.duration = savedData.duration
this.attributes = savedData.attributes
} else {
// 初始化
Attribute.loadEntries(
this.attributes = {},
data.attributes,
)
}
}
/** 读取技能冷却进度 */
get progress() {
return this.cooldown === 0 ? 0 : this.cooldown / this.duration
}
/** 施放角色技能 */
cast() {
// 如果冷却结束且施放角色已激活,返回技能释放事件
if (this.cooldown === 0 &&
this.parent?.actor.isActive()) {
return this.emit('skillcast')
}
}
/**
* 设置技能的冷却时间
* @param {number} cooldown 冷却时间(毫秒)
*/
setCooldown(cooldown) {
if (cooldown >= 0 &&
this.cooldown !== cooldown) {
this.cooldown = cooldown
this.duration = cooldown
// 添加技能到冷却列表
this.parent?.cooldownList.append(this)
}
}
/**
* 增加技能的冷却时间
* @param {number} cooldown 冷却时间(毫秒)
*/
increaseCooldown(cooldown) {
if (cooldown > 0) {
this.cooldown += cooldown
this.duration = Math.max(this.cooldown, this.duration)
// 添加技能到冷却列表
this.parent?.cooldownList.append(this)
}
}
/**
* 减少技能的冷却时间
* @param {number} cooldown 冷却时间(毫秒)
*/
decreaseCooldown(cooldown) {
if (cooldown > 0) {
this.cooldown = Math.max(this.cooldown - cooldown, 0)
}
}
/** 移除角色技能 */
remove() {
this.parent?.remove(this)
}
/**
* 调用技能事件
* @param {string} type 技能事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const actor = this.parent?.actor
const commands = this.events[type]
switch (type) {
case 'skilladd':
case 'skillremove':
EventManager.emit(type, null, {
triggerSkill: this,
triggerActor: actor,
casterActor: actor,
})
break
}
if (commands) {
const event = new EventHandler(commands)
event.triggerSkill = this
event.triggerActor = actor
event.casterActor = actor
EventHandler.call(event, actor?.updaters)
return event
}
}
/**
* 调用技能事件和脚本
* @param {string} type 技能事件类型
* @returns {EventHandler|undefined}
*/
emit(type) {
const event = this.callEvent(type)
this.script.emit(type, this)
return event
}
/** 保存技能数据 */
saveData() {
return {
id: this.id,
cooldown: this.cooldown,
duration: this.duration,
attributes: this.attributes,
}
}
/** 最新创建技能
* @type {Skill|undefined}
*/ static latest
}
// ******************************** 状态管理器类 ********************************
class StateManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 状态ID映射表
* @type {Object}
*/ idMap
/** 状态倒计时列表
* @type {StateCountdownList}
*/ countdownList
/** 状态管理器版本(随着状态添加和移除发生变化)
* @type {number}
*/ version
/**
* 角色状态管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.idMap = {}
this.countdownList = new StateCountdownList(this)
this.version = 0
}
/**
* 获取角色状态
* @param {string} id 状态文件ID
* @returns {State|undefined}
*/
get(id) {
return this.idMap[id]
}
/**
* 添加角色状态
* @param {State} state 状态实例
*/
add(state) {
const {id} = state
const {idMap} = this
// 如果存在该状态,先移除
if (id in idMap) {
this.remove(idMap[id])
}
idMap[id] = state
this.version++
this.countdownList.append(state)
state.parent = this
state.emit('stateadd')
}
/**
* 移除角色状态
* @param {State} state 状态实例
*/
remove(state) {
const {id} = state
const {idMap} = this
// 如果存在该状态,则移除,并触发状态移除事件
if (idMap[id] === state) {
delete idMap[id]
this.version++
this.countdownList.remove(state)
state.emit('stateremove')
state.parent = null
}
}
/**
* 删除角色状态
* @param {string} id 状态文件ID
*/
delete(id) {
// 从管理器中移除指定ID的状态
const state = this.idMap[id]
if (state) this.remove(state)
}
/** 保存状态列表数据 */
saveData() {
const states = Object.values(this.idMap)
const length = states.length
for (let i = 0; i < length; i++) {
states[i] = states[i].saveData()
}
return states
}
/**
* 加载状态列表数据
* @param {Object[]} states
*/
loadData(states) {
for (const savedData of states) {
const id = savedData.id
const data = Data.states[id]
if (data) {
// 重新创建状态实例
const state = new State(data, savedData)
this.countdownList.append(state)
this.idMap[id] = state
state.parent = this
}
}
}
}
// ******************************** 状态倒计时列表类 ********************************
class StateCountdownList extends Array {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 状态管理器
* @type {StateManager}
*/ manager
/**
* 角色状态倒计时列表
* @param {StateManager} stateManager 角色状态管理器实例
*/
constructor(stateManager) {
super()
this.actor = stateManager.actor
this.manager = stateManager
}
/**
* 添加角色状态
* @param {State} state 状态实例
*/
append(state) {
if (state.currentTime === 0) return
// 如果列表为空,延迟将本列表添加到角色的更新器列表中
if (this.length === 0) {
Callback.push(() => {
this.actor.updaters.add(this)
})
}
super.append(state)
}
/**
* 移除角色状态
* @param {State} state 状态实例
*/
remove(state) {
// 如果存在该状态,则移除
const i = this.indexOf(state)
if (i !== -1) {
this.splice(i, 1)
// 如果列表为空,延迟将本列表从角色的更新器列表中移除
if (this.length === 0) {
Callback.push(() => {
this.actor.updaters.remove(this)
})
}
}
}
/**
* 更新列表中的状态剩余时间
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
let i = this.length
// 逆序遍历倒计时中的状态
while (--i >= 0) {
const state = this[i]
state.autorun()
state.updaters.update(deltaTime)
// 如果倒计时结束,则将状态从列表中移除
if ((state.currentTime -= deltaTime) <= 0) {
state.currentTime = 0
this.manager.remove(state)
}
}
}
}
// ******************************** 状态类 ********************************
class State {
/** 状态文件ID
* @type {string}
*/ id
/** 状态文件数据
* @type {Object}
*/ data
/** 状态图标文件ID
* @type {string}
*/ icon
/** 状态图标矩形裁剪区域
* @type {Array<number>}
*/ clip
/** 状态当前时间
* @type {number}
*/ currentTime
/** 状态持续时间
* @type {number}
*/ duration
/** 状态更新器列表
* @type {ModuleList}
*/ updaters
/** 状态属性映射表
* @type {Object}
*/ attributes
/** 技能施放者
* @type {Actor|null}
*/ caster
/** 状态事件映射表
* @type {Object}
*/ events
/** 状态脚本管理器
* @type {Script}
*/ script
/** 状态管理器
* @type {StateManager|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
/**
* 角色状态对象
* @param {StateFile} data 状态文件数据
* @param {Object} [savedData] 状态存档数据
*/
constructor(data, savedData) {
this.id = data.id
this.data = data
this.icon = data.icon
this.clip = data.clip
this.currentTime = 0
this.duration = 0
this.updaters = new ModuleList()
this.attributes = null
this.caster = null
this.events = data.events
this.script = Script.create(this, data.scripts)
this.parent = null
this.started = false
State.latest = this
if (savedData) {
// 加载存档数据
this.currentTime = savedData.currentTime
this.duration = savedData.duration
this.attributes = savedData.attributes
if (savedData.caster) {
Callback.push(() => {
this.caster = EntityManager.get(savedData.caster) ?? null
})
}
} else {
// 初始化
Attribute.loadEntries(
this.attributes = {},
data.attributes,
)
}
}
/**
* 设置角色状态的时间
* @param {number} time 持续时间(毫秒)
*/
setTime(time) {
if (time >= 0) {
this.currentTime = time
this.duration = time
}
}
/**
* 增加角色状态的时间
* @param {number} time 持续时间(毫秒)
*/
increaseTime(time) {
if (time > 0) {
this.currentTime += time
this.duration = Math.max(this.currentTime, this.duration)
}
}
/**
* 减少角色状态的时间
* @param {number} time 持续时间(毫秒)
*/
decreaseTime(time) {
if (time > 0) {
this.currentTime = Math.max(this.currentTime - time, 0)
}
}
/**
* 调用状态事件
* @param {string} type 状态事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const actor = this.parent?.actor
const caster = this.caster ?? undefined
const commands = this.events[type]
switch (type) {
case 'stateadd':
case 'stateremove':
EventManager.emit(type, null, {
triggerState: this,
triggerActor: actor,
casterActor: caster,
})
break
}
if (commands) {
const event = new EventHandler(commands)
event.triggerState = this
event.triggerActor = actor
event.casterActor = caster
EventHandler.call(event, this.updaters)
return event
}
}
/**
* 调用状态事件和脚本
* @param {string} type 状态事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 保存状态数据 */
saveData() {
return {
id: this.id,
caster: this.caster?.entityId ?? '',
currentTime: this.currentTime,
duration: this.duration,
attributes: this.attributes,
}
}
/** 最新创建状态
* @type {State|undefined}
*/ static latest
}
// ******************************** 装备管理器类 ********************************
class EquipmentManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 装备槽->装备映射表
* @type {Object}
*/ slotMap
/** 装备管理器版本(随着装备添加和移除发生变化)
* @type {number}
*/ version
/**
* 角色装备管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.slotMap = {}
this.version = 0
}
/**
* 获取角色装备
* @param {string} slot 装备槽
* @returns {Equipment|undefined}
*/
get(slot) {
return this.slotMap[slot]
}
/**
* 设置角色装备
* @param {string} slot 装备槽
* @param {Equipment} equipment 装备实例
*/
set(slot, equipment) {
if (this.actor.active && slot && equipment.parent !== this) {
// 先从其他管理器中移除该装备
equipment.remove()
// 如果槽已被占用,则移除槽对应的装备
const slotMap = this.slotMap
const holder = slotMap[slot]
if (holder) {
this.remove(holder)
}
// 设置装备槽对应值为该装备,并发送装备添加事件
slotMap[slot] = equipment
this.version++
equipment.slot = slot
equipment.parent = this
equipment.emit('equipmentadd')
}
}
/**
* 移除角色装备
* @param {Equipment} equipment 装备实例
*/
remove(equipment) {
if (this.actor.active && equipment.parent === this) {
// 从管理器中移除该装备,重置键位,并发送装备移除事件
delete this.slotMap[equipment.slot]
this.version++
equipment.slot = ''
equipment.emit('equipmentremove')
equipment.parent = null
// 在角色库存中插入移除的装备
this.actor.inventory.insert(equipment)
}
}
/**
* 删除角色装备
* @param {string} slot 装备槽
*/
delete(slot) {
// 从管理器中移除指定键的装备
const equipment = this.slotMap[slot]
if (equipment) this.remove(equipment)
}
/**
* 通过ID获取装备
* @param {string} equipmentId 装备文件ID
* @returns {Equipment|undefined} 装备实例
*/
getById(equipmentId) {
for (const equipment of Object.values(this.slotMap)) {
if (equipment.id === equipmentId) return equipment
}
return undefined
}
/** 保存装备列表数据 */
saveData() {
const data = Object.values(this.slotMap)
const length = data.length
for (let i = 0; i < length; i++) {
data[i] = data[i].saveData()
}
return data
}
/**
* 加载装备列表数据
* @param {Object[]} equipments
*/
loadData(equipments) {
for (const savedData of equipments) {
const data = Data.equipments[savedData.id]
if (data) {
// 重新创建装备实例
const equipment = new Equipment(data, savedData)
equipment.parent = this
this.slotMap[savedData.slot] = equipment
}
}
}
}
// ******************************** 装备类 ********************************
class Equipment {
/** 装备文件ID
* @type {string}
*/ id
/** 装备槽
* @type {string}
*/ slot
/** 装备在库存中的位置
* 如果不在库存中为-1
* @type {number}
*/ order
/** 装备文件数据
* @type {Object}
*/ data
/** 装备图标文件ID
* @type {string}
*/ icon
/** 装备图标矩形裁剪区域
* @type {Array<number>}
*/ clip
/** 装备属性映射表
* @type {Object}
*/ attributes
/** 装备事件映射表
* @type {Object}
*/ events
/** 装备脚本管理器
* @type {Script}
*/ script
/** 父级对象
* @type {Inventory|EquipmentManager|null}
*/ parent
/**
* 角色装备对象
* @param {EquipmentFile} data 装备文件数据
* @param {Object} [savedData] 装备存档数据
*/
constructor(data, savedData) {
this.id = data.id
this.slot = ''
this.order = -1
this.data = data
this.icon = data.icon
this.clip = data.clip
this.attributes = null
this.events = data.events
this.script = Script.create(this, data.scripts)
this.parent = null
Equipment.latest = this
if (savedData) {
// 加载存档数据
this.slot = savedData.slot
this.order = savedData.order
this.attributes = savedData.attributes
} else {
// 初始化
Attribute.loadEntries(
this.attributes = {},
data.attributes,
)
this.emit('create')
}
}
/**
* 穿上角色装备(共享库存的代价:需要传递事件触发角色)
* @param {string} slot 装备槽
* @param {Actor} [actor] 事件触发角色
*/
equip(slot, actor = this.parent?.actor) {
if (this.parent instanceof Inventory) {
actor?.equipmentManager.set(slot, this)
}
}
/** 移除角色装备 */
remove() {
this.parent?.remove(this)
}
/**
* 调用装备事件
* @param {string} type 装备事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const actor = this.parent?.actor
const commands = this.events[type]
switch (type) {
case 'equipmentadd':
case 'equipmentremove':
case 'equipmentgain':
EventManager.emit(type, null, {
triggerActor: actor,
triggerEquipment: this,
})
break
}
if (commands) {
const event = new EventHandler(commands)
event.triggerActor = actor
event.triggerEquipment = this
EventHandler.call(event, actor?.updaters)
return event
}
}
/**
* 调用装备事件和脚本
* @param {string} type 装备事件类型
* @param {Actor} [actor] 事件触发角色
*/
emit(type, actor) {
this.callEvent(type, actor)
this.script.emit(type, this)
}
/** 保存装备数据 */
saveData() {
return {
id: this.id,
slot: this.slot,
order: this.order,
attributes: this.attributes,
}
}
/** 最新创建装备
* @type {Equipment|undefined}
*/ static latest
}
// ******************************** 物品类 ********************************
class Item {
/** 物品文件ID
* @type {string}
*/ id
/** 物品在库存中的位置
* 如果不在库存中为-1
* @type {number}
*/ order
/** 物品文件数据
* @type {Object}
*/ data
/** 物品图标文件ID
* @type {string}
*/ icon
/** 物品图标矩形裁剪区域
* @type {Array<number>}
*/ clip
/** 物品数量
* @type {number}
*/ quantity
/** 物品属性映射表
* @type {Object}
*/ attributes
/** 物品事件映射表
* @type {Object}
*/ events
/** 物品脚本管理器
* @type {Script}
*/ script
/** 父级对象
* @type {Inventory|null}
*/ parent
/**
* 角色物品对象
* @param {ItemFile} data 物品文件数据
* @param {Object} [savedData] 物品存档数据
*/
constructor(data, savedData) {
this.id = data.id
this.order = -1
this.data = data
this.icon = data.icon
this.clip = data.clip
this.quantity = 0
this.attributes = data.attributes
this.events = data.events
this.script = Script.create(this, data.scripts)
this.parent = null
if (savedData) {
// 加载存档数据
this.order = savedData.order
this.quantity = savedData.quantity
}
}
/**
* 使用角色物品
* @param {Actor|undefined} actor
* @returns {EventHandler|undefined}
*/
use(actor = this.parent?.actor) {
// 如果数量大于0,则返回物品使用事件
if (this.quantity > 0 && actor?.isActive()) {
return this.emit('itemuse', actor)
}
}
/**
* 增加物品的数量
* @param {number} quantity 物品数量
*/
increase(quantity) {
const {parent} = this
if (parent && quantity > 0) {
this.quantity += quantity
parent.version++
}
}
/**
* 减少物品的数量,当物品数量不够时将被从库存中移除
* @param {number} quantity 物品数量
*/
decrease(quantity) {
const {parent} = this
if (parent && quantity > 0) {
this.quantity -= quantity
// 如果物品数量不足,则移除
if (this.quantity <= 0) {
this.quantity = 0
this.remove()
}
parent.version++
}
}
/** 将货物从库存中移除 */
remove() {
this.parent?.remove(this)
}
/**
* 调用物品事件(共享库存的代价:需要传递事件触发角色)
* @param {string} type 物品事件类型
* @param {Actor} [actor] 事件触发角色
* @returns {EventHandler|undefined}
*/
callEvent(type, actor = this.parent?.actor) {
const commands = this.events[type]
switch (type) {
case 'itemgain':
EventManager.emit(type, null, {
triggerActor: actor,
triggerItem: this,
})
break
}
if (commands) {
const event = new EventHandler(commands)
event.triggerActor = actor
event.triggerItem = this
EventHandler.call(event, actor?.updaters)
return event
}
}
/**
* 调用物品事件和脚本
* @param {string} type 物品事件类型
* @param {Actor} [actor] 事件触发角色
* @returns
*/
emit(type, actor) {
const event = this.callEvent(type, actor)
this.script.emit(type, this)
return event
}
/** 保存物品数据 */
saveData() {
return {
id: this.id,
order: this.order,
quantity: this.quantity,
}
}
// 静态 - 最新获得物品
static latest
// 静态 - 最新获得物品的增量
static increment = 0
}
// ******************************** 库存类 ********************************
class Inventory {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 库存中的金钱
* @type {number}
*/ money
/** 预测下一个空槽的插入位置
* @type {number}
*/ pointer
/** 库存中的货物数量
* @type {number}
*/ size
/** 库存货物列表
* @type {Array<Item|Equipment>}
*/ list
/** ID->货物集合映射表
* @type {Object}
*/ idMap
/** 库存管理器版本(随着货物添加和移除发生变化)
* @type {number}
*/ version
/**
* 角色库存管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.money = 0
this.pointer = 0
this.size = 0
this.list = []
this.idMap = {}
this.version = 0
}
/**
* 获取库存货物
* @param {string} id 物品文件ID
* @returns {Item|Equipment|undefined}
*/
get(id) {
return this.idMap[id]?.[0]
}
/**
* 获取库存货物列表
* @param {string} id 物品文件ID
* @returns {Array<Item>|Array<Equipment>|undefined}
*/
getList(id) {
return this.idMap[id]
}
/** 重置库存中的物品、装备、金币 */
reset() {
// 遍历库存中的所有物品装备,重置属性
for (const goods of this.list) {
goods.parent = null
goods.order = -1
}
// 重置库存属性
this.money = 0
this.pointer = 0
this.size = 0
this.list = []
this.idMap = {}
this.version++
}
/**
* 插入物品或装备到库存中的空位置
* @param {Item|Equipment} goods 插入对象
*/
insert(goods) {
if (goods.parent === null) {
// 将物品插入到空槽位
let i = this.pointer
const {list} = this
while (list[i]?.order === i) {i++}
list.splice(i, 0, goods)
goods.order = i
goods.parent = this
// 将物品添加到映射表
this.addToMap(goods)
// 设置空槽位起始查找位置
this.pointer = i + 1
this.size++
this.version++
}
}
/**
* 从库存中移除物品或装备
* @param {Item|Equipment} goods 移除对象
*/
remove(goods) {
if (goods.parent === this) {
const {list} = this
const i = list.indexOf(goods)
list.splice(i, 1)
goods.order = -1
goods.parent = null
// 将物品从映射表中移除
this.removeFromMap(goods)
// 设置空槽位起始查找位置
if (this.pointer > i) {
this.pointer = i
}
this.size--
this.version++
}
}
/**
* 添加物品或装备到映射表
* @param {Item|Equipment} goods 添加对象
*/
addToMap(goods) {
if (this.idMap[goods.id]) {
this.idMap[goods.id].push(goods)
} else {
this.idMap[goods.id] = [goods]
}
}
/**
* 从映射表中移除物品或装备
* @param {Item|Equipment} goods 移除对象
*/
removeFromMap(goods) {
this.idMap[goods.id].remove(goods)
if (this.idMap[goods.id].length === 0) {
delete this.idMap[goods.id]
}
}
/**
* 交换物品或装备(如果存在)在库存中的位置
* @param {number} order1 货物1的位置
* @param {number} order2 货物2的位置
*/
swap(order1, order2) {
if (order1 >= 0 && order2 >= 0 && order1 !== order2) {
// 确保order1小于order2
if (order1 > order2) {
const temp = order1
order1 = order2
order2 = temp
}
const {list} = this
const goods1 = list.find(a => a.order === order1)
const goods2 = list.find(a => a.order === order2)
if (goods1 && goods2) {
// 同时存在两个物品
const pos1 = list.indexOf(goods1)
const pos2 = list.indexOf(goods2)
goods1.order = order2
goods2.order = order1
list[pos1] = goods2
list[pos2] = goods1
this.version++
} else if (goods1) {
// 存在索引较小的物品
const pos1 = list.indexOf(goods1)
list.splice(pos1, 1)
let pos2 = pos1
const {length} = list
while (pos2 < length) {
if (list[pos2].order > order2) {
break
}
pos2++
}
goods1.order = order2
list.splice(pos2, 0, goods1)
this.version++
// 设置空槽位起始查找位置
if (this.pointer > pos1) {
this.pointer = pos1
}
} else if (goods2) {
// 存在索引较大的物品
const pos2 = list.indexOf(goods2)
list.splice(pos2, 1)
let pos1 = pos2
while (--pos1 >= 0) {
if (list[pos1].order < order1) {
pos1++
break
}
}
pos1 = Math.max(pos1, 0)
goods2.order = order1
list.splice(pos1, 0, goods2)
this.version++
}
}
}
/**
* 排序库存中的对象
* @param {boolean} [byOrder = false] 如果设置为true,则物品优先于装备,通过文件名排序
*/
sort(byOrder = false) {
const {list} = this
const {length} = list
// 如果通过文件名排序
if (byOrder) list.sort((a, b) => {
const typeA = typeof a.quantity
const typeB = typeof b.quantity
// 物品优先于装备,然后再比较文件名
if (typeA !== typeB) {
return typeA === 'number' ? -1 : 1
}
return a.data.filename.localeCompare(b.data.filename)
})
// 遍历物品列表,更新索引
for (let i = 0; i < length; i++) {
list[i].order = i
}
this.pointer = length
this.version++
}
/**
* 查找指定的物品或装备数量
* @param {string} id 物品或装备的文件ID
* @returns {number}
*/
count(id) {
const list = this.getList(id)
if (!list) return 0
let count = 0
for (const goods of list) {
count += goods.quantity ?? 1
}
return count
}
//
/**
* 增加库存中的金钱
* @param {number} money 金钱数量
*/
increaseMoney(money) {
this.money += Math.max(money, 0)
Inventory.moneyIncrement = money
EventManager.emit('moneygain', null, {
triggerActor: this.actor,
})
}
/**
* 减少库存中的金钱
* @param {number} money 金钱数量
*/
decreaseMoney(money) {
this.money -= Math.max(money, 0)
}
/**
* 在库存中创建物品实例
* @param {string} id 物品文件ID
* @param {number} quantity 物品数量
*/
createItems(id, quantity) {
const data = Data.items[id]
if (data && quantity > 0) {
const item = new Item(data)
// 插入到库存
Item.latest = item
Item.increment = quantity
this.insert(item)
item.increase(quantity)
item.callEvent('itemgain')
}
}
/**
* 在库存中增加物品数量(如果找不到物品,新建一个实例)
* @param {string} id 物品文件ID
* @param {number} quantity 物品数量
*/
increaseItems(id, quantity) {
const item = this.get(id)
// 如果存在该物品,则增加数量,否则创建物品
if (item) {
Item.latest = item
Item.increment = quantity
item.increase(quantity)
item.callEvent('itemgain')
}
else {
this.createItems(id, quantity)
}
}
/**
* 在库存中减少物品数量(从多个物品实例中减去足够的数量)
* @param {string} id 物品文件ID
* @param {number} quantity 物品数量
*/
decreaseItems(id, quantity) {
const {list} = this
let i = list.length
while (--i >= 0) {
const item = list[i]
if (item.id === id) {
// 查找物品并减少数量
if (item.quantity >= quantity) {
item.decrease(quantity)
return
}
// 如果数量不够,继续查找
quantity -= item.quantity
item.decrease(item.quantity)
}
}
}
/**
* 在库存中创建装备实例(通过文件ID)
* @param {string} id 装备文件ID
*/
createEquipment(id) {
const data = Data.equipments[id]
if (data) {
this.gainEquipment(new Equipment(data))
}
}
/**
* 从库存中删除装备实例(通过文件ID)
* @param {string} id 装备文件ID
*/
deleteEquipment(id) {
const equipment = this.get(id)
if (equipment instanceof Equipment) {
this.loseEquipment(equipment)
}
}
/**
* 添加装备实例到库存
* @param {Equipment} equipment 装备实例
*/
gainEquipment(equipment) {
if (equipment.parent !== this) {
equipment.remove()
this.insert(equipment)
equipment.callEvent('equipmentgain')
}
}
/**
* 从库存中移除装备实例
* @param {Equipment} equipment 装备实例
*/
loseEquipment(equipment) {
if (equipment.parent === this) {
this.remove(equipment)
}
}
/** 保存库存数据 */
saveData(actor) {
if (this.actor !== actor) {
return {
ref: this.actor.data.id,
...actor.savedInventory.saveData(actor),
}
}
const {list} = this
const {length} = list
const data = new Array(length)
for (let i = 0; i < length; i++) {
data[i] = list[i].saveData()
}
return {
list: data,
money: this.money,
}
}
/**
* 加载库存数据
* @param {Object} inventory
*/
loadData(inventory) {
if ('ref' in inventory) {
Inventory.references.push({
actor: this.actor,
ref: inventory.ref,
})
}
const {list} = this
for (const savedData of inventory.list) {
const {id} = savedData
if ('quantity' in savedData) {
// 如果是物品数据
const data = Data.items[id]
if (data) {
// 重新创建物品实例
const item = new Item(data, savedData)
item.parent = this
list.push(item)
this.addToMap(item)
this.size++
}
} else {
// 如果是装备数据
const data = Data.equipments[id]
if (data) {
// 重新创建装备实例
const equipment = new Equipment(data, savedData)
equipment.parent = this
list.push(equipment)
this.addToMap(equipment)
this.size++
}
}
}
this.money = inventory.money
// 设置空槽位起始查找位置
let i = 0
while (list[i]?.order === i) {i++}
this.pointer = i
}
// 金钱增量
static moneyIncrement = 0
// 引用库存延迟处理列表
static references = []
// 恢复库存引用
static reference() {
for (const {actor, ref} of this.references) {
const target = ActorManager.get(ref)
if (target instanceof GlobalActor) {
actor.useInventory(target.inventory)
}
}
}
}
// ******************************** 快捷栏管理器类 ********************************
class ShortcutManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 快捷键映射表
* @type {Object}
*/ keyMap
/** 快捷栏管理器版本(随着快捷键的设置和删除发生变化)
* @type {number}
*/ version
constructor(actor) {
this.actor = actor
this.keyMap = {}
this.version = 0
}
/**
* 获取快捷栏重映射数据
* @param {string} key 快捷键
* @returns {Object|undefined}
*/
get(key) {
return this.keyMap[key]
}
/**
* 获取快捷栏物品
* @param {string} key 快捷键
* @returns {Item|undefined}
*/
getItem(key) {
const shortcut = this.keyMap[key]
if (shortcut?.type === 'item') {
return this.actor.inventory.get(shortcut.id)
}
return undefined
}
/**
* 获取快捷栏技能
* @param {string} key 快捷键
* @returns {Skill|undefined}
*/
getSkill(key) {
const shortcut = this.keyMap[key]
if (shortcut?.type === 'skill') {
return this.actor.skillManager.get(shortcut.id)
}
return undefined
}
/**
* 获取快捷栏目标对象
* @param {string} key 快捷键
* @returns {Skill|Item|undefined}
*/
getTarget(key) {
const shortcut = this.keyMap[key]
switch (shortcut?.type) {
case 'skill':
return this.actor.skillManager.get(shortcut.id)
case 'item':
return this.actor.inventory.get(shortcut.id)
default:
return undefined
}
}
/**
* 设置快捷栏项目
* @param {string} key 快捷键
* @param {Item|Skill} 物品或技能实例
*/
set(key, target) {
if (!key) return
if (target instanceof Skill) {
this.keyMap[key] = new Shortcut('skill', key, target.id, target.data)
this.version++
return
}
if (target instanceof Item) {
this.keyMap[key] = new Shortcut('item', key, target.id, target.data)
this.version++
return
}
}
/**
* 设置快捷栏项目(文件ID)
* @param {string} key 快捷键
* @param {string} 物品或技能的文件ID
*/
setId(key, id) {
if (id in Data.skills) {
this.keyMap[key] = new Shortcut('skill', key, id, Data.skills[id])
this.version++
return
}
if (id in Data.items) {
this.keyMap[key] = new Shortcut('item', key, id, Data.items[id])
this.version++
return
}
}
/**
* 删除快捷栏项目
* @param {string} key 快捷键
*/
delete(key) {
if (key in this.keyMap) {
delete this.keyMap[key]
this.version++
}
}
/**
* 交换快捷栏项目
* @param {string} sKey 源快捷键
* @param {string} dKey 目标快捷键
*/
swap(sKey, dKey) {
if (sKey !== dKey && sKey && dKey) {
const map = this.keyMap
const sItem = map[sKey]
const dItem = map[dKey]
if (sItem) {
sItem.key = dKey
map[dKey] = sItem
} else {
delete map[dKey]
}
if (dItem) {
dItem.key = sKey
map[sKey] = dItem
} else {
delete map[sKey]
}
this.version++
}
}
/** 保存快捷栏数据 */
saveData() {
const list = Object.values(this.keyMap)
const length = list.length
const data = new Array(length)
for (let i = 0; i < length; i++) {
const shortcut = list[i]
data[i] = {
key: shortcut.key,
id: shortcut.id,
}
}
return data
}
/**
* 加载快捷栏数据
* @param {Array} shortcuts
*/
loadData(shortcuts) {
for (const shortcut of shortcuts) {
this.setId(shortcut.key, shortcut.id)
}
}
}
// ******************************** 快捷栏项目类 ********************************
class Shortcut {
/** 类型
* @type {string}
*/ type
/** 快捷键
* @type {string}
*/ key
/** 数据ID
* @type {string}
*/ id
/** 目标文件数据
* @type {Object}
*/ data
/** 图标文件ID
* @type {string}
*/ icon
/** 图标矩形裁剪区域
* @type {Array<number>}
*/ clip
/**
* 快捷栏项目
* @param {string} type 类型
* @param {string} key 快捷键
* @param {string} id 数据ID
* @param {Object} data 目标对象的文件数据
*/
constructor(type, key, id, data) {
this.type = type
this.key = key
this.id = id
this.data = data
this.icon = data.icon
this.clip = data.clip
}
}
// ******************************** 冷却(GCD)管理器类 ********************************
class CooldownManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 冷却键->冷却项目映射表
* @type {Object}
*/ keyMap
/** 冷却项目列表
* @type {Array<CooldownItem>}
*/ cooldownList
/**
* 角色公共冷却管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.keyMap = {}
this.cooldownList = []
}
/**
* 获取冷却项目
* @param {string} key 冷却键
* @returns {CooldownItem|undefined}
*/
get(key) {
return this.keyMap[key]
}
/**
* 创建冷却项目
* @param {string} key 冷却键
* @returns {CooldownItem}
*/
create(key) {
let item = this.keyMap[key]
// 如果不存在冷却项目,则新建一个
if (item === undefined) {
// 如果列表为空,延迟将本列表添加到角色的更新器列表中
if (this.cooldownList.length === 0) {
Callback.push(() => {
this.actor.updaters.add(this)
})
}
// 创建冷却项目
item = new CooldownItem(key)
this.keyMap[key] = item
this.cooldownList.append(item)
}
return item
}
/**
* 删除冷却项目
* @param {string} key 冷却键
*/
delete(key) {
const item = this.keyMap[key]
if (item) {
delete this.keyMap[key]
this.cooldownList.remove(item)
// 如果列表为空,延迟将本列表从角色的更新器列表中移除
if (this.cooldownList.length === 0) {
Callback.push(() => {
this.actor.updaters.remove(this)
})
}
}
}
/**
* 设置冷却时间
* @param {string} key 冷却键
* @param {number} cooldown 冷却时间
*/
setCooldown(key, cooldown) {
if (key && cooldown > 0) {
const item = this.create(key)
item.cooldown = cooldown
item.duration = cooldown
}
}
/**
* 增加冷却时间
* @param {string} key 冷却键
* @param {number} cooldown 冷却时间
*/
increaseCooldown(key, cooldown) {
if (key && cooldown > 0) {
const item = this.create(key)
item.cooldown += cooldown
item.duration = Math.max(item.cooldown, item.duration)
}
}
/**
* 减少冷却时间
* @param {string} key 冷却键
* @param {number} cooldown 冷却时间
*/
decreaseCooldown(key, cooldown) {
const item = this.keyMap[key]
if (item && cooldown > 0) {
item.cooldown -= cooldown
// 如果冷却结束,删除键
if (item.cooldown <= 0) {
this.delete(key)
}
}
}
/**
* 更新公共冷却时间
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
const {cooldownList} = this
let i = cooldownList.length
// 逆序遍历冷却列表
while (--i >= 0) {
// 如果冷却结束,删除键
if ((cooldownList[i].cooldown -= deltaTime) <= 0) {
this.delete(cooldownList[i].key)
}
}
}
/** 保存公共冷却列表数据 */
saveData() {
return this.cooldownList
}
/**
* 加载公共冷却列表数据
* @param {Array} cooldowns
*/
loadData(cooldowns) {
if (cooldowns.length !== 0) {
// 重构冷却列表
for (const cooldown of cooldowns) {
const instance = new CooldownItem(cooldown.key)
instance.cooldown = cooldown.cooldown
instance.duration = cooldown.duration
this.keyMap[cooldown.key] = instance
this.cooldownList.push(instance)
}
this.actor.updaters.add(this)
}
}
}
// ******************************** 公共冷却项目类 ********************************
class CooldownItem {
/** 冷却键
* @type {string}
*/ key
/** 当前冷却时间
* @type {number}
*/ cooldown
/** 持续冷却时间
* @type {number}
*/ duration
/**
* 公共冷却项目
* @param {string} key 冷却键
*/
constructor(key) {
this.key = key
this.cooldown = 0
this.duration = 0
}
/** 读取公共冷却进度 */
get progress() {
return this.cooldown / this.duration
}
}
// ******************************** 角色目标管理器类 ********************************
class TargetManager {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 目标角色列表
* @type {Array<Actor>}
*/ targets
/** 仇恨值数据列表
* @type {Array<number>}
*/ threats
/** 相关目标角色列表
* @type {Array<Actor>}
*/ relatedTargets
/**
* 角色目标管理器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.actor = actor
this.targets = []
this.threats = []
this.relatedTargets = []
}
/**
* 增加对目标角色的仇恨值,如果还不是目标,则将他放到目标列表中
* @param {Actor} actor 目标角色
* @param {number} threat 增加的仇恨值
*/
increaseThreat(actor, threat) {
const index = this.targets.indexOf(actor)
if (index !== -1) {
// 如果存在目标角色,增加仇恨值
this.threats[index] += threat
} else if (actor.active) {
// 如果不存在目标角色,且目标角色已激活
// 添加目标角色和仇恨值,并让目标角色将自己添加为相关目标
this.targets.push(actor)
this.threats.push(threat)
actor.targetManager.relatedTargets.push(this.actor)
}
}
/**
* 减少对目标角色的仇恨值
* @param {Actor} actor 目标角色
* @param {number} threat 减少的仇恨值
*/
decreaseThreat(actor, threat) {
const index = this.targets.indexOf(actor)
if (index !== -1) {
// 如果存在目标角色,减少仇恨值
this.threats[index] = Math.max(this.threats[index] - threat, 0)
}
}
/**
* 探测目标角色,将符合条件的角色添加到目标列表中
* @param {number} distance 探测距离(单位:图块)
* @param {function} inspector 目标角色检查器
* @param {boolean} [inSight = false] 是否判断目标角色在视野中可见
*/
detect(distance, inspector, inSight = false) {
const owner = this.actor
const ox = owner.x
const oy = owner.y
// 获取探测范围所在的角色区间列表
const cells = Scene.actors.cells.get(
ox - distance,
oy - distance,
ox + distance,
oy + distance,
)
const square = distance ** 2
const count = cells.count
// 查找所有角色区间
for (let i = 0; i < count; i++) {
const actors = cells[i]
const length = actors.length
// 查找区间中的所有角色
for (let i = 0; i < length; i++) {
const actor = actors[i]
// 如果角色已激活,距离小于等于探测距离,且符合条件,则把该角色添加到目标列表中
if (actor.active && (ox - actor.x) ** 2 + (oy - actor.y) ** 2 <= square &&
inspector(owner, actor) && (inSight === false ||
actor.parent.scene.isInLineOfSight(ox, oy, actor.x, actor.y))) {
this.append(actor)
}
}
}
}
/**
* 放弃远处的目标角色
* @param {function} inspector 目标角色检查器
* @param {number} distance 如果与目标角色的距离达到这个阈值,将他从目标列表中移除
*/
discard(inspector, distance = 0) {
const owner = this.actor
const ox = owner.x
const oy = owner.y
const square = distance ** 2
const targets = this.targets
let i = targets.length
// 逆序查找目标列表中的所有角色
while (--i >= 0) {
const actor = targets[i]
// 如果角色符合条件,且距离大于等于放弃距离,则把该角色从目标列表中移除
if (inspector(owner, actor) && (ox - actor.x) ** 2 + (oy - actor.y) ** 2 >= square) {
this.remove(actor)
}
}
}
/** 重置角色目标管理器 */
reset() {
this.resetTargets()
this.resetRelatedTargets()
}
/** 重置目标角色列表 */
resetTargets() {
const targets = this.targets
const length = targets.length
if (length !== 0) {
const owner = this.actor
// 遍历所有目标,将本角色从它们的相关列表中删除
for (let i = 0; i < length; i++) {
targets[i].targetManager.relatedTargets.remove(owner)
}
// 重置目标和仇恨值列表
this.targets = []
this.threats = []
}
}
/** 重置相关目标角色列表 */
resetRelatedTargets() {
const relatedTargets = this.relatedTargets
const length = relatedTargets.length
if (length !== 0) {
const owner = this.actor
// 遍历所有相关目标,将本角色从它们的目标和仇恨值列表中删除
for (let i = 0; i < length; i++) {
const actor = relatedTargets[i]
const manager = actor.targetManager
const targets = manager.targets
const threats = manager.threats
const index = targets.indexOf(owner)
if (index !== -1) {
targets.splice(index, 1)
threats.splice(index, 1)
}
}
// 重置相关列表
this.relatedTargets = []
}
}
/**
* 添加角色到目标列表中
* @param {Actor} actor 目标角色
*/
append(actor) {
const index = this.targets.indexOf(actor)
if (index === -1) {
// 如果不存在该目标,则添加目标和仇恨值
// 并让目标角色将本角色添加到相关列表
this.targets.push(actor)
this.threats.push(0)
actor.targetManager.relatedTargets.push(this.actor)
}
}
/**
* 从目标列表中移除角色
* @param {Actor} actor 目标角色
*/
remove(actor) {
const index = this.targets.indexOf(actor)
if (index !== -1) {
// 如果存在该目标,则移除目标和仇恨值
// 并让目标角色将本角色从相关列表中移除
this.targets.splice(index, 1)
this.threats.splice(index, 1)
actor.targetManager.relatedTargets.remove(this.actor)
}
}
/**
* 获取目标角色 - 最大仇恨值
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetMaxThreat(inspector) {
let target
let weight = -1
const owner = this.actor
const targets = this.targets
const threats = this.threats
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最大仇恨值的目标
if (inspector(owner, actor)) {
const threat = threats[i]
if (threat > weight) {
target = actor
weight = threat
}
}
}
return target
}
/**
* 获取目标角色 - 最近距离
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetNearest(inspector) {
let target
let weight = Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最近距离的目标
if (inspector(owner, actor)) {
const distance = Math.dist(owner.x, owner.y, actor.x, actor.y)
if (distance < weight) {
target = actor
weight = distance
}
}
}
return target
}
/**
* 获取目标角色 - 最远距离
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetFarthest(inspector) {
let target
let weight = -Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最远距离的目标
if (inspector(owner, actor)) {
const distance = Math.dist(owner.x, owner.y, actor.x, actor.y)
if (distance > weight) {
target = actor
weight = distance
}
}
}
return target
}
/**
* 获取目标角色 - 最小属性值
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetMinAttributeValue(inspector, key) {
let target
let weight = Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最小属性值的目标
if (inspector(owner, actor)) {
const value = actor.attributes[key]
if (value < weight) {
target = actor
weight = value
}
}
}
return target
}
/**
* 获取目标角色 - 最大属性值
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetMaxAttributeValue(inspector, key) {
let target
let weight = -Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最大属性值的目标
if (inspector(owner, actor)) {
const value = actor.attributes[key]
if (value > weight) {
target = actor
weight = value
}
}
}
return target
}
/**
* 获取目标角色 - 最小属性比率
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetMinAttributeRatio(inspector, key, divisor) {
let target
let weight = Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最小属性比率的目标
if (inspector(owner, actor)) {
const attributes = actor.attributes
const ratio = attributes[key] / attributes[divisor]
if (ratio < weight) {
target = actor
weight = ratio
}
}
}
return target
}
/**
* 获得目标角色 - 最大属性比率
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetMaxAttributeRatio(inspector, key, divisor) {
let target
let weight = -Infinity
const owner = this.actor
const targets = this.targets
const length = targets.length
for (let i = 0; i < length; i++) {
const actor = targets[i]
// 检查角色关系,并找出最大属性值的目标
if (inspector(owner, actor)) {
const attributes = actor.attributes
const ratio = attributes[key] / attributes[divisor]
if (ratio > weight) {
target = actor
weight = ratio
}
}
}
return target
}
/**
* 获取目标角色 - 随机
* @param {function} inspector 目标角色检查器
* @returns {Actor|undefined}
*/
getTargetRandom(inspector) {
let target
let count = 0
const owner = this.actor
const targets = this.targets
const indices = GL.arrays[0].uint32
const length = targets.length
for (let i = 0; i < length; i++) {
// 检查角色关系,把索引保存在indices中
if (inspector(owner, targets[i])) {
indices[count++] = i
}
}
if (count !== 0) {
// 获取随机索引指向的角色
target = targets[indices[Math.floor(Math.random() * count)]]
}
return target
}
}
// ******************************** 角色动画控制器类 ********************************
class AnimationController {
/** 角色动画状态
* @type {string}
*/ state
/** 角色动画控制器是否已激活
* @type {boolean}
*/ active
/** 角色动画正在播放中
* @type {boolean}
*/ playing
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 绑定的角色动画
* @type {Animation|null}
*/ animation
/** 角色动画闲置动作名称
* @type {string}
*/ idleMotion
/** 角色动画移动动作名称
* @type {string}
*/ moveMotion
/**
* 角色动画控制器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
const {data} = actor
this.state = 'idle'
this.active = true
this.playing = false
this.actor = actor
this.animation = null
this.idleMotion = data.idleMotion
this.moveMotion = data.moveMotion
}
/**
* 绑定角色动画
* @param {Animation} [animation] 动画实例
*/
bindAnimation(animation) {
this.animation = animation
const active = !!animation
// 存在动画则激活,否则反激活
if (this.active !== active) {
this.active = active
if (active) {
// 激活状态:恢复默认方法
delete this.update
delete this.startIdle
delete this.startMoving
delete this.playMotion
} else {
// 非激活状态:禁用部分方法
this.update = Function.empty
this.startIdle = Function.empty
this.startMoving = Function.empty
this.playMotion = Function.empty
}
}
if (active) {
// 设置角色动画的初始动作
animation.setMotion(this.getCurrentMotionName())
animation.setAngle(this.actor.angle)
}
}
/** 改变角色动作 */
changeMotion(type, motionName) {
switch (type) {
case 'idle':
this.idleMotion = motionName
if (this.state === 'idle') {
this.startIdle()
}
break
case 'move':
this.moveMotion = motionName
if (this.state === 'move') {
this.startMoving()
}
break
}
}
/** 开始闲置动作 */
startIdle() {
this.state = 'idle'
if (this.playing === false) {
const {animation} = this
if (animation.motionName === this.idleMotion) return
if (animation.setMotion(this.idleMotion)) {
animation.restart()
}
}
}
/** 开始移动动作 */
startMoving() {
this.state = 'move'
if (this.playing === false) {
const {animation} = this
if (animation.motionName === this.moveMotion) return
if (animation.setMotion(this.moveMotion)) {
animation.restart()
}
}
}
/** 重新播放动作 */
restart() {
this.playing = false
this.animation.speed = 1
if (this.animation.setMotion(this.getCurrentMotionName())) {
this.animation.restart()
}
}
/** 获取当前动作名称 */
getCurrentMotionName() {
switch (this.state) {
case 'idle': return this.idleMotion
case 'move': return this.moveMotion
}
}
/**
* 播放角色动作(结束时恢复动作)
* @param {string} motionName 动作名称
* @param {number} speed 播放速度
* @returns {Animation|undefined}
*/
playMotion(motionName, speed = 1) {
const {animation} = this
// 播放新动作
if (animation.setMotion(motionName)) {
this.playing = true
animation.speed = speed
// 重新播放动画
animation.restart()
animation.onFinish(() => {
// 播放结束后设置回闲置或移动动作
this.restart()
})
// 返回动画对象
return animation
}
return undefined
}
/** 停止播放角色动作 */
stopMotion() {
this.animation.finish()
}
/** 保存角色动作设定 */
saveData() {
return {
idle: this.idleMotion,
move: this.moveMotion,
}
}
/**
* 加载角色动作设定
* @param {Object} motions
*/
loadData(motions) {
this.idleMotion = motions.idle
this.moveMotion = motions.move
}
}
// ******************************** 角色碰撞器类 ********************************
class ActorCollider {
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 碰撞器的形状
* @type {string}
*/ shape
/** 角色碰撞体积大小
* @type {number}
*/ size
/** 角色碰撞体积半径
* @type {number}
*/ half
/** 角色碰撞体重大小
* @type {number}
*/ weight
/** 角色是否已经移动
* @type {boolean}
*/ moved
/**
* 角色碰撞器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
const {data} = actor
this.actor = actor
this.shape = data.shape
this.size = data.size
this.half = data.size / 2
this.weight = data.weight
this.moved = false
}
// 设置体重
setWeight(weight) {
this.weight = weight
// 更新角色的障碍区域
this.actor.parent?.scene.obstacles.update(this.actor)
}
// 更新上一次的位置
updateLastPosition() {
this.lastX = this.actor.x
this.lastY = this.actor.y
}
// 角色碰撞系统开关
static actorCollisionEnabled = true
// 场景碰撞系统开关
static sceneCollisionEnabled = true
// 场景碰撞系统角色半径
static sceneCollisionRadius = 0
// 角色碰撞距离
static actorCollisionDist = 0
/** 初始化 */
static initialize() {
const {collision} = Data.config
// 设置角色碰撞系统开关
this.actorCollisionEnabled = collision.actor.enabled
// 设置场景碰撞系统开关
this.sceneCollisionEnabled = collision.scene.enabled
// 设置场景碰撞角色体积的半径
this.sceneCollisionRadius = collision.scene.actorSize / 2
// 设置角色碰撞的最小距离
this.actorCollisionDist = collision.scene.actorSize
}
/** 处理角色与场景之间的碰撞 */
static handleSceneCollisions() {
if (ActorCollider.sceneCollisionEnabled === false) return
const scene = Scene.binding
const radius = ActorCollider.sceneCollisionRadius
const radiusSquared = radius ** 2
const terrains = scene.terrainObstacles
const width = scene.width
const height = scene.height
if (width * height === 0) return
const right = width - 1
const bottom = height - 1
const actors = scene.actors
const length = actors.length
// 遍历场景角色,计算碰撞
for (let i = 0; i < length; i++) {
const actor = actors[i]
const collider = actor.collider
// 如果角色未移动,跳过
if (collider.moved === false) {
continue
}
// 如果角色在场景网格之外,跳过
if (actor.x < radius) actor.x = radius
if (actor.y < radius) actor.y = radius
if (actor.x > width - radius) actor.x = width - radius
if (actor.y > height - radius) actor.y = height - radius
const passage = actor.passage
if (passage === -1) continue
const sx = Math.clamp(actor.intX, 0, right)
const sy = Math.clamp(actor.intY, 0, bottom)
let dx = Math.floor(actor.x)
let dy = Math.floor(actor.y)
// 如果角色锚点穿过了水平网格
if (sx !== dx) {
const unitY = (dy - sy) / (dx - sx)
const step = sx < dx ? 1 : -1
let x = sx
do {
x += step
const y = Math.floor(sy + (x - sx) * unitY)
if (terrains[x + y * width] !== passage) {
actor.x = sx < dx ? x - radius : x + 1 + radius
dx = Math.floor(actor.x)
break
}
}
while (x !== dx)
}
// 如果角色锚点穿过了垂直网格
if (sy !== dy) {
const unitX = (dx - sx) / (dy - sy)
const step = sy < dy ? 1 : -1
let y = sy
do {
y += step
const x = Math.floor(sx + (y - sy) * unitX)
if (terrains[x + y * width] !== passage) {
actor.y = sy < dy ? y - radius : y + 1 + radius
dy = Math.floor(actor.y)
break
}
}
while (y !== dy)
}
const ax = actor.x
const ay = actor.y
const al = Math.floor(ax - radius)
const at = Math.floor(ay - radius)
const ar = Math.ceil(ax + radius)
const ab = Math.ceil(ay + radius)
const x = Math.round(ax)
const y = Math.round(ay)
let ox = 0
let oy = 0
// 如果角色跨越了水平网格
if (al + 1 !== ar) {
if (x === dx) {
// 如果角色锚点在网格中靠左的位置
if (terrains[al + dy * width] !== passage) {
// 如果左边是不能通行的区域,让角色贴墙
actor.x = x + radius
} else {
ox = -1
}
} else {
// 如果角色锚点在网格中靠右的位置
if (terrains[x + dy * width] !== passage) {
// 如果右边是不能通行的区域,让角色贴墙
actor.x = x - radius
} else {
ox = 1
}
}
}
// 如果角色跨越了垂直网格
if (at + 1 !== ab) {
if (y === dy) {
// 如果角色锚点在网格中靠上的位置
if (terrains[dx + at * width] !== passage) {
// 如果上边是不能通行的区域,让角色贴墙
actor.y = y + radius
} else {
oy = -1
}
} else {
// 如果角色锚点在网格中靠下的位置
if (terrains[dx + y * width] !== passage) {
// 如果下边是不能通行的区域,让角色贴墙
actor.y = y - radius
} else {
oy = 1
}
}
}
// 如果角色跨越了场景网格,但是未发生碰撞
// 则判断地形的一角是否与角色发生碰撞
if (ox !== 0 && oy !== 0 &&
terrains[dx + ox + (dy + oy) * width] !== passage) {
// 如果离角色最近的网格角不可通行,且距离小于碰撞半径,则判定为碰撞
const distSquared = (x - ax) ** 2 + (y - ay) ** 2
if (distSquared >= radiusSquared) continue
// 计算最小移动向量,把角色推离到碰撞边缘
const hypot = radius - Math.sqrt(distSquared)
const angle = Math.atan2(ay - y, ax - x)
actor.x += hypot * Math.cos(angle)
actor.y += hypot * Math.sin(angle)
}
}
}
/** 处理角色与角色之间的碰撞 */
static handleActorCollisions() {
if (ActorCollider.actorCollisionEnabled === false) return
const {cells} = Scene.actors
const {width, height, length} = cells
// 计算同一个区间的角色碰撞
for (let i = 0; i < length; i++) {
const cell = cells[i]
const length = cell.length
for (let si = 0; si < length; si++) {
const sActor = cell[si]
if (sActor.collider.weight === 0) continue
for (let di = si + 1; di < length; di++) {
ActorCollider.handleCollisionBetweenTwoActors(sActor, cell[di])
}
}
}
// 计算左右区间的角色碰撞
const ex = width - 1
for (let y = 0; y < height; y++) {
for (let x = 0; x < ex; x++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + 1])
}
}
// 计算上下区间的角色碰撞
const ey = height - 1
for (let x = 0; x < width; x++) {
for (let y = 0; y < ey; y++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + width])
}
}
// 计算左上到右下区间的角色碰撞
const lowerRight = width + 1
for (let i = 0; i < ex; i++) {
const end = Math.min(ex - i, ey)
for (let x = i, y = 0; y < end; x++, y++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + lowerRight])
}
}
for (let i = 1; i < ey; i++) {
const end = Math.min(ex, ey - i)
for (let x = 0, y = i; x < end; x++, y++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + lowerRight])
}
}
// 计算右上到左下区间的角色碰撞
const lowerLeft = width - 1
for (let i = ex; i > 0; i--) {
const end = Math.min(i, ey)
for (let x = i, y = 0; y < end; x--, y++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + lowerLeft])
}
}
for (let i = 1; i < ey; i++) {
const end = Math.min(ex + i, ey)
for (let x = ex, y = i; y < end; x--, y++) {
const i = x + y * width
ActorCollider.handleCollisionsBetweenTwoCells(cells[i], cells[i + lowerLeft])
}
}
}
/** 处理两个角色之间的碰撞 */
static handleCollisionBetweenTwoActors = (IIFE => {
// 添加容差值避免陷入无限碰撞
const TOLERANCE = 0.01
// 触发角色碰撞事件
const collide = (sActor, dActor) => {
const commands = sActor.events.collision
if (commands) {
const event = new EventHandler(commands)
event.triggerActor = sActor
event.targetActor = dActor
EventHandler.call(event, sActor.updaters)
}
sActor.script.emit('collision', dActor)
}
// 碰撞 - 正方形和正方形
const collideSquareAndSquare = (sCollider, dCollider) => {
const sActor = sCollider.actor
const dActor = dCollider.actor
const distMin = sCollider.half + dCollider.half
const distX = Math.abs(sActor.x - dActor.x)
const distY = Math.abs(sActor.y - dActor.y)
// 如果角色之间的水平和垂直距离小于最小距离,则发生碰撞
if (distX < distMin && distY < distMin) {
const sWeight = sCollider.weight
const dWeight = dCollider.weight
// 体重比值0.5~2映射为0~1的推力
const ratio = Math.clamp(dWeight * 3 / (sWeight + dWeight) - 1, 0, 1)
if (distX > distY) {
// 如果水平距离大于垂直距离,把两个角色从水平方向上分开
const offset = distMin - distX + TOLERANCE
const sOffset = offset * ratio
const dOffset = offset - sOffset
// 根据角色左右位置情况进行计算
if (sActor.x < dActor.x) {
sActor.x -= sOffset
dActor.x += dOffset
} else {
sActor.x += sOffset
dActor.x -= dOffset
}
} else {
// 如果垂直距离大于水平距离,把两个角色从垂直方向上分开
const offset = distMin - distY + TOLERANCE
const sOffset = offset * ratio
const dOffset = offset - sOffset
// 根据角色上下位置情况进行计算
if (sActor.y < dActor.y) {
sActor.y -= sOffset
dActor.y += dOffset
} else {
sActor.y += sOffset
dActor.y -= dOffset
}
}
// 设置角色为已移动状态
sCollider.moved = true
dCollider.moved = true
// 发送角色碰撞事件
collide(sActor, dActor)
collide(dActor, sActor)
}
}
// 碰撞 - 圆形和圆形
const collideCircleAndCircle = (sCollider, dCollider) => {
const sActor = sCollider.actor
const dActor = dCollider.actor
const distMin = sCollider.half + dCollider.half
const distX = dActor.x - sActor.x
const distY = dActor.y - sActor.y
const distSquared = distX ** 2 + distY ** 2
// 如果角色之间的水平和垂直距离小于最小距离,则发生碰撞
if (distSquared < distMin ** 2) {
const dist = Math.sqrt(distSquared)
const offset = distMin - dist
const offsetX = offset / distMin * distX
const offsetY = offset / distMin * distY
const sWeight = sCollider.weight
const dWeight = dCollider.weight
// 体重比值0.5~2映射为0~1的推力
const ratio = Math.clamp(dWeight * 3 / (sWeight + dWeight) - 1, 0, 1)
if (offsetX !== 0) {
// 如果水平距离大于垂直距离,把两个角色从水平方向上分开
const tOffsetX = offsetX + (offsetX > 0 ? TOLERANCE : -TOLERANCE)
const sOffset = tOffsetX * ratio
const dOffset = tOffsetX - sOffset
// 根据角色左右位置情况进行计算
sActor.x -= sOffset
dActor.x += dOffset
}
if (offsetY !== 0) {
// 如果垂直距离大于水平距离,把两个角色从垂直方向上分开
const tOffsetY = offsetY + (offsetY > 0 ? TOLERANCE : -TOLERANCE)
const sOffset = tOffsetY * ratio
const dOffset = tOffsetY - sOffset
// 根据角色上下位置情况进行计算
sActor.y -= sOffset
dActor.y += dOffset
}
// 设置角色为已移动状态
sCollider.moved = true
dCollider.moved = true
// 发送角色碰撞事件
collide(sActor, dActor)
collide(dActor, sActor)
}
}
// 碰撞 - 正方形和圆形
const collideSquareAndCircle = (sCollider, dCollider) => {
const sActor = sCollider.actor
const dActor = dCollider.actor
const distMin = dCollider.half
const sx = sActor.x
const sy = sActor.y
const dx = dActor.x
const dy = dActor.y
const sl = sx - sCollider.half
const sr = sx + sCollider.half
const st = sy - sCollider.half
const sb = sy + sCollider.half
const distX = dx - (dx < sl ? sl : dx > sr ? sr : dx)
const distY = dy - (dy < st ? st : dy > sb ? sb : dy)
const distSquared = distX ** 2 + distY ** 2
// 如果角色之间的水平和垂直距离小于最小距离,则发生碰撞
if (distSquared < distMin ** 2) {
const dist = Math.sqrt(distSquared)
const offset = distMin - dist
let offsetX
let offsetY
if (distX !== 0 && distY !== 0) {
offsetX = offset / distMin * distX
offsetY = offset / distMin * distY
} else {
const rx = dx - sx
const ry = dy - sy
if (Math.abs(rx) > Math.abs(ry)) {
offsetX = offset * Math.sign(rx)
offsetY = 0
} else {
offsetX = 0
offsetY = offset * Math.sign(ry)
}
}
const sWeight = sCollider.weight
const dWeight = dCollider.weight
// 体重比值0.5~2映射为0~1的推力
const ratio = Math.clamp(dWeight * 3 / (sWeight + dWeight) - 1, 0, 1)
if (offsetX !== 0) {
// 如果水平距离大于垂直距离,把两个角色从水平方向上分开
const tOffsetX = offsetX + (offsetX > 0 ? TOLERANCE : -TOLERANCE)
const sOffset = tOffsetX * ratio
const dOffset = tOffsetX - sOffset
// 根据角色左右位置情况进行计算
sActor.x -= sOffset
dActor.x += dOffset
}
if (offsetY !== 0) {
// 如果垂直距离大于水平距离,把两个角色从垂直方向上分开
const tOffsetY = offsetY + (offsetY > 0 ? TOLERANCE : -TOLERANCE)
const sOffset = tOffsetY * ratio
const dOffset = tOffsetY - sOffset
// 根据角色上下位置情况进行计算
sActor.y -= sOffset
dActor.y += dOffset
}
// 设置角色为已移动状态
sCollider.moved = true
dCollider.moved = true
// 发送角色碰撞事件
collide(sActor, dActor)
collide(dActor, sActor)
}
}
return (sActor, dActor) => {
const dCollider = dActor.collider
// 如果角色体重为0,不参与碰撞
if (dCollider.weight === 0) return
// 如果角色队伍之间不可碰撞
const code = sActor.teamIndex | dActor.teamIndex << 8
if (Team.collisionMap[code] === 0) return
const sCollider = sActor.collider
switch (sCollider.shape) {
case 'circle':
switch (dCollider.shape) {
case 'circle':
return collideCircleAndCircle(sCollider, dCollider)
case 'square':
return collideSquareAndCircle(dCollider, sCollider)
}
case 'square':
switch (dCollider.shape) {
case 'circle':
return collideSquareAndCircle(sCollider, dCollider)
case 'square':
return collideSquareAndSquare(sCollider, dCollider)
}
}
}
})()
/**
* 处理两个分区之间的角色碰撞
* @param {Actor[]} sCell 场景角色分区1
* @param {Actor[]} dCell 场景角色分区2
*/
static handleCollisionsBetweenTwoCells = (sCell, dCell) => {
const sLength = sCell.length
const dLength = dCell.length
for (let si = 0; si < sLength; si++) {
const sActor = sCell[si]
// 如果角色的体重为0,跳过
if (sActor.collider.weight === 0) continue
for (let di = 0; di < dLength; di++) {
ActorCollider.handleCollisionBetweenTwoActors(sActor, dCell[di])
}
}
}
}
// ******************************** 角色导航器类 ********************************
class ActorNavigator {
/** 角色导航模式
* @type {string}
*/ mode
/** 绑定的角色对象
* @type {Actor}
*/ actor
/** 跟随的目标角色
* @type {Actor|null}
*/ target
/** 角色移动角度
* @type {number}
*/ movementAngle
/** 角色移动速度
* @type {number}
*/ movementSpeed
/** 角色移动速度系数
* @type {number}
*/ movementFactor
/** 角色移动速度系数(临时)
* @type {number}
*/ movementFactorTemp
/** 角色移动路径
* @type {Float64Array|null}
*/ movementPath
/** 角色移动速度X
* @type {number}
*/ velocityX
/** 角色移动速度Y
* @type {number}
*/ velocityY
/** 角色移动结束后回调函数
* @type {Array<Function>|null}
*/ callbacks
/** 角色移动超时时间(毫秒)
* @type {number}
*/ timeout
/** 角色上一次的场景位置X
* @type {number}
*/ lastX
/** 角色上一次的场景位置Y
* @type {number}
*/ lastY
/** 角色跟随目标时的最小距离
* @type {number}
*/ minDist
/** 角色跟随目标时的最大距离
* @type {number}
*/ maxDist
/** 角色跟随目标时的最大垂直距离
* @type {number}
*/ vertDist
/** 计算路径的时候是否绕过角色
* @type {boolean}
*/ bypass
/** 角色圆形跟随模式的偏移值(-0.8~+0.8)
* @type {number}
*/ followingOffset
/** 角色在跟随目标时是否进行寻路
* @type {boolean}
*/ followingNavigate
/** 角色跟随目标一次之后停止移动
* @type {boolean}
*/ followOnce
/** 角色跟随目标时切换动作的缓冲时间
* @type {number}
*/ animBufferTime
/** 导航器的更新函数(状态机模式)
* @type {Function}
*/ update
/**
* 角色导航器
* @param {Actor} actor 绑定的角色对象
*/
constructor(actor) {
this.mode = 'stop'
this.actor = actor
this.target = null
this.movementAngle = 0
this.movementSpeed = actor.data.speed
this.velocityX = 0
this.velocityY = 0
this.movementFactor = 1
this.movementFactorTemp = 1
this.movementPath = null
this.callbacks = null
this.timeout = 0
this.lastX = 0
this.lastY = 0
this.update = Function.empty
}
/**
* 设置角色的移动速度
* @param {number} speed 移动速度(图块/秒)
*/
setMovementSpeed(speed) {
this.movementSpeed = speed
this.calculateVelocity(this.movementAngle)
}
/**
* 设置角色的移动速度系数
* @param {number} factor 移动速度系数
*/
setMovementFactor(factor) {
this.movementFactor = factor
this.calculateVelocity(this.movementAngle)
}
/**
* 设置角色的移动速度系数(临时)
* @param {number} factor 移动速度系数(不保存)
*/
setMovementFactorTemp(factor) {
this.movementFactorTemp = factor
this.calculateVelocity(this.movementAngle)
}
/**
* 计算角色的移动速度分量
* @param {number} angle 移动速度的角度(弧度)
*/
calculateVelocity(angle) {
const speed = this.movementSpeed
* this.movementFactor
* this.movementFactorTemp
this.movementAngle = angle
this.velocityX = speed * Math.cos(angle) / 1000
this.velocityY = speed * Math.sin(angle) / 1000
}
/** 角色停止移动 */
stopMoving() {
if (this.mode !== 'stop') {
this.mode = 'stop'
this.target = null
this.movementPath = null
this.actor.animationController.startIdle()
// 设置更新函数为:空函数
this.update = Function.empty
// 执行结束回调(如果有)
if (this.callbacks !== null) {
for (const callback of this.callbacks) {
callback()
}
this.callbacks = null
}
}
}
/**
* 设置移动结束回调函数
* @param {function} callback 在角色停止当前的移动行为后触发
*/
onFinish(callback) {
if (this.mode === 'stop') {
return callback()
}
if (this.callbacks !== null) {
this.callbacks.push(callback)
} else {
this.callbacks = [callback]
}
}
/**
* 角色向指定角度持续移动
* @param {number} angle 移动角度(弧度)
*/
moveTowardAngle(angle) {
if (this.mode !== 'keep') {
this.stopMoving()
this.mode = 'keep'
this.actor.animationController.startMoving()
}
this.calculateVelocity(angle)
// 设置更新函数为:向前移动
this.update = this.updateForwardMovement
}
/**
* 角色移动到指定位置
* @param {number} x 场景图块X
* @param {number} y 场景图块Y
*/
moveTo(x, y) {
this.route(ScenePathFinder.createUnitPath(x, y))
}
/**
* 角色导航到指定位置
* @param {number} x 场景图块X
* @param {number} y 场景图块Y
* @param {boolean} [bypass] 是否绕过角色
*/
navigateTo(x, y, bypass = false) {
this.bypass = bypass
this.route(ScenePathFinder.createPath(this.actor.x, this.actor.y, x, y, this.actor.passage, bypass), true)
}
/**
* 角色设置移动路线
* @param {Float64Array} path 移动路线,长度是2的整数倍
* @param {boolean} [navigate] 是否开启导航
*/
route(path, navigate = false) {
if (this.mode !== 'navigate') {
this.stopMoving()
this.mode = 'navigate'
this.actor.animationController.startMoving()
}
this.timeout = navigate ? 500 : -1
this.movementPath = path
// 设置更新函数为:路径移动
this.update = this.updatePathMovement
}
/**
* 跟随目标角色(圆形模式)
* @param {Actor} target 目标角色
* @param {number} minDist 保持最小距离
* @param {number} maxDist 保持最大距离
* @param {number} [offset = 0] 跟随位置偏移[-0.8 ~ +0.8]
* @param {boolean} [navigate = false] 是否开启自动寻路
* @param {boolean} [bypass = false] 自动寻路是否绕过角色
* @param {boolean} [once = false] 跟随一次(到达位置后停止移动)
*/
followCircle(target, minDist, maxDist, offset = 0, navigate = false, bypass = false, once = false) {
if (this.mode !== 'follow') {
this.stopMoving()
this.mode = 'follow'
} else {
this.movementPath = null
}
this.target = target
// 设置最小和最大距离(至少是最小距离 + 0.1)
this.minDist = minDist
this.maxDist = Math.max(maxDist, minDist + 0.1)
this.followingOffset = offset
this.followingNavigate = navigate
this.bypass = bypass
this.followOnce = once
this.followTarget = once
? this._circleFollowTargetOnce
: this._circleFollowTarget
// 设置更新函数为:跟随角色
this.update = this.followTarget
}
/**
* // 跟随目标角色(矩形模式)
* @param {Actor} target 目标角色
* @param {number} minDist 保持最小水平距离
* @param {number} maxDist 保持最大水平距离
* @param {number} [vertDist = 0] 保持最大垂直距离
* @param {boolean} [navigate = false] 是否开启自动寻路
* @param {boolean} [bypass = false] 自动寻路是否绕过角色
* @param {boolean} [once = false] 跟随一次(到达位置后停止移动)
*/
followRectangle(target, minDist, maxDist, vertDist = 0, navigate = false, bypass = false, once = false) {
if (this.mode !== 'follow') {
this.stopMoving()
this.mode = 'follow'
} else {
this.movementPath = null
}
this.target = target
// 设置最小和最大距离(至少是最小距离 + 0.1)
this.minDist = minDist
this.maxDist = Math.max(maxDist, minDist + 0.1)
this.vertDist = vertDist
this.followingNavigate = navigate
this.bypass = bypass
this.followOnce = once
this.followTarget = once
? this._rectangleFollowTargetOnce
: this._rectangleFollowTarget
// 设置更新函数为:跟随角色
this.update = this.followTarget
}
/**
* 更新角色向前移动
* @param {number} deltaTime 增量时间(毫秒)
*/
updateForwardMovement(deltaTime) {
const actor = this.actor
const x = this.velocityX * deltaTime
const y = this.velocityY * deltaTime
actor.updateAngle(this.movementAngle)
actor.move(x, y)
}
/**
* 更新角色路径移动
* @param {number} deltaTime 增量时间(毫秒)
*/
updatePathMovement(deltaTime) {
// 逐帧计算角度,并计算移动速度分量
const actor = this.actor
const path = this.movementPath
if (this.timeout !== -1 && (this.timeout -= deltaTime) <= 0) {
const destX = path[path.length - 2]
const destY = path[path.length - 1]
return this.navigateTo(destX, destY, this.bypass)
}
const pi = path.index
const dx = path[pi]
const dy = path[pi + 1]
const distX = dx - actor.x
const distY = dy - actor.y
const angle = Math.atan2(distY, distX)
actor.updateAngle(angle)
this.calculateVelocity(angle)
// 计算当前帧向前移动的距离
const mx = this.velocityX * deltaTime
const my = this.velocityY * deltaTime
if (Math.abs(distX) <= Math.abs(mx) + 0.0001 &&
Math.abs(distY) <= Math.abs(my) + 0.0001) {
// 如果目标点在当前帧移动范围内,则将角色位置设为目标点
// 并且将路径索引指向下一个路线节点
actor.setPosition(dx, dy)
path.index += 2
// 如果已经是终点,则停止移动
if (path.index === path.length) {
this.stopMoving()
}
} else {
// 将角色的位置加上当前帧移动距离
actor.move(mx, my)
}
}
/**
* 切换到跟随缓冲模式,如果是跟随一次则停止移动
*/
_switchToFollowTargetBuffer() {
if (this.followOnce) {
this.stopMoving()
} else {
this.update = this._followTargetBuffer
this.animBufferTime = 100
}
}
/**
* 跟随目标角色时用来切换状态的缓冲函数
* @param {number} deltaTime 增量时间(毫秒)
*/
_followTargetBuffer(deltaTime) {
// 缓冲时间结束后切换动画为idle动作
// 避免跟随者移动速度>=目标时频繁地切换动作
if ((this.animBufferTime -= deltaTime) <= 0) {
this.actor.animationController.startIdle()
// 设置更新函数为:跟随角色
this.update = this.followTarget
return this.update(deltaTime)
}
// 缓冲未结束,调用跟随角色函数
this.followTarget(deltaTime)
}
/**
* 圆形模式 - 跟随目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_circleFollowTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const dist = Math.sqrt(
(actor.x - target.x) ** 2
+ (actor.y - target.y) ** 2
)
// 如果角色距离大于最大距离,开始跟随
// 设置更新函数为:接近目标(圆形模式)
if (dist > this.maxDist) {
actor.animationController.startMoving()
this.update = this.followingNavigate
? this._circleNavigateToTarget
: this._circleApproachTarget
return this.update(deltaTime)
}
// 如果角色距离小于最小距离,开始跟随
// 设置更新函数为:远离目标(圆形模式)
if (dist < this.minDist) {
actor.animationController.startMoving()
this.update = this._circleLeaveTarget
return this.update(deltaTime)
}
}
/**
* 圆形模式 - 跟随目标角色(一次)
* @param {number} deltaTime 增量时间(毫秒)
*/
_circleFollowTargetOnce(deltaTime) {
this._circleFollowTarget(deltaTime)
// 如果当前跟随方法就是更新函数,表示跟随已经结束
if (this._circleFollowTargetOnce === this.update) {
this.stopMoving()
}
}
/**
* 圆形模式 - 接近目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_circleApproachTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
let distX = target.x - actor.x
let distY = target.y - actor.y
// 如果角色距离小于等于最大距离,进入跟随缓冲模式(100ms)
if (Math.sqrt(distX ** 2 + distY ** 2) <= this.maxDist) {
return this._switchToFollowTargetBuffer()
}
let angle = Math.atan2(distY, distX)
const offset = this.followingOffset
if (offset !== 0 && this.maxDist > 0) {
// 计算跟随偏移距离和偏移角度
const offsetDist = Math.abs(this.maxDist * offset)
angle += offset > 0 ? Math.PI / 2 : -Math.PI / 2
// 加上偏移分量,计算朝向偏移目标点的角度
distX += offsetDist * Math.cos(angle)
distY += offsetDist * Math.sin(angle)
angle = Math.atan2(distY, distX)
}
// 向接近目标的方向移动
this.calculateVelocity(angle)
this.updateForwardMovement(deltaTime)
}
/**
* 圆形模式 - 导航到目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_circleNavigateToTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const sx = actor.x
const sy = actor.y
const distX = target.x - sx
const distY = target.y - sy
// 如果角色距离小于等于最大距离,进入跟随缓冲模式(100ms)
if (Math.sqrt(distX ** 2 + distY ** 2) <= this.maxDist) {
this.movementPath = null
this._switchToFollowTargetBuffer()
return
}
// 每隔一段时间计算移动路径
if (!this.movementPath || (this.timeout -= deltaTime) <= 0) {
let {x, y} = target
const offset = this.followingOffset
if (offset !== 0 && this.maxDist > 0) {
// 计算跟随偏移距离和偏移角度
const offsetDist = Math.abs(this.maxDist * offset)
const offsetAngle = offset > 0 ? Math.PI / 2 : -Math.PI / 2
const angle = Math.atan2(distY, distX) + offsetAngle
x += offsetDist * Math.cos(angle)
y += offsetDist * Math.sin(angle)
}
this.movementPath = ScenePathFinder.createPath(sx, sy, x, y, actor.passage, this.bypass)
this.timeout = 500
}
// 逐帧计算角度,并计算移动速度分量
const path = this.movementPath
const pi = path.index
const dx = path[pi]
const dy = path[pi + 1]
const pDistX = dx - sx
const pDistY = dy - sy
const angle = Math.atan2(pDistY, pDistX)
actor.updateAngle(angle)
this.calculateVelocity(angle)
// 计算当前帧向前移动的距离
const mx = this.velocityX * deltaTime
const my = this.velocityY * deltaTime
if (Math.abs(pDistX) <= Math.abs(mx) + 0.0001 &&
Math.abs(pDistY) <= Math.abs(my) + 0.0001) {
actor.setPosition(dx, dy)
path.index += 2
if (path.index === path.length) {
this.movementPath = null
}
} else {
actor.move(mx, my)
}
}
/**
* 圆形模式 - 远离目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_circleLeaveTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const distX = actor.x - target.x
const distY = actor.y - target.y
// 如果角色距离大于等于最小距离,进入跟随缓冲模式(100ms)
if (Math.sqrt(distX ** 2 + distY ** 2) >= this.minDist) {
return this._switchToFollowTargetBuffer()
}
// 向远离目标的方向移动
const angle = Math.atan2(distY, distX)
this.calculateVelocity(angle)
this.updateForwardMovement(deltaTime)
}
/**
* 矩形模式 - 跟随目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_rectangleFollowTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const distX = Math.abs(actor.x - target.x)
const distY = Math.abs(actor.y - target.y)
// 如果角色水平距离大于最大距离或小于最小距离
// 或者角色垂直距离大于垂直距离(+0.0001容差)
// 设置更新函数为:接近目标(矩形模式)
if (distX > this.maxDist ||
distX < this.minDist ||
distY > this.vertDist + 0.0001) {
actor.animationController.startMoving()
this.update = this.followingNavigate
? this._rectangleNavigateToTarget
: this._rectangleApproachTarget
return this.update(deltaTime)
}
}
/**
* 矩形模式 - 跟随目标角色(一次)
* @param {number} deltaTime 增量时间(毫秒)
*/
_rectangleFollowTargetOnce(deltaTime) {
this._rectangleFollowTarget(deltaTime)
// 如果当前跟随方法就是更新函数,表示跟随已经结束
if (this._rectangleFollowTargetOnce === this.update) {
this.stopMoving()
}
}
/**
* 矩形模式 - 接近目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_rectangleApproachTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const sx = actor.x
const sy = actor.y
const tx = target.x
const ty = target.y
const dx = sx < tx
// 根据宿主角色在目标角色左侧或右侧的情况来计算终点水平坐标
? Math.clamp(sx, tx - this.maxDist, tx - this.minDist)
: Math.clamp(sx, tx + this.minDist, tx + this.maxDist)
// 计算终点垂直坐标
const dy = Math.clamp(sy, ty - this.vertDist, ty + this.vertDist)
const distX = dx - sx
const distY = dy - sy
const angle = Math.atan2(distY, distX)
// 设置角度并计算移动速度分量
actor.updateAngle(angle)
this.calculateVelocity(angle)
// 计算当前帧向前移动的距离并更新角色位置
const mx = this.velocityX * deltaTime
const my = this.velocityY * deltaTime
actor.move(mx, my)
const absDistX = Math.abs(actor.x - tx)
if (absDistX >= this.minDist &&
absDistX <= this.maxDist &&
Math.abs(distY) <= Math.abs(my) + 0.0001) {
// 角色进入最小和最大距离的范围
// 并且垂直移动距离超过了角色垂直距离
// 则将角色垂直位置设为目标点垂直位置
actor.setPosition(actor.x, dy)
// 进入跟随缓冲模式
this._switchToFollowTargetBuffer()
}
}
/**
* 矩形模式 - 导航到目标角色
* @param {number} deltaTime 增量时间(毫秒)
*/
_rectangleNavigateToTarget(deltaTime) {
const actor = this.actor
const target = this.target
// 如果目标已销毁,停止跟随
if (target.destroyed) {
return this.stopMoving()
}
const sx = actor.x
const sy = actor.y
const tx = target.x
const ty = target.y
const dx = sx < tx
// 根据宿主角色在目标角色左侧或右侧的情况来计算终点水平坐标
? Math.clamp(sx, tx - this.maxDist, tx - this.minDist)
: Math.clamp(sx, tx + this.minDist, tx + this.maxDist)
// 计算终点垂直坐标
const dy = Math.clamp(sy, ty - this.vertDist, ty + this.vertDist)
// 每隔一段时间计算移动路径
if (!this.movementPath || (this.timeout -= deltaTime) <= 0) {
this.movementPath = ScenePathFinder.createPath(sx, sy, dx, dy, actor.passage, this.bypass)
this.timeout = 500
}
// 逐帧计算角度,并计算移动速度分量
const path = this.movementPath
const pi = path.index
const px = path[pi]
const py = path[pi + 1]
const pDistX = px - sx
const pDistY = py - sy
if (pDistX === 0 && pDistY === 0) {
this.velocityX = 0
this.velocityY = 0
} else {
const angle = Math.atan2(pDistY, pDistX)
actor.updateAngle(angle)
this.calculateVelocity(angle)
}
// 计算当前帧向前移动的距离
const mx = this.velocityX * deltaTime
const my = this.velocityY * deltaTime
// 由于计算移动速度现在已经是精确值
// 这里的容差值0.0001有可能不需要了
if (Math.abs(pDistX) <= Math.abs(mx) + 0.0001 &&
Math.abs(pDistY) <= Math.abs(my) + 0.0001) {
actor.setPosition(px, py)
path.index += 2
if (path.index === path.length) {
this.movementPath = null
const absDistX = Math.abs(actor.x - tx)
const absDistY = Math.abs(actor.y - dy)
if (absDistX >= this.minDist &&
absDistX <= this.maxDist &&
absDistY <= Math.abs(my) + 0.0001) {
// 角色进入最小和最大距离的范围
// 并且垂直移动距离超过了角色垂直距离
// 则将角色垂直位置设为目标点垂直位置
actor.setPosition(actor.x, dy)
// 进入跟随缓冲模式
this._switchToFollowTargetBuffer()
}
}
} else {
actor.move(mx, my)
}
}
}