refactor: 把渲染系统迁移到 monorepo

This commit is contained in:
AncTe committed 2025-03-04 22:02:08 +08:00
1 parent 8cc0c76846
commit 1556e2c3ea
108 files changed
+756 -411

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
import {
RenderAdapter,
transformCanvas,
ERenderItemEvent,
RenderItem,
Transform,
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
export class LayerGroupAnimate implements ILayerGroupRenderExtends {
static animateList: Set<LayerGroupAnimate> = new Set();
id: string = 'animate';
group!: LayerGroup;
hero?: HeroRenderer;
animate!: Animate;
private animation: Set<AnimateData> = new Set();
/**
* 绘制一个跟随勇士的动画
* @param name 动画id
*/
async drawHeroAnimate(name: AnimationIds) {
const animate = this.animate.animate(name, 0, 0);
this.updatePosition(animate);
await this.animate.draw(animate);
this.animation.delete(animate);
}
private updatePosition(animate: AnimateData) {
if (!this.checkHero()) return;
if (!this.hero?.renderable) return;
const { x, y } = this.hero.renderable;
const cell = this.group.cellSize;
const half = cell / 2;
animate.centerX = x * cell + half;
animate.centerY = y * cell + half;
}
private onMoveTick = (x: number, y: number) => {
const cell = this.group.cellSize;
const half = cell / 2;
const ax = x * cell + half;
const ay = y * cell + half;
this.animation.forEach(v => {
v.centerX = ax;
v.centerY = ay;
});
};
private listen() {
if (this.checkHero()) {
this.hero!.on('moveTick', this.onMoveTick);
}
}
private checkHero() {
if (this.hero) return true;
const ex = this.group.getLayer('event')?.getExtends('floor-hero');
if (ex instanceof HeroRenderer) {
this.hero = ex;
return true;
}
return false;
}
awake(group: LayerGroup): void {
this.group = group;
this.animate = new Animate();
this.animate.size(group.width, group.height);
this.animate.setHD(true);
this.animate.setZIndex(100);
group.appendChild(this.animate);
LayerGroupAnimate.animateList.add(this);
this.listen();
}
onDestroy(_group: LayerGroup): void {
if (this.checkHero()) {
this.hero!.off('moveTick', this.onMoveTick);
LayerGroupAnimate.animateList.delete(this);
}
}
}
interface AnimateData {
obj: globalThis.Animate;
/** 第一帧是全局第几帧 */
readonly start: number;
/** 当前是第几帧 */
index: number;
/** 是否需要播放音频 */
sound: boolean;
centerX: number;
centerY: number;
onEnd?: () => void;
readonly absolute: boolean;
}
export interface EAnimateEvent extends ERenderItemEvent {}
export class Animate extends RenderItem<EAnimateEvent> {
/** 绝对位置的动画 */
private absoluteAnimates: Set<AnimateData> = new Set();
/** 静态位置的动画 */
private staticAnimates: Set<AnimateData> = new Set();
private delegation: number;
private frame: number = 0;
private lastTime: number = 0;
constructor() {
super('absolute', false, true);
this.delegation = this.delegateTicker(time => {
if (time - this.lastTime < 50) return;
this.lastTime = time;
this.frame++;
if (
this.absoluteAnimates.size > 0 ||
this.staticAnimates.size > 0
) {
this.update(this);
}
});
adapter.add(this);
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
if (
this.absoluteAnimates.size === 0 &&
this.staticAnimates.size === 0
) {
return;
}
this.drawAnimates(this.absoluteAnimates, canvas);
transformCanvas(canvas, transform);
this.drawAnimates(this.staticAnimates, canvas);
}
private drawAnimates(
data: Set<AnimateData>,
canvas: MotaOffscreenCanvas2D
) {
if (data.size === 0) return;
const { ctx } = canvas;
const toDelete = new Set<AnimateData>();
data.forEach(v => {
const obj = v.obj;
const index = v.index;
const frame = obj.frames[index];
const ratio = obj.ratio;
if (!v.sound) {
const se = (index % obj.frame) + 1;
core.playSound(v.obj.se[se], v.obj.pitch[se]);
v.sound = true;
}
const centerX = v.centerX;
const centerY = v.centerY;
frame.forEach(v => {
const img = obj.images[v.index];
if (!img) return;
const realWidth = (img.width * ratio * v.zoom) / 100;
const realHeight = (img.height * ratio * v.zoom) / 100;
ctx.globalAlpha = v.opacity / 255;
const cx = centerX + v.x;
const cy = centerY + v.y;
const ix = -realWidth / 2;
const iy = -realHeight / 2;
const angle = v.angle ? (-v.angle * Math.PI) / 180 : 0;
ctx.save();
ctx.translate(cx, cy);
if (v.mirror) {
ctx.scale(-1, 1);
}
ctx.rotate(angle);
ctx.drawImage(img, ix, iy, realWidth, realHeight);
ctx.restore();
});
const now = this.frame - v.start;
if (now !== v.index) v.sound = true;
v.index = now;
if (v.index === v.obj.frame) {
toDelete.add(v);
}
});
toDelete.forEach(v => {
data.delete(v);
v.onEnd?.();
});
}
/**
* 创建一个可以被执行的动画
* @param name 动画名称
* @param absolute 是否是绝对定位,绝对定位不会受到transform的影响
*/
animate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
const animate = core.material.animates[name];
const data: AnimateData = {
index: 0,
start: this.frame,
obj: animate,
centerX: x,
centerY: y,
absolute,
sound: false
};
return data;
}
/**
* 绘制动画,动画结束时兑现返回的Promise
* @param animate 动画信息
* @returns
*/
draw(animate: AnimateData): Promise<void> {
return new Promise(res => {
if (animate.absolute) {
this.absoluteAnimates.add(animate);
} else {
this.staticAnimates.add(animate);
}
animate.onEnd = () => {
res();
};
});
}
/**
* 根据动画名称、坐标、定位绘制动画
* @param name 动画名称
* @param absolute 是否是绝对定位
*/
drawAnimate(
name: AnimationIds,
x: number,
y: number,
absolute: boolean = false
) {
return this.draw(this.animate(name, x, y, absolute));
}
destroy(): void {
super.destroy();
this.removeTicker(this.delegation);
adapter.remove(this);
}
}
const adapter = new RenderAdapter<Animate>('animate');
adapter.receive('drawAnimate', (item, name, x, y, absolute) => {
return item.drawAnimate(name, x, y, absolute);
});
adapter.receiveGlobal('drawHeroAnimate', name => {
const execute: Promise<void>[] = [];
LayerGroupAnimate.animateList.forEach(v => {
execute.push(v.drawHeroAnimate(name));
});
return Promise.all(execute);
});
+314
View File
@@ -0,0 +1,314 @@
import { EventEmitter } from 'eventemitter3';
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
interface BlockCacherEvent {
split: [];
}
interface BlockData {
/** 横向宽度,包括rest的那一个块 */
width: number;
/** 纵向宽度,包括rest的那一个块 */
height: number;
/** 横向最后一个块的宽度 */
restWidth: number;
/** 纵向最后一个块的高度 */
restHeight: number;
}
export interface IBlockCacheable {
/**
* 摧毁这个缓存元素
*/
destroy(): void;
}
/**
* 简单分块缓存类,内容包含元素与分块两种,其中元素是最小单元,分块是缓存单元。
* 拿楼层举例,假如我将楼层按照13x13划分缓存,那么元素就是每个图块,而分块就是这13x13的缓存分块。
* 为方便区分,在相关函数的注释最后,都会有`xx -> yy`的说明,
* 其中xx说明传入的数据是元素还是分块的数据,而yy表示其返回值或转换为的值
*/
export class BlockCacher<
T extends IBlockCacheable
> extends EventEmitter<BlockCacherEvent> {
/** 区域宽度 */
width: number;
/** 区域高度 */
height: number;
/** 分块大小 */
blockSize: number;
/** 分块信息 */
blockData: BlockData = {
width: 0,
height: 0,
restWidth: 0,
restHeight: 0
};
/** 缓存深度,例如填4的时候表示每格包含4个缓存 */
cacheDepth: number = 1;
/** 缓存内容,计算公式为 (x + y * width) * depth + deep */
cache: Map<number, T> = new Map();
constructor(
width: number,
height: number,
size: number,
depth: number = 1
) {
super();
this.width = width;
this.height = height;
this.blockSize = size;
this.cacheDepth = depth;
this.split();
}
/**
* 设置区域大小
* @param width 区域宽度
* @param height 区域高度
*/
size(width: number, height: number) {
this.width = width;
this.height = height;
this.split();
}
/**
* 设置分块大小
*/
setBlockSize(size: number) {
this.blockSize = size;
this.split();
}
/**
* 设置缓存深度,设置后会自动将旧缓存移植到新缓存中,最大值为31
* @param depth 缓存深度
*/
setCacheDepth(depth: number) {
if (depth > 31) {
logger.error(11);
return;
}
const old = this.cache;
const before = this.cacheDepth;
this.cache = new Map();
old.forEach((v, k) => {
const index = Math.floor(k / before);
const deep = k % before;
this.cache.set(index * depth + deep, v);
});
old.clear();
this.cacheDepth = depth;
}
/**
* 执行分块
*/
split() {
this.blockData = {
width: Math.ceil(this.width / this.blockSize),
height: Math.ceil(this.height / this.blockSize),
restWidth: this.width % this.blockSize,
restHeight: this.height % this.blockSize
};
this.emit('split');
}
/**
* 清除指定块的索引(分块->void)
* @param index 要清除的缓存索引
* @param deep 清除哪些深度的缓存,至多31位二进制数,例如填0b111就是清除前三层的索引
*/
clearCache(index: number, deep: number) {
const depth = this.cacheDepth;
for (let i = 0; i < depth; i++) {
if (deep & (1 << i)) {
const nowIndex = index * this.cacheDepth + i;
const item = this.cache.get(nowIndex);
item?.destroy();
this.cache.delete(nowIndex);
}
}
}
/**
* 清空指定索引的缓存,与 {@link clearCache} 不同的是,这里会直接清空对应索引的缓存,而不是指定分块的缓存(分块->void)
*/
clearCacheByIndex(index: number) {
const item = this.cache.get(index);
item?.destroy();
this.cache.delete(index);
}
/**
* 清空所有缓存
*/
clearAllCache() {
this.cache.forEach(v => v.destroy());
this.cache.clear();
}
/**
* 根据分块的横纵坐标获取其索引(分块->分块)
*/
getIndex(x: number, y: number) {
return x + y * this.blockData.width;
}
/**
* 根据元素位置获取分块索引(注意是单个元素的位置,而不是分块的位置)(元素->分块)
*/
getIndexByLoc(x: number, y: number) {
return this.getIndex(
Math.floor(x / this.blockSize),
Math.floor(y / this.blockSize)
);
}
/**
* 根据块的索引获取其位置(分块->分块)
*/
getBlockXYByIndex(index: number): LocArr {
const width = this.blockData.width;
return [index % width, Math.floor(index / width)];
}
/**
* 获取一个元素位置所在的分块位置(即使它不在任何分块内)(元素->分块)
*/
getBlockXY(x: number, y: number): LocArr {
return [Math.floor(x / this.blockSize), Math.floor(y / this.blockSize)];
}
/**
* 根据分块坐标与deep获取一个分块的精确索引(分块->分块)
*/
getPreciseIndex(x: number, y: number, deep: number) {
return (x + y * this.blockSize) * this.cacheDepth + deep;
}
/**
* 根据元素坐标及deep获取元素所在块的精确索引(元素->分块)
*/
getPreciseIndexByLoc(x: number, y: number, deep: number) {
return this.getPreciseIndex(...this.getBlockXY(x, y), deep);
}
/**
* 更新指定元素区域内的缓存(注意坐标是元素坐标,而非分块坐标)(元素->分块)
* @param deep 缓存清除深度,默认全部清空
* @returns 更新区域内的所有有关分块索引
*/
updateElementArea(
x: number,
y: number,
width: number,
height: number,
deep: number = 2 ** 31 - 1
) {
const [bx, by] = this.getBlockXY(x, y);
const [ex, ey] = this.getBlockXY(x + width - 1, y + height - 1);
return this.updateArea(bx, by, ex - bx, ey - by, deep);
}
/**
* 更新指定分块区域内的缓存(注意坐标是分块坐标,而非元素坐标)(分块->分块)
* @param deep 缓存清除深度,默认全部清空
* @returns 更新区域内的所有分块索引
*/
updateArea(
x: number,
y: number,
width: number,
height: number,
deep: number = 2 ** 31 - 1
) {
const blocks = this.getIndexOf(x, y, width, height);
blocks.forEach(v => {
this.clearCache(v, deep);
});
return blocks;
}
/**
* 传入分块坐标与范围,获取该区域内包含的所有分块索引(分块->分块)
*/
getIndexOf(x: number, y: number, width: number, height: number) {
const res = new Set<number>();
const sx = Math.max(x, 0);
const sy = Math.max(y, 0);
const ex = Math.min(x + width, this.blockData.width);
const ey = Math.min(y + height, this.blockData.height);
for (let nx = sx; nx <= ex; nx++) {
for (let ny = sy; ny <= ey; ny++) {
const index = this.getIndex(nx, ny);
res.add(index);
}
}
return res;
}
/**
* 传入元素坐标与范围,获取该区域内包含的所有分块索引(元素->分块)
*/
getIndexOfElement(x: number, y: number, width: number, height: number) {
const [bx, by] = this.getBlockXY(x, y);
const [ex, ey] = this.getBlockXY(x + width, y + height);
return this.getIndexOf(bx, by, ex - bx, ey - by);
}
/**
* 根据分块索引,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
* @param block 分块索引
*/
getRectOfIndex(block: number) {
const [x, y] = this.getBlockXYByIndex(block);
return this.getRectOfBlockXY(x, y);
}
/**
* 根据分块坐标,获取这个分块所在区域的元素矩形范围(左上角横纵坐标及右下角横纵坐标)(分块->元素)
* @param x 分块横坐标
* @param y 分块纵坐标
*/
getRectOfBlockXY(x: number, y: number) {
return [
x * this.blockSize,
y * this.blockSize,
(x + 1) * this.blockSize,
(y + 1) * this.blockSize
];
}
/**
* 摧毁这个块缓存
*/
destroy() {
this.clearAllCache();
}
}
export interface ICanvasCacheItem extends IBlockCacheable {
readonly canvas: MotaOffscreenCanvas2D;
symbol: number;
}
export class CanvasCacheItem implements ICanvasCacheItem {
constructor(
public canvas: MotaOffscreenCanvas2D,
public symbol: number
) {}
destroy(): void {
this.canvas.delete();
}
}
+556
View File
@@ -0,0 +1,556 @@
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
import { SizedCanvasImageSource } from './types';
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
type ImageMapKeys = Exclude<Cls, 'tileset' | 'autotile'>;
type ImageMap = Record<ImageMapKeys, HTMLImageElement>;
const i = (img: ImageMapKeys) => {
return core.material.images[img];
};
const imageMap: Partial<ImageMap> = {};
Mota.require('var', 'loading').once('loaded', () => {
[
'enemys',
'enemy48',
'npcs',
'npc48',
'terrains',
'items',
'animates'
].forEach(v => (imageMap[v as ImageMapKeys] = i(v as ImageMapKeys)));
});
interface AutotileCache {
parent?: Set<AllNumbersOf<'autotile'>>;
frame: number;
cache: Record<string, HTMLCanvasElement>;
}
type AutotileCaches = Record<AllNumbersOf<'autotile'>, AutotileCache>;
interface TextureRequire {
tileset: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
autotile: AutotileCaches;
images: Record<ImageIds, HTMLImageElement>;
}
interface RenderableDataBase {
/** 图块的总帧数 */
frame: number;
/** 对应图块属性的动画帧数,-1表示没有设定,0表示第一帧 */
animate: number;
/** 是否是大怪物 */
bigImage: boolean;
render: [x: number, y: number, width: number, height: number][];
}
export interface RenderableData extends RenderableDataBase {
image: SizedCanvasImageSource;
autotile: false;
}
export interface AutotileRenderable extends RenderableDataBase {
image: Record<string, SizedCanvasImageSource>;
autotile: true;
bigImage: false;
}
class TextureCache {
tileset!: Record<string, HTMLImageElement>;
material: Record<ImageMapKeys, HTMLImageElement>;
autotile!: AutotileCaches;
images!: Record<ImageIds, HTMLImageElement>;
idNumberMap!: IdToNumber;
/** 渲染信息 */
renderable: Map<number, RenderableData | AutotileRenderable> = new Map();
/** 自动元件额外连接信息,用于对非自身图块进行连接 */
autoConn: Map<number, Set<number>> = new Map();
/** 行走图朝向绑定 */
characterDirection: Record<Dir2, number> = {
down: 0,
left: 1,
right: 2,
up: 3,
leftup: 1,
leftdown: 1,
rightup: 2,
rightdown: 2
};
/** 行走图转向顺序 */
characterTurn: Dir[] = ['up', 'right', 'down', 'left'];
characterTurn2: Dir2[] = ['leftup', 'rightup', 'rightdown', 'leftdown'];
constructor() {
this.material = imageMap as Record<ImageMapKeys, HTMLImageElement>;
Mota.require('var', 'loading').once('loaded', () => {
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
// @ts-expect-error 无法推导
this.idNumberMap = {};
for (const [key, { id }] of Object.entries(map)) {
// @ts-expect-error 无法推导
this.idNumberMap[id] = parseInt(key) as AllNumbers;
}
this.tileset = core.material.images.tilesets;
this.autotile = splitAutotiles(this.idNumberMap);
this.images = core.material.images.images;
this.calRenderable();
this.calAutotileConnections();
});
}
/**
* 获取纹理
* @param type 纹理类型
* @param key 纹理名称
*/
require<T extends keyof TextureRequire, K extends keyof TextureRequire[T]>(
type: T,
key: K
): TextureRequire[T][K] {
return this[type][key];
}
/**
* 计算每个图块的可渲染信息
*/
private calRenderable() {
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
for (const key of Object.keys(map)) {
this.calRenderableByNum(parseInt(key));
}
}
/**
* 根据图块数字计算出它的渲染信息
* @param num 图块数字
*/
calRenderableByNum(
num: number
): RenderableData | AutotileRenderable | null {
const map = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
const enemys = enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80;
const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
/** 特判空图块与空气墙 */
if (num === 0 || num === 17) return null;
// 额外素材
if (num >= 10000) {
const offset = core.getTilesetOffset(num);
if (!offset) return null;
const { image, x, y } = offset;
const data: RenderableData = {
image: this.tileset[image],
frame: 1,
render: [[x * 32, y * 32, 32, 32]],
animate: 0,
autotile: false,
bigImage: false
};
this.renderable.set(num, data);
return data;
}
const data = map[num as Exclude<AllNumbers, 0>];
// 地狱般的分支if
if (data) {
let { faceIds, bigImage } = data;
const { cls, id, animate } = data;
if (cls === 'enemys' || cls === 'enemy48') {
// 怪物需要特殊处理,因为它的大怪物信息不在 maps 里面
({ bigImage, faceIds } = enemys[id as EnemyIds]);
}
if (bigImage) {
const image = core.material.images.images[bigImage];
if (!image) {
logger.warn(10, id);
return null;
}
let line = 0;
if (faceIds) {
const arr = ['down', 'left', 'right', 'up'];
for (let i = 0; i < arr.length; i++) {
if (faceIds[arr[i] as Dir] === id) {
line = i;
break;
}
}
}
const totalLines = image.width / image.height >= 2 ? 1 : 4;
const w = Math.round(image.width / 4);
const h = Math.round(image.height / totalLines);
const y = h * line;
const data: RenderableData = {
image,
frame: 4,
render: [
[0, y, w, h],
[w, y, w, h],
[w * 2, y, w, h],
[w * 3, y, w, h]
],
animate: (animate ?? 0) - 1,
autotile: false,
bigImage: true
};
this.renderable.set(num, data);
return data;
}
// enemy48和npc48都应该视为大怪物
if (cls === 'enemy48' || cls === 'npc48') {
const img = core.material.images[cls];
if (!img) return null;
// @ts-expect-error 无法推导
const line = icons[cls][id];
const w = 32;
const h = 48;
const y = h * line;
const data: RenderableData = {
image: img,
frame: 4,
render: [
[0, y, w, h],
[w, y, w, h],
[w * 2, y, w, h],
[w * 3, y, w, h]
],
animate: (animate ?? 0) - 1,
autotile: false,
bigImage: true
};
this.renderable.set(num, data);
return data;
}
// 自动元件
if (cls === 'autotile') {
if (!this.autotile) return null;
const auto = this.autotile[num as AllNumbersOf<'autotile'>];
const cell = 32;
const render: [number, number, number, number][] = [];
if (auto.frame >= 1) {
render.push([0, 0, cell, cell]);
}
if (auto.frame >= 3) {
render.push(
[cell, 0, cell, cell],
[cell * 2, 0, cell, cell]
);
}
if (auto.frame >= 4) {
render.push([cell * 3, 0, cell, cell]);
}
const data: AutotileRenderable = {
image: auto.cache,
frame: auto.frame,
render,
autotile: true,
bigImage: false,
animate: (animate ?? 0) - 1
};
this.renderable.set(num, data);
return data;
} else {
const image =
core.material.images[
cls as Exclude<Cls, 'tileset' | 'autotile'>
];
if (!image) return null;
const frame = core.getAnimateFrames(cls);
const cell = 32;
// @ts-expect-error 无法推导
const offset = (icons[cls][id] as number) * cell;
const render: [number, number, number, number][] = [
[0, offset, cell, cell]
];
if (frame === 2) {
render.push([cell, offset, cell, cell]);
}
if (frame === 4) {
render.push(
[cell, offset, cell, cell],
[cell * 2, offset, cell, cell],
[cell * 3, offset, cell, cell]
);
}
const data: RenderableData = {
image,
frame: frame,
render,
autotile: false,
bigImage: false,
animate: (animate ?? 0) - 1
};
this.renderable.set(num, data);
return data;
}
} else {
logger.warn(11, num.toString());
return null;
}
}
/**
* 获取一个图块的渲染信息,自动元件会特别标明autotile属性
* @param num 图块数字
*/
getRenderable(num: number) {
return this.renderable.get(num) ?? this.calRenderableByNum(num);
}
/**
* 计算自动元件的额外连接
*/
private calAutotileConnections() {
const icons = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
const maps = maps_90f36752_8815_4be8_b32b_d7fad1d0542e;
Object.keys(icons.autotile).forEach(v => {
const num = this.idNumberMap[v as AllIdsOf<'autotile'>];
const data = maps[num];
const { autotileConnection } = data;
if (!autotileConnection) return;
const list = new Set<AllNumbers>();
autotileConnection.forEach(v => {
if (typeof v === 'number') {
list.add(v);
} else {
list.add(this.idNumberMap[v]);
}
});
this.autoConn.set(num, list);
});
}
/**
* 获取自动元件的额外连接情况
* @param num 自动元件的图块数字
*/
getAutotileConnections(num: number) {
return this.autoConn.get(num);
}
}
export const texture = new TextureCache();
// 3x4 与 2x3 的自动元件信息
// 将自动元件按 16x16 切分后,数组分别表示 左上 右上 右下 左下 所在图块位置
const bigAutotile: Record<number, [number, number, number, number]> = {};
const smallAutotile: Record<number, [number, number, number, number]> = {};
function getAutotileIndices() {
// 应当从 0 - 255 进行枚举
// 二进制从高位到低位依次是 左上 上 右上 右 右下 下 左下 左
// 有兴趣可以研究下这个算法
const get = (
target: Record<number, [number, number, number, number]>,
mode: 1 | 2
) => {
const h = mode === 1 ? 4 : 2;
const v = mode === 1 ? 24 : 8;
const luo = mode === 1 ? 12 : 8; // leftup origin
const ruo = mode === 1 ? 17 : 11; // rightup origin
const ldo = mode === 1 ? 42 : 20; // leftdown origin
const rdo = mode === 1 ? 47 : 23; // rightdown origin
const luc = mode === 1 ? 4 : 2; // leftup corner
const ruc = mode === 1 ? 5 : 3; // rightup corner
const rdc = mode === 1 ? 11 : 7; // rightdown corner
const ldc = mode === 1 ? 10 : 6; // leftdown corner
for (let i = 0; i <= 0b11111111; i++) {
let lu = luo; // leftup
let ru = ruo; // rightup
let ld = ldo; // leftdown
let rd = rdo; // rightdown
// 先看四个方向,最后看斜角方向
if (i & 0b00000001) {
lu += h;
ld += h;
if (i & 0b00010000) {
ru += h / 2;
rd += h / 2;
}
}
if (i & 0b00000100) {
ld -= v;
rd -= v;
if (i & 0b01000000) {
lu -= v / 2;
ru -= v / 2;
}
}
if (i & 0b00010000) {
ru -= h;
rd -= h;
if (i & 0b00000001) {
lu -= h / 2;
ld -= h / 2;
}
}
if (i & 0b01000000) {
lu += v;
ru += v;
if (i & 0b00000100) {
ld += v / 2;
rd += v / 2;
}
}
// 斜角
if ((i & 0b11000001) === 0b01000001) {
lu = luc;
}
if ((i & 0b01110000) === 0b01010000) {
ru = ruc;
}
if ((i & 0b00011100) === 0b00010100) {
rd = rdc;
}
if ((i & 0b00000111) === 0b00000101) {
ld = ldc;
}
target[i] = [lu, ru, rd, ld];
}
};
get(bigAutotile, 1);
get(smallAutotile, 2);
}
getAutotileIndices();
function getRepeatMap() {
// 实际上3x4与2x3的重复映射是一致的,因此只需要计算一个就行了
// 这里使用2x3的进行计算
const calculated: Record<string, number> = {};
const repeatMap: Record<number, number> = {};
for (const [num, [lu, ru, rd, ld]] of Object.entries(smallAutotile)) {
const n = lu + ru * 24 + rd * 24 ** 2 + ld * 24 ** 3;
calculated[num] = n;
for (const [num2, n2] of Object.entries(calculated)) {
if (n2 === n) {
repeatMap[Number(num)] = Number(num2);
break;
}
}
}
return repeatMap;
}
function splitAutotiles(map: IdToNumber): AutotileCaches {
const cache: Partial<AutotileCaches> = {};
/** 重复映射,由于自动元件只有48种,其余的208种是重复的,因此需要获取重复映射 */
const repeatMap: Record<number, number> = getRepeatMap();
/** 每个自动元件左上角32*32的内容,用于判断父子关系 */
const masterMap: Partial<Record<AllNumbersOf<'autotile'>, string>> = {};
for (const [key, img] of Object.entries(core.material.images.autotile)) {
const auto = map[key as AllIdsOf<'autotile'>];
// 判断自动元件类型
let mode: 1 | 2 = 1;
let frame = 1;
if (img.width === 384) {
frame = 4;
} else if (img.width === 192) {
mode = 2;
frame = 3;
} else if (img.width === 64) {
mode = 2;
}
cache[auto] = {
frame,
cache: {}
};
// 父子关系,截取本图块的左上角存入map
const master = new MotaOffscreenCanvas2D();
master.setHD(false);
master.setAntiAliasing(false);
master.withGameScale(false);
master.size(32, 32);
master.ctx.drawImage(img, 0, 0, 32, 32, 0, 0, 32, 32);
masterMap[auto] = master.canvas.toDataURL('image/png');
master.delete();
// 自动图块的绘制信息
for (let i = 0; i <= 0b11111111; i++) {
const re = repeatMap[i];
if (re) {
const cached = cache[auto]!.cache[re];
if (cached) {
cache[auto]!.cache[i] = cached;
continue;
}
}
const data = (mode === 1 ? bigAutotile : smallAutotile)[i];
const row = mode === 1 ? 6 : 4;
const info: [number, number][] = data.map(v => [
(v % row) * 16,
Math.floor(v / row) * 16
]);
const canvas = new MotaOffscreenCanvas2D();
canvas.setHD(false);
canvas.setAntiAliasing(false);
canvas.withGameScale(false);
canvas.size(32 * frame, 32);
canvas.freeze();
const ctx = canvas.ctx;
for (let i = 0; i < frame; i++) {
const dx = 32 * i;
const sx1 = info[0][0] + (i * row * 32) / 2;
const sx2 = info[1][0] + (i * row * 32) / 2;
const sx3 = info[2][0] + (i * row * 32) / 2;
const sx4 = info[3][0] + (i * row * 32) / 2;
const sy1 = info[0][1];
const sy2 = info[1][1];
const sy3 = info[2][1];
const sy4 = info[3][1];
ctx.drawImage(img, sx1, sy1, 16, 16, dx, 0, 16, 16);
ctx.drawImage(img, sx2, sy2, 16, 16, dx + 16, 0, 16, 16);
ctx.drawImage(img, sx3, sy3, 16, 16, dx + 16, 16, 16, 16);
ctx.drawImage(img, sx4, sy4, 16, 16, dx, 16, 16, 16);
}
cache[auto]!.cache[i] = canvas.canvas;
}
}
const judge = new MotaOffscreenCanvas2D();
judge.setHD(false);
judge.setAntiAliasing(false);
judge.withGameScale(false);
judge.size(32, 32);
// 进行父子关系判断
for (const [key, img] of Object.entries(core.material.images.autotile)) {
const auto = map[key as AllIdsOf<'autotile'>];
// 只针对3*4的图块进行,截取第一行中间的,然后判断
judge.clear();
judge.ctx.drawImage(img, 32, 0, 32, 32, 0, 0, 32, 32);
const data = judge.canvas.toDataURL('image/png');
for (const [key, data2] of Object.entries(masterMap)) {
const auto2 = map[key as AllIdsOf<'autotile'>];
if (data === data2) {
cache[auto]!.parent ??= new Set();
cache[auto]!.parent!.add(auto2);
}
}
}
judge.delete();
return cache as AutotileCaches;
}
+624
View File
@@ -0,0 +1,624 @@
import { Animation, TimingFn, Transition } from 'mutate-animate';
import { RenderItem, Transform } from '@motajs/render-core';
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
export interface ICameraTranslate {
readonly type: 'translate';
readonly from: Camera;
x: number;
y: number;
}
export interface ICameraRotate {
readonly type: 'rotate';
readonly from: Camera;
/** 旋转角,单位弧度 */
angle: number;
}
export interface ICameraScale {
readonly type: 'scale';
readonly from: Camera;
x: number;
y: number;
}
type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate;
interface CameraEvent {
destroy: [];
}
export class Camera extends EventEmitter<CameraEvent> {
/** 当前绑定的渲染元素 */
readonly binded: RenderItem;
/** 目标变换矩阵,默认与 `this.binded.transform` 同引用 */
transform: Transform;
/** 委托ticker的id */
private delegation: number;
/** 所有的动画id */
private animationIds: Set<number> = new Set();
/** 是否需要更新视角 */
private needUpdate: boolean = false;
/** 是否启用摄像机 */
private enabled: boolean = true;
/** 变换操作列表,因为矩阵乘法跟顺序有关,因此需要把各个操作拆分成列表进行 */
protected operation: CameraOperation[] = [];
/** 渲染元素到摄像机的映射 */
private static cameraMap: Map<RenderItem, Camera> = new Map();
/**
* 获取一个渲染元素的摄像机,如果不存在则为它创建一个并返回。注意使用`new Camera`创建的摄像机不在此列
* @param item 渲染元素
*/
static for(item: RenderItem) {
const camera = this.cameraMap.get(item);
if (!camera) {
const ca = new Camera(item);
this.cameraMap.set(item, ca);
return ca;
} else {
return camera;
}
}
constructor(item: RenderItem) {
super();
this.binded = item;
this.delegation = item.delegateTicker(() => this.tick());
this.transform = item.transform;
item.on('destroy', () => {
this.destroy();
});
const ca = Camera.cameraMap.get(item);
if (ca && !ca.enabled) {
logger.warn(22);
}
}
private tick = () => {
if (!this.needUpdate || !this.enabled) return;
const trans = this.transform;
trans.reset();
for (const o of this.operation) {
if (o.type === 'translate') {
trans.translate(-o.x, -o.y);
} else if (o.type === 'rotate') {
trans.rotate(o.angle);
} else {
trans.scale(o.x, o.y);
}
}
this.binded.update(this.binded);
this.needUpdate = false;
};
/**
* 禁用这个摄像机
*/
disable() {
this.enabled = false;
}
/**
* 启用这个摄像机
*/
enable() {
this.enabled = true;
}
/**
* 在下一帧进行强制更新
*/
requestUpdate() {
this.needUpdate = true;
}
/**
* 移除一个变换操作
* @param operation 要移除的操作
*/
removeOperation(operation: CameraOperation) {
const index = this.operation.indexOf(operation);
if (index === -1) return;
this.operation.splice(index, 1);
}
/**
* 清空变换操作列表
*/
clearOperation() {
this.operation.splice(0);
}
/**
* 添加一个平移操作
* @returns 添加的平移变换操作
*/
addTranslate(): ICameraTranslate {
const item: ICameraTranslate = {
type: 'translate',
x: 0,
y: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
* 添加一个旋转操作
* @returns 添加的旋转变换操作
*/
addRotate(): ICameraRotate {
const item: ICameraRotate = {
type: 'rotate',
angle: 0,
from: this
};
this.operation.push(item);
return item;
}
/**
* 添加一个放缩操作
* @returns 添加的放缩变换操作
*/
addScale(): ICameraScale {
const item: ICameraScale = {
type: 'scale',
x: 1,
y: 1,
from: this
};
this.operation.push(item);
return item;
}
/**
* 施加动画
* @param time 动画时长
* @param update 每帧的更新函数
*/
applyAnimation(time: number, update: () => void) {
const delegation = this.binded.delegateTicker(
() => {
update();
this.needUpdate = true;
},
time,
() => {
update();
this.needUpdate = true;
this.animationIds.delete(delegation);
}
);
this.animationIds.add(delegation);
}
/**
* 为一个平移操作实施动画
* @param operation 平移操作
* @param animate 动画实例
* @param time 动画时长
*/
applyTranslateAnimation(
operation: ICameraTranslate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.x;
operation.y = animate.y;
};
this.applyAnimation(time, update);
}
/**
* 为一个旋转操作实施动画
* @param operation 旋转操作
* @param animate 动画实例
* @param time 动画时长
*/
applyRotateAnimation(
operation: ICameraRotate,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.angle = animate.angle;
};
this.applyAnimation(time, update);
}
/**
* 为一个缩放操作实施动画
* @param operation 缩放操作
* @param animate 动画实例
* @param time 动画时长
*/
applyScaleAnimation(
operation: ICameraScale,
animate: Animation,
time: number
) {
if (operation.from !== this) {
logger.warn(20);
return;
}
const update = () => {
operation.x = animate.size;
operation.y = animate.size;
};
this.applyAnimation(time, update);
}
/**
* 为一个平移操作实施渐变,使用渐变的 x,y 值,即`transition.value.x`与`transition.value.y`
* @param operation 平移操作
* @param animate 渐变实例
* @param time 渐变时长
*/
applyTranslateTransition(
operation: ICameraTranslate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.x;
operation.y = animate.value.y;
};
this.applyAnimation(time, update);
}
/**
* 为一个旋转操作实施渐变,使用渐变的 angle 值,即`transition.value.angle`
* @param operation 旋转操作
* @param animate 渐变实例
* @param time 渐变时长
*/
applyRotateTransition(
operation: ICameraRotate,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.angle = animate.value.angle;
};
this.applyAnimation(time, update);
}
/**
* 为一个缩放操作实施渐变,使用渐变的 size 值,即`transition.value.size`
* @param operation 缩放操作
* @param animate 渐变实例
* @param time 渐变时长
*/
applyScaleTransition(
operation: ICameraScale,
animate: Transition,
time: number
) {
if (operation.from !== this) {
logger.warn(21);
return;
}
const update = () => {
operation.x = animate.value.size;
operation.y = animate.value.size;
};
this.applyAnimation(time, update);
}
/**
* 停止所有动画
*/
stopAllAnimates() {
this.animationIds.forEach(v => this.binded.removeTicker(v));
}
/**
* 摧毁这个摄像机,当绑定元素被摧毁之后摄像机会一并摧毁,如果这个摄像机不使用了,一定要将它摧毁
*/
destroy() {
this.binded.removeTicker(this.delegation);
this.animationIds.forEach(v => this.binded.removeTicker(v));
Camera.cameraMap.delete(this.binded);
this.emit('destroy');
}
}
interface CameraAnimationBase {
type: string;
time: number;
start: number;
}
interface TranslateAnimation extends CameraAnimationBase {
type: 'translate';
timing: TimingFn;
x: number;
y: number;
}
interface TranslateAsAnimation extends CameraAnimationBase {
type: 'translateAs';
timing: TimingFn<2>;
time: number;
}
interface RotateAnimation extends CameraAnimationBase {
type: 'rotate';
timing: TimingFn;
angle: number;
time: number;
}
interface ScaleAnimation extends CameraAnimationBase {
type: 'scale';
timing: TimingFn;
scale: number;
time: number;
}
type CameraAnimationData =
| TranslateAnimation
| TranslateAsAnimation
| RotateAnimation
| ScaleAnimation;
interface CameraAnimationExecution {
data: CameraAnimationData[];
animation: Animation;
}
interface CameraAnimationEvent {
animate: [
operation: CameraOperation,
execution: CameraAnimationExecution,
item: CameraAnimationData
];
}
export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
camera: Camera;
/** 动画开始时刻 */
private startTime: number = 0;
/** 动画结束时刻 */
private endTime: number = 0;
/** 委托ticker的id */
private delegation: number;
/** 动画是否开始 */
private started: boolean = false;
/** 每个摄像机操作的动画映射 */
private animateMap: Map<CameraOperation, CameraAnimationExecution> =
new Map();
constructor(camera: Camera) {
super();
this.camera = camera;
this.delegation = camera.binded.delegateTicker(this.tick);
}
private tick = () => {
if (!this.started) return;
const now = Date.now();
const time = now - this.startTime;
if (now - this.startTime > this.endTime + 50) {
this.destroy();
return;
}
this.animateMap.forEach((exe, ope) => {
const data = exe.data;
if (data.length === 0) return;
const item = data[0];
if (item.start < time) {
this.executeAnimate(exe, item);
data.shift();
this.emit('animate', ope, exe, item);
}
});
this.camera.requestUpdate();
};
private executeAnimate(
execution: CameraAnimationExecution,
animate: CameraAnimationData
) {
if (animate.type === 'translateAs') {
const ani = this.ensureAnimate(execution);
ani.time(animate.time).moveAs(animate.timing);
} else if (animate.type === 'translate') {
const ani = this.ensureAnimate(execution);
const { x, y, time, timing } = animate;
ani.mode(timing).time(time).move(x, y);
} else if (animate.type === 'rotate') {
const ani = this.ensureAnimate(execution);
const { angle, time, timing } = animate;
ani.mode(timing).time(time).rotate(angle);
} else {
const ani = this.ensureAnimate(execution);
const { scale, time, timing } = animate;
ani.mode(timing).time(time).scale(scale);
}
}
private ensureAnimate(execution: CameraAnimationExecution) {
if (execution.animation) return execution.animation;
const ani = new Animation();
execution.animation = ani;
return ani;
}
private ensureOperation(operation: CameraOperation) {
if (!this.animateMap.has(operation)) {
const data: CameraAnimationExecution = {
data: [],
animation: new Animation()
};
this.animateMap.set(operation, data);
return data;
} else {
return this.animateMap.get(operation)!;
}
}
/**
* 添加一个平移动画
* @param operation 摄像机操作对象
* @param x 目标横坐标
* @param y 目标纵坐标
* @param time 动画时长
* @param start 动画开始时间
* @param timing 动画的缓动函数
*/
translate(
operation: ICameraTranslate,
x: number,
y: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: TranslateAnimation = {
type: 'translate',
timing,
x: x * 32,
y: y * 32,
time,
start
};
exe.data.push(data);
}
/**
* 添加一个旋转动画
* @param operation 摄像机操作
* @param angle 目标旋转弧度,单位弧度
* @param time 动画时长
* @param start 动画开始时间
* @param timing 动画的缓动函数
*/
rotate(
operation: ICameraRotate,
angle: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: RotateAnimation = {
type: 'rotate',
timing,
angle,
time,
start
};
exe.data.push(data);
}
/**
* 添加一个缩放动画
* @param operation 摄像机操作
* @param scale 目标缩放倍率
* @param time 动画时长
* @param start 动画开始时间
* @param timing 动画的缓动函数
*/
scale(
operation: ICameraScale,
scale: number,
time: number,
start: number,
timing: TimingFn
) {
const exe = this.ensureOperation(operation);
const data: ScaleAnimation = {
type: 'scale',
timing,
scale,
time,
start
};
exe.data.push(data);
}
/**
* 开始执行这个动画
*/
start() {
if (this.started) return;
this.startTime = Date.now();
this.started = true;
let endTime = 0;
this.animateMap.forEach((exe, ope) => {
const data = exe.data;
data.sort((a, b) => a.start - b.start);
const end = data.at(-1);
if (!end) return;
const t = end.start + end.time;
if (t > endTime) endTime = t;
const cam = this.camera;
if (ope.type === 'translate') {
cam.applyTranslateAnimation(ope, exe.animation, t + 100);
} else if (ope.type === 'rotate') {
cam.applyRotateAnimation(ope, exe.animation, t + 100);
} else {
cam.applyScaleAnimation(ope, exe.animation, t + 100);
}
});
this.endTime = endTime + this.startTime;
this.tick();
}
destroy() {
this.camera.binded.removeTicker(this.delegation);
this.camera.stopAllAnimates();
this.animateMap.forEach(v => {
v.animation.ticker.destroy();
});
this.animateMap.clear();
}
}
+595
View File
@@ -0,0 +1,595 @@
import {
ERenderItemEvent,
RenderItem,
MotaOffscreenCanvas2D,
Transform,
transformCanvas
} from '@motajs/render-core';
import { logger } from '@motajs/common';
import EventEmitter from 'eventemitter3';
import { isNil } from 'lodash-es';
import { LayerGroupFloorBinder } from './floor';
import {
calNeedRenderOf,
ILayerGroupRenderExtends,
Layer,
LayerGroup
} from './layer';
import { BlockCacher, CanvasCacheItem, ICanvasCacheItem } from './block';
// todo: 改成接口形式
import type {
DamageEnemy,
EnemyCollection,
MapDamage
} from '@/game/enemy/damage';
const ensureFloorDamage = Mota.require('fn', 'ensureFloorDamage');
/**
* 根据伤害大小获取颜色
* @param damage 伤害大小
*/
export function getDamageColor(damage: number): string {
if (typeof damage !== 'number') return '#f00';
if (damage === 0) return '#2f2';
if (damage < 0) return '#7f7';
if (damage < core.status.hero.hp / 3) return '#fff';
if (damage < (core.status.hero.hp * 2) / 3) return '#ff4';
if (damage < core.status.hero.hp) return '#f93';
return '#f22';
}
interface EFloorDamageEvent {
update: [floor: FloorIds];
}
export class FloorDamageExtends
extends EventEmitter<EFloorDamageEvent>
implements ILayerGroupRenderExtends
{
id: string = 'floor-damage';
floorBinder!: LayerGroupFloorBinder;
group!: LayerGroup;
sprite!: Damage;
/**
* 立刻刷新伤害渲染
*/
update(floor: FloorIds) {
if (!this.sprite || !floor) return;
const map = core.status.maps[floor];
this.sprite.setMapSize(map.width, map.height);
ensureFloorDamage(floor);
const enemy = core.status.maps[floor].enemy;
this.sprite.updateCollection(enemy);
this.emit('update', floor);
}
/**
* 创建显伤层
*/
private create() {
if (this.sprite) return;
const sprite = new Damage();
sprite.setZIndex(80);
this.group.appendChild(sprite);
this.sprite = sprite;
}
private onUpdate = (floor: FloorIds) => {
if (!this.floorBinder.bindThisFloor) {
ensureFloorDamage(floor);
core.status.maps[floor].enemy.calRealAttribute();
}
this.update(floor);
};
// private onSetBlock = (x: number, y: number, floor: FloorIds) => {
// this.sprite.enemy?.once('extract', () => {
// if (floor !== this.sprite.enemy?.floorId) return;
// this.sprite.updateBlocks();
// });
// if (!this.floorBinder.bindThisFloor) {
// this.sprite.enemy?.extract();
// }
// };
/**
* 进行楼层更新监听
*/
private listen() {
this.floorBinder.on('update', this.onUpdate);
// this.floorBinder.on('setBlock', this.onSetBlock);
}
awake(group: LayerGroup): void {
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
this.floorBinder = ex;
this.group = group;
this.create();
this.listen();
} else {
logger.warn(17);
group.removeExtends('floor-damage');
}
}
onDestroy(_group: LayerGroup): void {
this.floorBinder.off('update', this.onUpdate);
// this.floorBinder.off('setBlock', this.onSetBlock);
}
}
export interface DamageRenderable {
x: number;
y: number;
align: CanvasTextAlign;
baseline: CanvasTextBaseline;
text: string;
color: CanvasStyle;
font?: string;
stroke?: CanvasStyle;
strokeWidth?: number;
}
export interface EDamageEvent extends ERenderItemEvent {
setMapSize: [width: number, height: number];
beforeDamageRender: [need: Set<number>, transform: Transform];
updateBlocks: [blocks: Set<number>];
dirtyUpdate: [block: number];
}
export class Damage extends RenderItem<EDamageEvent> {
mapWidth: number = 0;
mapHeight: number = 0;
block: BlockCacher<ICanvasCacheItem>;
/** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */
renderable: Map<number, Set<DamageRenderable>> = new Map();
/** 当前渲染怪物列表 */
enemy?: EnemyCollection;
/** 每个分块中包含的怪物集合 */
blockData: Map<number, Map<number, DamageEnemy>> = new Map();
/** 单元格大小 */
cellSize: number = 32;
/** 默认伤害字体 */
font: string = '300 9px Verdana';
/** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */
strokeStyle: CanvasStyle = '#000';
/** 默认描边宽度 */
strokeWidth: number = 2;
/** 要懒更新的所有分块 */
private dirtyBlocks: Set<number> = new Set();
constructor() {
super('absolute', false, true);
this.block = new BlockCacher(0, 0, core._WIDTH_, 1);
this.type = 'absolute';
this.size(core._PX_, core._PY_);
this.setHD(true);
this.setAntiAliasing(true);
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
this.renderDamage(canvas, transform);
}
private onExtract = () => {
if (this.enemy) this.updateCollection(this.enemy);
};
/**
* 设置地图大小,后面应紧跟更新怪物列表
*/
setMapSize(width: number, height: number) {
this.mapWidth = width;
this.mapHeight = height;
this.enemy = void 0;
this.blockData.clear();
this.renderable.clear();
this.block.size(width, height);
// 预留blockData
const w = this.block.blockData.width;
const h = this.block.blockData.height;
const num = w * h;
for (let i = 0; i < num; i++) {
this.blockData.set(i, new Map());
this.renderable.set(i, new Set());
this.dirtyBlocks.add(i);
}
this.emit('setMapSize', width, height);
}
/**
* 设置每个图块的大小
*/
setCellSize(size: number) {
this.cellSize = size;
this.update();
}
/**
* 更新怪物列表。更新后,{@link Damage.enemy} 会丢失原来的怪物列表引用,换为传入的列表引用
* @param enemy 怪物列表
*/
updateCollection(enemy: EnemyCollection) {
if (this.enemy !== enemy) {
this.enemy?.off('calculated', this.onExtract);
enemy.on('calculated', this.onExtract);
}
this.enemy = enemy;
this.blockData.forEach(v => v.clear());
this.renderable.forEach(v => v.clear());
this.block.clearAllCache();
const w = this.block.blockData.width;
const h = this.block.blockData.height;
const num = w * h;
for (let i = 0; i < num; i++) {
this.dirtyBlocks.add(i);
}
enemy.list.forEach(v => {
if (isNil(v.x) || isNil(v.y)) return;
const index = this.block.getIndexByLoc(v.x, v.y);
this.blockData.get(index)?.set(v.y * this.mapWidth + v.x, v);
});
// this.updateBlocks();
this.update(this);
}
/**
* 更新指定矩形区域内的渲染信息
* @param x 左上角横坐标
* @param y 左上角纵坐标
* @param width 宽度
* @param height 高度
*/
updateRenderable(x: number, y: number, width: number, height: number) {
this.updateBlocks(this.block.updateElementArea(x, y, width, height));
}
/**
* 更新指定分块
* @param blocks 要更新的分块集合
* @param map 是否更新地图伤害
*/
updateBlocks(blocks?: Set<number>) {
if (blocks) {
blocks.forEach(v => this.dirtyBlocks.add(v));
this.emit('updateBlocks', blocks);
} else {
this.blockData.forEach((_v, i) => {
this.dirtyBlocks.add(i);
});
this.emit('updateBlocks', new Set(this.blockData.keys()));
}
this.update(this);
}
/**
* 更新指定位置的怪物信息
*/
updateEnemyOn(x: number, y: number) {
const enemy = this.enemy?.get(x, y);
const block = this.block.getIndexByLoc(x, y);
const data = this.blockData.get(block);
const index = x + y * this.mapWidth;
if (!data) return;
if (!enemy) {
data.delete(index);
} else {
data.set(index, enemy);
}
this.update(this);
// 渲染懒更新,优化性能表现
this.dirtyBlocks.add(block);
}
/**
* 更新单个分块
* @param block 更新的分块
* @param map 是否更新地图伤害
*/
private updateBlock(block: number, map: boolean = true) {
const data = this.blockData.get(block);
if (!data) return;
this.block.clearCache(block, 1);
const renderable = this.renderable.get(block)!;
renderable.clear();
data.forEach(v => this.extract(v, renderable));
if (map) this.extractMapDamage(block, renderable);
}
/**
* 将怪物解析为renderable的伤害
* @param enemy 怪物
* @param block 怪物所属分块
*/
private extract(enemy: DamageEnemy, block: Set<DamageRenderable>) {
if (enemy.progress !== 4) return;
const x = enemy.x!;
const y = enemy.y!;
const { damage } = enemy.calDamage();
const cri = enemy.calCritical(1)[0]?.atkDelta ?? Infinity;
const real = enemy.getRealInfo();
const dam1: DamageRenderable = {
align: 'left',
baseline: 'alphabetic',
text: isFinite(damage) ? core.formatBigNumber(damage, true) : '???',
color: getDamageColor(damage),
x: x * this.cellSize + 1,
y: y * this.cellSize + this.cellSize - 1
};
const dam2: DamageRenderable = {
align: 'left',
baseline: 'alphabetic',
text: isFinite(cri) ? core.formatBigNumber(cri, true) : '?',
color: '#fff',
x: x * this.cellSize + 1,
y: y * this.cellSize + this.cellSize - 11
};
block.add(dam1).add(dam2);
const hasHorn = real.special.has(33);
if (real.special.has(8) && real.togetherNum && !hasHorn) {
const dam3: DamageRenderable = {
align: 'right',
baseline: 'top',
text: real.togetherNum.toString(),
color: '#09FF5B',
x: x * this.cellSize + this.cellSize - 1,
y: y * this.cellSize + 2,
strokeWidth: 3
};
block.add(dam3);
}
if (real.special.has(30)) {
const dam4: DamageRenderable = {
align: 'left',
baseline: 'top',
text: '乾',
color: '#FDCD0B',
x: x * this.cellSize + 1,
y: y * this.cellSize + 2,
strokeWidth: 2,
font: '500 10px Verdana'
};
block.add(dam4);
}
if (enemy.col && hasHorn) {
const dam5: DamageRenderable = {
align: 'right',
baseline: 'top',
text: enemy.col.list.size.toString(),
color: '#fff866',
x: x * this.cellSize + this.cellSize - 1,
y: y * this.cellSize + 2,
strokeWidth: 3
};
block.add(dam5);
}
}
/**
* 解析指定分块的地图伤害
* @param block 分块索引
*/
private extractMapDamage(block: number, renderable: Set<DamageRenderable>) {
if (!this.enemy) return;
const damage = this.enemy.mapDamage;
const [sx, sy, ex, ey] = this.block.getRectOfIndex(block);
for (let x = sx; x < ex; x++) {
for (let y = sy; y < ey; y++) {
const loc = `${x},${y}`;
const dam = damage[loc];
if (!dam) continue;
this.pushMapDamage(x, y, renderable, dam);
}
}
}
/**
* 解析所有地图伤害
*/
private extractAllMapDamage() {
// todo: 测试性能,这样真的会更快吗?或许能更好的优化?或者是根本不需要这个函数?
if (!this.enemy) return;
for (const [loc, enemy] of Object.entries(this.enemy.mapDamage)) {
const [sx, sy] = loc.split(',');
const x = Number(sx);
const y = Number(sy);
const block = this.renderable.get(this.block.getIndexByLoc(x, y))!;
this.pushMapDamage(x, y, block, enemy);
}
}
private pushMapDamage(
x: number,
y: number,
block: Set<DamageRenderable>,
dam: MapDamage
) {
// todo: 这个应当可以自定义,通过地图伤害注册实现
let text = '';
let color = '#fa3';
let font = '300 9px Verdana';
if (dam.damage > 0) {
text = core.formatBigNumber(dam.damage, true);
} else if (dam.mockery) {
dam.mockery.sort((a, b) =>
a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]
);
const [tx, ty] = dam.mockery[0];
const dir = x > tx ? '←' : x < tx ? '→' : y > ty ? '↑' : '↓';
text = '嘲' + dir;
color = '#fd4';
font = '500 11px Verdana';
} else if (dam.hunt) {
text = '猎';
color = '#fd4';
font = '500 11px Verdana';
} else {
return;
}
const mapDam: DamageRenderable = {
align: 'center',
baseline: 'middle',
text,
color,
font,
x: x * this.cellSize + this.cellSize / 2,
y: y * this.cellSize + this.cellSize / 2
};
block.add(mapDam);
}
/**
* 计算需要渲染哪些块
*/
calNeedRender(transform: Transform) {
if (this.parent instanceof LayerGroup) {
// 如果处于地图组中,每个地图的渲染区域应该是一样的,因此可以缓存优化
return this.parent.cacheNeedRender(transform, this.block);
} else if (this.parent instanceof Layer) {
// 如果是地图的子元素,直接调用Layer的计算函数
return this.parent.calNeedRender(transform);
} else {
return calNeedRenderOf(transform, this.cellSize, this.block);
}
}
/**
* 渲染伤害层
* @param transform 变换矩阵
*/
renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) {
// console.time('damage');
const { ctx } = canvas;
transformCanvas(canvas, transform);
const render = this.calNeedRender(transform);
const block = this.block;
const cell = this.cellSize;
const size = cell * block.blockSize;
this.emit('beforeDamageRender', render, transform);
render.forEach(v => {
const [x, y] = block.getBlockXYByIndex(v);
const bx = x * block.blockSize;
const by = y * block.blockSize;
const px = bx * cell;
const py = by * cell;
// todo: 是否真的需要缓存
// 检查有没有缓存
const cache = block.cache.get(v);
if (cache && cache.symbol === cache.canvas.symbol) {
ctx.drawImage(cache.canvas.canvas, px, py, size, size);
return;
}
if (this.dirtyBlocks.has(v)) {
this.updateBlock(v, true);
}
this.emit('dirtyUpdate', v);
// 否则依次渲染并写入缓存
const temp = block.cache.get(v)?.canvas ?? this.requireCanvas();
temp.clear();
temp.setHD(true);
temp.setAntiAliasing(true);
temp.withGameScale(true);
temp.size(size, size);
const { ctx: ct } = temp;
ct.translate(-px, -py);
const render = this.renderable.get(v);
render?.forEach(v => {
if (!v) return;
ct.fillStyle = v.color;
ct.textAlign = v.align;
ct.textBaseline = v.baseline;
ct.font = v.font ?? this.font;
ct.strokeStyle = v.stroke ?? this.strokeStyle;
ct.lineWidth = v.strokeWidth ?? this.strokeWidth;
ct.strokeText(v.text, v.x, v.y);
ct.fillText(v.text, v.x, v.y);
});
ctx.drawImage(temp.canvas, px, py, size, size);
block.cache.set(v, new CanvasCacheItem(temp, temp.symbol));
});
ctx.restore();
// console.timeEnd('damage');
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'mapWidth':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(nextValue, this.mapHeight);
return true;
case 'mapHeight':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setMapSize(this.mapWidth, nextValue);
return true;
case 'cellSize':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setCellSize(nextValue);
return true;
case 'enemy':
if (!this.assertType(nextValue, 'object', key)) return false;
this.updateCollection(nextValue);
return true;
case 'font':
if (!this.assertType(nextValue, 'string', key)) return false;
this.font = nextValue;
this.update();
return true;
case 'strokeStyle':
this.strokeStyle = nextValue;
this.update();
return true;
case 'strokeWidth':
if (!this.assertType(nextValue, 'number', key)) return false;
this.strokeWidth = nextValue;
this.update();
return true;
}
return false;
}
destroy(): void {
super.destroy();
this.block.destroy();
this.enemy?.off('extract', this.onExtract);
}
}
// const adapter = new RenderAdapter<Damage>('damage');
+415
View File
@@ -0,0 +1,415 @@
import EventEmitter from 'eventemitter3';
import {
FloorLayer,
ILayerGroupRenderExtends,
ILayerRenderExtends,
Layer,
LayerGroup,
LayerMovingRenderable
} from './layer';
import { texture } from './cache';
import { sleep } from 'mutate-animate';
import { RenderAdapter } from '@motajs/render-core';
const hook = Mota.require('var', 'hook');
hook.on('setBlock', (x, y, floor, block) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock('event', block, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === 'event') {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(block, x, y);
}
}
});
});
hook.on('changingFloor', floor => {
// 潜在隐患:如果putRenderData改成异步,那么会变成两帧后才能真正刷新并渲染
// 考虑到楼层转换一般不会同时执行很多次,因此这里改为立刻更新
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
v.emit('floorChange', floor);
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.bindThisFloor) v.updateBindData();
});
});
hook.on('setBgFgBlock', (name, number, x, y, floor) => {
const isNow = floor === core.status.floorId;
LayerGroupFloorBinder.activedBinder.forEach(v => {
if (floor === v.floor || (isNow && v.bindThisFloor)) {
v.setBlock(name, number, x, y);
}
});
LayerFloorBinder.listenedBinder.forEach(v => {
if (v.layer.layer === name) {
if (v.floor === floor || (isNow && v.bindThisFloor)) {
v.setBlock(number, x, y);
}
}
});
});
interface LayerGroupBinderEvent {
update: [floor: FloorIds];
setBlock: [x: number, y: number, floor: FloorIds, block: AllNumbers];
floorChange: [floor: FloorIds];
}
/**
* 楼层绑定拓展,用于LayerGroup,将楼层数据传输到渲染系统。
* 添加后,会自动在LayerGroup包含的子Layer上添加LayerFloorBinder拓展,用于后续处理。
* 当移除这个拓展时,其附属的所有子拓展也会一并被移除。
*/
export class LayerGroupFloorBinder
extends EventEmitter<LayerGroupBinderEvent>
implements ILayerGroupRenderExtends
{
id: string = 'floor-binder';
bindThisFloor: boolean = true;
floor?: FloorIds;
group!: LayerGroup;
/** 附属的子LayerFloorBinder拓展 */
layerBinders: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
static activedBinder: Set<LayerGroupFloorBinder> = new Set();
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.layerBinders.forEach(v => v.bindThis());
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.layerBinders.forEach(v => v.bindFloor(floorId));
this.updateBind();
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate || !this.group) return;
this.needUpdate = true;
this.group.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 立刻进行数据绑定更新
*/
updateBindData() {
this.layerBinders.forEach(v => {
v.updateBindData();
});
const floor = this.getFloor();
this.emit('update', floor);
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置图块
*/
setBlock(layer: FloorLayer, block: AllNumbers, x: number, y: number) {
const ex = this.group
.getLayer(layer)
?.getExtends('floor-binder') as LayerFloorBinder;
if (!ex) return;
ex.setBlock(block, x, y);
const floor = this.bindThisFloor ? core.status.floorId : this.floor!;
this.emit('setBlock', x, y, floor, block);
}
checkLayerExtends(layer: Layer) {
const ex = layer.getExtends('floor-binder');
if (!ex) {
const extend = new LayerFloorBinder(this);
layer.extends(extend);
this.layerBinders.add(extend);
} else {
if (ex instanceof LayerFloorBinder) {
ex.setParent(this);
this.layerBinders.add(ex);
}
}
}
awake(group: LayerGroup) {
this.group = group;
for (const layer of group.layers.values()) {
this.checkLayerExtends(layer);
}
LayerGroupFloorBinder.activedBinder.add(this);
}
onLayerAdd(_group: LayerGroup, layer: Layer): void {
this.checkLayerExtends(layer);
}
onDestroy(group: LayerGroup) {
LayerGroupFloorBinder.activedBinder.delete(this);
group.layers.forEach(v => {
v.removeExtends('floor-binder');
});
this.removeAllListeners();
}
}
/**
* 楼层绑定拓展,用于Layer的楼层渲染。
* 注意,如果目标Layer是LayerGroup的子元素,那么会自动检测父元素是否包含LayerGroupFloorBinder拓展,
* 如果包含,那么会自动将此拓展附加至父元素的拓展。当父元素的拓展被移除时,此拓展也会一并被移除。
*/
export class LayerFloorBinder implements ILayerRenderExtends {
id: string = 'floor-binder';
parent?: LayerGroupFloorBinder;
layer!: Layer;
bindThisFloor: boolean = true;
floor?: FloorIds;
static listenedBinder: Set<LayerFloorBinder> = new Set();
private needUpdate: boolean = false;
constructor(parent?: LayerGroupFloorBinder) {
this.parent = parent;
}
/**
* 绑定楼层为当前楼层,并跟随变化
*/
bindThis() {
this.floor = void 0;
this.bindThisFloor = true;
this.updateBind();
}
/**
* 绑定楼层为指定楼层
* @param floorId 楼层id
*/
bindFloor(floorId: FloorIds) {
this.bindThisFloor = false;
this.floor = floorId;
this.updateBind();
}
getFloor() {
return this.bindThisFloor ? core.status.floorId : this.floor!;
}
/**
* 设置这个拓展附属至的父拓展(LayerGroupFloorBinder拓展)
* @param parent 父拓展
*/
setParent(parent?: LayerGroupFloorBinder) {
this.parent = parent;
this.checkListen();
}
private checkListen() {
if (this.parent) LayerFloorBinder.listenedBinder.delete(this);
else LayerFloorBinder.listenedBinder.add(this);
}
/**
* 在下一帧进行绑定数据更新
*/
updateBind() {
if (this.needUpdate) return;
this.needUpdate = true;
this.layer.requestBeforeFrame(() => {
this.needUpdate = false;
this.updateBindData();
});
}
/**
* 设置图块
*/
setBlock(block: AllNumbers, x: number, y: number) {
this.layer.putRenderData([block], 1, x, y);
}
/**
* 立刻更新绑定数据,而非下一帧
*/
updateBindData() {
const floor = this.getFloor();
if (!floor) return;
core.extractBlocks(floor);
const map = core.status.maps[floor];
this.layer.setMapSize(map.width, map.height);
const image = core.status.maps[this.getFloor()].images;
if (this.layer.layer === 'event') {
const m = map.map;
this.layer.putRenderData(m.flat(), map.width, 0, 0);
} else {
const m = core.maps._getBgFgMapArray(this.layer.layer!, floor);
this.layer.putRenderData(m.flat(), map.width, 0, 0);
}
if (this.layer.layer === 'bg') {
// 别忘了背景图块
this.layer.setBackground(texture.idNumberMap[map.defaultGround]);
}
const toDraw = image?.filter(v => v.canvas === this.layer.layer);
this.layer.setFloorImage(toDraw ?? []);
}
awake(layer: Layer) {
this.layer = layer;
if (!this.parent) {
const group = layer.parent;
if (group instanceof LayerGroup) {
const ex = group.getExtends('floor-binder');
if (ex instanceof LayerGroupFloorBinder) {
ex.checkLayerExtends(layer);
this.parent = ex;
}
}
}
this.checkListen();
}
onDestroy(_layer: Layer) {
LayerFloorBinder.listenedBinder.delete(this);
this.parent?.layerBinders.delete(this);
}
}
interface DoorAnimateRenderable {
renderable: LayerMovingRenderable;
count: number;
perTime: number;
}
export class LayerDoorAnimate implements ILayerRenderExtends {
id: string = 'door-animate';
layer!: Layer;
private moving: Set<LayerMovingRenderable> = new Set();
private getRenderable(block: Block): DoorAnimateRenderable | null {
const { x, y, id } = block;
const renderable = texture.getRenderable(id);
if (!renderable) return null;
const image = renderable.autotile
? renderable.image[0]
: renderable.image;
const time = block.event.doorInfo?.time ?? 160;
const frame = renderable.render.length;
const perTime = time / frame;
const data: LayerMovingRenderable = {
x,
y,
zIndex: y,
image,
autotile: false,
animate: 0,
frame,
bigImage: false,
render: renderable.render,
alpha: 1
};
return { renderable: data, count: frame, perTime };
}
/**
* 开门
* @param block 图块信息
*/
async openDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = 0;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now < frame) {
await sleep(perTime);
data.animate = ++now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
/**
* 关门
* @param block 图块信息
*/
async closeDoor(block: Block) {
const renderable = this.getRenderable(block);
if (!renderable) return Promise.reject();
const { renderable: data, count: frame, perTime } = renderable;
data.animate = frame - 1;
this.moving.add(data);
this.layer.requestUpdateMoving();
let now = 0;
while (now >= 0) {
await sleep(perTime);
data.animate = --now;
this.layer.update(this.layer);
}
this.moving.delete(data);
this.layer.requestUpdateMoving();
return Promise.resolve();
}
awake(layer: Layer) {
this.layer = layer;
doorAdapter.add(this);
}
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
renderable.push(...this.moving);
}
onDestroy(_layer: Layer): void {
doorAdapter.remove(this);
}
}
const doorAdapter = new RenderAdapter<LayerDoorAnimate>('door-animate');
doorAdapter.receive('openDoor', (item, block: Block) => {
return item.openDoor(block);
});
doorAdapter.receive('closeDoor', (item, block: Block) => {
return item.closeDoor(block);
});
+52
View File
@@ -0,0 +1,52 @@
import EventEmitter from 'eventemitter3';
import { RenderItem } from '@motajs/render-core';
export interface IAnimateFrame {
updateFrameAnimate(frame: number, time: number): void;
}
interface RenderEvent {
animateFrame: [frame: number, time: number];
}
class RenderEmits extends EventEmitter<RenderEvent> {
private framer: Set<IAnimateFrame> = new Set();
/**
* 添加一个可更新帧动画的对象
*/
addFramer(framer: IAnimateFrame) {
this.framer.add(framer);
}
/**
* 移除一个可更新帧动画的对象
*/
removeFramer(framer: IAnimateFrame) {
this.framer.delete(framer);
}
/**
* 更新所有帧动画
* @param frame 帧数
* @param time 帧动画时刻
*/
emitAnimateFrame(frame: number, time: number) {
this.framer.forEach(v => v.updateFrameAnimate(frame, time));
this.emit('animateFrame', frame, time);
}
}
export const renderEmits = new RenderEmits();
Mota.require('var', 'hook').once('reset', () => {
let lastTime = 0;
RenderItem.ticker.add(time => {
if (!core.isPlaying()) return;
if (time - lastTime > core.values.animateSpeed) {
RenderItem.animatedFrame++;
lastTime = time;
renderEmits.emitAnimateFrame(RenderItem.animatedFrame, time);
}
});
});
File diff suppressed because it is too large. Load diff
+477
View File
@@ -0,0 +1,477 @@
import { RenderAdapter } from '@motajs/render-core';
import { logger } from '@motajs/common';
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
import { SizedCanvasImageSource } from './types';
import EventEmitter from 'eventemitter3';
import { texture } from './cache';
import { TimingFn } from 'mutate-animate';
import { isNil } from 'lodash-es';
// type HeroMovingStatus = 'stop' | 'moving' | 'moving-as';
// export const enum HeroMovingStatus {}
interface HeroRenderEvent {
stepEnd: [];
moveTick: [x: number, y: number];
append: [renderable: LayerMovingRenderable[]];
}
export class HeroRenderer
extends EventEmitter<HeroRenderEvent>
implements ILayerRenderExtends
{
id: string = 'floor-hero';
/** 勇士的图片资源 */
image?: SizedCanvasImageSource;
cellWidth?: number;
cellHeight?: number;
/** 勇士的渲染信息 */
renderable?: LayerMovingRenderable;
layer!: Layer;
/** 当前移动帧数 */
movingFrame: number = 0;
/** 是否正在移动 */
moving: boolean = false;
/** 是否正在播放动画,与移动分开以实现原地踏步 */
animate: boolean = false;
/** 勇士移动速度 */
speed: number = 100;
/** 勇士移动的真正速度,经过录像修正 */
realSpeed: number = 100;
/** 当前的移动方向 */
moveDir: Dir2 = 'down';
/** 当前移动的勇士显示方向 */
showDir: Dir = 'down';
/** 帧动画是否反向播放,例如后退时就应该设为true */
animateReverse: boolean = false;
/** 勇士移动定时器id */
private moveId: number = -1;
/** 上一次帧数切换的时间 */
private lastFrameTime: number = 0;
/** 上一步走到格子上的时间 */
private lastStepTime: number = 0;
/** 执行当前步移动的Promise */
private moveDetached?: Promise<void>;
/** endMove的Promise */
private moveEnding?: Promise<void>;
/**
* 这一步的移动方向,与{@link moveDir}不同的是,在这一步走完之前,它都不会变,
* 当这一步走完之后,才会将其设置为{@link moveDir}的值
*/
stepDir: Dir2 = 'down';
/** 每步的格子增量 */
private stepDelta: Loc = { x: 0, y: 1 };
/**
* 设置勇士所用的图片资源
* @param image 图片资源
*/
setImage(image: SizedCanvasImageSource) {
this.image = image;
this.split();
this.resetRenderable(true);
this.layer.requestUpdateMoving();
}
/**
* 设置移动的移动速度
* @param speed 移动速度
*/
setMoveSpeed(speed: number) {
this.speed = speed;
this.fixMoveSpeed();
}
/**
* 分割勇士图像,生成renderable信息
*/
split() {
this.cellWidth = this.image!.width / 4;
this.cellHeight = this.image!.height / 4;
this.generateRenderable();
}
/**
* 生成渲染信息
*/
generateRenderable() {
if (!this.image) return;
this.renderable = {
image: this.image,
frame: 4,
x: core.status.hero.loc.x,
y: core.status.hero.loc.y,
zIndex: core.status.hero.loc.y,
autotile: false,
bigImage: true,
render: this.getRenderFromDir(this.showDir),
animate: 0,
alpha: 1
};
}
/**
* 根据方向获取勇士的裁切信息
* @param dir 方向
*/
getRenderFromDir(dir: Dir): [number, number, number, number][] {
if (!this.cellWidth || !this.cellHeight) return [];
const index = texture.characterDirection[dir];
const y = index * this.cellHeight;
const res: [number, number, number, number][] = [0, 1, 2, 3].map(v => {
return [v * this.cellWidth!, y, this.cellWidth!, this.cellHeight!];
});
if (this.animateReverse) return res.reverse();
else return res;
}
/**
* 开始勇士帧动画
*/
startAnimate() {
this.animate = true;
this.lastFrameTime = Date.now();
}
/**
* 结束勇士帧动画
*/
endAnimate() {
this.animate = false;
this.resetRenderable(false);
}
/**
* 设置帧动画是否反向播放
* @param reverse 帧动画是否反向播放
*/
setAnimateReversed(reverse: boolean) {
this.animateReverse = reverse;
this.resetRenderable(true);
}
/**
* 设置勇士的动画显示朝向
* @param dir 显示朝向
*/
setAnimateDir(dir: Dir) {
if (dir !== this.showDir) {
this.showDir = dir;
this.resetRenderable(true);
}
}
/**
* 重置renderable状态,恢复至第一帧状态
* @param getInfo 是否重新获取渲染信息
*/
resetRenderable(getInfo: boolean) {
this.movingFrame = 0;
if (this.renderable) {
this.renderable.animate = 0;
if (getInfo) {
this.renderable.render = this.getRenderFromDir(this.showDir);
}
}
this.layer.update(this.layer);
}
/**
* 勇士帧动画定时器
*/
private animateTick(time: number) {
if (!this.animate) return;
if (time - this.lastFrameTime > this.speed) {
this.lastFrameTime = time;
this.movingFrame++;
this.movingFrame %= 4;
if (this.renderable) this.renderable.animate = this.movingFrame;
}
this.layer.update(this.layer);
}
/**
* 勇士移动定时器
*/
private moveTick(time: number) {
if (!this.renderable) return;
if (this.moving) {
const progress = (time - this.lastStepTime) / this.realSpeed;
const { x: dx, y: dy } = this.stepDelta;
const { x, y } = core.status.hero.loc;
if (progress >= 1) {
this.renderable.x = x + dx;
this.renderable.y = y + dy;
this.fixMoveSpeed();
this.emit('stepEnd');
} else {
const rx = dx * progress + x;
const ry = dy * progress + y;
this.renderable.x = rx;
this.renderable.y = ry;
}
this.layer.update(this.layer);
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
}
/**
* 进行下一步的移动准备,设置移动信息
*/
private step() {
this.stepDir = this.moveDir;
this.lastStepTime = Date.now();
this.stepDelta = core.utils.scan2[this.stepDir];
this.turn(this.stepDir);
}
private fixMoveSpeed() {
if (!core.isReplaying()) {
this.realSpeed = this.speed;
} else {
const replay = core.status.replay.speed;
this.realSpeed = replay === 24 ? 1 : this.speed / replay;
}
}
/**
* 准备开始移动
*/
readyMove() {
this.moving = true;
this.fixMoveSpeed();
}
/**
* 移动勇士
*/
move(dir: Dir2): Promise<void> {
if (!this.moving) {
logger.error(12);
return Promise.reject(
'Cannot moving hero while hero not in moving!'
);
}
this.moveDir = dir;
if (this.moveDetached) {
return this.moveDetached;
} else {
this.step();
this.moveDetached = new Promise(res => {
this.once('stepEnd', () => {
this.moveDetached = void 0;
res();
});
});
return this.moveDetached;
}
}
/**
* 结束勇士的移动过程
*/
endMove(): Promise<void> {
if (!this.moving) return Promise.resolve();
if (this.moveEnding) return this.moveEnding;
else {
const promise = new Promise<void>(resolve => {
this.once('stepEnd', () => {
this.moveEnding = void 0;
this.moving = false;
const { x, y } = core.status.hero.loc;
this.setHeroLoc(x, y);
this.render();
resolve();
});
});
return (this.moveEnding = promise);
}
}
/**
* 勇士转向,不填表示顺时针转一个方向
* @param dir 移动方向
*/
turn(dir?: Dir2): void {
if (!dir) {
const index = texture.characterTurn2.indexOf(this.stepDir);
if (index === -1) {
const length = texture.characterTurn.length;
const index = texture.characterTurn.indexOf(
this.stepDir as Dir
);
const next = texture.characterTurn[index % length];
return this.turn(next);
} else {
return this.turn(texture.characterTurn[index]);
}
}
this.moveDir = dir;
this.stepDir = dir;
if (!this.renderable) return;
this.renderable.render = this.getRenderFromDir(this.showDir);
this.layer.update(this.layer);
}
/**
* 设置勇士的坐标
* @param x 横坐标
* @param y 纵坐标
*/
setHeroLoc(x?: number, y?: number) {
if (!this.renderable) return;
if (!isNil(x)) {
this.renderable.x = x;
}
if (!isNil(y)) {
this.renderable.y = y;
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
}
/**
* 按照指定函数移动勇士
* @param x 目标横坐标
* @param y 目标纵坐标
* @param time 移动时间
* @param fn 移动函数,传入一个完成度(范围0-1),返回一个三元素数组,表示横纵格子坐标,可以是小数。
* 第三个元素表示图块纵深,一般图块的纵深就是其纵坐标,当地图上有大怪物时,此举可以辅助渲染,
* 否则可能会导致移动过程中与大怪物的层级关系不正确,比如全在大怪物身后。注意不建议频繁改动这个值,
* 因为此举会导致层级的重新排序,降低渲染性能。
*/
moveAs(x: number, y: number, time: number, fn: TimingFn<3>): Promise<void> {
if (!this.moving) return Promise.resolve();
if (!this.renderable) return Promise.resolve();
const nowZIndex = fn(0)[2];
const startTime = Date.now();
return new Promise(res => {
this.layer.delegateTicker(
() => {
if (!this.renderable) return;
const now = Date.now();
const progress = (now - startTime) / time;
const [nx, ny, nz] = fn(progress);
this.renderable.x = nx;
this.renderable.y = ny;
this.renderable.zIndex = nz;
if (nz !== nowZIndex) {
this.layer.sortMovingRenderable();
}
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
},
time,
() => {
this.moving = false;
if (!this.renderable) return res();
this.renderable.x = x;
this.renderable.y = y;
this.emit('moveTick', this.renderable.x, this.renderable.y);
this.layer.update(this.layer);
res();
}
);
});
}
/**
* 渲染勇士
*/
render() {
if (!this.renderable) return;
if (!this.animate) {
this.renderable.animate = 0;
} else {
this.renderable.animate = this.movingFrame;
}
this.layer.update(this.layer);
}
awake(layer: Layer): void {
this.layer = layer;
adapter.add(this);
this.moveId = layer.delegateTicker(() => {
const time = Date.now();
this.animateTick(time);
this.moveTick(time);
});
}
onDestroy(layer: Layer): void {
adapter.remove(this);
layer.removeTicker(this.moveId);
}
onMovingUpdate(_layer: Layer, renderable: LayerMovingRenderable[]): void {
if (this.renderable) {
renderable.push(this.renderable);
this.emit('append', renderable);
}
}
}
const adapter = new RenderAdapter<HeroRenderer>('hero-adapter');
adapter.receive('readyMove', item => {
item.readyMove();
return Promise.resolve();
});
adapter.receive('move', (item, dir: Dir) => {
return item.move(dir);
});
adapter.receive('endMove', item => {
return item.endMove();
});
adapter.receive(
'moveAs',
(item, x: number, y: number, time: number, fn: TimingFn<3>) => {
return item.moveAs(x, y, time, fn);
}
);
adapter.receive('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
return Promise.resolve();
});
adapter.receive('turn', (item, dir: Dir2) => {
item.turn(dir);
return Promise.resolve();
});
// 同步适配函数,这些函数用于同步设置信息等
adapter.receiveSync('setImage', (item, image: SizedCanvasImageSource) => {
item.setImage(image);
});
adapter.receiveSync('setMoveSpeed', (item, speed: number) => {
item.setMoveSpeed(speed);
});
adapter.receiveSync('setAnimateReversed', (item, reverse: boolean) => {
item.setAnimateReversed(reverse);
});
adapter.receiveSync('startAnimate', item => {
item.startAnimate();
});
adapter.receiveSync('endAnimate', item => {
item.endAnimate();
});
adapter.receiveSync('setAnimateDir', (item, dir: Dir) => {
item.setAnimateDir(dir);
});
// 不分同步fallback,用于适配现在的样板,之后会删除
adapter.receiveSync('setHeroLoc', (item, x?: number, y?: number) => {
item.setHeroLoc(x, y);
});
adapter.receiveSync('turn', (item, dir: Dir2) => {
item.turn(dir);
});
+14
View File
@@ -0,0 +1,14 @@
export * from './animate';
export * from './block';
export * from './cache';
export * from './camera';
export * from './damage';
export * from './floor';
export * from './frame';
export * from './graphics';
export * from './hero';
export * from './layer';
export * from './misc';
export * from './types';
export * from './utils';
export * from './viewport';
File diff suppressed because it is too large. Load diff
+550
View File
@@ -0,0 +1,550 @@
import {
ERenderItemEvent,
RenderItem,
RenderItemPosition,
Transform,
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { Font } from '@motajs/render-style';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
import { IAnimateFrame, renderEmits } from './frame';
import { AutotileRenderable, RenderableData, texture } from './cache';
import { SizedCanvasImageSource } from './types';
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
const SAFE_PAD = 1;
type CanvasStyle = string | CanvasGradient | CanvasPattern;
export interface ETextEvent extends ERenderItemEvent {
setText: [text: string, width: number, height: number];
}
export class Text extends RenderItem<ETextEvent> {
text: string;
fillStyle?: CanvasStyle = '#fff';
strokeStyle?: CanvasStyle;
font: Font = new Font();
strokeWidth: number = 1;
private length: number = 0;
private descent: number = 0;
private static measureCanvas = new MotaOffscreenCanvas2D();
constructor(text: string = '', type: RenderItemPosition = 'static') {
super(type, false);
this.text = text;
if (text.length > 0) {
this.requestBeforeFrame(() => {
this.calBox();
this.emit('setText', text, this.width, this.height);
});
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
const stroke = this.strokeWidth;
ctx.textBaseline = 'bottom';
ctx.fillStyle = this.fillStyle ?? 'transparent';
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
ctx.font = this.font.string();
ctx.lineWidth = this.strokeWidth;
if (this.strokeStyle) {
ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD);
}
if (this.fillStyle) {
ctx.fillText(this.text, stroke, this.descent + stroke + SAFE_PAD);
}
}
/**
* 获取文字的长度
*/
measure() {
const ctx = Text.measureCanvas.ctx;
ctx.textBaseline = 'bottom';
ctx.font = this.font.string();
const res = ctx.measureText(this.text);
return res;
}
/**
* 设置显示文字
* @param text 显示的文字
*/
setText(text: string) {
this.text = text;
this.calBox();
this.update(this);
this.emit('setText', text, this.width, this.height);
}
/**
* 设置使用的字体
* @param font 字体
*/
setFont(font: Font) {
this.font = font;
this.calBox();
this.update(this);
}
/**
* 设置字体样式
* @param fill 填充样式
* @param stroke 描边样式
*/
setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) {
this.fillStyle = fill;
this.strokeStyle = stroke;
this.update();
}
/**
* 设置描边宽度
* @param width 宽度
*/
setStrokeWidth(width: number) {
const before = this.strokeWidth;
this.strokeWidth = width;
const dw = width - before;
this.size(this.width + dw * 2, this.height + dw * 2);
this.update();
}
/**
* 计算字体所占空间,从而确定这个元素的大小
*/
calBox() {
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
this.measure();
this.length = width;
this.descent = actualBoundingBoxAscent;
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
const stroke = this.strokeWidth * 2;
this.size(width + stroke, height + stroke + SAFE_PAD * 2);
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'text':
if (!this.assertType(nextValue, 'string', key)) return false;
this.setText(nextValue);
return true;
case 'fillStyle':
this.setStyle(nextValue, this.strokeStyle);
return true;
case 'strokeStyle':
this.setStyle(this.fillStyle, nextValue);
return true;
case 'font':
if (!this.assertType(nextValue, Font, key)) return false;
this.setFont(nextValue);
return true;
case 'strokeWidth':
this.setStrokeWidth(nextValue);
return true;
}
return false;
}
}
export interface EImageEvent extends ERenderItemEvent {}
export class Image extends RenderItem<EImageEvent> {
image: CanvasImageSource;
constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') {
super(type);
this.image = image;
if (image instanceof VideoFrame || image instanceof SVGElement) {
this.size(200, 200);
} else {
this.size(image.width, image.height);
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
ctx.drawImage(this.image, 0, 0, canvas.width, canvas.height);
}
/**
* 设置图片资源
* @param image 图片资源
*/
setImage(image: CanvasImageSource) {
this.image = image;
this.update();
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'image':
this.setImage(nextValue);
return true;
}
return false;
}
}
export class Comment extends RenderItem {
readonly isComment: boolean = true;
constructor(public text: string = '') {
super('static', false, false);
this.hide();
}
getBoundingRect(): DOMRectReadOnly {
return new DOMRectReadOnly(0, 0, 0, 0);
}
protected render(
_canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {}
protected handleProps(): boolean {
return false;
}
}
export interface EIconEvent extends ERenderItemEvent {}
export class Icon extends RenderItem<EIconEvent> implements IAnimateFrame {
/** 图标id */
icon: AllNumbers = 0;
/** 帧数 */
frame?: number = 0;
/** 是否启用动画 */
animate?: boolean = false;
/** 图标的渲染信息 */
private renderable?: RenderableData | AutotileRenderable;
private pendingIcon?: AllNumbers;
constructor(type: RenderItemPosition, cache?: boolean, fall?: boolean) {
super(type, cache, fall);
this.setAntiAliasing(false);
this.setHD(false);
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
const renderable = this.renderable;
if (!renderable) return;
const [x, y, w, h] = renderable.render[0];
const cw = this.width;
const ch = this.height;
const frame = this.animate
? RenderItem.animatedFrame % renderable.frame
: 0;
if (!this.animate) {
if (renderable.autotile) {
ctx.drawImage(renderable.image[0], x, y, w, h, 0, 0, cw, ch);
} else {
ctx.drawImage(renderable.image, x, y, w, h, 0, 0, cw, ch);
}
} else {
const [x1, y1, w1, h1] = renderable.render[frame];
if (renderable.autotile) {
const img = renderable.image[0];
ctx.drawImage(img, x1, y1, w1, h1, 0, 0, cw, ch);
} else {
ctx.drawImage(renderable.image, x1, y1, w1, h1, 0, 0, cw, ch);
}
}
}
/**
* 设置图标
* @param id 图标id
*/
setIcon(id: AllIds | AllNumbers) {
if (id === 0) {
this.renderable = void 0;
return;
}
const num = typeof id === 'number' ? id : texture.idNumberMap[id];
const loading = Mota.require('var', 'loading');
if (loading.loaded) {
this.setIconRenderable(num);
} else {
if (isNil(this.pendingIcon)) {
loading.once('loaded', () => {
this.setIconRenderable(this.pendingIcon ?? 0);
delete this.pendingIcon;
});
}
this.pendingIcon = num;
}
}
private setIconRenderable(num: AllNumbers) {
const renderable = texture.getRenderable(num);
if (!renderable) {
logger.warn(43, num.toString());
return;
} else {
this.icon = num;
this.renderable = renderable;
this.frame = renderable.frame;
}
this.update();
}
/**
* 更新动画帧
*/
updateFrameAnimate(): void {
this.update(this);
}
destroy(): void {
renderEmits.removeFramer(this);
super.destroy();
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'icon':
this.setIcon(nextValue);
return true;
case 'animate':
if (!this.assertType(nextValue, 'boolean', key)) return false;
this.animate = nextValue;
if (nextValue) renderEmits.addFramer(this);
else renderEmits.removeFramer(this);
this.update();
return true;
case 'frame':
if (!this.assertType(nextValue, 'number', key)) return false;
this.frame = nextValue;
this.update();
return true;
}
return false;
}
}
interface WinskinPatterns {
top: CanvasPattern;
left: CanvasPattern;
bottom: CanvasPattern;
right: CanvasPattern;
}
export interface EWinskinEvent extends ERenderItemEvent {}
export class Winskin extends RenderItem<EWinskinEvent> {
image: SizedCanvasImageSource;
/** 边框宽度,32表示原始宽度 */
borderSize: number = 32;
/** 图片名称 */
imageName?: string;
private pendingImage?: ImageIds;
private patternCache?: WinskinPatterns;
private patternTransform: DOMMatrix;
private static patternMap: Map<string, WinskinPatterns> = new Map();
constructor(
image: SizedCanvasImageSource,
type: RenderItemPosition = 'static'
) {
super(type, false, false);
this.image = image;
this.setAntiAliasing(false);
if (window.DOMMatrix) {
this.patternTransform = new DOMMatrix();
} else if (window.WebKitCSSMatrix) {
this.patternTransform = new WebKitCSSMatrix();
} else {
this.patternTransform = new SVGMatrix();
}
}
private generatePattern() {
const pattern = this.requireCanvas();
const img = this.image;
pattern.size(32, 16);
pattern.withGameScale(false);
pattern.setHD(false);
pattern.setAntiAliasing(false);
const ctx = pattern.ctx;
ctx.drawImage(img, 144, 0, 32, 16, 0, 0, 32, 16);
const topPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 32, 16);
ctx.drawImage(img, 144, 48, 32, 16, 0, 0, 32, 16);
const bottomPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 32, 16);
pattern.size(16, 32);
ctx.drawImage(img, 128, 16, 16, 32, 0, 0, 16, 32);
const leftPattern = ctx.createPattern(pattern.canvas, 'repeat');
ctx.clearRect(0, 0, 16, 32);
ctx.drawImage(img, 176, 16, 16, 32, 0, 0, 16, 32);
const rightPattern = ctx.createPattern(pattern.canvas, 'repeat');
if (!topPattern || !bottomPattern || !leftPattern || !rightPattern) {
return null;
}
const winskinPattern: WinskinPatterns = {
top: topPattern,
bottom: bottomPattern,
left: leftPattern,
right: rightPattern
};
if (this.imageName) {
Winskin.patternMap.set(this.imageName, winskinPattern);
}
this.patternCache = winskinPattern;
this.deleteCanvas(pattern);
return winskinPattern;
}
private getPattern() {
if (!this.imageName) {
if (this.patternCache) return this.patternCache;
return this.generatePattern();
} else {
const pattern = Winskin.patternMap.get(this.imageName);
if (pattern) return pattern;
return this.generatePattern();
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
const img = this.image;
const w = this.width;
const h = this.height;
const pad = this.borderSize / 2;
// 背景
ctx.drawImage(img, 0, 0, 128, 128, 2, 2, w - 4, h - 4);
const pattern = this.getPattern();
if (!pattern) return;
const { top, left, right, bottom } = pattern;
top.setTransform(this.patternTransform);
left.setTransform(this.patternTransform);
right.setTransform(this.patternTransform);
bottom.setTransform(this.patternTransform);
// 上下左右边框
ctx.save();
ctx.fillStyle = top;
ctx.translate(pad, 0);
ctx.fillRect(0, 0, w - pad * 2, pad);
ctx.fillStyle = bottom;
ctx.translate(0, h - pad);
ctx.fillRect(0, 0, w - pad * 2, pad);
ctx.fillStyle = left;
ctx.translate(-pad, pad * 2 - h);
ctx.fillRect(0, 0, pad, h - pad * 2);
ctx.fillStyle = right;
ctx.translate(w - pad, 0);
ctx.fillRect(0, 0, pad, h - pad * 2);
ctx.restore();
// 四个角的边框
ctx.drawImage(img, 128, 0, 16, 16, 0, 0, pad, pad);
ctx.drawImage(img, 176, 0, 16, 16, w - pad, 0, pad, pad);
ctx.drawImage(img, 128, 48, 16, 16, 0, h - pad, pad, pad);
ctx.drawImage(img, 176, 48, 16, 16, w - pad, h - pad, pad, pad);
}
/**
* 设置winskin图片
* @param image winskin图片
*/
setImage(image: SizedCanvasImageSource) {
this.image = image;
this.patternCache = void 0;
this.update();
}
/**
* 通过图片名称设置winskin
* @param name 图片名称
*/
setImageByName(name: ImageIds) {
const loading = Mota.require('var', 'loading');
if (loading.loaded) {
const image = core.material.images.images[name];
this.setImage(image);
} else {
if (isNil(this.pendingImage)) {
loading.once('loaded', () => {
const id = this.pendingImage;
if (!id) return;
const image = core.material.images.images[id];
this.setImage(image);
delete this.pendingImage;
});
}
this.pendingImage = name;
}
this.imageName = name;
}
/**
* 设置边框大小
* @param size 边框大小
*/
setBorderSize(size: number) {
this.borderSize = size;
this.patternTransform.a = size / 32;
this.patternTransform.d = size / 32;
this.update();
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'image':
if (!this.assertType(nextValue, 'string', key)) return false;
this.setImageByName(nextValue);
return true;
case 'borderSize':
if (!this.assertType(nextValue, 'number', key)) return false;
this.setBorderSize(nextValue);
return true;
}
return false;
}
}
+4
View File
@@ -0,0 +1,4 @@
export type SizedCanvasImageSource = Exclude<
CanvasImageSource,
VideoFrame | SVGElement
>;
+14
View File
@@ -0,0 +1,14 @@
import { RenderAdapter } from '@motajs/render-core';
import { FloorViewport } from './viewport';
export function disableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport');
if (!adapter) return;
adapter.sync('disable');
}
export function enableViewport() {
const adapter = RenderAdapter.get<FloorViewport>('viewport');
if (!adapter) return;
adapter.sync('enable');
}
+352
View File
@@ -0,0 +1,352 @@
import { RenderAdapter } from '@motajs/render-core';
import { HeroRenderer } from './hero';
import { ILayerGroupRenderExtends, LayerGroup } from './layer';
import { LayerGroupFloorBinder } from './floor';
import { hyper, TimingFn } from 'mutate-animate';
export class FloorViewport implements ILayerGroupRenderExtends {
id: string = 'viewport';
group!: LayerGroup;
hero?: HeroRenderer;
binder?: LayerGroupFloorBinder;
/** 是否启用视角控制拓展 */
enabled: boolean = true;
/** 是否自动限定视角范围至地图范围 */
boundX: boolean = true;
boundY: boolean = true;
/** 渐变的速率曲线 */
transitionFn: TimingFn = hyper('sin', 'out');
/** 瞬移的速率曲线 */
mutateFn: TimingFn = hyper('sin', 'out');
/** 突变时的渐变时长 */
transitionTime: number = 600;
/** 当前视角位置 */
private nx: number = 0;
private ny: number = 0;
/** 移动时的偏移位置 */
private ox: number = 0;
private oy: number = 0;
/** 移动时的偏移最大值 */
private maxOffset: number = 1;
/** 委托ticker */
private delegation: number = -1;
/** 渐变委托ticker */
private transition: number = -1;
/** 移动委托ticker */
private moving: number = -1;
/** 是否在渐变过程中 */
private inTransition: boolean = false;
/** 是否在移动过程中 */
private inMoving: boolean = false;
/** 移动的监听函数 */
private movingFramer?: () => void;
/**
* 禁用自动视角控制
*/
disable() {
this.enabled = false;
}
/**
* 启用自动视角控制
*/
enable() {
this.enabled = true;
const { x, y } = core.status.hero.loc;
const { x: nx, y: ny } = this.group.camera;
const halfWidth = core._PX_ / 2;
const halfHeight = core._PY_ / 2;
const cell = this.group.cellSize;
const half = cell / 2;
this.applyPosition(
-(nx - halfWidth + half) / this.group.cellSize,
-(ny - halfHeight + half) / this.group.cellSize
);
this.mutateTo(x, y);
}
/**
* 设置是否自动限定视角范围至地图范围
* @param boundX 是否自动限定水平视角范围
* @param boundY 是否自动限定竖直视角范围
*/
setAutoBound(boundX: boolean = this.boundX, boundY: boolean = this.boundY) {
this.boundX = boundX;
this.boundY = boundY;
this.group.requestBeforeFrame(() => {
this.setPosition(this.nx, this.ny);
});
}
/**
* 传入视角的目标位置,将其限定在地图范围内后返回
* @param x 图格横坐标
* @param y 图格纵坐标
*/
getBoundedPosition(x: number, y: number) {
if (!this.checkDependency()) return { x, y };
if (!this.boundX && !this.boundY) return { x, y };
const width = core._WIDTH_;
const height = core._HEIGHT_;
const minX = (width - 1) / 2;
const minY = (height - 1) / 2;
const floor = core.status.maps[this.binder!.getFloor()];
const maxX = floor.width - minX - 1;
const maxY = floor.height - minY - 1;
return {
x: this.boundX ? core.clamp(x, minX, maxX) : x,
y: this.boundY ? core.clamp(y, minY, maxY) : y
};
}
/**
* 设置视角位置
* @param x 目标图格横坐标
* @param y 目标图格纵坐标
*/
setPosition(x: number, y: number) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.group.removeTicker(this.transition, false);
this.applyPosition(nx, ny);
}
/**
* 开始移动
*/
startMove() {
if (this.inMoving) return;
this.inMoving = true;
this.createMoveTransition();
}
/**
* 结束移动
*/
endMove() {
this.inMoving = false;
}
/**
* 当勇士通过移动改变至指定位置时移动视角
* @param x 目标图格横坐标
* @param y 目标图格纵坐标
*/
moveTo(x: number, y: number, time: number = 200) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
if (this.inTransition) {
const distance = Math.hypot(this.nx - nx, this.ny - ny);
const t = core.clamp(distance * time, time, time * 3);
this.createTransition(nx, ny, t, this.transitionFn);
}
}
private createMoveTransition() {
if (!this.checkDependency()) return;
let xTarget: number = 0;
let yTarget: number = 0;
let xStart: number = this.ox;
let yStart: number = this.oy;
let xStartTime: number = Date.now();
let yStartTime: number = Date.now();
let ending: boolean = false;
// 这个数等于 sinh(2),用这个数的话,可以正好在刚开始移动的时候达到1的斜率,效果会比较好
const transitionTime = this.hero!.speed * 3.626860407847019;
const setTargetX = (x: number, time: number) => {
if (x === xTarget) return;
xTarget = x;
xStartTime = time;
xStart = this.ox;
};
const setTargetY = (y: number, time: number) => {
if (y === yTarget) return;
yTarget = y;
yStart = this.oy;
yStartTime = time;
};
if (this.movingFramer) {
this.hero!.off('moveTick', this.movingFramer);
}
this.movingFramer = () => {
if (this.inTransition) return;
const now = Date.now();
if (!this.inMoving && !ending) {
setTargetX(0, now);
setTargetY(0, now);
ending = true;
}
if (!ending) {
const dir = this.hero!.stepDir;
const { x, y } = core.utils.scan2[dir];
setTargetX(-x * this.maxOffset, now);
setTargetY(-y * this.maxOffset, now);
}
if (!this.hero!.renderable) return;
const { x, y } = this.hero!.renderable;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.applyPosition(nx, ny);
if (ending) {
if (this.ox === xTarget && this.oy == yTarget) {
this.hero!.off('moveTick', this.movingFramer);
return;
}
}
// todo: 效果太差了,需要优化
return;
if (this.ox !== xTarget) {
const time = transitionTime * Math.abs(xStart - xTarget);
const progress = (now - xStartTime) / time;
if (progress > 1) {
this.ox = xTarget;
} else {
const p = this.transitionFn(progress);
this.ox = (xTarget - xStart) * p + xStart;
}
}
if (this.oy !== yTarget) {
const time = transitionTime * Math.abs(yStart - yTarget);
const progress = (now - yStartTime) / time;
if (progress > 1) {
this.oy = yTarget;
} else {
const p = this.transitionFn(progress);
this.oy = (yTarget - yStart) * p + yStart;
}
}
};
this.hero!.on('moveTick', this.movingFramer);
}
/**
* 当勇士位置突变至指定位置时移动视角
* @param x 目标图格横坐标
* @param y 目标图格纵坐标
*/
mutateTo(x: number, y: number, time: number = this.transitionTime) {
if (!this.enabled) return;
const { x: nx, y: ny } = this.getBoundedPosition(x, y);
this.createTransition(nx, ny, time, this.mutateFn);
}
private createTransition(x: number, y: number, time: number, fn: TimingFn) {
const start = Date.now();
const end = start + time;
const sx = this.nx;
const sy = this.ny;
const dx = x - sx;
const dy = y - sy;
this.inTransition = true;
this.group.removeTicker(this.transition, false);
this.transition = this.group.delegateTicker(
() => {
const now = Date.now();
if (now >= end) {
this.group.removeTicker(this.transition, true);
return;
}
const progress = fn((now - start) / time);
const tx = dx * progress;
const ty = dy * progress;
this.applyPosition(tx + sx, ty + sy);
},
time,
() => {
this.applyPosition(x, y);
this.inTransition = false;
}
);
}
private applyPosition(x: number, y: number) {
if (!this.enabled) return;
if (x === this.nx && y === this.ny) return;
const halfWidth = core._PX_ / 2;
const halfHeight = core._PY_ / 2;
const cell = this.group.cellSize;
const half = cell / 2;
this.nx = x;
this.ny = y;
const { x: bx, y: by } = this.getBoundedPosition(x, y);
const rx = bx * cell - halfWidth + half;
const ry = by * cell - halfHeight + half;
core.bigmap.offsetX = rx;
core.bigmap.offsetY = ry;
this.group.camera.setTranslate(-rx, -ry);
this.group.update(this.group);
}
private checkDependency() {
if (this.hero && this.binder) return true;
const group = this.group;
const ex1 = group.getLayer('event')?.getExtends('floor-hero');
const ex2 = group.getExtends('floor-binder');
if (
ex1 instanceof HeroRenderer &&
ex2 instanceof LayerGroupFloorBinder
) {
this.hero = ex1;
this.binder = ex2;
return true;
}
return false;
}
awake(group: LayerGroup): void {
this.group = group;
adapter.add(this);
}
onDestroy(group: LayerGroup): void {
group.removeTicker(this.delegation);
group.removeTicker(this.transition);
group.removeTicker(this.moving);
adapter.remove(this);
}
}
const adapter = new RenderAdapter<FloorViewport>('viewport');
adapter.receive('mutateTo', (item, x, y, time) => {
item.mutateTo(x, y, time);
return Promise.resolve();
});
adapter.receive('moveTo', (item, x, y, time) => {
item.moveTo(x, y, time);
return Promise.resolve();
});
adapter.receive('setPosition', (item, x, y) => {
item.setPosition(x, y);
return Promise.resolve();
});
adapter.receiveSync('disable', item => {
item.disable();
});
adapter.receiveSync('enable', item => {
item.enable();
});
adapter.receiveSync('startMove', item => {
item.startMove();
});
adapter.receiveSync('endMove', item => {
item.endMove();
});
const hook = Mota.require('var', 'hook');
hook.on('changingFloor', (_, loc) => {
adapter.all('setPosition', loc.x, loc.y);
});