mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-04 19:07:06 +08:00
1407 lines
44 KiB
TypeScript
1407 lines
44 KiB
TypeScript
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
|
||
import { Container } from '../container';
|
||
import { Sprite } from '../sprite';
|
||
import { TimingFn } from 'mutate-animate';
|
||
import { IAnimateFrame, renderEmits, RenderItem } from '../item';
|
||
import { logger } from '@/core/common/logger';
|
||
import { RenderableData, texture } from '../cache';
|
||
import { glMatrix } from 'gl-matrix';
|
||
import { BlockCacher } from './block';
|
||
import { Transform } from '../transform';
|
||
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
|
||
|
||
export interface ILayerGroupRenderExtends {
|
||
/** 拓展的唯一标识符 */
|
||
readonly id: string;
|
||
|
||
/**
|
||
* 当拓展被激活时执行的函数(一般就是拓展加载至目标LayerGroup实例时立刻执行)
|
||
* @param group 目标LayerGroup实例
|
||
*/
|
||
awake?(group: LayerGroup): void;
|
||
|
||
/**
|
||
* 当一个Layer层级被添加时执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
* @param layer 添加的Layer层实例
|
||
*/
|
||
onLayerAdd?(group: LayerGroup, layer: Layer): void;
|
||
|
||
/**
|
||
* 当一个Layer层级被移除时执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
* @param layer 移除的Layer层实例
|
||
*/
|
||
onLayerRemove?(group: LayerGroup, layer: Layer): void;
|
||
|
||
/**
|
||
* 当一个Layer层级从显示到隐藏的状态切换时执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
* @param layer 隐藏的Layer层实例
|
||
*/
|
||
onLayerHide?(group: LayerGroup, layer: Layer): void;
|
||
|
||
/**
|
||
* 当一个Layer层级从隐藏到显示状态切换时执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
* @param layer 显示的Layer层实例
|
||
*/
|
||
onLayerShow?(group: LayerGroup, layer: Layer): void;
|
||
|
||
/**
|
||
* 当执行 {@link LayerGroup.emptyLayer} 时执行的函数,即清空所有挂载的Layer时执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
*/
|
||
onEmptyLayer?(group: LayerGroup): void;
|
||
|
||
/**
|
||
* 当帧动画更新时执行的函数,例如从第一帧变成第二帧时
|
||
* @param group 目标LayerGroup实例
|
||
* @param frame 当前帧数
|
||
*/
|
||
onFrameUpdate?(group: LayerGroup, frame: number): void;
|
||
|
||
/**
|
||
* 在渲染之前执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
*/
|
||
onBeforeRender?(group: LayerGroup): void;
|
||
|
||
/**
|
||
* 在渲染之后执行的函数
|
||
* @param group 目标LayerGroup实例
|
||
*/
|
||
onAfterRender?(group: LayerGroup): void;
|
||
|
||
/**
|
||
* 当拓展被取消挂载时执行的函数(LayerGroup被销毁,拓展被移除等)
|
||
* @param group 目标LayerGroup实例
|
||
*/
|
||
onDestroy?(group: LayerGroup): void;
|
||
}
|
||
|
||
export type FloorLayer = 'bg' | 'bg2' | 'event' | 'fg' | 'fg2';
|
||
|
||
export class LayerGroup extends Container implements IAnimateFrame {
|
||
/** 地图组列表 */
|
||
// static list: Set<LayerGroup> = new Set();
|
||
|
||
cellSize: number = 32;
|
||
blockSize: number = core._WIDTH_;
|
||
|
||
/** 当前楼层 */
|
||
floorId?: FloorIds;
|
||
/** 是否绑定了当前层 */
|
||
bindThisFloor: boolean = false;
|
||
/** 伤害显示层 */
|
||
// damage?: Damage;
|
||
/** 地图显示层 */
|
||
layers: Map<FloorLayer, Layer> = new Map();
|
||
|
||
private needRender?: NeedRenderData;
|
||
private extend: Map<string, ILayerGroupRenderExtends> = new Map();
|
||
|
||
constructor() {
|
||
super('static', false);
|
||
|
||
this.setHD(true);
|
||
this.setAntiAliasing(false);
|
||
this.size(core._PX_, core._PY_);
|
||
|
||
this.on('afterRender', () => {
|
||
this.releaseNeedRender();
|
||
});
|
||
|
||
renderEmits.addFramer(this);
|
||
|
||
const binder = new LayerGroupFloorBinder();
|
||
this.extends(binder);
|
||
binder.bindThis();
|
||
}
|
||
|
||
/**
|
||
* 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数,
|
||
* 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。
|
||
* 具体能干什么参考 {@link ILayerGroupRenderExtends}
|
||
* @param ex 渲染拓展对象
|
||
*/
|
||
extends(ex: ILayerGroupRenderExtends) {
|
||
this.extend.set(ex.id, ex);
|
||
ex.awake?.(this);
|
||
}
|
||
|
||
/**
|
||
* 移除一个渲染拓展
|
||
* @param id 要移除的拓展
|
||
*/
|
||
removeExtends(id: string) {
|
||
const ex = this.extend.get(id);
|
||
if (!ex) return;
|
||
this.extend.delete(id);
|
||
ex.onDestroy?.(this);
|
||
}
|
||
|
||
/**
|
||
* 获取一个已装载的拓展
|
||
* @param id 拓展id
|
||
*/
|
||
getExtends(id: string) {
|
||
return this.extend.get(id);
|
||
}
|
||
|
||
/**
|
||
* 设置渲染分块大小
|
||
* @param size 分块大小
|
||
*/
|
||
setBlockSize(size: number) {
|
||
this.blockSize = size;
|
||
this.layers.forEach(v => {
|
||
v.block.setBlockSize(size);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 设置每个图块的大小
|
||
* @param size 每个图块的大小
|
||
*/
|
||
setCellSize(size: number) {
|
||
this.cellSize = size;
|
||
}
|
||
|
||
/**
|
||
* 清空所有层
|
||
*/
|
||
emptyLayer() {
|
||
this.removeChild(...this.layers.values());
|
||
this.layers.forEach(v => v.destroy());
|
||
this.layers.clear();
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onEmptyLayer?.(this);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加显示层
|
||
* @param layer 显示层
|
||
*/
|
||
addLayer(layer: FloorLayer) {
|
||
const l = new Layer();
|
||
l.layer = layer;
|
||
if (l.layer) this.layers.set(l.layer, l);
|
||
this.appendChild(l);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onLayerAdd?.(this, l);
|
||
}
|
||
|
||
return l;
|
||
}
|
||
|
||
/**
|
||
* 移除指定层
|
||
* @param layer 要移除的层,可以是Layer实例,也可以是字符串
|
||
*/
|
||
removeLayer(layer: FloorLayer | Layer) {
|
||
let ins: Layer | undefined;
|
||
if (typeof layer === 'string') {
|
||
const la = this.layers.get(layer);
|
||
if (!la) return;
|
||
this.removeChild(la);
|
||
this.layers.delete(layer);
|
||
la.destroy();
|
||
ins = la;
|
||
} else {
|
||
const arr = [...this.layers];
|
||
const la = arr.find(v => v[1] === layer)?.[0];
|
||
if (la && this.layers.delete(la)) {
|
||
this.removeChild(layer);
|
||
layer.destroy();
|
||
ins = layer;
|
||
}
|
||
}
|
||
if (ins) {
|
||
for (const ex of this.extend.values()) {
|
||
ex.onLayerRemove?.(this, ins);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取一个地图层实例,例如获取背景层等
|
||
* @param layer 地图层
|
||
*/
|
||
getLayer(layer: FloorLayer) {
|
||
return this.layers.get(layer);
|
||
}
|
||
|
||
/**
|
||
* 隐藏某个层
|
||
* @param layer 要隐藏的层
|
||
*/
|
||
hideLayer(layer: FloorLayer) {
|
||
const la = this.getLayer(layer);
|
||
if (!la) return;
|
||
la.hide();
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onLayerHide?.(this, la);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示某个层
|
||
* @param layer 要显示的层
|
||
*/
|
||
showLayer(layer: FloorLayer) {
|
||
const la = this.getLayer(layer);
|
||
if (!la) return;
|
||
la.show();
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onLayerShow?.(this, la);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 缓存计算应该渲染的块
|
||
* @param transform 变换矩阵
|
||
* @param blockData 分块信息
|
||
*/
|
||
cacheNeedRender(transform: Transform, block: BlockCacher<any>) {
|
||
return (
|
||
this.needRender ??
|
||
(this.needRender = calNeedRenderOf(transform, this.cellSize, block))
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 释放应该渲染块缓存
|
||
*/
|
||
releaseNeedRender() {
|
||
this.needRender = void 0;
|
||
}
|
||
|
||
/**
|
||
* 更新动画帧
|
||
*/
|
||
updateFrameAnimate() {
|
||
this.update(this);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onFrameUpdate?.(this, RenderItem.animatedFrame % 4);
|
||
}
|
||
}
|
||
|
||
destroy(): void {
|
||
for (const ex of this.extend.values()) {
|
||
ex.onDestroy?.(this);
|
||
}
|
||
super.destroy();
|
||
renderEmits.removeFramer(this);
|
||
}
|
||
}
|
||
|
||
export function calNeedRenderOf(
|
||
transform: Transform,
|
||
cell: number,
|
||
block: BlockCacher<any>
|
||
): NeedRenderData {
|
||
const w = (core._WIDTH_ * cell) / 2;
|
||
const h = (core._HEIGHT_ * cell) / 2;
|
||
const size = block.blockSize;
|
||
|
||
// -1是因为宽度是core._PX_,从0开始的话,末尾索引就是core._PX_ - 1
|
||
const [x1, y1] = Transform.untransformed(transform, -w, -h);
|
||
const [x2, y2] = Transform.untransformed(transform, w - 1, -h);
|
||
const [x3, y3] = Transform.untransformed(transform, w - 1, h - 1);
|
||
const [x4, y4] = Transform.untransformed(transform, -w, h - 1);
|
||
|
||
const res: Set<number> = new Set();
|
||
/** 一个纵坐标对应的所有横坐标,用于填充 */
|
||
const xyMap: Map<number, number[]> = new Map();
|
||
|
||
const pushXY = (x: number, y: number) => {
|
||
let arr = xyMap.get(y);
|
||
if (!arr) {
|
||
arr = [];
|
||
xyMap.set(y, arr);
|
||
}
|
||
arr.push(x);
|
||
return arr;
|
||
};
|
||
|
||
[
|
||
[x1, y1, x2, y2],
|
||
[x2, y2, x3, y3],
|
||
[x3, y3, x4, y4],
|
||
[x4, y4, x1, y1]
|
||
].forEach(([fx0, fy0, tx0, ty0]) => {
|
||
const fx = Math.floor(fx0 / cell);
|
||
const fy = Math.floor(fy0 / cell);
|
||
const tx = Math.floor(tx0 / cell);
|
||
const ty = Math.floor(ty0 / cell);
|
||
const dx = tx - fx;
|
||
const dy = ty - fy;
|
||
const k = dy / dx;
|
||
|
||
// 斜率无限的时候,竖直
|
||
if (!isFinite(k)) {
|
||
const min = k < 0 ? ty : fy;
|
||
const max = k < 0 ? fy : ty;
|
||
const [x, y] = block.getBlockXY(fx, min);
|
||
const [, ey] = block.getBlockXY(fx, max);
|
||
for (let i = y; i <= ey; i++) {
|
||
pushXY(x, i);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
const [fbx, fby] = block.getBlockXY(fx, fy);
|
||
|
||
// 当斜率为0时
|
||
if (glMatrix.equals(k, 0)) {
|
||
const [ex] = block.getBlockXY(tx, fy);
|
||
pushXY(fby, fbx).push(ex);
|
||
return;
|
||
}
|
||
|
||
// 否则使用 Bresenham 直线算法
|
||
if (Math.abs(k) >= 1) {
|
||
// 斜率大于一,y方向递增
|
||
const d = Math.sign(dy) * size;
|
||
const f = fby * size;
|
||
const dir = dy > 0;
|
||
|
||
const ex = Math.floor(tx / size);
|
||
const ey = Math.floor(ty / size);
|
||
pushXY(ex, ey);
|
||
|
||
let now = f;
|
||
let last = fbx;
|
||
let ny = fby;
|
||
do {
|
||
const bx = Math.floor((fx + (now - fy) / k) / size);
|
||
pushXY(bx, ny);
|
||
if (bx !== last) {
|
||
if (dir) pushXY(bx, ny - Math.sign(dy));
|
||
else pushXY(last, ny);
|
||
}
|
||
|
||
last = bx;
|
||
ny += Math.sign(dy);
|
||
now += d;
|
||
} while (dir ? ny <= ey : ny >= ey);
|
||
} else {
|
||
// 斜率小于一,x方向递增
|
||
const d = Math.sign(dx) * size;
|
||
const f = fbx * size;
|
||
const dir = dx > 0;
|
||
|
||
const ex = Math.floor(tx / size);
|
||
const ey = Math.floor(ty / size);
|
||
pushXY(ex, ey);
|
||
|
||
let now = f;
|
||
let last = fby;
|
||
let nx = fbx;
|
||
do {
|
||
const by = Math.floor((fy + k * (now - fx)) / size);
|
||
if (by !== last) {
|
||
if (dir) pushXY(nx - Math.sign(dx), by);
|
||
else pushXY(nx, last);
|
||
}
|
||
pushXY(nx, by);
|
||
last = by;
|
||
nx += Math.sign(dx);
|
||
now += d;
|
||
} while (dir ? nx <= ex : nx >= ex);
|
||
}
|
||
});
|
||
|
||
// 然后进行填充
|
||
const { width: bw, height: bh } = block.blockData;
|
||
const back: [number, number][] = [];
|
||
xyMap.forEach((x, y) => {
|
||
if (x.length === 1) {
|
||
const index = y * bw + x[0];
|
||
|
||
if (!back.some(v => v[0] === x[0] && v[1] === y))
|
||
back.push([x[0], y]);
|
||
if (index < 0 || index >= bw * bh) return;
|
||
res.add(index);
|
||
}
|
||
const max = Math.max(...x);
|
||
const min = Math.min(...x);
|
||
|
||
for (let i = min; i <= max; i++) {
|
||
const index = y * bw + i;
|
||
|
||
if (!back.some(v => v[0] === i && v[1] === y)) back.push([i, y]);
|
||
if (index < 0 || index >= bw * bh) continue;
|
||
res.add(index);
|
||
}
|
||
});
|
||
|
||
return { res, back };
|
||
}
|
||
|
||
export interface ILayerRenderExtends {
|
||
/** 拓展的唯一标识符 */
|
||
readonly id: string;
|
||
|
||
/**
|
||
* 当拓展被激活时执行的函数(一般就是拓展加载至目标Layer实例时立刻执行)
|
||
* @param layer 目标Layer实例
|
||
*/
|
||
awake?(layer: Layer): void;
|
||
|
||
/**
|
||
* 当楼层的背景图块被设置时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param background 设置为的背景图块数字
|
||
*/
|
||
onBackgroundSet?(layer: Layer, background: AllNumbers): void;
|
||
|
||
/**
|
||
* 当背景图块图片被生成时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param images 生成出的背景图块的单个分块图像,数组是因为背景图块可能是多帧图块
|
||
*/
|
||
onBackgroundGenerated?(layer: Layer, images: HTMLCanvasElement[]): void;
|
||
|
||
/**
|
||
* 当修改渲染数据时执行的函数,参见 {@link Layer.putRenderData}
|
||
* @param layer 目标Layer实例
|
||
* @param data 扁平化的数据信息
|
||
* @param width 数据宽度
|
||
* @param x 数据左上角横坐标
|
||
* @param y 数据左上角纵坐标
|
||
* @param calAutotile 是否重新计算自动元件的连接情况
|
||
*/
|
||
onDataPut?(
|
||
layer: Layer,
|
||
data: number[],
|
||
width: number,
|
||
x: number,
|
||
y: number,
|
||
calAutotile: boolean
|
||
): void;
|
||
|
||
/**
|
||
* 当更新某个区域内的大怪物renderable信息时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param x 左上角横坐标
|
||
* @param y 左上角纵坐标
|
||
* @param width 区域宽度
|
||
* @param height 区域高度
|
||
* @param images 最终的大怪物renderable信息,等同于 {@link Layer.bigImages}
|
||
*/
|
||
onBigImagesUpdate?(
|
||
layer: Layer,
|
||
x: number,
|
||
y: number,
|
||
width: number,
|
||
height: number,
|
||
images: Map<number, LayerMovingRenderable>
|
||
): void;
|
||
|
||
/**
|
||
* 当计算完成区域内自动元件连接信息时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param x 左上角横坐标
|
||
* @param y 左上角纵坐标
|
||
* @param width 区域宽度
|
||
* @param height 区域高度
|
||
* @param autotiles 计算出的自动元件连接信息,等同于 {@link Layer.autotiles}
|
||
*/
|
||
onAutotilesCaled?(
|
||
layer: Layer,
|
||
x: number,
|
||
y: number,
|
||
width: number,
|
||
height: number,
|
||
autotiles: Record<number, number>
|
||
): void;
|
||
|
||
/**
|
||
* 当地图大小修改时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param width 地图宽度
|
||
* @param height 地图高度
|
||
*/
|
||
onMapResize?(layer: Layer, width: number, height: number): void;
|
||
|
||
/**
|
||
* 当更新指定区域的分块缓存时执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param blocks 更新区域内包含的分块索引
|
||
* @param x 区域的图格左上角横坐标
|
||
* @param y 区域的图格右上角横坐标
|
||
* @param width 区域的图格宽度
|
||
* @param height 区域的图格高度
|
||
*/
|
||
onBlocksUpdate?(
|
||
layer: Layer,
|
||
blocks: Set<number>,
|
||
x: number,
|
||
y: number,
|
||
width: number,
|
||
height: number
|
||
): void;
|
||
|
||
/**
|
||
* 当更新移动层的渲染信息是执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param renderable 移动层的渲染信息(包含大怪物),未排序
|
||
*/
|
||
onMovingUpdate?(layer: Layer, renderable: LayerMovingRenderable[]): void;
|
||
|
||
/**
|
||
* 在地图渲染之前执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param transform 渲染的变换矩阵
|
||
* @param need 需要渲染的分块信息
|
||
*/
|
||
onBeforeRender?(
|
||
layer: Layer,
|
||
transform: Transform,
|
||
need: NeedRenderData
|
||
): void;
|
||
|
||
/**
|
||
* 在地图渲染之后执行的函数
|
||
* @param layer 目标Layer实例
|
||
* @param transform 渲染的变换矩阵
|
||
* @param need 需要渲染的分块信息
|
||
*/
|
||
onAfterRender?(
|
||
layer: Layer,
|
||
transform: Transform,
|
||
need: NeedRenderData
|
||
): void;
|
||
|
||
/**
|
||
* 当拓展被取消挂载时执行的函数(Layer被销毁,拓展被移除等)
|
||
* @param layer 目标Layer实例
|
||
*/
|
||
onDestroy?(layer: Layer): void;
|
||
}
|
||
|
||
interface LayerCacheItem {
|
||
symbol: number;
|
||
canvas: MotaOffscreenCanvas2D;
|
||
}
|
||
|
||
export interface LayerMovingRenderable extends RenderableData {
|
||
zIndex: number;
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
interface NeedRenderData {
|
||
/** 需要渲染的分块索引 */
|
||
res: Set<number>;
|
||
/** 需要渲染的背景的左上角横纵坐标,因为背景是可能渲染在地图之外的,所以不能使用分块索引的形式存储 */
|
||
back: [x: number, y: number][];
|
||
}
|
||
|
||
export class Layer extends Container {
|
||
// 一些会用到的常量
|
||
static readonly FRAME_0 = 1;
|
||
static readonly FRAME_1 = 2;
|
||
static readonly FRAME_2 = 4;
|
||
static readonly FRAME_3 = 8;
|
||
static readonly FRAME_ALL = 15;
|
||
|
||
/** 静态层,包含除大怪物及正在移动的内容外的内容 */
|
||
protected staticMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
|
||
/** 移动层,包含大怪物及正在移动的内容 */
|
||
protected movingMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
|
||
/** 背景图层 */
|
||
protected backMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
|
||
|
||
/** 最终渲染至的Sprite */
|
||
main: Sprite = new Sprite('static', false);
|
||
|
||
/** 渲染的层 */
|
||
layer?: FloorLayer;
|
||
// todo: renderable分块存储,优化循环绘制性能
|
||
/** 渲染数据 */
|
||
renderData: number[] = [];
|
||
/** 自动元件的连接信息,键表示图块在渲染数据中的索引,值表示连接信息,是个8位二进制 */
|
||
autotiles: Record<number, number> = {};
|
||
/** 楼层宽度 */
|
||
mapWidth: number = 0;
|
||
/** 楼层高度 */
|
||
mapHeight: number = 0;
|
||
/** 每个图块的大小 */
|
||
cellSize: number = 32;
|
||
|
||
/** 背景图块 */
|
||
background: AllNumbers = 0;
|
||
/** 背景图块画布 */
|
||
backImage: HTMLCanvasElement[] = [];
|
||
|
||
/** 分块信息 */
|
||
block: BlockCacher<LayerCacheItem> = new BlockCacher(0, 0, core._WIDTH_, 4);
|
||
|
||
/** 大怪物渲染信息 */
|
||
bigImages: Map<number, LayerMovingRenderable> = new Map();
|
||
// todo: 是否需要桶排?
|
||
/** 移动层的渲染信息 */
|
||
movingRenderable: LayerMovingRenderable[] = [];
|
||
/** 下一此渲染时是否需要更新移动层的渲染信息 */
|
||
needUpdateMoving: boolean = false;
|
||
|
||
private extend: Map<string, ILayerRenderExtends> = new Map();
|
||
/** 正在移动的图块的渲染信息 */
|
||
private moving: Set<LayerMovingRenderable> = new Set();
|
||
|
||
constructor() {
|
||
super('absolute', false);
|
||
|
||
// this.setHD(false);
|
||
this.setAntiAliasing(false);
|
||
this.size(core._PX_, core._PY_);
|
||
|
||
this.staticMap.setHD(false);
|
||
// this.staticMap.setAntiAliasing(false);
|
||
this.staticMap.withGameScale(false);
|
||
this.staticMap.size(core._PX_, core._PY_);
|
||
this.movingMap.setHD(false);
|
||
// this.movingMap.setAntiAliasing(false);
|
||
this.movingMap.withGameScale(false);
|
||
this.movingMap.size(core._PX_, core._PY_);
|
||
this.backMap.setHD(false);
|
||
// this.backMap.setAntiAliasing(false);
|
||
this.backMap.withGameScale(false);
|
||
this.backMap.size(core._PX_, core._PY_);
|
||
this.main.setAntiAliasing(false);
|
||
this.main.setHD(false);
|
||
this.main.size(core._PX_, core._PY_);
|
||
|
||
this.appendChild(this.main);
|
||
this.main.setRenderFn((canvas, transform) => {
|
||
const { ctx } = canvas;
|
||
const { width, height } = canvas;
|
||
const need = this.calNeedRender(transform);
|
||
this.renderMap(transform, need);
|
||
ctx.drawImage(this.backMap.canvas, 0, 0, width, height);
|
||
ctx.drawImage(this.staticMap.canvas, 0, 0, width, height);
|
||
ctx.drawImage(this.movingMap.canvas, 0, 0, width, height);
|
||
});
|
||
|
||
this.extends(new LayerFloorBinder());
|
||
}
|
||
|
||
/**
|
||
* 添加渲染拓展,可以将渲染拓展理解为一类插件,通过指定的函数在对应时刻执行一些函数,
|
||
* 来达到执行自己想要的功能的效果。例如样板自带的勇士渲染、伤害渲染等都由此实现。
|
||
* 具体能干什么参考 {@link ILayerRenderExtends}
|
||
* @param ex 渲染拓展对象
|
||
*/
|
||
extends(ex: ILayerRenderExtends) {
|
||
this.extend.set(ex.id, ex);
|
||
ex.awake?.(this);
|
||
}
|
||
|
||
/**
|
||
* 移除一个渲染拓展
|
||
* @param id 要移除的拓展
|
||
*/
|
||
removeExtends(id: string) {
|
||
const ex = this.extend.get(id);
|
||
if (!ex) return;
|
||
this.extend.delete(id);
|
||
ex.onDestroy?.(this);
|
||
}
|
||
|
||
/**
|
||
* 获取一个已装载的拓展
|
||
* @param id 拓展id
|
||
*/
|
||
getExtends(id: string) {
|
||
return this.extend.get(id);
|
||
}
|
||
|
||
/**
|
||
* 判断一个点是否在地图范围内
|
||
* @param x 横坐标
|
||
* @param y 纵坐标
|
||
*/
|
||
isPointOutside(x: number, y: number) {
|
||
return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight;
|
||
}
|
||
|
||
/**
|
||
* 判断一个矩形是否完全在地图之外
|
||
* @param x 矩形左上角横坐标
|
||
* @param y 矩形左上角纵坐标
|
||
* @param width 矩形长度
|
||
* @param height 矩形高度
|
||
*/
|
||
isRectOutside(x: number, y: number, width: number, height: number) {
|
||
return (
|
||
x >= this.mapWidth ||
|
||
y >= this.mapHeight ||
|
||
x + width < 0 ||
|
||
y + height < 0
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 判断一个矩形是否完全在地图之内
|
||
* @param x 矩形左上角横坐标
|
||
* @param y 矩形左上角纵坐标
|
||
* @param width 矩形长度
|
||
* @param height 矩形高度
|
||
*/
|
||
containsRect(x: number, y: number, width: number, height: number) {
|
||
return (
|
||
x + width <= this.mapWidth &&
|
||
y + height <= this.mapHeight &&
|
||
x >= 0 &&
|
||
y >= 0
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 设置背景图块
|
||
* @param background 背景图块
|
||
*/
|
||
setBackground(background: AllNumbers) {
|
||
this.background = background;
|
||
this.generateBackground();
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onBackgroundSet?.(this, background);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将当前地图的背景图块绑定为一个地图的背景图块
|
||
* @param floorId 楼层id
|
||
*/
|
||
bindBackground(floorId: FloorIds) {
|
||
const { defaultGround } = core.status.maps[floorId];
|
||
if (defaultGround) {
|
||
this.setBackground(texture.idNumberMap[defaultGround]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成背景图块
|
||
*/
|
||
generateBackground() {
|
||
const num = this.background;
|
||
|
||
const data = texture.getRenderable(num);
|
||
this.backImage = [];
|
||
if (!data) return;
|
||
|
||
const frame = data.frame;
|
||
for (let i = 0; i < frame; i++) {
|
||
const canvas = new MotaOffscreenCanvas2D();
|
||
const temp = new MotaOffscreenCanvas2D();
|
||
const ctx = canvas.ctx;
|
||
const tempCtx = temp.ctx;
|
||
const [sx, sy, w, h] = data.render[i];
|
||
canvas.setHD(false);
|
||
canvas.setAntiAliasing(false);
|
||
canvas.withGameScale(false);
|
||
canvas.size(core._PX_, core._PY_);
|
||
temp.setHD(false);
|
||
temp.setAntiAliasing(false);
|
||
temp.withGameScale(false);
|
||
temp.size(w, h);
|
||
|
||
const img = data.autotile ? data.image[0b11111111] : data.image;
|
||
tempCtx.drawImage(img, sx, sy, w, h, 0, 0, w, h);
|
||
const pattern = ctx.createPattern(temp.canvas, 'repeat');
|
||
if (!pattern) continue;
|
||
ctx.fillStyle = pattern;
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
this.backImage.push(canvas.canvas);
|
||
}
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onBackgroundGenerated?.(this, this.backImage);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修改地图渲染数据,对于溢出的内容会进行裁剪
|
||
* @param data 要渲染的地图数据
|
||
* @param width 数据的宽度
|
||
* @param x 第一个数据的横坐标,默认是0
|
||
* @param y 第一个数据的纵坐标,默认是0
|
||
*/
|
||
putRenderData(
|
||
data: number[],
|
||
width: number,
|
||
x: number = 0,
|
||
y: number = 0,
|
||
calAutotile: boolean = true
|
||
) {
|
||
if (data.length % width !== 0) {
|
||
logger.warn(
|
||
8,
|
||
`Incomplete render data is put. None will be filled to the lacked data.`
|
||
);
|
||
data.push(...Array(width - (data.length % width)).fill(0));
|
||
}
|
||
const height = Math.round(data.length / width);
|
||
if (!this.containsRect(x, y, width, height)) {
|
||
logger.warn(
|
||
9,
|
||
`Data transfered is partially (or totally) out of range. Overflowed data will be ignored.`
|
||
);
|
||
if (this.isRectOutside(x, y, width, height)) return;
|
||
}
|
||
// 特判特殊情况-全地图更新
|
||
if (
|
||
x === 0 &&
|
||
y === 0 &&
|
||
width === this.mapWidth &&
|
||
height === this.mapHeight
|
||
) {
|
||
// 为了不丢失引用,需要先清空,然后填充,不能直接赋值
|
||
this.renderData.splice(0);
|
||
this.renderData.push(...data);
|
||
} else if (data.length === 1) {
|
||
// 特判单个图块的情况
|
||
const index = x + y * this.mapWidth;
|
||
this.renderData[index] = data[0];
|
||
} else {
|
||
// 限定更新区域
|
||
const startX = Math.max(0, x);
|
||
const startY = Math.max(0, y);
|
||
const endX = Math.min(this.mapWidth, width);
|
||
const endY = Math.min(this.mapHeight, height);
|
||
for (let nx = startX; nx < endX; nx++) {
|
||
for (let ny = startY; ny < endY; ny++) {
|
||
// dx和dy表示数据在传入的data中的位置
|
||
const dx = nx - x;
|
||
const dy = ny - y;
|
||
const index = dx + dy * width;
|
||
const indexData = nx + nx * this.mapWidth;
|
||
this.renderData[indexData] = data[index];
|
||
}
|
||
}
|
||
}
|
||
// todo: 异步优化,到下一帧再更新
|
||
if (calAutotile) this.calAutotiles(x, y, width, height);
|
||
this.updateBlocks(x, y, width, height);
|
||
this.updateBigImages(x, y, width, height);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onDataPut?.(this, data, width, x, y, calAutotile);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新大怪物的渲染信息
|
||
*/
|
||
updateBigImages(x: number, y: number, width: number, height: number) {
|
||
const ex = x + width;
|
||
const ey = y + height;
|
||
const w = this.mapWidth;
|
||
const data = this.renderData;
|
||
|
||
for (let nx = x; nx < ex; nx++) {
|
||
for (let ny = y; ny < ey; ny++) {
|
||
const index = ny * w + nx;
|
||
this.bigImages.delete(index);
|
||
const num = data[index];
|
||
const renderable = texture.getRenderable(num);
|
||
if (!renderable || !renderable.bigImage) continue;
|
||
this.bigImages.set(index, {
|
||
...renderable,
|
||
x: nx,
|
||
y: ny,
|
||
zIndex: ny
|
||
});
|
||
}
|
||
}
|
||
|
||
this.needUpdateMoving = true;
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onBigImagesUpdate?.(this, x, y, width, height, this.bigImages);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算自动元件的连接信息(会丢失autotiles属性的引用)
|
||
*/
|
||
calAutotiles(x: number, y: number, width: number, height: number) {
|
||
const ex = x + width;
|
||
const ey = y + height;
|
||
const data = this.renderData;
|
||
const tile = texture.autotile;
|
||
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
|
||
|
||
const w = this.mapWidth;
|
||
const h = this.mapHeight;
|
||
|
||
this.autotiles = {};
|
||
|
||
/**
|
||
* 检查连接信息
|
||
* @param id 比较对象的id(就是正在检查周围的那个自动元件,九宫格中心的)
|
||
* @param index1 比较对象
|
||
* @param index2 被比较对象
|
||
* @param replace1 被比较对象相对比较对象应该处理的位数
|
||
* @param replace2 比较对象相对被比较对象应该处理的位数
|
||
*/
|
||
const check = (
|
||
x1: number,
|
||
y1: number,
|
||
x2: number,
|
||
y2: number,
|
||
replace1: number,
|
||
_replace2: number
|
||
) => {
|
||
const index1 = x1 + y1 * w;
|
||
const index2 = x2 + y2 * w;
|
||
this.autotiles[index1] ??= 0;
|
||
this.autotiles[index2] ??= 0;
|
||
// 与地图边缘,视为连接
|
||
if (x2 < 0 || y2 < 0 || x2 >= w || y2 >= h) {
|
||
this.autotiles[index1] |= replace1;
|
||
return;
|
||
}
|
||
const num1 = data[index1] as AllNumbersOf<'autotile'>; // 这个一定是自动元件
|
||
const num2 = data[index2] as AllNumbersOf<'autotile'>;
|
||
// 对于额外连接的情况
|
||
const autoConn = texture.getAutotileConnections(num1);
|
||
if (autoConn?.has(num2)) {
|
||
this.autotiles[index1] |= replace1;
|
||
return;
|
||
}
|
||
const info = map[num2 as Exclude<AllNumbers, 0>];
|
||
if (!info || info.cls !== 'autotile') {
|
||
// 被比较对象不是自动元件
|
||
this.autotiles[index1] &= ~replace1;
|
||
} else {
|
||
const parent2 = tile[num2].parent;
|
||
if (num2 === num1) {
|
||
// 二者一样,视为连接
|
||
this.autotiles[index1] |= replace1;
|
||
} else if (parent2?.has(num1)) {
|
||
// 被比较对象是比较对象的父元件,那么比较对象视为连接
|
||
this.autotiles[index1] |= replace1;
|
||
} else {
|
||
// 上述条件都不满足,那么不连接
|
||
this.autotiles[index1] &= ~replace1;
|
||
}
|
||
}
|
||
};
|
||
|
||
for (let nx = x; nx < ex; nx++) {
|
||
for (let ny = y; ny < ey; ny++) {
|
||
if (nx > w || ny > h) continue;
|
||
const index = nx + ny * w;
|
||
const num = data[index];
|
||
// 特判空气墙与空图块
|
||
if (num === 0 || num === 17 || num >= 10000) continue;
|
||
|
||
const info = map[num as Exclude<AllNumbers, 0>];
|
||
const { cls } = info;
|
||
if (cls !== 'autotile') continue;
|
||
|
||
// 太地狱了这个,看看就好
|
||
// 左上 左 左下
|
||
check(nx, ny, nx - 1, ny - 1, 0b10000000, 0b00001000);
|
||
check(nx, ny, nx - 1, ny, 0b00000001, 0b00010000);
|
||
check(nx, ny, nx - 1, ny + 1, 0b00000010, 0b00100000);
|
||
// 上 右上
|
||
check(nx, ny, nx, ny - 1, 0b01000000, 0b00000100);
|
||
check(nx, ny, nx + 1, ny - 1, 0b00100000, 0b00000010);
|
||
// 右 右下 下
|
||
check(nx, ny, nx + 1, ny, 0b00010000, 0b00000001);
|
||
check(nx, ny, nx + 1, ny + 1, 0b00001000, 0b10000000);
|
||
check(nx, ny, nx, ny + 1, 0b00000100, 0b01000000);
|
||
}
|
||
}
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onAutotilesCaled?.(this, x, y, width, height, this.autotiles);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置地图大小,会清空渲染数据(且丢失引用),因此后面应当紧跟 putRenderData,以保证渲染正常进行
|
||
* @param width 地图宽度
|
||
* @param height 地图高度
|
||
*/
|
||
setMapSize(width: number, height: number) {
|
||
this.mapWidth = width;
|
||
this.mapHeight = height;
|
||
this.renderData = Array(width * height).fill(0);
|
||
this.autotiles = {};
|
||
this.block.size(width, height);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onMapResize?.(this, width, height);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 给定一个矩形,更新其包含的块信息,注意由于自动元件的存在,实际判定范围会大一圈
|
||
* @param x 图格的左上角横坐标
|
||
* @param y 图格的左上角纵坐标
|
||
* @param width 横向有多少个图格
|
||
* @param height 纵向有多少个图格
|
||
*/
|
||
updateBlocks(x: number, y: number, width: number, height: number) {
|
||
const blocks = this.block.updateElementArea(
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
Layer.FRAME_ALL
|
||
);
|
||
|
||
this.update(this);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onBlocksUpdate?.(this, blocks, x, y, width, height);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算在传入的变换矩阵下,应该渲染哪些内容
|
||
* @param transform 变换矩阵
|
||
*/
|
||
calNeedRender(transform: Transform): NeedRenderData {
|
||
if (this.parent instanceof LayerGroup) {
|
||
// 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化
|
||
return this.parent.cacheNeedRender(transform, this.block);
|
||
} else {
|
||
return calNeedRenderOf(transform, this.cellSize, this.block);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新移动层的渲染信息
|
||
*/
|
||
updateMovingRenderable() {
|
||
this.movingRenderable = [];
|
||
this.movingRenderable.push(...this.bigImages.values());
|
||
this.movingRenderable.push(...this.moving);
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onMovingUpdate?.(this, this.movingRenderable);
|
||
}
|
||
this.movingRenderable.sort((a, b) => a.zIndex - b.zIndex);
|
||
}
|
||
|
||
/**
|
||
* 渲染当前地图
|
||
*/
|
||
renderMap(transform: Transform, need: NeedRenderData) {
|
||
this.staticMap.clear();
|
||
this.movingMap.clear();
|
||
this.backMap.clear();
|
||
|
||
if (this.needUpdateMoving) this.updateMovingRenderable();
|
||
|
||
for (const ex of this.extend.values()) {
|
||
ex.onBeforeRender?.(this, transform, need);
|
||
}
|
||
|
||
this.renderBack(transform, need);
|
||
this.renderStatic(transform, need);
|
||
this.renderMoving(transform);
|
||
for (const ex of this.extend.values()) {
|
||
ex.onAfterRender?.(this, transform, need);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染背景图
|
||
* @param transform 变换矩阵
|
||
* @param need 需要渲染的块
|
||
*/
|
||
protected renderBack(transform: Transform, need: NeedRenderData) {
|
||
const cell = this.cellSize;
|
||
const frame = (RenderItem.animatedFrame % 4) + 1;
|
||
const blockSize = this.block.blockSize;
|
||
const { back } = need;
|
||
const { ctx } = this.backMap;
|
||
|
||
const mat = transform.mat;
|
||
const [a, b, , c, d, , e, f] = mat;
|
||
ctx.setTransform(a, b, c, d, e, f);
|
||
|
||
if (this.background !== 0) {
|
||
// 画背景图
|
||
const length = this.backImage.length;
|
||
const img = this.backImage[frame % length];
|
||
back.forEach(([x, y]) => {
|
||
const sx = x * blockSize;
|
||
const sy = y * blockSize;
|
||
ctx.drawImage(
|
||
img,
|
||
sx * cell,
|
||
sy * cell,
|
||
blockSize * cell,
|
||
blockSize * cell
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 渲染静态层
|
||
*/
|
||
protected renderStatic(transform: Transform, need: NeedRenderData) {
|
||
const cell = this.cellSize;
|
||
const frame = RenderItem.animatedFrame % 4;
|
||
const { width } = this.block.blockData;
|
||
const blockSize = this.block.blockSize;
|
||
const { ctx } = this.staticMap;
|
||
|
||
ctx.save();
|
||
|
||
const { res: render } = need;
|
||
const [a, b, , c, d, , e, f] = transform.mat;
|
||
ctx.setTransform(a, b, c, d, e, f);
|
||
|
||
render.forEach(v => {
|
||
const x = v % width;
|
||
const y = Math.floor(v / width);
|
||
const sx = x * blockSize;
|
||
const sy = y * blockSize;
|
||
const index = v * 4 + frame;
|
||
|
||
const cache = this.block.cache.get(index);
|
||
if (cache) {
|
||
ctx.drawImage(
|
||
cache.canvas.canvas,
|
||
sx * cell,
|
||
sy * cell,
|
||
blockSize * cell,
|
||
blockSize * cell
|
||
);
|
||
return;
|
||
}
|
||
|
||
const ex = Math.min(sx + blockSize, this.mapWidth);
|
||
const ey = Math.min(sy + blockSize, this.mapHeight);
|
||
|
||
const temp = new MotaOffscreenCanvas2D();
|
||
temp.setAntiAliasing(false);
|
||
temp.setHD(false);
|
||
temp.withGameScale(false);
|
||
temp.size(core._PX_, core._PY_);
|
||
|
||
// 先画到临时画布,用于缓存
|
||
for (let nx = sx; nx < ex; nx++) {
|
||
for (let ny = sy; ny < ey; ny++) {
|
||
const blockIndex = nx + ny * this.mapWidth;
|
||
const num = this.renderData[blockIndex];
|
||
if (num === 0 || num === 17) continue;
|
||
const data = texture.getRenderable(num);
|
||
if (!data || data.bigImage) continue;
|
||
const f = frame % data.frame;
|
||
const i = data.animate === -1 ? f : data.animate;
|
||
const [isx, isy, w, h] = data.render[i];
|
||
const px = (nx - sx) * cell;
|
||
const py = (ny - sy) * cell;
|
||
const { image, autotile } = data;
|
||
if (!autotile) {
|
||
temp.ctx.drawImage(image, isx, isy, w, h, px, py, w, h);
|
||
} else {
|
||
const link = this.autotiles[blockIndex];
|
||
const i = image[link];
|
||
temp.ctx.drawImage(i, isx, isy, w, h, px, py, w, h);
|
||
}
|
||
}
|
||
}
|
||
ctx.drawImage(
|
||
temp.canvas,
|
||
sx * cell,
|
||
sy * cell,
|
||
blockSize * cell,
|
||
blockSize * cell
|
||
);
|
||
this.block.cache.set(index, {
|
||
canvas: temp,
|
||
symbol: temp.symbol
|
||
});
|
||
});
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
/**
|
||
* 渲染移动/大怪物层
|
||
*/
|
||
protected renderMoving(transform: Transform) {
|
||
const frame = (RenderItem.animatedFrame % 4) + 1;
|
||
const cell = this.cellSize;
|
||
const halfCell = cell / 2;
|
||
const { ctx } = this.movingMap;
|
||
|
||
ctx.save();
|
||
const mat = transform.mat;
|
||
const [a, b, , c, d, , e, f] = mat;
|
||
ctx.setTransform(a, b, c, d, e, f);
|
||
const max1 = Math.max(a, b, c, d) ** 2;
|
||
const max2 = Math.max(core._PX_, core._PY_) * 2;
|
||
const r = (max1 * max2) ** 2;
|
||
|
||
this.movingRenderable.forEach(v => {
|
||
const { x, y, image, render, animate } = v;
|
||
const ff = frame % 4;
|
||
const i = animate === -1 ? ff : animate;
|
||
const [sx, sy, w, h] = render[i];
|
||
const px = x * cell - w / 2 + halfCell;
|
||
const py = y * cell - h + cell;
|
||
const ex = px + w;
|
||
const ey = py + h;
|
||
|
||
if (
|
||
(px + e) ** 2 > r ||
|
||
(py + f) ** 2 > r ||
|
||
(ex + e) ** 2 > r ||
|
||
(ey + f) ** 2 > r
|
||
) {
|
||
return;
|
||
}
|
||
|
||
ctx.drawImage(image, sx, sy, w, h, px, py, w, h);
|
||
});
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
/**
|
||
* 对图块进行线性插值移动或瞬移\
|
||
* 线性插值移动:就是匀速平移,可以斜向移动\
|
||
* 瞬移:立刻移动到目标点
|
||
* @param index 要移动的图块在渲染数据中的索引位置
|
||
* @param type 线性插值移动或瞬移
|
||
* @param x 目标点横坐标
|
||
* @param y 目标点纵坐标
|
||
* @param time 移动总时长,注意不是每格时长
|
||
*/
|
||
move(
|
||
index: number,
|
||
type: 'linear' | 'swap',
|
||
x: number,
|
||
y: number,
|
||
time?: number
|
||
): Promise<void> {
|
||
const block = this.renderData[index];
|
||
const fx = index % this.width;
|
||
const fy = Math.floor(index / this.width);
|
||
|
||
if (type === 'swap' || time === 0) {
|
||
this.putRenderData([0], 1, fx, fy);
|
||
this.putRenderData([block], 1, x, y);
|
||
return Promise.resolve();
|
||
} else {
|
||
if (!time) return Promise.reject();
|
||
const dx = x - fx;
|
||
const dy = y - fy;
|
||
return this.moveAs(
|
||
index,
|
||
x,
|
||
y,
|
||
progress => {
|
||
return [dx * progress, dy * progress, Math.floor(dy + fy)];
|
||
},
|
||
time
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 让图块按照一个函数进行移动
|
||
* @param index 要移动的图块在渲染数据中的索引位置
|
||
* @param type 函数式移动
|
||
* @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。
|
||
* 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染,
|
||
* 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值,
|
||
* 因为此举会导致层级的重新排序,降低渲染性能。
|
||
* @param time 移动总时长
|
||
* @param relative 是否是相对模式
|
||
*/
|
||
moveAs(
|
||
index: number,
|
||
x: number,
|
||
y: number,
|
||
fn: TimingFn<3>,
|
||
time: number,
|
||
relative: boolean = true
|
||
): Promise<void> {
|
||
const block = this.renderData[index];
|
||
const fx = index % this.width;
|
||
const fy = Math.floor(index / this.width);
|
||
const renderable = texture.getRenderable(block);
|
||
if (!renderable) return Promise.reject();
|
||
|
||
const image = renderable.autotile
|
||
? renderable.image[0]
|
||
: renderable.image;
|
||
|
||
const moving: LayerMovingRenderable = {
|
||
x: fx,
|
||
y: fy,
|
||
zIndex: fy,
|
||
image: image,
|
||
autotile: false,
|
||
frame: renderable.frame,
|
||
bigImage: renderable.bigImage,
|
||
animate: -1,
|
||
render: renderable.render
|
||
};
|
||
this.moving.add(moving);
|
||
|
||
// 删除原始位置的图块
|
||
this.putRenderData([0], 1, fx, fy);
|
||
|
||
let nowZ = fy;
|
||
const startTime = Date.now();
|
||
return new Promise<void>(resolve => {
|
||
this.delegateTicker(
|
||
() => {
|
||
const now = Date.now();
|
||
const progress = (now - startTime) / time;
|
||
const [nx, ny, nz] = fn(progress);
|
||
const tx = relative ? nx + fx : nx;
|
||
const ty = relative ? ny + fy : ny;
|
||
moving.x = tx;
|
||
moving.y = ty;
|
||
moving.zIndex = nz;
|
||
if (nz !== nowZ) {
|
||
this.movingRenderable.sort(
|
||
(a, b) => a.zIndex - b.zIndex
|
||
);
|
||
}
|
||
this.update(this);
|
||
},
|
||
time,
|
||
() => {
|
||
this.putRenderData([block], 1, x, y);
|
||
this.moving.delete(moving);
|
||
resolve();
|
||
}
|
||
);
|
||
});
|
||
}
|
||
|
||
destroy(): void {
|
||
for (const ex of this.extend.values()) {
|
||
ex.onDestroy?.(this);
|
||
}
|
||
super.destroy();
|
||
}
|
||
}
|