mirror of
https://github.com/motajs/template.git
synced 2026-04-12 15:11:10 +08:00
feat: 将 HumanBreak 新内容移植入该样板
This commit is contained in:
parent
6bf32174d8
commit
bea2725d6a
@ -633,7 +633,7 @@ export interface IWheelEvent extends IActionEvent {
|
|||||||
|
|
||||||
1. 按下、抬起、点击**永远**保持为同一个 `identifier`
|
1. 按下、抬起、点击**永远**保持为同一个 `identifier`
|
||||||
2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier`
|
2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier`
|
||||||
3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifer`,**不会**回退至上一个按下的按键
|
3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifier`,**不会**回退至上一个按下的按键
|
||||||
|
|
||||||
除此之外,滚轮事件中的 `identifier` 永远为 -1。
|
除此之外,滚轮事件中的 `identifier` 永远为 -1。
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
"type": "vue-tsc --noEmit",
|
"type": "vue-tsc --noEmit",
|
||||||
"lines": "tsx script/lines.ts packages packages-user",
|
"lines": "tsx script/lines.ts packages packages-user",
|
||||||
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
|
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
|
||||||
"build:game": "tsx script/declare.ts vue-tsc --noEmit && tsx script/build-game.ts",
|
"build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
|
||||||
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
|
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
|
||||||
"docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"",
|
"docs:dev": "concurrently -k -n SIDEBAR,VITEPRESS -c blue,green \"tsx docs/.vitepress/api.ts\" \"vitepress dev docs\"",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
@ -30,6 +30,7 @@
|
|||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lz-string": "^1.5.0",
|
"lz-string": "^1.5.0",
|
||||||
|
"maxrects-packer": "^2.7.3",
|
||||||
"mutate-animate": "^1.4.2",
|
"mutate-animate": "^1.4.2",
|
||||||
"ogg-opus-decoder": "^1.6.14",
|
"ogg-opus-decoder": "^1.6.14",
|
||||||
"opus-decoder": "^0.7.7",
|
"opus-decoder": "^0.7.7",
|
||||||
|
|||||||
7
packages-user/client-base/package.json
Normal file
7
packages-user/client-base/package.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "@user/client-base",
|
||||||
|
"dependencies": {
|
||||||
|
"@motajs/render-assets": "workspace:*",
|
||||||
|
"@motajs/client-base": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages-user/client-base/src/index.ts
Normal file
7
packages-user/client-base/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createMaterial } from './material';
|
||||||
|
|
||||||
|
export function create() {
|
||||||
|
createMaterial();
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './material';
|
||||||
454
packages-user/client-base/src/material/autotile.ts
Normal file
454
packages-user/client-base/src/material/autotile.ts
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
import {
|
||||||
|
IRect,
|
||||||
|
ITextureRenderable,
|
||||||
|
SizedCanvasImageSource
|
||||||
|
} from '@motajs/render-assets';
|
||||||
|
import {
|
||||||
|
AutotileConnection,
|
||||||
|
AutotileType,
|
||||||
|
BlockCls,
|
||||||
|
IAutotileConnection,
|
||||||
|
IAutotileProcessor,
|
||||||
|
IMaterialFramedData,
|
||||||
|
IMaterialManager
|
||||||
|
} from './types';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
|
interface ConnectedAutotile {
|
||||||
|
readonly lt: Readonly<IRect>;
|
||||||
|
readonly rt: Readonly<IRect>;
|
||||||
|
readonly rb: Readonly<IRect>;
|
||||||
|
readonly lb: Readonly<IRect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAutotileData {
|
||||||
|
/** 图像源 */
|
||||||
|
readonly source: SizedCanvasImageSource;
|
||||||
|
/** 自动元件帧数 */
|
||||||
|
readonly frames: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
|
||||||
|
const connectionMap3x4 = new Map<number, [number, number, number, number]>();
|
||||||
|
/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
|
||||||
|
const connectionMap2x3 = new Map<number, [number, number, number, number]>();
|
||||||
|
/** 3x4 自动元件各方向连接的矩形映射 */
|
||||||
|
const rectMap3x4 = new Map<number, ConnectedAutotile>();
|
||||||
|
/** 2x3 自动元件各方向连接的矩形映射 */
|
||||||
|
const rectMap2x3 = new Map<number, ConnectedAutotile>();
|
||||||
|
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
|
||||||
|
const distinctConnectionMap = new Map<number, number>();
|
||||||
|
|
||||||
|
export class AutotileProcessor implements IAutotileProcessor {
|
||||||
|
/** 自动元件父子关系映射,子元件 -> 父元件 */
|
||||||
|
readonly parentMap: Map<number, number> = new Map();
|
||||||
|
/** 自动元件父子关系映射,父元件 -> 子元件列表 */
|
||||||
|
readonly childMap: Map<number, Set<number>> = new Map();
|
||||||
|
|
||||||
|
constructor(readonly manager: IMaterialManager) {}
|
||||||
|
|
||||||
|
private ensureChildSet(num: number) {
|
||||||
|
const set = this.childMap.get(num);
|
||||||
|
if (set) return set;
|
||||||
|
const ensure = new Set<number>();
|
||||||
|
this.childMap.set(num, ensure);
|
||||||
|
return ensure;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnection(autotile: number, parent: number): void {
|
||||||
|
this.parentMap.set(autotile, parent);
|
||||||
|
const child = this.ensureChildSet(parent);
|
||||||
|
child.add(autotile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectEdge(length: number, index: number, width: number): number {
|
||||||
|
// 最高位表示左上,低位依次顺时针旋转
|
||||||
|
|
||||||
|
// 如果地图大小只有 1
|
||||||
|
if (length === 1) {
|
||||||
|
return 0b1111_1111;
|
||||||
|
}
|
||||||
|
// 如果地图高度只有 1
|
||||||
|
if (length === width) {
|
||||||
|
if (index === 0) {
|
||||||
|
return 0b1110_1111;
|
||||||
|
} else if (index === length - 1) {
|
||||||
|
return 0b1111_1110;
|
||||||
|
} else {
|
||||||
|
return 0b1110_1110;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果地图宽度只有 1
|
||||||
|
if (width === 1) {
|
||||||
|
if (index === 0) {
|
||||||
|
return 0b1111_1011;
|
||||||
|
} else if (index === length - 1) {
|
||||||
|
return 0b1011_1111;
|
||||||
|
} else {
|
||||||
|
return 0b1011_1011;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常地图
|
||||||
|
|
||||||
|
const lastLine = length - width;
|
||||||
|
const x = index % width;
|
||||||
|
|
||||||
|
// 四个角,左上,右上,右下,左下
|
||||||
|
if (index === 0) {
|
||||||
|
return 0b1110_0011;
|
||||||
|
} else if (index === width - 1) {
|
||||||
|
return 0b1111_1000;
|
||||||
|
} else if (index === length - 1) {
|
||||||
|
return 0b0011_1110;
|
||||||
|
} else if (index === lastLine) {
|
||||||
|
return 0b1000_1111;
|
||||||
|
}
|
||||||
|
// 四条边,上,右,下,左
|
||||||
|
else if (index < width) {
|
||||||
|
return 0b1110_0000;
|
||||||
|
} else if (x === width - 1) {
|
||||||
|
return 0b0011_1000;
|
||||||
|
} else if (index > lastLine) {
|
||||||
|
return 0b0000_1110;
|
||||||
|
} else if (x === 0) {
|
||||||
|
return 0b1000_0011;
|
||||||
|
}
|
||||||
|
// 不在边缘
|
||||||
|
else {
|
||||||
|
return 0b0000_0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(
|
||||||
|
array: Uint32Array,
|
||||||
|
index: number,
|
||||||
|
width: number
|
||||||
|
): IAutotileConnection {
|
||||||
|
const block = array[index];
|
||||||
|
if (block === 0) {
|
||||||
|
return {
|
||||||
|
connection: 0,
|
||||||
|
center: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let res: number = this.connectEdge(array.length, index, width);
|
||||||
|
const childList = this.childMap.get(block);
|
||||||
|
|
||||||
|
// 最高位表示左上,低位依次顺时针旋转
|
||||||
|
const a7 = array[index - width - 1] ?? 0;
|
||||||
|
const a6 = array[index - width] ?? 0;
|
||||||
|
const a5 = array[index - width + 1] ?? 0;
|
||||||
|
const a4 = array[index + 1] ?? 0;
|
||||||
|
const a3 = array[index + width + 1] ?? 0;
|
||||||
|
const a2 = array[index + width] ?? 0;
|
||||||
|
const a1 = array[index + width - 1] ?? 0;
|
||||||
|
const a0 = array[index - 1] ?? 0;
|
||||||
|
|
||||||
|
// Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number
|
||||||
|
|
||||||
|
if (!childList || childList.size === 0) {
|
||||||
|
// 不包含子元件,那么直接跟相同的连接
|
||||||
|
res |=
|
||||||
|
+(a0 === block) |
|
||||||
|
(+(a1 === block) << 1) |
|
||||||
|
(+(a2 === block) << 2) |
|
||||||
|
(+(a3 === block) << 3) |
|
||||||
|
(+(a4 === block) << 4) |
|
||||||
|
(+(a5 === block) << 5) |
|
||||||
|
(+(a6 === block) << 6) |
|
||||||
|
(+(a7 === block) << 7);
|
||||||
|
} else {
|
||||||
|
res |=
|
||||||
|
+childList.has(a0) |
|
||||||
|
(+childList.has(a1) << 1) |
|
||||||
|
(+childList.has(a2) << 2) |
|
||||||
|
(+childList.has(a3) << 3) |
|
||||||
|
(+childList.has(a4) << 4) |
|
||||||
|
(+childList.has(a5) << 5) |
|
||||||
|
(+childList.has(a6) << 6) |
|
||||||
|
(+childList.has(a7) << 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection: res,
|
||||||
|
center: block
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConnectionFor(
|
||||||
|
connection: number,
|
||||||
|
center: number,
|
||||||
|
target: number,
|
||||||
|
direction: AutotileConnection
|
||||||
|
): number {
|
||||||
|
const childList = this.childMap.get(center);
|
||||||
|
if (!childList || !childList.has(target)) {
|
||||||
|
return connection & ~direction;
|
||||||
|
} else {
|
||||||
|
return connection | direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查贴图是否是一个自动元件
|
||||||
|
* @param tile 贴图数据
|
||||||
|
*/
|
||||||
|
private checkAutotile(tile: IMaterialFramedData) {
|
||||||
|
if (tile.cls !== BlockCls.Autotile) return false;
|
||||||
|
const { texture, frames } = tile;
|
||||||
|
if (texture.width !== 96 * frames) return false;
|
||||||
|
if (texture.height === 128 || texture.height === 144) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(autotile: number, connection: number): ITextureRenderable | null {
|
||||||
|
const tile = this.manager.getTile(autotile);
|
||||||
|
if (!tile) return null;
|
||||||
|
if (!this.checkAutotile(tile)) return null;
|
||||||
|
return this.renderWithoutCheck(tile, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWith(
|
||||||
|
tile: IMaterialFramedData,
|
||||||
|
connection: number
|
||||||
|
): ITextureRenderable | null {
|
||||||
|
if (!this.checkAutotile(tile)) return null;
|
||||||
|
return this.renderWithoutCheck(tile, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWithoutCheck(
|
||||||
|
tile: IMaterialFramedData,
|
||||||
|
connection: number
|
||||||
|
): ITextureRenderable | null {
|
||||||
|
const { texture } = tile;
|
||||||
|
const size = texture.height === 32 * 48 ? 32 : 48;
|
||||||
|
const index = distinctConnectionMap.get(connection);
|
||||||
|
if (isNil(index)) return null;
|
||||||
|
const { rect } = texture.render();
|
||||||
|
return {
|
||||||
|
source: texture.source,
|
||||||
|
rect: { x: rect.x, y: rect.y + size * index, w: size, h: size }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
*renderAnimated(
|
||||||
|
autotile: number,
|
||||||
|
connection: number
|
||||||
|
): Generator<ITextureRenderable, void> {
|
||||||
|
const tile = this.manager.getTile(autotile);
|
||||||
|
if (!tile) return;
|
||||||
|
yield* this.renderAnimatedWith(tile, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
*renderAnimatedWith(
|
||||||
|
tile: IMaterialFramedData,
|
||||||
|
connection: number
|
||||||
|
): Generator<ITextureRenderable, void> {
|
||||||
|
if (!this.checkAutotile(tile)) return;
|
||||||
|
const { texture, frames } = tile;
|
||||||
|
const size = texture.height === 128 ? 32 : 48;
|
||||||
|
const index = distinctConnectionMap.get(connection);
|
||||||
|
if (isNil(index)) return;
|
||||||
|
for (let i = 0; i < frames; i++) {
|
||||||
|
yield {
|
||||||
|
source: texture.source,
|
||||||
|
rect: { x: i * size, y: size * index, w: size, h: size }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将自动元件图片展平,平铺存储 48 种样式,此时可以只通过一次绘制来绘制出自动元件,不需要四次绘制
|
||||||
|
* @param image 原始自动元件图片
|
||||||
|
*/
|
||||||
|
static flatten(image: IAutotileData): SizedCanvasImageSource | null {
|
||||||
|
const { source, frames } = image;
|
||||||
|
if (source.width !== frames * 96) return null;
|
||||||
|
if (source.height !== 128 && source.height !== 144) return null;
|
||||||
|
const type =
|
||||||
|
source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
|
||||||
|
const size = type === AutotileType.Big3x4 ? 32 : 48;
|
||||||
|
const width = frames * size;
|
||||||
|
const height = 48 * size;
|
||||||
|
// 画到画布上
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
|
const half = size / 2;
|
||||||
|
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
|
||||||
|
const used = new Set<number>();
|
||||||
|
// 遍历每个组合
|
||||||
|
distinctConnectionMap.forEach((index, conn) => {
|
||||||
|
if (used.has(conn)) return;
|
||||||
|
used.add(conn);
|
||||||
|
const { lt, rt, rb, lb } = map.get(conn)!;
|
||||||
|
const y = index * size;
|
||||||
|
for (let i = 0; i < frames; i++) {
|
||||||
|
const x = i * size;
|
||||||
|
// prettier-ignore
|
||||||
|
ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half);
|
||||||
|
// prettier-ignore
|
||||||
|
ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half);
|
||||||
|
// prettier-ignore
|
||||||
|
ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half);
|
||||||
|
// prettier-ignore
|
||||||
|
ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射自动元件连接
|
||||||
|
* @param target 输出映射对象
|
||||||
|
* @param mode 自动元件类型,1 表示 3x4,2 表示 2x3
|
||||||
|
*/
|
||||||
|
function mapAutotile(
|
||||||
|
target: Map<number, [number, number, number, number]>,
|
||||||
|
mode: 1 | 2
|
||||||
|
) {
|
||||||
|
const h = mode === 1 ? 2 : 1; // 横向偏移因子
|
||||||
|
const v = mode === 1 ? 12 : 4; // 纵向偏移因子
|
||||||
|
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 <= 0b1111_1111; i++) {
|
||||||
|
// 自动元件由四个更小的矩形组合而成
|
||||||
|
// 初始状态下,四个矩形分别处在四个角的位置
|
||||||
|
// 而且对应角落的矩形只可能出现在每个大区块的对应角落
|
||||||
|
|
||||||
|
let lu = luo; // leftup
|
||||||
|
let ru = ruo; // rightup
|
||||||
|
let ld = ldo; // leftdown
|
||||||
|
let rd = rdo; // rightdown
|
||||||
|
|
||||||
|
// 先看四个方向,最后看斜角方向
|
||||||
|
if (i & 0b0000_0001) {
|
||||||
|
// 左侧有连接,左侧两个矩形向右偏移两个因子
|
||||||
|
lu += h * 2;
|
||||||
|
ld += h * 2;
|
||||||
|
// 如果右侧还有连接,那么右侧矩形和左侧矩形需要移动至中间
|
||||||
|
// 但是由于后面还处理了先右侧再左侧的情况,因此需要先向右偏移一个因子
|
||||||
|
// 结果就是先向右移动了一个因子,在后面又向左移动了两个因子,因此相当于向左移动了一个因子
|
||||||
|
if (i & 0b0001_0000) {
|
||||||
|
ru += h;
|
||||||
|
rd += h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i & 0b0000_0100) {
|
||||||
|
// 下侧有连接,下侧两个矩形向上偏移两个因子
|
||||||
|
ld -= v * 2;
|
||||||
|
rd -= v * 2;
|
||||||
|
if (i & 0b0100_0000) {
|
||||||
|
lu -= v;
|
||||||
|
ru -= v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i & 0b0001_0000) {
|
||||||
|
// 右侧有连接,右侧矩形向左移动两个因子
|
||||||
|
ru -= h * 2;
|
||||||
|
rd -= h * 2;
|
||||||
|
if (i & 0b0000_0001) {
|
||||||
|
lu -= h;
|
||||||
|
ld -= h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i & 0b0100_0000) {
|
||||||
|
// 上侧有链接,上侧矩形向下移动两个因子
|
||||||
|
lu += v * 2;
|
||||||
|
ru += v * 2;
|
||||||
|
if (i & 0b0000_0100) {
|
||||||
|
ld += v;
|
||||||
|
rd += v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 斜角
|
||||||
|
// 如果左上仅与上和左连接
|
||||||
|
if ((i & 0b1100_0001) === 0b0100_0001) {
|
||||||
|
lu = luc;
|
||||||
|
}
|
||||||
|
// 如果右上仅与上和右连接
|
||||||
|
if ((i & 0b0111_0000) === 0b0101_0000) {
|
||||||
|
ru = ruc;
|
||||||
|
}
|
||||||
|
// 如果右下仅与右和下连接
|
||||||
|
if ((i & 0b0001_1100) === 0b0001_0100) {
|
||||||
|
rd = rdc;
|
||||||
|
}
|
||||||
|
// 如果左下仅与左和下连接
|
||||||
|
if ((i & 0b0000_0111) === 0b0000_0101) {
|
||||||
|
ld = ldc;
|
||||||
|
}
|
||||||
|
target.set(i, [lu, ru, rd, ld]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAutotile() {
|
||||||
|
mapAutotile(connectionMap3x4, 1);
|
||||||
|
mapAutotile(connectionMap2x3, 2);
|
||||||
|
|
||||||
|
connectionMap3x4.forEach((data, connection) => {
|
||||||
|
const [ltd, rtd, rbd, lbd] = data;
|
||||||
|
const ltx = (ltd % 6) * 16;
|
||||||
|
const lty = Math.floor(ltd / 6) * 16;
|
||||||
|
const rtx = (rtd % 6) * 16;
|
||||||
|
const rty = Math.floor(rtd / 6) * 16;
|
||||||
|
const rbx = (rbd % 6) * 16;
|
||||||
|
const rby = Math.floor(rbd / 6) * 16;
|
||||||
|
const lbx = (lbd % 6) * 16;
|
||||||
|
const lby = Math.floor(lbd / 6) * 16;
|
||||||
|
rectMap3x4.set(connection, {
|
||||||
|
lt: { x: ltx, y: lty, w: 16, h: 16 },
|
||||||
|
rt: { x: rtx, y: rty, w: 16, h: 16 },
|
||||||
|
rb: { x: rbx, y: rby, w: 16, h: 16 },
|
||||||
|
lb: { x: lbx, y: lby, w: 16, h: 16 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
connectionMap2x3.forEach((data, connection) => {
|
||||||
|
const [ltd, rtd, rbd, lbd] = data;
|
||||||
|
const ltx = (ltd % 4) * 24;
|
||||||
|
const lty = Math.floor(ltd / 4) * 24;
|
||||||
|
const rtx = (rtd % 4) * 24;
|
||||||
|
const rty = Math.floor(rtd / 4) * 24;
|
||||||
|
const rbx = (rbd % 4) * 24;
|
||||||
|
const rby = Math.floor(rbd / 4) * 24;
|
||||||
|
const lbx = (lbd % 4) * 24;
|
||||||
|
const lby = Math.floor(lbd / 4) * 24;
|
||||||
|
rectMap2x3.set(connection, {
|
||||||
|
lt: { x: ltx, y: lty, w: 24, h: 24 },
|
||||||
|
rt: { x: rtx, y: rty, w: 24, h: 24 },
|
||||||
|
rb: { x: rbx, y: rby, w: 24, h: 24 },
|
||||||
|
lb: { x: lbx, y: lby, w: 24, h: 24 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const usedRect: [number, number, number, number][] = [];
|
||||||
|
let flag = 0;
|
||||||
|
// 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次
|
||||||
|
connectionMap2x3.forEach((conn, num) => {
|
||||||
|
const index = usedRect.findIndex(
|
||||||
|
used =>
|
||||||
|
used[0] === conn[0] &&
|
||||||
|
used[1] === conn[1] &&
|
||||||
|
used[2] === conn[2] &&
|
||||||
|
used[3] === conn[3]
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
distinctConnectionMap.set(num, flag);
|
||||||
|
usedRect.push(conn.slice() as [number, number, number, number]);
|
||||||
|
flag++;
|
||||||
|
} else {
|
||||||
|
distinctConnectionMap.set(num, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
159
packages-user/client-base/src/material/builder.ts
Normal file
159
packages-user/client-base/src/material/builder.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
ITextureStore,
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureStreamComposer,
|
||||||
|
TextureMaxRectsStreamComposer,
|
||||||
|
SizedCanvasImageSource
|
||||||
|
} from '@motajs/render-assets';
|
||||||
|
import { IAssetBuilder, IMaterialGetter, ITrackedAssetData } from './types';
|
||||||
|
import { logger, PrivateListDirtyTracker } from '@motajs/common';
|
||||||
|
|
||||||
|
export class AssetBuilder implements IAssetBuilder {
|
||||||
|
readonly composer: ITextureStreamComposer<void> =
|
||||||
|
new TextureMaxRectsStreamComposer(4096, 4096, 0);
|
||||||
|
|
||||||
|
private output: ITextureStore | null = null;
|
||||||
|
private started: boolean = false;
|
||||||
|
|
||||||
|
private readonly trackedData: TrackedAssetData;
|
||||||
|
|
||||||
|
/** 当前的索引 */
|
||||||
|
private index: number = -1;
|
||||||
|
|
||||||
|
/** 贴图更新的 promise */
|
||||||
|
private pending: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
constructor(readonly materials: IMaterialGetter) {
|
||||||
|
this.trackedData = new TrackedAssetData(materials, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe(store: ITextureStore): void {
|
||||||
|
if (this.started) {
|
||||||
|
logger.warn(76);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.output = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTexture(texture: ITexture): ITextureComposedData {
|
||||||
|
this.started = true;
|
||||||
|
const res = [...this.composer.add([texture])];
|
||||||
|
const data = res[0];
|
||||||
|
|
||||||
|
if (this.output) {
|
||||||
|
if (data.index > this.index) {
|
||||||
|
this.output.addTexture(data.index, data.texture);
|
||||||
|
this.index = data.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending = this.pending.then(() =>
|
||||||
|
this.trackedData.updateSource(data.index, data.texture.source)
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateSourceList(source: Set<ITextureComposedData>) {
|
||||||
|
for (const data of source) {
|
||||||
|
await this.trackedData.updateSource(
|
||||||
|
data.index,
|
||||||
|
data.texture.source
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTextureList(
|
||||||
|
texture: Iterable<ITexture>
|
||||||
|
): Iterable<ITextureComposedData> {
|
||||||
|
this.started = true;
|
||||||
|
const res = [...this.composer.add(texture)];
|
||||||
|
const toUpdate = new Set<ITextureComposedData>();
|
||||||
|
if (this.output) {
|
||||||
|
res.forEach(data => {
|
||||||
|
if (data.index > this.index) {
|
||||||
|
this.output!.addTexture(data.index, data.texture);
|
||||||
|
this.index = data.index;
|
||||||
|
toUpdate.add(data);
|
||||||
|
} else {
|
||||||
|
toUpdate.add(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending = this.pending.then(() => this.updateSourceList(toUpdate));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracked(): ITrackedAssetData {
|
||||||
|
return this.trackedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.composer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackedAssetData
|
||||||
|
extends PrivateListDirtyTracker<number>
|
||||||
|
implements ITrackedAssetData
|
||||||
|
{
|
||||||
|
readonly sourceList: Map<number, ImageBitmap> = new Map();
|
||||||
|
readonly skipRef: Map<SizedCanvasImageSource, number> = new Map();
|
||||||
|
|
||||||
|
private originSourceMap: Map<number, SizedCanvasImageSource> = new Map();
|
||||||
|
|
||||||
|
private promises: Set<Promise<ImageBitmap>> = new Set();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly materials: IMaterialGetter,
|
||||||
|
readonly builder: AssetBuilder
|
||||||
|
) {
|
||||||
|
super(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(index: number) {
|
||||||
|
if (index >= this.length) {
|
||||||
|
this.updateLength(index + 1);
|
||||||
|
}
|
||||||
|
this.dirty(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSource(index: number, source: SizedCanvasImageSource) {
|
||||||
|
if (index >= this.length) {
|
||||||
|
this.updateLength(this.length + 1);
|
||||||
|
}
|
||||||
|
const origin = this.originSourceMap.get(index);
|
||||||
|
const prev = this.sourceList.get(index);
|
||||||
|
if (origin && origin !== source) {
|
||||||
|
this.skipRef.delete(origin);
|
||||||
|
}
|
||||||
|
if (prev) {
|
||||||
|
this.skipRef.delete(prev);
|
||||||
|
}
|
||||||
|
this.originSourceMap.set(index, source);
|
||||||
|
if (source instanceof ImageBitmap) {
|
||||||
|
if (this.skipRef.has(source)) return;
|
||||||
|
this.sourceList.set(index, source);
|
||||||
|
this.skipRef.set(source, index);
|
||||||
|
} else {
|
||||||
|
const promise = createImageBitmap(source);
|
||||||
|
this.promises.add(promise);
|
||||||
|
const bitmap = await promise;
|
||||||
|
this.promises.delete(promise);
|
||||||
|
this.sourceList.set(index, bitmap);
|
||||||
|
this.skipRef.set(bitmap, index);
|
||||||
|
// 要把源也加到映射中,因为这里的 bitmap 与外部源并不同引用
|
||||||
|
this.skipRef.set(source, index);
|
||||||
|
}
|
||||||
|
this.dirty(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async then(): Promise<void> {
|
||||||
|
await Promise.all([...this.promises]);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {}
|
||||||
|
}
|
||||||
140
packages-user/client-base/src/material/fallback.ts
Normal file
140
packages-user/client-base/src/material/fallback.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { ITexture } from '@motajs/render-assets';
|
||||||
|
import { materials } from './ins';
|
||||||
|
import { IBlockIdentifier, IIndexedIdentifier } from './types';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
|
function extractClsBlocks<C extends Exclude<Cls, 'tileset'>>(
|
||||||
|
cls: C,
|
||||||
|
map: Record<string, number>,
|
||||||
|
icons: Record<string, number>
|
||||||
|
): IBlockIdentifier[] {
|
||||||
|
const max = Math.max(...Object.values(icons));
|
||||||
|
const arr = Array(max).fill(void 0);
|
||||||
|
for (const [key, value] of Object.entries(icons)) {
|
||||||
|
// 样板编辑器 bug 可能会导致多个 id 使用一个偏移,因此要判断下
|
||||||
|
if (!(key in map) || !isNil(arr[value])) continue;
|
||||||
|
const id = key as AllIdsOf<C>;
|
||||||
|
const num = map[id] as keyof NumberToId;
|
||||||
|
const identifier: IBlockIdentifier = {
|
||||||
|
id: id as string,
|
||||||
|
cls,
|
||||||
|
num
|
||||||
|
};
|
||||||
|
arr[value] = identifier;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTileset(set: Set<number>, map?: readonly (readonly number[])[]) {
|
||||||
|
if (!map) return;
|
||||||
|
map.forEach(line => {
|
||||||
|
line.forEach(v => {
|
||||||
|
if (v >= 10000) set.add(v);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
|
||||||
|
if (!map) return;
|
||||||
|
map.forEach(line => {
|
||||||
|
line.forEach(v => {
|
||||||
|
const id = core.maps.blocksInfo[v as keyof NumberToId];
|
||||||
|
if (id?.cls === 'autotile') set.add(v);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容旧版加载
|
||||||
|
*/
|
||||||
|
export function fallbackLoad() {
|
||||||
|
// 基本素材
|
||||||
|
const icons = core.icons.icons;
|
||||||
|
const images = core.material.images;
|
||||||
|
const idNumMap: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
|
||||||
|
const num = Number(key);
|
||||||
|
idNumMap[value.id] = Number(num);
|
||||||
|
if (!isNil(value.animate)) {
|
||||||
|
materials.setDefaultFrame(num, value.animate - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const terrains = extractClsBlocks('terrains', idNumMap, icons.terrains);
|
||||||
|
const animates = extractClsBlocks('animates', idNumMap, icons.animates);
|
||||||
|
const items = extractClsBlocks('items', idNumMap, icons.items);
|
||||||
|
const enemys = extractClsBlocks('enemys', idNumMap, icons.enemys);
|
||||||
|
const npcs = extractClsBlocks('npcs', idNumMap, icons.npcs);
|
||||||
|
const enemy48 = extractClsBlocks('enemy48', idNumMap, icons.enemy48);
|
||||||
|
const npc48 = extractClsBlocks('npc48', idNumMap, icons.npc48);
|
||||||
|
|
||||||
|
// Grid
|
||||||
|
materials.addGrid(images.terrains, terrains);
|
||||||
|
materials.addGrid(images.items, items);
|
||||||
|
|
||||||
|
// Row Animates
|
||||||
|
materials.addRowAnimate(images.animates, animates, 32);
|
||||||
|
materials.addRowAnimate(images.enemys, enemys, 32);
|
||||||
|
materials.addRowAnimate(images.npcs, npcs, 32);
|
||||||
|
materials.addRowAnimate(images.enemy48, enemy48, 48);
|
||||||
|
materials.addRowAnimate(images.npc48, npc48, 48);
|
||||||
|
|
||||||
|
// Autotile
|
||||||
|
for (const key of Object.keys(icons.autotile)) {
|
||||||
|
const id = key as AllIdsOf<'autotile'>;
|
||||||
|
const img = images.autotile[id];
|
||||||
|
const identifier: IBlockIdentifier = {
|
||||||
|
id,
|
||||||
|
num: idNumMap[id],
|
||||||
|
cls: 'autotile'
|
||||||
|
};
|
||||||
|
materials.addAutotile(img, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tilesets
|
||||||
|
core.tilesets.forEach((v, i) => {
|
||||||
|
const img = images.tilesets[v];
|
||||||
|
const identifier: IIndexedIdentifier = {
|
||||||
|
index: i,
|
||||||
|
alias: v
|
||||||
|
};
|
||||||
|
materials.addTileset(img, identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Images
|
||||||
|
core.images.forEach((v, i) => {
|
||||||
|
const img = core.material.images.images[v];
|
||||||
|
materials.addImage(img, { index: i, alias: v });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 地图上出现过的 tileset
|
||||||
|
const tilesetSet = new Set<number>();
|
||||||
|
const autotileSet = new Set<number>();
|
||||||
|
core.floorIds.forEach(v => {
|
||||||
|
const floor = core.floors[v];
|
||||||
|
addTileset(tilesetSet, floor.bgmap);
|
||||||
|
addTileset(tilesetSet, floor.bg2map);
|
||||||
|
addTileset(tilesetSet, floor.map);
|
||||||
|
addTileset(tilesetSet, floor.fgmap);
|
||||||
|
addTileset(tilesetSet, floor.fg2map);
|
||||||
|
addAutotile(autotileSet, floor.bgmap);
|
||||||
|
addAutotile(autotileSet, floor.bg2map);
|
||||||
|
addAutotile(autotileSet, floor.map);
|
||||||
|
addAutotile(autotileSet, floor.fgmap);
|
||||||
|
addAutotile(autotileSet, floor.fg2map);
|
||||||
|
});
|
||||||
|
|
||||||
|
const heroTextures: ITexture[] = [];
|
||||||
|
|
||||||
|
data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d.main.heroImages.forEach(v => {
|
||||||
|
const tex = materials.getImageByAlias(v);
|
||||||
|
if (tex) heroTextures.push(tex);
|
||||||
|
});
|
||||||
|
|
||||||
|
materials.buildAssets();
|
||||||
|
|
||||||
|
materials.cacheAutotileList(autotileSet);
|
||||||
|
materials.cacheTilesetList(tilesetSet);
|
||||||
|
materials.buildListToAsset(heroTextures);
|
||||||
|
}
|
||||||
19
packages-user/client-base/src/material/index.ts
Normal file
19
packages-user/client-base/src/material/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { loading } from '@user/data-base';
|
||||||
|
import { fallbackLoad } from './fallback';
|
||||||
|
import { createAutotile } from './autotile';
|
||||||
|
|
||||||
|
export function createMaterial() {
|
||||||
|
createAutotile();
|
||||||
|
loading.once('loaded', () => {
|
||||||
|
fallbackLoad();
|
||||||
|
loading.emit('assetBuilt');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './autotile';
|
||||||
|
export * from './builder';
|
||||||
|
export * from './fallback';
|
||||||
|
export * from './ins';
|
||||||
|
export * from './manager';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
5
packages-user/client-base/src/material/ins.ts
Normal file
5
packages-user/client-base/src/material/ins.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { AutotileProcessor } from './autotile';
|
||||||
|
import { MaterialManager } from './manager';
|
||||||
|
|
||||||
|
export const materials = new MaterialManager();
|
||||||
|
export const autotile = new AutotileProcessor(materials);
|
||||||
602
packages-user/client-base/src/material/manager.ts
Normal file
602
packages-user/client-base/src/material/manager.ts
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
import {
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureRenderable,
|
||||||
|
ITextureSplitter,
|
||||||
|
ITextureStore,
|
||||||
|
SizedCanvasImageSource,
|
||||||
|
Texture,
|
||||||
|
TextureGridSplitter,
|
||||||
|
TextureRowSplitter,
|
||||||
|
TextureStore
|
||||||
|
} from '@motajs/render-assets';
|
||||||
|
import {
|
||||||
|
IBlockIdentifier,
|
||||||
|
IMaterialData,
|
||||||
|
IMaterialManager,
|
||||||
|
IIndexedIdentifier,
|
||||||
|
IMaterialAssetData,
|
||||||
|
BlockCls,
|
||||||
|
IBigImageReturn,
|
||||||
|
IAssetBuilder,
|
||||||
|
IMaterialFramedData,
|
||||||
|
ITrackedAssetData
|
||||||
|
} from './types';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { getClsByString, getTextureFrame } from './utils';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { AssetBuilder } from './builder';
|
||||||
|
import { AutotileProcessor } from './autotile';
|
||||||
|
|
||||||
|
interface TilesetCache {
|
||||||
|
/** 是否已经在贴图库中存在 */
|
||||||
|
readonly existed: boolean;
|
||||||
|
/** 贴图对象 */
|
||||||
|
readonly texture: ITexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaterialManager implements IMaterialManager {
|
||||||
|
readonly tileStore: ITextureStore = new TextureStore();
|
||||||
|
readonly tilesetStore: ITextureStore = new TextureStore();
|
||||||
|
readonly imageStore: ITextureStore = new TextureStore();
|
||||||
|
readonly assetStore: ITextureStore = new TextureStore();
|
||||||
|
readonly bigImageStore: ITextureStore = new TextureStore();
|
||||||
|
|
||||||
|
/** 自动元件图像源映射 */
|
||||||
|
readonly autotileSource: Map<number, SizedCanvasImageSource> = new Map();
|
||||||
|
|
||||||
|
/** 图集信息存储 */
|
||||||
|
readonly assetDataStore: Map<number, ITextureComposedData> = new Map();
|
||||||
|
/** 贴图到图集索引的映射 */
|
||||||
|
readonly assetMap: Map<ITexture, number> = new Map();
|
||||||
|
/** 带有脏标记追踪的图集对象 */
|
||||||
|
readonly trackedAsset: ITrackedAssetData;
|
||||||
|
|
||||||
|
/** 大怪物数据 */
|
||||||
|
readonly bigImageData: Map<number, IMaterialFramedData> = new Map();
|
||||||
|
/** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
|
||||||
|
readonly tilesetOffsetMap: Map<number, number> = new Map();
|
||||||
|
/** 图集打包器 */
|
||||||
|
readonly assetBuilder: IAssetBuilder;
|
||||||
|
|
||||||
|
/** 图块 id 到图块数字的映射 */
|
||||||
|
readonly idNumMap: Map<string, number> = new Map();
|
||||||
|
/** 图块数字到图块 id 的映射 */
|
||||||
|
readonly numIdMap: Map<number, string> = new Map();
|
||||||
|
/** 图块数字到图块类型的映射 */
|
||||||
|
readonly clsMap: Map<number, BlockCls> = new Map();
|
||||||
|
/** 图块的默认帧数 */
|
||||||
|
readonly defaultFrames: Map<number, number> = new Map();
|
||||||
|
|
||||||
|
/** 网格切分器 */
|
||||||
|
readonly gridSplitter: TextureGridSplitter = new TextureGridSplitter();
|
||||||
|
/** 行切分器 */
|
||||||
|
readonly rowSplitter: TextureRowSplitter = new TextureRowSplitter();
|
||||||
|
|
||||||
|
/** 大怪物贴图的标识符 */
|
||||||
|
private bigImageId: number = 0;
|
||||||
|
/** 当前 tileset 索引 */
|
||||||
|
private nowTilesetIndex: number = -1;
|
||||||
|
/** 当前 tileset 偏移 */
|
||||||
|
private nowTilesetOffset: number = 0;
|
||||||
|
/** 是否已经构建过素材 */
|
||||||
|
private built: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.assetBuilder = new AssetBuilder(this);
|
||||||
|
this.assetBuilder.pipe(this.assetStore);
|
||||||
|
this.trackedAsset = this.assetBuilder.tracked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加由分割器和图块映射组成的图像源贴图
|
||||||
|
* @param source 图像源
|
||||||
|
* @param map 图块 id 与图块数字映射
|
||||||
|
* @param store 要添加至的贴图存储对象
|
||||||
|
* @param splitter 使用的分割器
|
||||||
|
* @param splitterData 传递给分割器的数据
|
||||||
|
* @param processTexture 对每个纹理进行处理
|
||||||
|
*/
|
||||||
|
private addMappedSource<T>(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
map: ArrayLike<IBlockIdentifier>,
|
||||||
|
store: ITextureStore,
|
||||||
|
splitter: ITextureSplitter<T>,
|
||||||
|
splitterData: T,
|
||||||
|
processTexture?: (tex: ITexture) => void
|
||||||
|
): Iterable<IMaterialData> {
|
||||||
|
const tex = new Texture(source);
|
||||||
|
const textures = [...splitter.split(tex, splitterData)];
|
||||||
|
if (textures.length !== map.length) {
|
||||||
|
logger.warn(75, textures.length.toString(), map.length.toString());
|
||||||
|
}
|
||||||
|
const res: IMaterialData[] = textures.map((v, i) => {
|
||||||
|
if (!map[i]) {
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
texture: v,
|
||||||
|
identifier: -1,
|
||||||
|
alias: '@internal-unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { id, num, cls } = map[i];
|
||||||
|
store.addTexture(num, v);
|
||||||
|
store.alias(num, id);
|
||||||
|
this.clsMap.set(num, getClsByString(cls));
|
||||||
|
processTexture?.(v);
|
||||||
|
const data: IMaterialData = {
|
||||||
|
store,
|
||||||
|
texture: v,
|
||||||
|
identifier: num,
|
||||||
|
alias: id
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
addGrid(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
map: ArrayLike<IBlockIdentifier>
|
||||||
|
): Iterable<IMaterialData> {
|
||||||
|
return this.addMappedSource(
|
||||||
|
source,
|
||||||
|
map,
|
||||||
|
this.tileStore,
|
||||||
|
this.gridSplitter,
|
||||||
|
[32, 32]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRowAnimate(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
map: ArrayLike<IBlockIdentifier>,
|
||||||
|
height: number
|
||||||
|
): Iterable<IMaterialData> {
|
||||||
|
return this.addMappedSource(
|
||||||
|
source,
|
||||||
|
map,
|
||||||
|
this.tileStore,
|
||||||
|
this.rowSplitter,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAutotile(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IBlockIdentifier
|
||||||
|
): void {
|
||||||
|
this.autotileSource.set(identifier.num, source);
|
||||||
|
this.tileStore.alias(identifier.num, identifier.id);
|
||||||
|
this.clsMap.set(identifier.num, BlockCls.Autotile);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTileset(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IIndexedIdentifier
|
||||||
|
): IMaterialData | null {
|
||||||
|
const tex = new Texture(source);
|
||||||
|
this.tilesetStore.addTexture(identifier.index, tex);
|
||||||
|
this.tilesetStore.alias(identifier.index, identifier.alias);
|
||||||
|
const width = Math.floor(source.width / 32);
|
||||||
|
const height = Math.floor(source.height / 32);
|
||||||
|
const count = width * height;
|
||||||
|
const offset = Math.ceil(count / 10000);
|
||||||
|
if (identifier.index === 0) {
|
||||||
|
this.tilesetOffsetMap.set(0, 0);
|
||||||
|
this.nowTilesetIndex = 0;
|
||||||
|
this.nowTilesetOffset = offset;
|
||||||
|
} else {
|
||||||
|
if (identifier.index - 1 !== this.nowTilesetIndex) {
|
||||||
|
logger.warn(78);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 一个 tileset 可能不止 10000 个图块,需要计算偏移
|
||||||
|
const width = Math.floor(source.width / 32);
|
||||||
|
const height = Math.floor(source.height / 32);
|
||||||
|
const count = width * height;
|
||||||
|
const offset = Math.ceil(count / 10000);
|
||||||
|
const end = this.nowTilesetOffset + offset;
|
||||||
|
for (let i = this.nowTilesetOffset; i < end; i++) {
|
||||||
|
this.tilesetOffsetMap.set(i, identifier.index);
|
||||||
|
}
|
||||||
|
this.nowTilesetOffset = end;
|
||||||
|
this.nowTilesetIndex = identifier.index;
|
||||||
|
}
|
||||||
|
const data: IMaterialData = {
|
||||||
|
store: this.tilesetStore,
|
||||||
|
texture: tex,
|
||||||
|
identifier: identifier.index,
|
||||||
|
alias: identifier.alias
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
addImage(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IIndexedIdentifier
|
||||||
|
): IMaterialData {
|
||||||
|
const texture = new Texture(source);
|
||||||
|
this.imageStore.addTexture(identifier.index, texture);
|
||||||
|
this.imageStore.alias(identifier.index, identifier.alias);
|
||||||
|
const data: IMaterialData = {
|
||||||
|
store: this.imageStore,
|
||||||
|
texture,
|
||||||
|
identifier: identifier.index,
|
||||||
|
alias: identifier.alias
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultFrame(identifier: number, defaultFrame: number): void {
|
||||||
|
this.defaultFrames.set(identifier, defaultFrame);
|
||||||
|
const bigImageData = this.bigImageData.get(identifier);
|
||||||
|
if (bigImageData) {
|
||||||
|
bigImageData.defaultFrame = defaultFrame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultFrame(identifier: number): number {
|
||||||
|
return this.defaultFrames.get(identifier) ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTile(identifier: number): Readonly<IMaterialFramedData> | null {
|
||||||
|
if (identifier < 10000) {
|
||||||
|
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
|
||||||
|
if (
|
||||||
|
cls === BlockCls.Autotile &&
|
||||||
|
this.autotileSource.has(identifier)
|
||||||
|
) {
|
||||||
|
this.cacheAutotile(identifier);
|
||||||
|
}
|
||||||
|
const texture = this.tileStore.getTexture(identifier);
|
||||||
|
if (!texture) return null;
|
||||||
|
return {
|
||||||
|
texture,
|
||||||
|
cls,
|
||||||
|
offset: 32,
|
||||||
|
frames: getTextureFrame(cls, texture),
|
||||||
|
defaultFrame: this.defaultFrames.get(identifier) ?? -1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const texture = this.cacheTileset(identifier);
|
||||||
|
if (!texture) return null;
|
||||||
|
return {
|
||||||
|
texture,
|
||||||
|
cls: BlockCls.Tileset,
|
||||||
|
offset: 32,
|
||||||
|
frames: 1,
|
||||||
|
defaultFrame: -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTileset(identifier: number): ITexture | null {
|
||||||
|
return this.tilesetStore.getTexture(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImage(identifier: number): ITexture | null {
|
||||||
|
return this.imageStore.getTexture(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTileByAlias(alias: string): Readonly<IMaterialFramedData> | null {
|
||||||
|
if (/X\d{5,}/.test(alias)) {
|
||||||
|
return this.getTile(parseInt(alias.slice(1)));
|
||||||
|
} else {
|
||||||
|
const identifier = this.tileStore.identifierOf(alias);
|
||||||
|
if (isNil(identifier)) return null;
|
||||||
|
return this.getTile(identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTilesetByAlias(alias: string): ITexture | null {
|
||||||
|
return this.tilesetStore.fromAlias(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageByAlias(alias: string): ITexture | null {
|
||||||
|
return this.imageStore.fromAlias(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTilesetOwnTexture(identifier: number): TilesetCache | null {
|
||||||
|
const texture = this.tileStore.getTexture(identifier);
|
||||||
|
if (texture) return { existed: true, texture };
|
||||||
|
// 如果 tileset 不存在,那么执行缓存操作
|
||||||
|
const offset = Math.floor(identifier / 10000);
|
||||||
|
const index = this.tilesetOffsetMap.get(offset - 1);
|
||||||
|
if (isNil(index)) return null;
|
||||||
|
// 获取对应的 tileset 贴图
|
||||||
|
const tileset = this.tilesetStore.getTexture(index);
|
||||||
|
if (!tileset) return null;
|
||||||
|
// 计算图块位置
|
||||||
|
const rest = identifier - offset * 10000;
|
||||||
|
const { width, height } = tileset;
|
||||||
|
const tileWidth = Math.floor(width / 32);
|
||||||
|
const tileHeight = Math.floor(height / 32);
|
||||||
|
// 如果图块位置超出了贴图范围
|
||||||
|
if (rest > tileWidth * tileHeight) return null;
|
||||||
|
// 裁剪 tileset,生成贴图
|
||||||
|
const x = rest % tileWidth;
|
||||||
|
const y = Math.floor(rest / tileWidth);
|
||||||
|
const newTexture = new Texture(tileset.source);
|
||||||
|
newTexture.clip(x * 32, y * 32, 32, 32);
|
||||||
|
return { existed: false, texture: newTexture };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查图集状态,如果已存在图集则标记为脏,否则新增图集
|
||||||
|
* @param data 图集数据
|
||||||
|
*/
|
||||||
|
private checkAssetDirty(data: ITextureComposedData) {
|
||||||
|
if (!this.built) return;
|
||||||
|
const asset = this.assetDataStore.get(data.index);
|
||||||
|
if (!asset) {
|
||||||
|
// 如果有新图集,需要添加
|
||||||
|
const alias = `asset-${data.index}`;
|
||||||
|
this.assetStore.alias(data.index, alias);
|
||||||
|
this.assetDataStore.set(data.index, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定的贴图列表转换至指定的图集数据中
|
||||||
|
* @param composedData 组合数据
|
||||||
|
* @param textures 贴图列表
|
||||||
|
*/
|
||||||
|
private cacheToAsset(
|
||||||
|
composedData: ITextureComposedData[],
|
||||||
|
textures: ITexture[]
|
||||||
|
) {
|
||||||
|
textures.forEach(tex => {
|
||||||
|
const assetData = composedData.find(v => v.assetMap.has(tex));
|
||||||
|
if (!assetData) {
|
||||||
|
logger.error(38);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tex.toAsset(assetData);
|
||||||
|
});
|
||||||
|
composedData.forEach(v => this.checkAssetDirty(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheTileset(identifier: number): ITexture | null {
|
||||||
|
const newTexture = this.getTilesetOwnTexture(identifier);
|
||||||
|
if (!newTexture) return null;
|
||||||
|
const { existed, texture } = newTexture;
|
||||||
|
if (existed) return texture;
|
||||||
|
// 缓存贴图
|
||||||
|
this.tileStore.addTexture(identifier, texture);
|
||||||
|
this.idNumMap.set(`X${identifier}`, identifier);
|
||||||
|
this.numIdMap.set(identifier, `X${identifier}`);
|
||||||
|
const data = this.assetBuilder.addTexture(texture);
|
||||||
|
texture.toAsset(data);
|
||||||
|
this.checkAssetDirty(data);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheTilesetList(
|
||||||
|
identifierList: Iterable<number>
|
||||||
|
): Iterable<ITexture | null> {
|
||||||
|
const arr = [...identifierList];
|
||||||
|
const toAdd: ITexture[] = [];
|
||||||
|
|
||||||
|
arr.forEach(v => {
|
||||||
|
const newTexture = this.getTilesetOwnTexture(v);
|
||||||
|
if (!newTexture) return;
|
||||||
|
const { existed, texture } = newTexture;
|
||||||
|
if (existed) return;
|
||||||
|
toAdd.push(texture);
|
||||||
|
this.tileStore.addTexture(v, texture);
|
||||||
|
this.idNumMap.set(`X${v}`, v);
|
||||||
|
this.numIdMap.set(v, `X${v}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = this.assetBuilder.addTextureList(toAdd);
|
||||||
|
const res = [...data];
|
||||||
|
this.cacheToAsset(res, toAdd);
|
||||||
|
|
||||||
|
return toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自动元件展开后的图片,如果图片不存在,或是已经展开并存储至了 `tileStore`,那么返回 `null`
|
||||||
|
* @param identifier 自动元件标识符
|
||||||
|
*/
|
||||||
|
private getFlattenedAutotile(
|
||||||
|
identifier: number
|
||||||
|
): SizedCanvasImageSource | null {
|
||||||
|
const cls = this.clsMap.get(identifier);
|
||||||
|
if (cls !== BlockCls.Autotile) return null;
|
||||||
|
if (this.tileStore.getTexture(identifier)) return null;
|
||||||
|
const source = this.autotileSource.get(identifier);
|
||||||
|
if (!source) return null;
|
||||||
|
const frames = source.width === 96 ? 1 : 4;
|
||||||
|
const flattened = AutotileProcessor.flatten({ source, frames });
|
||||||
|
if (!flattened) return null;
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheAutotile(identifier: number): ITexture | null {
|
||||||
|
const flattened = this.getFlattenedAutotile(identifier);
|
||||||
|
if (!flattened) return null;
|
||||||
|
const tex = new Texture(flattened);
|
||||||
|
this.tileStore.addTexture(identifier, tex);
|
||||||
|
const data = this.assetBuilder.addTexture(tex);
|
||||||
|
tex.toAsset(data);
|
||||||
|
this.autotileSource.delete(identifier);
|
||||||
|
this.checkAssetDirty(data);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheAutotileList(
|
||||||
|
identifierList: Iterable<number>
|
||||||
|
): Iterable<ITexture | null> {
|
||||||
|
const arr = [...identifierList];
|
||||||
|
const toAdd: ITexture[] = [];
|
||||||
|
|
||||||
|
arr.forEach(v => {
|
||||||
|
const flattened = this.getFlattenedAutotile(v);
|
||||||
|
if (!flattened) return;
|
||||||
|
const tex = new Texture(flattened);
|
||||||
|
this.tileStore.addTexture(v, tex);
|
||||||
|
toAdd.push(tex);
|
||||||
|
this.autotileSource.delete(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = this.assetBuilder.addTextureList(toAdd);
|
||||||
|
const res = [...data];
|
||||||
|
this.cacheToAsset(res, toAdd);
|
||||||
|
|
||||||
|
return toAdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAssets(): Iterable<IMaterialAssetData> {
|
||||||
|
if (this.built) {
|
||||||
|
logger.warn(79);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
this.built = true;
|
||||||
|
return this.buildListToAsset(this.tileStore.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
buildToAsset(texture: ITexture): IMaterialAssetData {
|
||||||
|
const data = this.assetBuilder.addTexture(texture);
|
||||||
|
const assetData: IMaterialAssetData = {
|
||||||
|
data: data,
|
||||||
|
identifier: data.index,
|
||||||
|
alias: `asset-${data.index}`,
|
||||||
|
store: this.assetStore
|
||||||
|
};
|
||||||
|
this.checkAssetDirty(data);
|
||||||
|
texture.toAsset(data);
|
||||||
|
return assetData;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildListToAsset(
|
||||||
|
texture: Iterable<ITexture>
|
||||||
|
): Iterable<IMaterialAssetData> {
|
||||||
|
const data = this.assetBuilder.addTextureList(texture);
|
||||||
|
const arr = [...data];
|
||||||
|
const res: IMaterialAssetData[] = [];
|
||||||
|
arr.forEach(v => {
|
||||||
|
const alias = `asset-${v.index}`;
|
||||||
|
if (!this.assetDataStore.has(v.index)) {
|
||||||
|
this.assetDataStore.set(v.index, v);
|
||||||
|
}
|
||||||
|
const data: IMaterialAssetData = {
|
||||||
|
data: v,
|
||||||
|
identifier: v.index,
|
||||||
|
alias,
|
||||||
|
store: this.assetStore
|
||||||
|
};
|
||||||
|
for (const tex of v.assetMap.keys()) {
|
||||||
|
tex.toAsset(v);
|
||||||
|
}
|
||||||
|
res.push(data);
|
||||||
|
});
|
||||||
|
arr.forEach(v => {
|
||||||
|
this.checkAssetDirty(v);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAsset(identifier: number): ITextureComposedData | null {
|
||||||
|
return this.assetDataStore.get(identifier) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetByAlias(alias: string): ITextureComposedData | null {
|
||||||
|
const id = this.assetStore.identifierOf(alias);
|
||||||
|
if (isNil(id)) return null;
|
||||||
|
return this.assetDataStore.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTextureOf(identifier: number, cls: BlockCls): ITexture | null {
|
||||||
|
if (cls === BlockCls.Unknown) return null;
|
||||||
|
if (cls !== BlockCls.Tileset) {
|
||||||
|
return this.tileStore.getTexture(identifier);
|
||||||
|
}
|
||||||
|
if (identifier < 10000) return null;
|
||||||
|
return this.cacheTileset(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderable(identifier: number): ITextureRenderable | null {
|
||||||
|
const cls = this.clsMap.get(identifier);
|
||||||
|
if (isNil(cls)) return null;
|
||||||
|
const texture = this.getTextureOf(identifier, cls);
|
||||||
|
if (!texture) return null;
|
||||||
|
return texture.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderableByAlias(alias: string): ITextureRenderable | null {
|
||||||
|
const identifier = this.idNumMap.get(alias);
|
||||||
|
if (isNil(identifier)) return null;
|
||||||
|
return this.getRenderable(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockCls(identifier: number): BlockCls {
|
||||||
|
return this.clsMap.get(identifier) ?? BlockCls.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockClsByAlias(alias: string): BlockCls {
|
||||||
|
const id = this.idNumMap.get(alias);
|
||||||
|
if (isNil(id)) return BlockCls.Unknown;
|
||||||
|
return this.clsMap.get(id) ?? BlockCls.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentifierByAlias(alias: string): number | undefined {
|
||||||
|
return this.idNumMap.get(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAliasByIdentifier(identifier: number): string | undefined {
|
||||||
|
return this.numIdMap.get(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBigImage(
|
||||||
|
identifier: number,
|
||||||
|
image: ITexture,
|
||||||
|
frames: number
|
||||||
|
): IBigImageReturn {
|
||||||
|
const bigImageId = this.bigImageId++;
|
||||||
|
this.bigImageStore.addTexture(bigImageId, image);
|
||||||
|
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
|
||||||
|
const store: IMaterialFramedData = {
|
||||||
|
texture: image,
|
||||||
|
cls,
|
||||||
|
offset: image.width / 4,
|
||||||
|
frames,
|
||||||
|
defaultFrame: this.defaultFrames.get(identifier) ?? -1
|
||||||
|
};
|
||||||
|
this.bigImageData.set(identifier, store);
|
||||||
|
const data: IBigImageReturn = {
|
||||||
|
identifier: bigImageId,
|
||||||
|
store: this.bigImageStore
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBigImage(identifier: number): boolean {
|
||||||
|
return this.bigImageData.has(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBigImage(identifier: number): Readonly<IMaterialFramedData> | null {
|
||||||
|
return this.bigImageData.get(identifier) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null {
|
||||||
|
const identifier = this.idNumMap.get(alias);
|
||||||
|
if (isNil(identifier)) return null;
|
||||||
|
return this.bigImageData.get(identifier) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null {
|
||||||
|
const bigImage = this.bigImageData.get(identifier);
|
||||||
|
if (bigImage) return bigImage;
|
||||||
|
else return this.getTile(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
assetContainsTexture(texture: ITexture): boolean {
|
||||||
|
return this.trackedAsset.skipRef.has(texture.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextureAsset(texture: ITexture): number | undefined {
|
||||||
|
return this.trackedAsset.skipRef.get(texture.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
528
packages-user/client-base/src/material/types.ts
Normal file
528
packages-user/client-base/src/material/types.ts
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
import { IDirtyTracker, IDirtyMarker } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureRenderable,
|
||||||
|
ITextureStore,
|
||||||
|
SizedCanvasImageSource
|
||||||
|
} from '@motajs/render-assets';
|
||||||
|
|
||||||
|
export const enum BlockCls {
|
||||||
|
Unknown,
|
||||||
|
Terrains,
|
||||||
|
Animates,
|
||||||
|
Enemys,
|
||||||
|
Npcs,
|
||||||
|
Items,
|
||||||
|
Enemy48,
|
||||||
|
Npc48,
|
||||||
|
Tileset,
|
||||||
|
Autotile
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum AutotileType {
|
||||||
|
Small2x3,
|
||||||
|
Big3x4
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum AutotileConnection {
|
||||||
|
LeftUp = 0b1000_0000,
|
||||||
|
Up = 0b0100_0000,
|
||||||
|
RightUp = 0b0010_0000,
|
||||||
|
Right = 0b0001_0000,
|
||||||
|
RightDown = 0b0000_1000,
|
||||||
|
Down = 0b0000_0100,
|
||||||
|
LeftDown = 0b0000_0010,
|
||||||
|
Left = 0b0000_0001
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialData {
|
||||||
|
/** 此素材的贴图对象存入了哪个贴图存储对象 */
|
||||||
|
readonly store: ITextureStore;
|
||||||
|
/** 贴图对象 */
|
||||||
|
readonly texture: ITexture;
|
||||||
|
/** 此素材的贴图对象的数字 id,一般对应到图块数字 */
|
||||||
|
readonly identifier: number;
|
||||||
|
/** 此素材的贴图对象的字符串别名,一般对应到图块 id */
|
||||||
|
readonly alias?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBlockIdentifier {
|
||||||
|
/** 图块 id */
|
||||||
|
readonly id: string;
|
||||||
|
/** 图块数字 */
|
||||||
|
readonly num: number;
|
||||||
|
/** 图块类型 */
|
||||||
|
readonly cls: Cls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIndexedIdentifier {
|
||||||
|
/** 标识符索引 */
|
||||||
|
readonly index: number;
|
||||||
|
/** 标识符别名 */
|
||||||
|
readonly alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialAssetData {
|
||||||
|
/** 图集数据 */
|
||||||
|
readonly data: ITextureComposedData;
|
||||||
|
/** 贴图的标识符 */
|
||||||
|
readonly identifier: number;
|
||||||
|
/** 贴图的别名 */
|
||||||
|
readonly alias: string;
|
||||||
|
/** 贴图所属的存储对象 */
|
||||||
|
readonly store: ITextureStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAutotileConnection {
|
||||||
|
/** 连接方式,最高位表示左上,低位依次顺时针旋转 */
|
||||||
|
readonly connection: number;
|
||||||
|
/** 中心自动元件对应的图块数字 */
|
||||||
|
readonly center: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBigImageReturn {
|
||||||
|
/** 大怪物贴图在 store 中的标识符 */
|
||||||
|
readonly identifier: number;
|
||||||
|
/** 存储大怪物贴图的存储对象 */
|
||||||
|
readonly store: ITextureStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialFramedData {
|
||||||
|
/** 贴图对象 */
|
||||||
|
texture: ITexture;
|
||||||
|
/** 图块类型 */
|
||||||
|
cls: BlockCls;
|
||||||
|
/** 贴图总帧数 */
|
||||||
|
frames: number;
|
||||||
|
/** 每帧的横向偏移量 */
|
||||||
|
offset: number;
|
||||||
|
/** 默认帧数 */
|
||||||
|
defaultFrame: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialAsset
|
||||||
|
extends IDirtyTracker<boolean>,
|
||||||
|
IDirtyMarker<void> {
|
||||||
|
/** 图集的贴图数据 */
|
||||||
|
readonly data: ITextureComposedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAutotileProcessor {
|
||||||
|
/** 该自动元件处理器使用的素材管理器 */
|
||||||
|
readonly manager: IMaterialManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标元件连接,
|
||||||
|
* 一个自动元件可以与多个自动元件有特殊连接
|
||||||
|
* @param autotile 自动元件
|
||||||
|
* @param target 当前自动元件将会连接至的自动元件
|
||||||
|
*/
|
||||||
|
setConnection(autotile: number, target: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自动元件的连接情况
|
||||||
|
* @param array 地图图块数组
|
||||||
|
* @param index 自动元件图块所在的索引
|
||||||
|
* @param width 地图每一行的宽度
|
||||||
|
*/
|
||||||
|
connect(
|
||||||
|
array: Uint32Array,
|
||||||
|
index: number,
|
||||||
|
width: number
|
||||||
|
): IAutotileConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查一个图块与指定方向的连接方式
|
||||||
|
* @param connection 当前的连接
|
||||||
|
* @param center 中心点的图块数字
|
||||||
|
* @param target 连接点的图块数字
|
||||||
|
* @param direction 连接点的方向
|
||||||
|
* @returns 经过连接后的连接数字
|
||||||
|
*/
|
||||||
|
updateConnectionFor(
|
||||||
|
connection: number,
|
||||||
|
center: number,
|
||||||
|
target: number,
|
||||||
|
direction: AutotileConnection
|
||||||
|
): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块数字,获取指定自动元件经过连接的可渲染对象
|
||||||
|
* @param autotile 自动元件的图块数字
|
||||||
|
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
|
||||||
|
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
|
||||||
|
*/
|
||||||
|
render(autotile: number, connection: number): ITextureRenderable | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象
|
||||||
|
* @param tile 自动元件的图块贴图数据
|
||||||
|
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
|
||||||
|
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
|
||||||
|
*/
|
||||||
|
renderWith(
|
||||||
|
tile: Readonly<IMaterialFramedData>,
|
||||||
|
connection: number
|
||||||
|
): ITextureRenderable | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象,但是会假设传入的图块就是自动元件,不做不必要的判断
|
||||||
|
* @param tile 自动元件的图块贴图数据
|
||||||
|
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
|
||||||
|
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
|
||||||
|
*/
|
||||||
|
renderWithoutCheck(
|
||||||
|
tile: Readonly<IMaterialFramedData>,
|
||||||
|
connection: number
|
||||||
|
): ITextureRenderable | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块数字,获取指定自动元件经过链接的动态可渲染对象
|
||||||
|
* @param autotile 自动元件的图块数字
|
||||||
|
* @param connection 自动元件的连接方式
|
||||||
|
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
|
||||||
|
*/
|
||||||
|
renderAnimated(
|
||||||
|
autotile: number,
|
||||||
|
connection: number
|
||||||
|
): Generator<ITextureRenderable, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象
|
||||||
|
* @param autotile 自动元件的图块数字
|
||||||
|
* @param connection 自动元件的连接方式
|
||||||
|
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
|
||||||
|
*/
|
||||||
|
renderAnimatedWith(
|
||||||
|
tile: Readonly<IMaterialFramedData>,
|
||||||
|
connection: number
|
||||||
|
): Generator<ITextureRenderable, void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialGetter {
|
||||||
|
/**
|
||||||
|
* 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
|
||||||
|
* @param identifier 图块的图块数字
|
||||||
|
*/
|
||||||
|
getTile(identifier: number): Readonly<IMaterialFramedData> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块标识符获取图块类型
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getBlockCls(identifier: number): BlockCls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个图块是否包含 `bigImage` 贴图,即是否是大怪物
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
isBigImage(identifier: number): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块标识符获取一个图块的 `bigImage` 贴图
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块标识符,首先判断是否是 `bigImage` 贴图,如果是,则返回 `bigImage` 贴图,
|
||||||
|
* 否则返回普通贴图。如果图块不存在,则返回 `null`
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getIfBigImage(identifier: number): Readonly<IMaterialFramedData> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标识符获取图集信息
|
||||||
|
* @param identifier 图集的标识符
|
||||||
|
*/
|
||||||
|
getAsset(identifier: number): ITextureComposedData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据额外素材索引获取额外素材
|
||||||
|
* @param identifier 额外素材的索引
|
||||||
|
*/
|
||||||
|
getTileset(identifier: number): ITexture | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图片的索引获取图片
|
||||||
|
* @param identifier 图片的索引
|
||||||
|
*/
|
||||||
|
getImage(identifier: number): ITexture | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialAliasGetter {
|
||||||
|
/**
|
||||||
|
* 根据图块 id 获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
|
||||||
|
* @param alias 图块 id
|
||||||
|
*/
|
||||||
|
getTileByAlias(alias: string): Readonly<IMaterialFramedData> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据额外素材名称获取额外素材
|
||||||
|
* @param alias 额外素材名称
|
||||||
|
*/
|
||||||
|
getTilesetByAlias(alias: string): ITexture | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图片名称获取图片
|
||||||
|
* @param alias 图片名称
|
||||||
|
*/
|
||||||
|
getImageByAlias(alias: string): ITexture | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据别名获取图集信息
|
||||||
|
* @param alias 图集的别名
|
||||||
|
*/
|
||||||
|
getAssetByAlias(alias: string): ITextureComposedData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块别名获取图块类型
|
||||||
|
* @param alias 图块别名,即图块的 id
|
||||||
|
*/
|
||||||
|
getBlockClsByAlias(alias: string): BlockCls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块别名获取一个图块的 `bigImage` 贴图
|
||||||
|
* @param alias 图块别名,即图块的 id
|
||||||
|
*/
|
||||||
|
getBigImageByAlias(alias: string): Readonly<IMaterialFramedData> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaterialManager
|
||||||
|
extends IMaterialGetter,
|
||||||
|
IMaterialAliasGetter {
|
||||||
|
/** 贴图存储,把 terrains 等内容单独分开存储 */
|
||||||
|
readonly tileStore: ITextureStore;
|
||||||
|
/** tilesets 贴图存储,每个 tileset 是一个贴图对象 */
|
||||||
|
readonly tilesetStore: ITextureStore;
|
||||||
|
/** 存储注册的图像的存储对象 */
|
||||||
|
readonly imageStore: ITextureStore;
|
||||||
|
/** 图集存储,将常用贴图存入其中 */
|
||||||
|
readonly assetStore: ITextureStore;
|
||||||
|
/** bigImage 存储,存储大怪物数据 */
|
||||||
|
readonly bigImageStore: ITextureStore;
|
||||||
|
|
||||||
|
/** 图集信息存储 */
|
||||||
|
readonly assetDataStore: Iterable<[number, ITextureComposedData]>;
|
||||||
|
/** 带有脏标记追踪的图集信息 */
|
||||||
|
readonly trackedAsset: ITrackedAssetData;
|
||||||
|
|
||||||
|
/** 图块类型映射 */
|
||||||
|
readonly clsMap: Map<number, BlockCls>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加网格类型的贴图,包括 terrains 和 items 类型
|
||||||
|
* @param source 图像源
|
||||||
|
* @param map 贴图字符串 id 与图块数字映射,按照先从左到右,再从上到下的顺序映射
|
||||||
|
*/
|
||||||
|
addGrid(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
map: ArrayLike<IBlockIdentifier>
|
||||||
|
): Iterable<IMaterialData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加行动画的贴图,包括 animates enemys npcs enemy48 npc48 类型
|
||||||
|
* @param source 图像源
|
||||||
|
* @param map 贴图字符串 id 与图块数字映射,按从上到下的顺序映射
|
||||||
|
* @param frames 每一行的帧数
|
||||||
|
* @param height 每一行的高度
|
||||||
|
*/
|
||||||
|
addRowAnimate(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
map: ArrayLike<IBlockIdentifier>,
|
||||||
|
frames: number,
|
||||||
|
height: number
|
||||||
|
): Iterable<IMaterialData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加自动元件
|
||||||
|
* @param source 图像源
|
||||||
|
* @param identifier 自动元件的字符串 id 及图块数字
|
||||||
|
* @returns 由于自动元件是懒加载的,因此不会返回任何东西
|
||||||
|
*/
|
||||||
|
addAutotile(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IBlockIdentifier
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个 tileset 类型的素材
|
||||||
|
* @param source 图像源
|
||||||
|
* @param alias tileset 的标识符,包含其在 tilesets 列表中的索引和图片名称
|
||||||
|
*/
|
||||||
|
addTileset(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IIndexedIdentifier
|
||||||
|
): IMaterialData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个图片
|
||||||
|
* @param source 图像源
|
||||||
|
* @param identifier 图片的标识符,包含其在 images 列表中的索引和图片名称
|
||||||
|
*/
|
||||||
|
addImage(
|
||||||
|
source: SizedCanvasImageSource,
|
||||||
|
identifier: IIndexedIdentifier
|
||||||
|
): IMaterialData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定图块默认显示第几帧
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
* @param defaultFrame 图块的默认帧数
|
||||||
|
*/
|
||||||
|
setDefaultFrame(identifier: number, defaultFrame: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图块的默认帧数,-1 表示正常动画,非负整数表示默认使用指定帧数,除非单独指定
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getDefaultFrame(identifier: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存某个 tileset,当需要缓存多个时,请使用 {@link cacheTilesetList} 方法
|
||||||
|
* @param identifier tileset 的标识符,即图块数字
|
||||||
|
*/
|
||||||
|
cacheTileset(identifier: number): ITexture | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存一系列 tileset
|
||||||
|
* @param identifierList 标识符列表,即图块数字列表
|
||||||
|
*/
|
||||||
|
cacheTilesetList(
|
||||||
|
identifierList: Iterable<number>
|
||||||
|
): Iterable<ITexture | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存某个自动元件,当需要缓存多个时,请使用 {@link cacheAutotileList} 方法
|
||||||
|
* @param identifier 自动元件标识符,即图块数字
|
||||||
|
*/
|
||||||
|
cacheAutotile(identifier: number): ITexture | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存一系列自动元件
|
||||||
|
* @param identifierList 自动元件标识符列表,即图块数字列表
|
||||||
|
*/
|
||||||
|
cacheAutotileList(
|
||||||
|
identifierList: Iterable<number>
|
||||||
|
): Iterable<ITexture | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把常用素材打包成为图集形式供后续使用
|
||||||
|
*/
|
||||||
|
buildAssets(): Iterable<IMaterialAssetData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定贴图打包进图集
|
||||||
|
* @param texture 贴图对象
|
||||||
|
*/
|
||||||
|
buildToAsset(texture: ITexture): IMaterialAssetData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一系列贴图打包进贴图对象
|
||||||
|
* @param texture 贴图列表
|
||||||
|
*/
|
||||||
|
buildListToAsset(texture: Iterable<ITexture>): Iterable<IMaterialAssetData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块标识符在图集中获取对应的可渲染对象
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getRenderable(identifier: number): ITextureRenderable | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块别名在图集中获取对应的可渲染对象
|
||||||
|
* @param alias 图块的别名,即图块的 id
|
||||||
|
*/
|
||||||
|
getRenderableByAlias(alias: string): ITextureRenderable | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块别名获取图块标识符,即图块数字
|
||||||
|
* @param alias 图块别名,即图块的 id
|
||||||
|
*/
|
||||||
|
getIdentifierByAlias(alias: string): number | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图块标识符获取图块别名,即图块的 id
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
*/
|
||||||
|
getAliasByIdentifier(identifier: number): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置一个图块的 `bigImage` 贴图,即大怪物贴图,但不止怪物能用
|
||||||
|
* @param identifier 图块标识符,即图块数字
|
||||||
|
* @param image `bigImage` 对应的贴图对象
|
||||||
|
* @param frames `bigImage` 的帧数,即贴图有多少帧
|
||||||
|
*/
|
||||||
|
setBigImage(
|
||||||
|
identifier: number,
|
||||||
|
image: ITexture,
|
||||||
|
frames: number
|
||||||
|
): IBigImageReturn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前的所有图集中是否包含指定的贴图对象
|
||||||
|
* @param texture 贴图对象
|
||||||
|
*/
|
||||||
|
assetContainsTexture(texture: ITexture): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定贴图对象所属的图集索引
|
||||||
|
* @param texture 贴图对象
|
||||||
|
*/
|
||||||
|
getTextureAsset(texture: ITexture): number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAssetBuilder {
|
||||||
|
/**
|
||||||
|
* 将图集打包器输出至指定贴图存储对象,只能输出到一个存储对象中,设置多个仅最后一个有效
|
||||||
|
* @param store 贴图存储对象
|
||||||
|
*/
|
||||||
|
pipe(store: ITextureStore): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加贴图对象至打包器
|
||||||
|
* @param texture 贴图对象
|
||||||
|
* @returns 当前打包的贴图对象对应的组合数据
|
||||||
|
*/
|
||||||
|
addTexture(texture: ITexture): ITextureComposedData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个贴图对象列表至打包器
|
||||||
|
* @param texture 贴图对象列表
|
||||||
|
* @returns 当前打包的贴图列表的组合数据,每一项代表一个图集,只包含使用到的图集,之前已经打包完成的将不会在列表内
|
||||||
|
*/
|
||||||
|
addTextureList(texture: Iterable<ITexture>): Iterable<ITextureComposedData>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可追踪贴图对象
|
||||||
|
*/
|
||||||
|
tracked(): ITrackedAssetData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束此打包器
|
||||||
|
*/
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITrackedAssetData extends IDirtyTracker<Set<number>> {
|
||||||
|
/** 图像源列表 */
|
||||||
|
readonly sourceList: Map<number, ImageBitmap>;
|
||||||
|
/**
|
||||||
|
* 贴图引用跳接,`ImageBitmap` 的传递性能远好于其他类型,而贴图图集为了能够动态增加内容会使用画布类型,
|
||||||
|
* 因此需要把贴图生成为额外的 `ImageBitmap`,并提供引用跳接映射。值代表在 `sourceList` 中的索引。
|
||||||
|
*/
|
||||||
|
readonly skipRef: Map<SizedCanvasImageSource, number>;
|
||||||
|
/** 贴图数据 */
|
||||||
|
readonly materials: IMaterialGetter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消使用此图集,释放相关资源
|
||||||
|
*/
|
||||||
|
close(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待所有打包操作结束
|
||||||
|
*/
|
||||||
|
then(): Promise<void>;
|
||||||
|
}
|
||||||
47
packages-user/client-base/src/material/utils.ts
Normal file
47
packages-user/client-base/src/material/utils.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ITexture } from '@motajs/render-assets';
|
||||||
|
import { BlockCls } from './types';
|
||||||
|
|
||||||
|
export function getClsByString(cls: Cls): BlockCls {
|
||||||
|
switch (cls) {
|
||||||
|
case 'terrains':
|
||||||
|
return BlockCls.Terrains;
|
||||||
|
case 'animates':
|
||||||
|
return BlockCls.Animates;
|
||||||
|
case 'autotile':
|
||||||
|
return BlockCls.Autotile;
|
||||||
|
case 'enemys':
|
||||||
|
return BlockCls.Enemys;
|
||||||
|
case 'items':
|
||||||
|
return BlockCls.Items;
|
||||||
|
case 'npcs':
|
||||||
|
return BlockCls.Npcs;
|
||||||
|
case 'npc48':
|
||||||
|
return BlockCls.Npc48;
|
||||||
|
case 'enemy48':
|
||||||
|
return BlockCls.Enemy48;
|
||||||
|
case 'tileset':
|
||||||
|
return BlockCls.Tileset;
|
||||||
|
default:
|
||||||
|
return BlockCls.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextureFrame(cls: BlockCls, texture: ITexture) {
|
||||||
|
switch (cls) {
|
||||||
|
case BlockCls.Animates:
|
||||||
|
case BlockCls.Enemy48:
|
||||||
|
case BlockCls.Npc48:
|
||||||
|
return 4;
|
||||||
|
case BlockCls.Autotile:
|
||||||
|
return texture.width === 384 ? 4 : 1;
|
||||||
|
case BlockCls.Enemys:
|
||||||
|
case BlockCls.Npcs:
|
||||||
|
return 2;
|
||||||
|
case BlockCls.Items:
|
||||||
|
case BlockCls.Terrains:
|
||||||
|
case BlockCls.Tileset:
|
||||||
|
return 1;
|
||||||
|
case BlockCls.Unknown:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@
|
|||||||
"@motajs/client-base": "workspace:*",
|
"@motajs/client-base": "workspace:*",
|
||||||
"@motajs/common": "workspace:*",
|
"@motajs/common": "workspace:*",
|
||||||
"@motajs/render": "workspace:*",
|
"@motajs/render": "workspace:*",
|
||||||
|
"@motajs/render-assets": "workspace:*",
|
||||||
"@motajs/render-core": "workspace:*",
|
"@motajs/render-core": "workspace:*",
|
||||||
"@motajs/legacy-common": "workspace:*",
|
"@motajs/legacy-common": "workspace:*",
|
||||||
"@motajs/legacy-ui": "workspace:*",
|
"@motajs/legacy-ui": "workspace:*",
|
||||||
|
|||||||
@ -76,6 +76,11 @@ gameKey
|
|||||||
name: '快捷商店',
|
name: '快捷商店',
|
||||||
defaults: KeyCode.KeyV
|
defaults: KeyCode.KeyV
|
||||||
})
|
})
|
||||||
|
.register({
|
||||||
|
id: 'statistics',
|
||||||
|
name: '数据统计',
|
||||||
|
defaults: KeyCode.KeyB
|
||||||
|
})
|
||||||
.register({
|
.register({
|
||||||
id: 'viewMap_1',
|
id: 'viewMap_1',
|
||||||
name: '浏览地图_1',
|
name: '浏览地图_1',
|
||||||
|
|||||||
@ -17,8 +17,7 @@ export function patchAudio() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
patch.add('playBgm', function (bgm, startTime) {
|
patch.add('playBgm', function (bgm, startTime) {
|
||||||
const name = core.getMappedName(bgm) as BgmIds;
|
play(bgm, startTime);
|
||||||
play(name, startTime);
|
|
||||||
});
|
});
|
||||||
patch.add('pauseBgm', function () {
|
patch.add('pauseBgm', function () {
|
||||||
pause();
|
pause();
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import {
|
|||||||
saveLoad,
|
saveLoad,
|
||||||
openSettings,
|
openSettings,
|
||||||
openViewMap,
|
openViewMap,
|
||||||
openReplay
|
openReplay,
|
||||||
|
openStatistics
|
||||||
} from './ui';
|
} from './ui';
|
||||||
import { ElementLocator } from '@motajs/render-core';
|
import { ElementLocator } from '@motajs/render-core';
|
||||||
|
|
||||||
@ -15,6 +16,9 @@ export function createAction() {
|
|||||||
.realize('save', () => {
|
.realize('save', () => {
|
||||||
saveSave(mainUIController, FULL_LOC);
|
saveSave(mainUIController, FULL_LOC);
|
||||||
})
|
})
|
||||||
|
.realize('statistics', () => {
|
||||||
|
openStatistics(mainUIController);
|
||||||
|
})
|
||||||
.realize('load', () => {
|
.realize('load', () => {
|
||||||
saveLoad(mainUIController, FULL_LOC);
|
saveLoad(mainUIController, FULL_LOC);
|
||||||
})
|
})
|
||||||
|
|||||||
22
packages-user/client-modules/src/render/commonIns.ts
Normal file
22
packages-user/client-modules/src/render/commonIns.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { state } from '@user/data-state';
|
||||||
|
import { materials } from '@user/client-base';
|
||||||
|
import { MapRenderer, MapExtensionManager } from './map';
|
||||||
|
|
||||||
|
/** 主地图渲染器,用于渲染游戏画面 */
|
||||||
|
export const mainMapRenderer = new MapRenderer(materials, state.layer);
|
||||||
|
/** 主地图渲染器拓展 */
|
||||||
|
export const mainMapExtension = new MapExtensionManager(mainMapRenderer);
|
||||||
|
/** 副地图渲染器,用于渲染缩略图、浏览地图等 */
|
||||||
|
// export const expandMapRenderer = new MapRenderer(materials, state.layer);
|
||||||
|
|
||||||
|
export async function createMainExtension() {
|
||||||
|
// 算是一种妥协吧,等之后加载系统重构之后应该会清晰很多
|
||||||
|
await materials.trackedAsset.then();
|
||||||
|
|
||||||
|
mainMapRenderer.useAsset(materials.trackedAsset);
|
||||||
|
const layer = state.layer.getLayerByAlias('event');
|
||||||
|
if (layer) {
|
||||||
|
mainMapExtension.addHero(state.hero, layer);
|
||||||
|
mainMapExtension.addDoor(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,8 @@ import {
|
|||||||
import { SetupComponentOptions } from '@motajs/system-ui';
|
import { SetupComponentOptions } from '@motajs/system-ui';
|
||||||
import { texture } from '../elements';
|
import { texture } from '../elements';
|
||||||
|
|
||||||
|
// todo: TextContent 应该改成渲染元素?
|
||||||
|
|
||||||
//#region TextContent
|
//#region TextContent
|
||||||
|
|
||||||
export interface TextContentProps
|
export interface TextContentProps
|
||||||
|
|||||||
@ -817,7 +817,7 @@ export class TextContentParser {
|
|||||||
return pointer;
|
return pointer;
|
||||||
}
|
}
|
||||||
const time = parseInt(param);
|
const time = parseInt(param);
|
||||||
this.addWaitRenderable(end, time);
|
this.addWaitRenderable(end + 1, time);
|
||||||
return end;
|
return end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -830,19 +830,19 @@ export class TextContentParser {
|
|||||||
}
|
}
|
||||||
if (/^\d+$/.test(param)) {
|
if (/^\d+$/.test(param)) {
|
||||||
const num = Number(param);
|
const num = Number(param);
|
||||||
this.addIconRenderable(end, num as AllNumbers);
|
this.addIconRenderable(end + 1, num as AllNumbers);
|
||||||
} else {
|
} else {
|
||||||
if (/^X\d+$/.test(param)) {
|
if (/^X\d+$/.test(param)) {
|
||||||
// 额外素材
|
// 额外素材
|
||||||
const num = Number(param.slice(1));
|
const num = Number(param.slice(1));
|
||||||
this.addIconRenderable(end, num as AllNumbers);
|
this.addIconRenderable(end + 1, num as AllNumbers);
|
||||||
} else {
|
} else {
|
||||||
const num = texture.idNumberMap[param as AllIds];
|
const num = texture.idNumberMap[param as AllIds];
|
||||||
if (num === void 0) {
|
if (num === void 0) {
|
||||||
logger.warn(59, param);
|
logger.warn(59, param);
|
||||||
return end;
|
return end;
|
||||||
}
|
}
|
||||||
this.addIconRenderable(end, num);
|
this.addIconRenderable(end + 1, num);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return end;
|
return end;
|
||||||
@ -960,6 +960,7 @@ export class TextContentParser {
|
|||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
// 在这里预先将换行处理为多个 node,会比在分行时再处理更方便
|
// 在这里预先将换行处理为多个 node,会比在分行时再处理更方便
|
||||||
|
pointer++;
|
||||||
this.addTextNode(pointer + 1, true);
|
this.addTextNode(pointer + 1, true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -985,7 +986,9 @@ export class TextContentParser {
|
|||||||
this.resolved += char;
|
this.resolved += char;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addTextNode(text.length, false);
|
if (this.nodePointer < text.length) {
|
||||||
|
this.addTextNode(text.length, false);
|
||||||
|
}
|
||||||
return this.splitLines(width);
|
return this.splitLines(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import {
|
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
||||||
MotaOffscreenCanvas2D,
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
SizedCanvasImageSource
|
|
||||||
} from '@motajs/render-core';
|
|
||||||
|
|
||||||
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
|
// 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading)
|
||||||
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
// 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { RenderAdapter, SizedCanvasImageSource } from '@motajs/render-core';
|
import { RenderAdapter } from '@motajs/render-core';
|
||||||
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
|
import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer';
|
||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import { createViewport } from './viewport';
|
|||||||
import { Icon, Winskin } from './misc';
|
import { Icon, Winskin } from './misc';
|
||||||
import { Animate } from './animate';
|
import { Animate } from './animate';
|
||||||
import { createItemDetail } from './itemDetail';
|
import { createItemDetail } from './itemDetail';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { MapRender, MapRenderer } from '../map';
|
||||||
|
import { state } from '@user/data-state';
|
||||||
|
import { materials } from '@user/client-base';
|
||||||
|
|
||||||
export function createElements() {
|
export function createElements() {
|
||||||
createCache();
|
createCache();
|
||||||
@ -66,6 +70,31 @@ export function createElements() {
|
|||||||
return new Animate();
|
return new Animate();
|
||||||
});
|
});
|
||||||
tagMap.register('icon', standardElementNoCache(Icon));
|
tagMap.register('icon', standardElementNoCache(Icon));
|
||||||
|
tagMap.register('map-render', (_0, _1, props) => {
|
||||||
|
if (!props) {
|
||||||
|
logger.error(42);
|
||||||
|
return new MapRender(
|
||||||
|
state.layer,
|
||||||
|
new MapRenderer(materials, state.layer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { layerState, renderer } = props;
|
||||||
|
if (!layerState) {
|
||||||
|
logger.error(42, 'layerState');
|
||||||
|
return new MapRender(
|
||||||
|
state.layer,
|
||||||
|
new MapRenderer(materials, state.layer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!renderer) {
|
||||||
|
logger.error(42, 'renderer');
|
||||||
|
return new MapRender(
|
||||||
|
state.layer,
|
||||||
|
new MapRenderer(materials, state.layer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new MapRender(layerState, renderer);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './animate';
|
export * from './animate';
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import {
|
|||||||
RenderItem,
|
RenderItem,
|
||||||
RenderItemPosition,
|
RenderItemPosition,
|
||||||
MotaOffscreenCanvas2D,
|
MotaOffscreenCanvas2D,
|
||||||
Transform,
|
Transform
|
||||||
SizedCanvasImageSource
|
|
||||||
} from '@motajs/render-core';
|
} from '@motajs/render-core';
|
||||||
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { RenderableData, AutotileRenderable, texture } from './cache';
|
import { RenderableData, AutotileRenderable, texture } from './cache';
|
||||||
import { IAnimateFrame, renderEmits } from './frame';
|
import { IAnimateFrame, renderEmits } from './frame';
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||||
import { CanvasStyle, Transform } from '@motajs/render-core';
|
import { ERenderItemEvent, Transform } from '@motajs/render-core';
|
||||||
|
import { CanvasStyle } from '@motajs/render-assets';
|
||||||
import {
|
import {
|
||||||
ILayerGroupRenderExtends,
|
ILayerGroupRenderExtends,
|
||||||
FloorLayer,
|
FloorLayer,
|
||||||
@ -10,6 +11,8 @@ import {
|
|||||||
import { EAnimateEvent } from './animate';
|
import { EAnimateEvent } from './animate';
|
||||||
import { EIconEvent, EWinskinEvent } from './misc';
|
import { EIconEvent, EWinskinEvent } from './misc';
|
||||||
import { IEnemyCollection } from '@motajs/types';
|
import { IEnemyCollection } from '@motajs/types';
|
||||||
|
import { ILayerState } from '@user/data-state';
|
||||||
|
import { IMapRenderer } from '../map';
|
||||||
|
|
||||||
export interface AnimateProps extends BaseProps {}
|
export interface AnimateProps extends BaseProps {}
|
||||||
|
|
||||||
@ -59,6 +62,11 @@ export interface LayerProps extends BaseProps {
|
|||||||
ex?: readonly ILayerRenderExtends[];
|
ex?: readonly ILayerRenderExtends[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MapRenderProps extends BaseProps {
|
||||||
|
layerState: ILayerState;
|
||||||
|
renderer: IMapRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'vue/jsx-runtime' {
|
declare module 'vue/jsx-runtime' {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
export interface IntrinsicElements {
|
export interface IntrinsicElements {
|
||||||
@ -67,6 +75,7 @@ declare module 'vue/jsx-runtime' {
|
|||||||
animation: TagDefine<AnimateProps, EAnimateEvent>;
|
animation: TagDefine<AnimateProps, EAnimateEvent>;
|
||||||
icon: TagDefine<IconProps, EIconEvent>;
|
icon: TagDefine<IconProps, EIconEvent>;
|
||||||
winskin: TagDefine<WinskinProps, EWinskinEvent>;
|
winskin: TagDefine<WinskinProps, EWinskinEvent>;
|
||||||
|
'map-render': TagDefine<MapRenderProps, ERenderItemEvent>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { EffectBase } from './base';
|
|||||||
|
|
||||||
export class Image3DEffect
|
export class Image3DEffect
|
||||||
extends EffectBase<void>
|
extends EffectBase<void>
|
||||||
implements ITransformUpdatable
|
implements ITransformUpdatable<Transform3D>
|
||||||
{
|
{
|
||||||
/** 图片的模型变换 */
|
/** 图片的模型变换 */
|
||||||
private model: Transform3D = new Transform3D();
|
private model: Transform3D = new Transform3D();
|
||||||
@ -65,7 +65,7 @@ export class Image3DEffect
|
|||||||
* @param model 模型变换
|
* @param model 模型变换
|
||||||
*/
|
*/
|
||||||
setModel(model: Transform3D) {
|
setModel(model: Transform3D) {
|
||||||
this.model.bind();
|
this.model.unbind(this);
|
||||||
this.model = model;
|
this.model = model;
|
||||||
model.bind(this);
|
model.bind(this);
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export class Image3DEffect
|
|||||||
* @param model 视角变换
|
* @param model 视角变换
|
||||||
*/
|
*/
|
||||||
setView(view: Transform3D) {
|
setView(view: Transform3D) {
|
||||||
this.view.bind();
|
this.view.unbind(this);
|
||||||
this.view = view;
|
this.view = view;
|
||||||
view.bind(this);
|
view.bind(this);
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ export class Image3DEffect
|
|||||||
* @param model 投影变换
|
* @param model 投影变换
|
||||||
*/
|
*/
|
||||||
setProj(proj: Transform3D) {
|
setProj(proj: Transform3D) {
|
||||||
this.proj.bind();
|
this.proj.unbind(this);
|
||||||
this.proj = proj;
|
this.proj = proj;
|
||||||
proj.bind(this);
|
proj.bind(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,21 +9,19 @@ import { createAction } from './action';
|
|||||||
import { sceneController } from './scene';
|
import { sceneController } from './scene';
|
||||||
import { GameTitleUI } from './ui/title';
|
import { GameTitleUI } from './ui/title';
|
||||||
import { createWeather } from './weather';
|
import { createWeather } from './weather';
|
||||||
|
import { createMainExtension } from './commonIns';
|
||||||
|
|
||||||
export function createGameRenderer() {
|
export function createGameRenderer() {
|
||||||
const App = defineComponent(_props => {
|
const App = defineComponent(_props => {
|
||||||
return () => (
|
return () => (
|
||||||
<container noanti width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
||||||
{sceneController.render()}
|
{sceneController.render()}
|
||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
mainRenderer.setAntiAliasing(false);
|
|
||||||
mainRenderer.hide();
|
mainRenderer.hide();
|
||||||
createApp(App).mount(mainRenderer);
|
createApp(App).mount(mainRenderer);
|
||||||
|
|
||||||
console.log(mainRenderer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRender() {
|
export function createRender() {
|
||||||
@ -32,11 +30,15 @@ export function createRender() {
|
|||||||
createAction();
|
createAction();
|
||||||
createWeather();
|
createWeather();
|
||||||
|
|
||||||
loading.on('loaded', () => {
|
loading.once('loaded', () => {
|
||||||
sceneController.open(GameTitleUI, {});
|
sceneController.open(GameTitleUI, {});
|
||||||
mainRenderer.show();
|
mainRenderer.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loading.once('assetBuilt', () => {
|
||||||
|
createMainExtension();
|
||||||
|
});
|
||||||
|
|
||||||
hook.on('restart', () => {
|
hook.on('restart', () => {
|
||||||
sceneController.closeAll();
|
sceneController.closeAll();
|
||||||
sceneController.open(GameTitleUI, {});
|
sceneController.open(GameTitleUI, {});
|
||||||
@ -45,6 +47,7 @@ export function createRender() {
|
|||||||
Font.setDefaults(DEFAULT_FONT);
|
Font.setDefaults(DEFAULT_FONT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './commonIns';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './elements';
|
export * from './elements';
|
||||||
export * from './fx';
|
export * from './fx';
|
||||||
|
|||||||
351
packages-user/client-modules/src/render/map/block.ts
Normal file
351
packages-user/client-modules/src/render/map/block.ts
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
IBlockData,
|
||||||
|
IBlockIndex,
|
||||||
|
IBlockInfo,
|
||||||
|
IBlockSplitter,
|
||||||
|
IBlockSplitterConfig
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export class BlockSplitter<T> implements IBlockSplitter<T> {
|
||||||
|
blockWidth: number = 0;
|
||||||
|
blockHeight: number = 0;
|
||||||
|
dataWidth: number = 0;
|
||||||
|
dataHeight: number = 0;
|
||||||
|
width: number = 1;
|
||||||
|
height: number = 1;
|
||||||
|
|
||||||
|
/** 分块映射 */
|
||||||
|
readonly blockMap: Map<number, IBlockData<T>> = new Map();
|
||||||
|
|
||||||
|
/** 数据宽度配置 */
|
||||||
|
private splitDataWidth: number = 0;
|
||||||
|
/** 数据高度配置 */
|
||||||
|
private splitDataHeight: number = 0;
|
||||||
|
/** 单个分块的宽度配置 */
|
||||||
|
private splitBlockWidth: number = 0;
|
||||||
|
/** 单个分块的高度配置 */
|
||||||
|
private splitBlockHeight: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查坐标范围
|
||||||
|
* @param x 分块横坐标
|
||||||
|
* @param y 分块纵坐标
|
||||||
|
*/
|
||||||
|
private checkLocRange(x: number, y: number) {
|
||||||
|
return x >= 0 && y >= 0 && x < this.width && y < this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockByLoc(x: number, y: number): IBlockData<T> | null {
|
||||||
|
if (!this.checkLocRange(x, y)) return null;
|
||||||
|
const index = y * this.width + x;
|
||||||
|
return this.blockMap.get(index) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockByIndex(index: number): IBlockData<T> | null {
|
||||||
|
return this.blockMap.get(index) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null {
|
||||||
|
if (!this.checkLocRange(x, y)) return null;
|
||||||
|
const index = y * this.width + x;
|
||||||
|
const block = this.blockMap.get(index);
|
||||||
|
if (!block) return null;
|
||||||
|
block.data = data;
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlockByIndex(data: T, index: number): IBlockData<T> | null {
|
||||||
|
const block = this.blockMap.get(index);
|
||||||
|
if (!block) return null;
|
||||||
|
block.data = data;
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void> {
|
||||||
|
if (!this.checkLocRange(x, y)) return;
|
||||||
|
const index = y * this.width + x;
|
||||||
|
yield* this.iterateBlockByIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateBlockByIndex(index: number): Generator<IBlockIndex, void> {
|
||||||
|
const block = this.blockMap.get(index);
|
||||||
|
if (!block) return;
|
||||||
|
const startX = block.x * this.blockWidth;
|
||||||
|
const startY = block.y * this.blockHeight;
|
||||||
|
const endX = startX + block.width;
|
||||||
|
const endY = startY + block.height;
|
||||||
|
for (let ny = startY; ny < endY; ny++) {
|
||||||
|
for (let nx = startX; nx < endX; nx++) {
|
||||||
|
const index: IBlockIndex = {
|
||||||
|
x: nx,
|
||||||
|
y: ny,
|
||||||
|
dataX: nx * this.blockWidth,
|
||||||
|
dataY: ny * this.blockHeight,
|
||||||
|
index: ny * this.dataWidth + nx
|
||||||
|
};
|
||||||
|
yield index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateBlockByIndices(
|
||||||
|
indices: Iterable<number>
|
||||||
|
): Generator<IBlockIndex, void> {
|
||||||
|
for (const index of indices) {
|
||||||
|
yield* this.iterateBlockByIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iterateBlocks(): Iterable<IBlockData<T>> {
|
||||||
|
return this.blockMap.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateBlocksOfDataArea(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Generator<IBlockData<T>> {
|
||||||
|
const r = this.width - 1;
|
||||||
|
const b = this.height - 1;
|
||||||
|
const rx = x + width;
|
||||||
|
const by = y + height;
|
||||||
|
const left = clamp(Math.floor(x / this.blockWidth), 0, r);
|
||||||
|
const top = clamp(Math.floor(y / this.blockHeight), 0, b);
|
||||||
|
const right = clamp(Math.floor(rx / this.blockWidth), 0, r);
|
||||||
|
const bottom = clamp(Math.floor(by / this.blockHeight), 0, b);
|
||||||
|
for (let ny = left; ny <= right; ny++) {
|
||||||
|
for (let nx = top; nx <= bottom; nx++) {
|
||||||
|
const index = ny * this.width + nx;
|
||||||
|
const block = this.blockMap.get(index);
|
||||||
|
if (!block) continue;
|
||||||
|
yield block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexByLoc(x: number, y: number): number {
|
||||||
|
if (!this.checkLocRange(x, y)) return -1;
|
||||||
|
return y * this.width + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocByIndex(index: number): Loc | null {
|
||||||
|
if (index >= this.width * this.height) return null;
|
||||||
|
return {
|
||||||
|
x: index % this.width,
|
||||||
|
y: Math.floor(index / this.width)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
|
||||||
|
const res: number[] = [];
|
||||||
|
for (const { x, y } of list) {
|
||||||
|
res.push(this.getIndexByLoc(x, y));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null> {
|
||||||
|
const res: (Loc | null)[] = [];
|
||||||
|
for (const index of list) {
|
||||||
|
res.push(this.getLocByIndex(index));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null {
|
||||||
|
const bx = Math.floor(x / this.blockWidth);
|
||||||
|
const by = Math.floor(y / this.blockHeight);
|
||||||
|
if (!this.checkLocRange(bx, by)) return null;
|
||||||
|
const index = by * this.width + bx;
|
||||||
|
return this.blockMap.get(index) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockByDataIndex(index: number): IBlockData<T> | null {
|
||||||
|
const x = index % this.dataWidth;
|
||||||
|
const y = Math.floor(index / this.dataWidth);
|
||||||
|
return this.getBlockByDataLoc(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndicesByDataLocList(list: Iterable<Loc>): Set<number> {
|
||||||
|
const res = new Set<number>();
|
||||||
|
for (const { x, y } of list) {
|
||||||
|
const bx = Math.floor(x / this.blockWidth);
|
||||||
|
const by = Math.floor(y / this.blockHeight);
|
||||||
|
if (!this.checkLocRange(bx, by)) continue;
|
||||||
|
res.add(bx + by * this.width);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndicesByDataIndices(list: Iterable<number>): Set<number> {
|
||||||
|
const res = new Set<number>();
|
||||||
|
for (const index of list) {
|
||||||
|
const x = index % this.dataWidth;
|
||||||
|
const y = Math.floor(index / this.dataWidth);
|
||||||
|
const bx = Math.floor(x / this.blockWidth);
|
||||||
|
const by = Math.floor(y / this.blockHeight);
|
||||||
|
if (!this.checkLocRange(bx, by)) continue;
|
||||||
|
res.add(bx + by * this.width);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>> {
|
||||||
|
const res = new Set<IBlockData<T>>();
|
||||||
|
for (const { x, y } of list) {
|
||||||
|
const bx = Math.floor(x / this.blockWidth);
|
||||||
|
const by = Math.floor(y / this.blockHeight);
|
||||||
|
if (!this.checkLocRange(bx, by)) continue;
|
||||||
|
const index = bx + by * this.width;
|
||||||
|
const data = this.blockMap.get(index);
|
||||||
|
if (data) res.add(data);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>> {
|
||||||
|
const res = new Set<IBlockData<T>>();
|
||||||
|
for (const index of list) {
|
||||||
|
const x = index % this.dataWidth;
|
||||||
|
const y = Math.floor(index / this.dataWidth);
|
||||||
|
const bx = Math.floor(x / this.blockWidth);
|
||||||
|
const by = Math.floor(y / this.blockHeight);
|
||||||
|
if (!this.checkLocRange(bx, by)) continue;
|
||||||
|
const blockIndex = bx + by * this.width;
|
||||||
|
const data = this.blockMap.get(blockIndex);
|
||||||
|
if (data) res.add(data);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
configSplitter(config: IBlockSplitterConfig): void {
|
||||||
|
this.splitDataWidth = config.dataWidth;
|
||||||
|
this.splitDataHeight = config.dataHeight;
|
||||||
|
this.splitBlockWidth = config.blockWidth;
|
||||||
|
this.splitBlockHeight = config.blockHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapBlock(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
realWidth: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
fn: (block: IBlockInfo) => T
|
||||||
|
) {
|
||||||
|
const index = y * realWidth + x;
|
||||||
|
const block: IBlockInfo = {
|
||||||
|
index,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
dataX: x * this.blockWidth,
|
||||||
|
dataY: y * this.blockHeight,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
};
|
||||||
|
const data = fn(block);
|
||||||
|
const blockData = new SplittedBlockData(this, block, data);
|
||||||
|
this.blockMap.set(index, blockData);
|
||||||
|
}
|
||||||
|
|
||||||
|
splitBlocks(mapFn: (block: IBlockInfo) => T): void {
|
||||||
|
this.blockMap.clear();
|
||||||
|
this.blockWidth = this.splitBlockWidth;
|
||||||
|
this.blockHeight = this.splitBlockHeight;
|
||||||
|
this.dataWidth = this.splitDataWidth;
|
||||||
|
this.dataHeight = this.splitDataHeight;
|
||||||
|
const restX = this.splitDataWidth % this.splitBlockWidth;
|
||||||
|
const restY = this.splitDataHeight % this.splitBlockHeight;
|
||||||
|
const width = Math.floor(this.splitDataWidth / this.splitBlockWidth);
|
||||||
|
const height = Math.floor(this.splitDataHeight / this.splitBlockHeight);
|
||||||
|
const hasXRest = restX > 0;
|
||||||
|
const hasYRest = restY > 0;
|
||||||
|
const realWidth = hasXRest ? width + 1 : width;
|
||||||
|
const bw = this.blockWidth;
|
||||||
|
const bh = this.blockHeight;
|
||||||
|
this.width = realWidth;
|
||||||
|
this.height = hasYRest ? height + 1 : height;
|
||||||
|
for (let ny = 0; ny < height; ny++) {
|
||||||
|
for (let nx = 0; nx < width; nx++) {
|
||||||
|
this.mapBlock(nx, ny, realWidth, bw, bh, mapFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasXRest) {
|
||||||
|
for (let ny = 0; ny < height; ny++) {
|
||||||
|
this.mapBlock(width, ny, realWidth, restX, bh, mapFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasYRest) {
|
||||||
|
for (let nx = 0; nx < width; nx++) {
|
||||||
|
this.mapBlock(nx, height, realWidth, bw, restY, mapFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasXRest && hasYRest) {
|
||||||
|
this.mapBlock(width, height, realWidth, restX, restY, mapFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SplittedBlockData<T> implements IBlockData<T> {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
dataX: number;
|
||||||
|
dataY: number;
|
||||||
|
index: number;
|
||||||
|
data: T;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly splitter: BlockSplitter<T>,
|
||||||
|
info: IBlockInfo,
|
||||||
|
data: T
|
||||||
|
) {
|
||||||
|
this.width = info.width;
|
||||||
|
this.height = info.height;
|
||||||
|
this.x = info.x;
|
||||||
|
this.y = info.y;
|
||||||
|
this.dataX = info.dataX;
|
||||||
|
this.dataY = info.dataY;
|
||||||
|
this.index = info.index;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
left(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x - 1, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
right(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x + 1, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
up(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x, this.y - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
down(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x, this.y + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
leftUp(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x - 1, this.y - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
leftDown(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x - 1, this.y + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
rightUp(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x + 1, this.y - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
rightDown(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByLoc(this.x + 1, this.y + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
next(): IBlockData<T> | null {
|
||||||
|
return this.splitter.getBlockByIndex(this.index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages-user/client-modules/src/render/map/constant.ts
Normal file
2
packages-user/client-modules/src/render/map/constant.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** 单个图块的实例化数据数量 */
|
||||||
|
export const INSTANCED_COUNT = 4 + 4 + 4 + 4;
|
||||||
70
packages-user/client-modules/src/render/map/element.ts
Normal file
70
packages-user/client-modules/src/render/map/element.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render-core';
|
||||||
|
import { ILayerState } from '@user/data-state';
|
||||||
|
import { IMapRenderer } from './types';
|
||||||
|
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||||
|
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../shared';
|
||||||
|
|
||||||
|
export class MapRender extends RenderItem {
|
||||||
|
/**
|
||||||
|
* @param layerState 地图状态对象
|
||||||
|
* @param renderer 地图渲染器对象
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly layerState: ILayerState,
|
||||||
|
readonly renderer: IMapRenderer
|
||||||
|
) {
|
||||||
|
super('static', false, false);
|
||||||
|
|
||||||
|
renderer.setLayerState(layerState);
|
||||||
|
renderer.setCellSize(CELL_WIDTH, CELL_HEIGHT);
|
||||||
|
renderer.setRenderSize(MAP_WIDTH, MAP_HEIGHT);
|
||||||
|
|
||||||
|
this.delegateTicker(time => {
|
||||||
|
this.renderer.tick(time);
|
||||||
|
if (this.renderer.needUpdate()) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sizeGL(width: number, height: number) {
|
||||||
|
const ratio = this.highResolution ? devicePixelRatio : 1;
|
||||||
|
const scale = ratio * this.scale;
|
||||||
|
const w = width * scale;
|
||||||
|
const h = height * scale;
|
||||||
|
this.renderer.setCanvasSize(w, h);
|
||||||
|
this.renderer.setViewport(0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(scale: number): void {
|
||||||
|
super.onResize(scale);
|
||||||
|
this.sizeGL(this.width, this.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
size(width: number, height: number): void {
|
||||||
|
super.size(width, height);
|
||||||
|
this.sizeGL(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(canvas: MotaOffscreenCanvas2D): void {
|
||||||
|
this.renderer.clear(true, true);
|
||||||
|
const map = this.renderer.render();
|
||||||
|
canvas.ctx.drawImage(map, 0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
patchProp(
|
||||||
|
key: string,
|
||||||
|
prevValue: any,
|
||||||
|
nextValue: any,
|
||||||
|
namespace?: ElementNamespace,
|
||||||
|
parentComponent?: ComponentInternalInstance | null
|
||||||
|
): void {
|
||||||
|
switch (key) {
|
||||||
|
case 'layerState': {
|
||||||
|
this.renderer.setLayerState(nextValue);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerHookController,
|
||||||
|
IMapLayerHooks
|
||||||
|
} from '@user/data-state';
|
||||||
|
import { IMapDoorRenderer } from './types';
|
||||||
|
import { IMapRenderer } from '../types';
|
||||||
|
import { sleep } from 'mutate-animate';
|
||||||
|
import { DOOR_ANIMATE_INTERVAL } from '../../shared';
|
||||||
|
|
||||||
|
export class MapDoorRenderer implements IMapDoorRenderer {
|
||||||
|
/** 钩子控制器 */
|
||||||
|
readonly controller: IMapLayerHookController;
|
||||||
|
|
||||||
|
/** 动画间隔 */
|
||||||
|
private interval: number = DOOR_ANIMATE_INTERVAL;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly renderer: IMapRenderer,
|
||||||
|
readonly layer: IMapLayer
|
||||||
|
) {
|
||||||
|
this.controller = layer.addHook(new MapDoorHook(this));
|
||||||
|
this.controller.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnimateInterval(interval: number): void {
|
||||||
|
this.interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openDoor(x: number, y: number): Promise<void> {
|
||||||
|
const status = this.renderer.getBlockStatus(this.layer, x, y);
|
||||||
|
if (!status) return;
|
||||||
|
const array = this.layer.getMapRef().array;
|
||||||
|
const index = y * this.layer.width + x;
|
||||||
|
const num = array[index];
|
||||||
|
const data = this.renderer.manager.getIfBigImage(num);
|
||||||
|
if (!data) return;
|
||||||
|
const frames = data.frames;
|
||||||
|
for (let i = 0; i < frames; i++) {
|
||||||
|
status.useSpecifiedFrame(i);
|
||||||
|
await sleep(this.interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeDoor(num: number, x: number, y: number): Promise<void> {
|
||||||
|
const data = this.renderer.manager.getIfBigImage(num);
|
||||||
|
if (!data) return;
|
||||||
|
const moving = this.renderer.addMovingBlock(this.layer, num, x, y);
|
||||||
|
const frames = data.frames;
|
||||||
|
|
||||||
|
for (let i = frames - 1; i >= 0; i--) {
|
||||||
|
moving.useSpecifiedFrame(i);
|
||||||
|
await sleep(this.interval);
|
||||||
|
}
|
||||||
|
moving.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.controller.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapDoorHook implements Partial<IMapLayerHooks> {
|
||||||
|
constructor(readonly renderer: MapDoorRenderer) {}
|
||||||
|
|
||||||
|
onOpenDoor(x: number, y: number): Promise<void> {
|
||||||
|
return this.renderer.openDoor(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseDoor(num: number, x: number, y: number): Promise<void> {
|
||||||
|
return this.renderer.closeDoor(num, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
518
packages-user/client-modules/src/render/map/extension/hero.ts
Normal file
518
packages-user/client-modules/src/render/map/extension/hero.ts
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
import {
|
||||||
|
degradeFace,
|
||||||
|
FaceDirection,
|
||||||
|
getFaceMovement,
|
||||||
|
HeroAnimateDirection,
|
||||||
|
IHeroState,
|
||||||
|
IHeroStateHooks,
|
||||||
|
IMapLayer,
|
||||||
|
nextFaceDirection,
|
||||||
|
state
|
||||||
|
} from '@user/data-state';
|
||||||
|
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { IHookController, logger } from '@motajs/common';
|
||||||
|
import { BlockCls, IMaterialFramedData } from '@user/client-base';
|
||||||
|
import {
|
||||||
|
ITexture,
|
||||||
|
ITextureSplitter,
|
||||||
|
TextureRowSplitter
|
||||||
|
} from '@motajs/render-assets';
|
||||||
|
import { IMapHeroRenderer } from './types';
|
||||||
|
import { TimingFn } from 'mutate-animate';
|
||||||
|
|
||||||
|
/** 默认的移动时长 */
|
||||||
|
const DEFAULT_TIME = 100;
|
||||||
|
|
||||||
|
interface HeroRenderEntity {
|
||||||
|
/** 移动图块对象 */
|
||||||
|
readonly block: IMovingBlock;
|
||||||
|
/** 标识符,用于判定跟随者 */
|
||||||
|
readonly identifier: string;
|
||||||
|
/** 目标横坐标,移动时有效 */
|
||||||
|
targetX: number;
|
||||||
|
/** 目标纵坐标,移动时有效 */
|
||||||
|
targetY: number;
|
||||||
|
/** 当前的移动朝向 */
|
||||||
|
direction: FaceDirection;
|
||||||
|
/** 下一个跟随者的移动方向 */
|
||||||
|
nextDirection: FaceDirection;
|
||||||
|
|
||||||
|
/** 当前是否正在移动 */
|
||||||
|
moving: boolean;
|
||||||
|
/** 当前是否正在动画,移动跟动画要分开,有的操作比如跳跃就是在移动中但是没动画 */
|
||||||
|
animating: boolean;
|
||||||
|
/** 帧动画间隔 */
|
||||||
|
animateInterval: number;
|
||||||
|
/** 当一次动画时刻 */
|
||||||
|
lastAnimateTime: number;
|
||||||
|
/** 当前的动画帧数 */
|
||||||
|
animateFrame: number;
|
||||||
|
/** 移动的 `Promise`,移动完成时兑现,如果停止,则一直是兑现状态 */
|
||||||
|
promise: Promise<void>;
|
||||||
|
/** 勇士移动的动画方向 */
|
||||||
|
animateDirection: HeroAnimateDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapHeroRenderer implements IMapHeroRenderer {
|
||||||
|
private static readonly splitter: ITextureSplitter<number> =
|
||||||
|
new TextureRowSplitter();
|
||||||
|
|
||||||
|
/** 勇士钩子 */
|
||||||
|
readonly controller: IHookController<IHeroStateHooks>;
|
||||||
|
/** 勇士每个朝向的贴图对象 */
|
||||||
|
readonly textureMap: Map<FaceDirection, IMaterialFramedData> = new Map();
|
||||||
|
/** 勇士渲染实体,与 `entities[0]` 同引用 */
|
||||||
|
readonly heroEntity: HeroRenderEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染实体,索引 0 表示勇士,后续索引依次表示跟随的跟随者。
|
||||||
|
* 整体是一个状态机,而且下一个跟随者只与上一个跟随者有关,下一个跟随者移动的方向就是上一个跟随者上一步移动后指向的方向。
|
||||||
|
*/
|
||||||
|
readonly entities: HeroRenderEntity[] = [];
|
||||||
|
|
||||||
|
/** 每帧执行的帧动画对象 */
|
||||||
|
readonly ticker: IMapRendererTicker;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly renderer: IMapRenderer,
|
||||||
|
readonly layer: IMapLayer,
|
||||||
|
readonly hero: IHeroState
|
||||||
|
) {
|
||||||
|
this.controller = hero.addHook(new MapHeroHook(this));
|
||||||
|
this.controller.load();
|
||||||
|
const moving = this.addHeroMoving(renderer, layer, hero);
|
||||||
|
const heroEntity: HeroRenderEntity = {
|
||||||
|
block: moving,
|
||||||
|
identifier: '',
|
||||||
|
targetX: hero.x,
|
||||||
|
targetY: hero.y,
|
||||||
|
direction: hero.direction,
|
||||||
|
nextDirection: FaceDirection.Unknown,
|
||||||
|
moving: false,
|
||||||
|
animating: false,
|
||||||
|
animateInterval: 0,
|
||||||
|
lastAnimateTime: 0,
|
||||||
|
animateFrame: 0,
|
||||||
|
promise: Promise.resolve(),
|
||||||
|
animateDirection: HeroAnimateDirection.Forward
|
||||||
|
};
|
||||||
|
this.heroEntity = heroEntity;
|
||||||
|
this.entities.push(heroEntity);
|
||||||
|
this.ticker = renderer.requestTicker(time => this.tick(time));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加勇士对应的移动图块
|
||||||
|
* @param renderer 渲染器
|
||||||
|
* @param layer 图块所属图层
|
||||||
|
* @param hero 勇士状态对象
|
||||||
|
*/
|
||||||
|
private addHeroMoving(
|
||||||
|
renderer: IMapRenderer,
|
||||||
|
layer: IMapLayer,
|
||||||
|
hero: IHeroState
|
||||||
|
) {
|
||||||
|
if (isNil(hero.image)) {
|
||||||
|
logger.warn(88);
|
||||||
|
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
|
||||||
|
}
|
||||||
|
const image = this.renderer.manager.getImageByAlias(hero.image);
|
||||||
|
if (!image) {
|
||||||
|
logger.warn(89, hero.image);
|
||||||
|
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
|
||||||
|
}
|
||||||
|
this.updateHeroTexture(image);
|
||||||
|
const tex = this.textureMap.get(degradeFace(hero.direction));
|
||||||
|
if (!tex) {
|
||||||
|
return renderer.addMovingBlock(layer, 0, hero.x, hero.y);
|
||||||
|
}
|
||||||
|
const block = renderer.addMovingBlock(layer, tex, hero.x, hero.y);
|
||||||
|
block.useSpecifiedFrame(0);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新勇士贴图
|
||||||
|
* @param image 勇士使用的贴图,包含四个方向
|
||||||
|
*/
|
||||||
|
private updateHeroTexture(image: ITexture) {
|
||||||
|
const textures = [
|
||||||
|
...image.split(MapHeroRenderer.splitter, image.height / 4)
|
||||||
|
];
|
||||||
|
if (textures.length !== 4) {
|
||||||
|
logger.warn(90, hero.image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const faceList = [
|
||||||
|
FaceDirection.Down,
|
||||||
|
FaceDirection.Left,
|
||||||
|
FaceDirection.Right,
|
||||||
|
FaceDirection.Up
|
||||||
|
];
|
||||||
|
faceList.forEach((v, i) => {
|
||||||
|
const dirImage = textures[i];
|
||||||
|
const data: IMaterialFramedData = {
|
||||||
|
offset: dirImage.width / 4,
|
||||||
|
texture: dirImage,
|
||||||
|
cls: BlockCls.Unknown,
|
||||||
|
frames: 4,
|
||||||
|
defaultFrame: 0
|
||||||
|
};
|
||||||
|
this.textureMap.set(v, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick(time: number) {
|
||||||
|
this.entities.forEach(v => {
|
||||||
|
if (v.animating) {
|
||||||
|
const dt = time - v.lastAnimateTime;
|
||||||
|
if (dt > v.animateInterval) {
|
||||||
|
if (v.animateDirection === HeroAnimateDirection.Forward) {
|
||||||
|
v.animateFrame++;
|
||||||
|
} else {
|
||||||
|
v.animateFrame--;
|
||||||
|
if (v.animateFrame < 0) {
|
||||||
|
// 小于 0,则加上帧数的整数倍,就写个 10000 倍吧
|
||||||
|
v.animateFrame += v.block.texture.frames * 10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.lastAnimateTime = time;
|
||||||
|
v.block.useSpecifiedFrame(v.animateFrame);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (v.animateFrame !== 0) {
|
||||||
|
v.animateFrame = 0;
|
||||||
|
v.block.useSpecifiedFrame(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(image: ITexture): void {
|
||||||
|
this.updateHeroTexture(image);
|
||||||
|
const tex = this.textureMap.get(degradeFace(this.hero.direction));
|
||||||
|
if (!tex) return;
|
||||||
|
this.heroEntity.block.setTexture(tex);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlpha(alpha: number): void {
|
||||||
|
this.heroEntity.block.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(x: number, y: number): void {
|
||||||
|
this.entities.forEach(v => {
|
||||||
|
v.block.setPos(x, y);
|
||||||
|
v.nextDirection = FaceDirection.Unknown;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动指定渲染实体,不会影响其他渲染实体。多次调用时会按顺序依次移动
|
||||||
|
* @param entity 渲染实体
|
||||||
|
* @param direction 移动方向
|
||||||
|
* @param time 移动时长
|
||||||
|
*/
|
||||||
|
private moveEntity(
|
||||||
|
entity: HeroRenderEntity,
|
||||||
|
direction: FaceDirection,
|
||||||
|
time: number
|
||||||
|
) {
|
||||||
|
const { x: dx, y: dy } = getFaceMovement(direction);
|
||||||
|
if (dx === 0 && dy === 0) return;
|
||||||
|
const block = entity.block;
|
||||||
|
const tx = block.x + dx;
|
||||||
|
const ty = block.y + dy;
|
||||||
|
const nextTile = state.roleFace.getFaceOf(block.tile, direction);
|
||||||
|
const nextTex = this.renderer.manager.getIfBigImage(
|
||||||
|
nextTile?.identifier ?? block.tile
|
||||||
|
);
|
||||||
|
entity.animateInterval = time;
|
||||||
|
entity.promise = entity.promise.then(async () => {
|
||||||
|
entity.moving = true;
|
||||||
|
entity.animating = true;
|
||||||
|
entity.direction = direction;
|
||||||
|
if (nextTex) block.setTexture(nextTex);
|
||||||
|
await block.lineTo(tx, ty, time);
|
||||||
|
entity.nextDirection = entity.direction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成跳跃曲线
|
||||||
|
* @param dx 横向偏移量
|
||||||
|
* @param dy 纵向偏移量
|
||||||
|
*/
|
||||||
|
private generateJumpFn(dx: number, dy: number): TimingFn<2> {
|
||||||
|
const distance = Math.hypot(dx, dy);
|
||||||
|
const peak = 3 + distance;
|
||||||
|
|
||||||
|
return (progress: number) => {
|
||||||
|
const x = dx * progress;
|
||||||
|
const y = progress * dy + (progress ** 2 - progress) * peak;
|
||||||
|
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定渲染实体跳跃至目标点,多次调用时会按顺序依次执行,可以与 `moveEntity` 混用
|
||||||
|
* @param entity 渲染实体
|
||||||
|
* @param x 目标横坐标
|
||||||
|
* @param y 目标纵坐标
|
||||||
|
* @param time 跳跃时长
|
||||||
|
*/
|
||||||
|
private jumpEntity(
|
||||||
|
entity: HeroRenderEntity,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number
|
||||||
|
) {
|
||||||
|
const block = entity.block;
|
||||||
|
entity.promise = entity.promise.then(async () => {
|
||||||
|
const dx = block.x - x;
|
||||||
|
const dy = block.y - y;
|
||||||
|
const fn = this.generateJumpFn(dx, dy);
|
||||||
|
entity.moving = true;
|
||||||
|
entity.animating = false;
|
||||||
|
entity.animateFrame = 0;
|
||||||
|
await block.moveRelative(fn, time);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startMove(): void {
|
||||||
|
this.heroEntity.moving = true;
|
||||||
|
this.heroEntity.animating = true;
|
||||||
|
this.heroEntity.lastAnimateTime = this.ticker.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private endEntityMoving(entity: HeroRenderEntity) {
|
||||||
|
entity.moving = false;
|
||||||
|
entity.animating = false;
|
||||||
|
entity.animateFrame = 0;
|
||||||
|
entity.block.useSpecifiedFrame(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitMoveEnd(): Promise<void> {
|
||||||
|
await Promise.all(this.entities.map(v => v.promise));
|
||||||
|
this.entities.forEach(v => this.endEntityMoving(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopMove(): void {
|
||||||
|
this.entities.forEach(v => {
|
||||||
|
v.block.endMoving();
|
||||||
|
this.endEntityMoving(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(direction: FaceDirection, time: number): Promise<void> {
|
||||||
|
this.moveEntity(this.heroEntity, direction, time);
|
||||||
|
for (let i = 1; i < this.entities.length; i++) {
|
||||||
|
const last = this.entities[i - 1];
|
||||||
|
this.moveEntity(this.entities[i], last.nextDirection, time);
|
||||||
|
}
|
||||||
|
await Promise.all(this.entities.map(v => v.promise));
|
||||||
|
}
|
||||||
|
|
||||||
|
async jumpTo(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number,
|
||||||
|
waitFollower: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
// 首先要把所有的跟随者移动到勇士所在位置
|
||||||
|
for (let i = 1; i < this.entities.length; i++) {
|
||||||
|
// 对于每一个跟随者,需要向前遍历每一个跟随者,然后朝向移动,这样就可以聚集在一起了
|
||||||
|
const now = this.entities[i];
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
const last = this.entities[j];
|
||||||
|
this.moveEntity(now, last.nextDirection, DEFAULT_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.entities.forEach(v => {
|
||||||
|
this.jumpEntity(v, x, y, time);
|
||||||
|
});
|
||||||
|
if (waitFollower) {
|
||||||
|
await Promise.all(this.entities.map(v => v.promise));
|
||||||
|
} else {
|
||||||
|
return this.heroEntity.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFollower(image: number, id: string): void {
|
||||||
|
const last = this.entities[this.entities.length - 1];
|
||||||
|
if (last.moving) {
|
||||||
|
logger.warn(92);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nowFace = degradeFace(last.nextDirection, FaceDirection.Down);
|
||||||
|
const faced = state.roleFace.getFaceOf(image, nowFace);
|
||||||
|
const tex = this.renderer.manager.getIfBigImage(faced?.face ?? image);
|
||||||
|
if (!tex) {
|
||||||
|
logger.warn(91, image.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { x: dxn, y: dyn } = getFaceMovement(last.nextDirection);
|
||||||
|
const { x: dx, y: dy } = getFaceMovement(last.direction);
|
||||||
|
const x = last.block.x - dxn;
|
||||||
|
const y = last.block.y - dyn;
|
||||||
|
const moving = this.renderer.addMovingBlock(this.layer, tex, x, y);
|
||||||
|
const entity: HeroRenderEntity = {
|
||||||
|
block: moving,
|
||||||
|
identifier: id,
|
||||||
|
targetX: last.targetX - dx,
|
||||||
|
targetY: last.targetY - dy,
|
||||||
|
direction: nowFace,
|
||||||
|
nextDirection: FaceDirection.Unknown,
|
||||||
|
moving: false,
|
||||||
|
animating: false,
|
||||||
|
animateInterval: 0,
|
||||||
|
lastAnimateTime: 0,
|
||||||
|
animateFrame: 0,
|
||||||
|
promise: Promise.resolve(),
|
||||||
|
animateDirection: HeroAnimateDirection.Forward
|
||||||
|
};
|
||||||
|
moving.useSpecifiedFrame(0);
|
||||||
|
this.entities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFollower(follower: string, animate: boolean): Promise<void> {
|
||||||
|
const index = this.entities.findIndex(v => v.identifier === follower);
|
||||||
|
if (index === -1) return;
|
||||||
|
if (this.entities[index].moving) {
|
||||||
|
logger.warn(93);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === this.entities.length - 1) {
|
||||||
|
this.entities[index].block.destroy();
|
||||||
|
this.entities.splice(index, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 展示动画
|
||||||
|
if (animate) {
|
||||||
|
for (let i = index + 1; i < this.entities.length; i++) {
|
||||||
|
const last = this.entities[i - 1];
|
||||||
|
const moving = this.entities[i];
|
||||||
|
this.moveEntity(moving, last.nextDirection, DEFAULT_TIME);
|
||||||
|
}
|
||||||
|
this.entities[index].block.destroy();
|
||||||
|
this.entities.splice(index, 1);
|
||||||
|
await Promise.all(this.entities.map(v => v.promise));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 不展示动画
|
||||||
|
for (let i = index + 1; i < this.entities.length; i++) {
|
||||||
|
const last = this.entities[i - 1];
|
||||||
|
const moving = this.entities[i];
|
||||||
|
moving.block.setPos(last.block.x, last.block.y);
|
||||||
|
const nextFace = state.roleFace.getFaceOf(
|
||||||
|
moving.block.tile,
|
||||||
|
last.nextDirection
|
||||||
|
);
|
||||||
|
if (!nextFace) continue;
|
||||||
|
const tile = this.renderer.manager.getIfBigImage(
|
||||||
|
nextFace.identifier
|
||||||
|
);
|
||||||
|
if (!tile) continue;
|
||||||
|
moving.block.setTexture(tile);
|
||||||
|
moving.direction = last.nextDirection;
|
||||||
|
moving.nextDirection = moving.direction;
|
||||||
|
}
|
||||||
|
this.entities[index].block.destroy();
|
||||||
|
this.entities.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllFollowers(): void {
|
||||||
|
for (let i = 1; i < this.entities.length; i++) {
|
||||||
|
this.entities[i].block.destroy();
|
||||||
|
}
|
||||||
|
this.entities.length = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFollowerAlpha(identifier: string, alpha: number): void {
|
||||||
|
const follower = this.entities.find(v => v.identifier === identifier);
|
||||||
|
if (!follower) return;
|
||||||
|
follower.block.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeroAnimateDirection(direction: HeroAnimateDirection): void {
|
||||||
|
this.heroEntity.animateDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
turn(direction?: FaceDirection): void {
|
||||||
|
const next = isNil(direction)
|
||||||
|
? nextFaceDirection(this.heroEntity.direction)
|
||||||
|
: direction;
|
||||||
|
const tex = this.textureMap.get(next);
|
||||||
|
if (tex) {
|
||||||
|
this.heroEntity.block.setTexture(tex);
|
||||||
|
this.heroEntity.direction = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.controller.unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapHeroHook implements Partial<IHeroStateHooks> {
|
||||||
|
constructor(readonly hero: MapHeroRenderer) {}
|
||||||
|
|
||||||
|
onSetImage(image: ImageIds): void {
|
||||||
|
const texture = this.hero.renderer.manager.getImageByAlias(image);
|
||||||
|
if (!texture) {
|
||||||
|
logger.warn(89, hero.image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hero.setImage(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetPosition(x: number, y: number): void {
|
||||||
|
this.hero.setPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTurnHero(direction: FaceDirection): void {
|
||||||
|
this.hero.turn(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
onStartMove(): void {
|
||||||
|
this.hero.startMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMoveHero(direction: FaceDirection, time: number): Promise<void> {
|
||||||
|
return this.hero.move(direction, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEndMove(): Promise<void> {
|
||||||
|
return this.hero.waitMoveEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
onJumpHero(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number,
|
||||||
|
waitFollower: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
return this.hero.jumpTo(x, y, time, waitFollower);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetAlpha(alpha: number): void {
|
||||||
|
this.hero.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddFollower(follower: number, identifier: string): void {
|
||||||
|
this.hero.addFollower(follower, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveFollower(identifier: string, animate: boolean): void {
|
||||||
|
this.hero.removeFollower(identifier, animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveAllFollowers(): void {
|
||||||
|
this.hero.removeAllFollowers();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetFollowerAlpha(identifier: string, alpha: number): void {
|
||||||
|
this.hero.setFollowerAlpha(identifier, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from './hero';
|
||||||
|
export * from './manager';
|
||||||
|
export * from './types';
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import { IHeroState, IMapLayer } from '@user/data-state';
|
||||||
|
import {
|
||||||
|
IMapDoorRenderer,
|
||||||
|
IMapExtensionManager,
|
||||||
|
IMapHeroRenderer
|
||||||
|
} from './types';
|
||||||
|
import { IMapRenderer } from '../types';
|
||||||
|
import { MapHeroRenderer } from './hero';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { MapDoorRenderer } from './door';
|
||||||
|
|
||||||
|
export class MapExtensionManager implements IMapExtensionManager {
|
||||||
|
/** 勇士状态至勇士渲染器的映射 */
|
||||||
|
readonly heroMap: Map<IHeroState, IMapHeroRenderer> = new Map();
|
||||||
|
/** 地图图层到门渲染器的映射 */
|
||||||
|
readonly doorMap: Map<IMapLayer, IMapDoorRenderer> = new Map();
|
||||||
|
|
||||||
|
constructor(readonly renderer: IMapRenderer) {}
|
||||||
|
|
||||||
|
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null {
|
||||||
|
if (this.heroMap.has(state)) {
|
||||||
|
logger.error(45, 'hero renderer');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const heroRenderer = new MapHeroRenderer(this.renderer, layer, state);
|
||||||
|
this.heroMap.set(state, heroRenderer);
|
||||||
|
return heroRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHero(state: IHeroState): void {
|
||||||
|
const renderer = this.heroMap.get(state);
|
||||||
|
if (!renderer) return;
|
||||||
|
renderer.destroy();
|
||||||
|
this.heroMap.delete(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDoor(layer: IMapLayer): IMapDoorRenderer | null {
|
||||||
|
if (this.doorMap.has(layer)) {
|
||||||
|
logger.error(45, 'door renderer');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const doorRenderer = new MapDoorRenderer(this.renderer, layer);
|
||||||
|
this.doorMap.set(layer, doorRenderer);
|
||||||
|
return doorRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDoor(layer: IMapLayer): void {
|
||||||
|
const renderer = this.doorMap.get(layer);
|
||||||
|
if (!renderer) return;
|
||||||
|
renderer.destroy();
|
||||||
|
this.doorMap.delete(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.heroMap.forEach(v => void v.destroy());
|
||||||
|
this.doorMap.forEach(v => void v.destroy());
|
||||||
|
this.heroMap.clear();
|
||||||
|
this.doorMap.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { IMapRenderer } from '../types';
|
||||||
|
import { IMapTextArea, IMapTextRenderable, IOnMapTextRenderer } from './types';
|
||||||
|
|
||||||
|
export class OnMapTextRenderer implements IOnMapTextRenderer {
|
||||||
|
/** 画布元素 */
|
||||||
|
readonly canvas: HTMLCanvasElement;
|
||||||
|
/** 画布 Canvas2D 上下文 */
|
||||||
|
readonly ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
/** 图块索引到图块文本对象的映射 */
|
||||||
|
readonly areaMap: Map<number, MapTextArea> = new Map();
|
||||||
|
private dirty: boolean = false;
|
||||||
|
|
||||||
|
constructor(readonly renderer: IMapRenderer) {
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.ctx = this.canvas.getContext('2d')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): HTMLCanvasElement {
|
||||||
|
return this.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
requireBlockArea(x: number, y: number): Readonly<IMapTextArea> {
|
||||||
|
const index = y * this.renderer.mapWidth + x;
|
||||||
|
const exist = this.areaMap.get(index);
|
||||||
|
if (exist) return exist;
|
||||||
|
const area = new MapTextArea(this, x, y);
|
||||||
|
this.areaMap.set(index, area);
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
needUpdate(): boolean {
|
||||||
|
return this.dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapTextArea implements IMapTextArea {
|
||||||
|
index: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly renderer: OnMapTextRenderer,
|
||||||
|
public mapX: number,
|
||||||
|
public mapY: number
|
||||||
|
) {
|
||||||
|
this.index = mapY * renderer.renderer.mapWidth + mapX;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTextRenderable(renderable: IMapTextRenderable): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTextRenderable(renderable: IMapTextRenderable): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
246
packages-user/client-modules/src/render/map/extension/types.ts
Normal file
246
packages-user/client-modules/src/render/map/extension/types.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { ITexture } from '@motajs/render-assets';
|
||||||
|
import {
|
||||||
|
FaceDirection,
|
||||||
|
HeroAnimateDirection,
|
||||||
|
IHeroState,
|
||||||
|
IMapLayer
|
||||||
|
} from '@user/data-state';
|
||||||
|
import { Font } from '@motajs/render-style';
|
||||||
|
|
||||||
|
export interface IMapExtensionManager {
|
||||||
|
/**
|
||||||
|
* 添加勇士渲染拓展
|
||||||
|
* @param state 勇士状态
|
||||||
|
* @param layer 勇士所在图层
|
||||||
|
*/
|
||||||
|
addHero(state: IHeroState, layer: IMapLayer): IMapHeroRenderer | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除勇士渲染拓展
|
||||||
|
* @param state 勇士状态
|
||||||
|
*/
|
||||||
|
removeHero(state: IHeroState): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加开门动画拓展
|
||||||
|
*/
|
||||||
|
addDoor(layer: IMapLayer): IMapDoorRenderer | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除开门动画拓展
|
||||||
|
*/
|
||||||
|
removeDoor(layer: IMapLayer): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摧毁这个拓展管理对象,释放相关资源
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapHeroRenderer {
|
||||||
|
/**
|
||||||
|
* 设置勇士图片
|
||||||
|
* @param image 勇士使用的图片
|
||||||
|
*/
|
||||||
|
setImage(image: ITexture): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加跟随者
|
||||||
|
* @param image 跟随者图块数字
|
||||||
|
* @param id 跟随者的 id,用于删除操作
|
||||||
|
*/
|
||||||
|
addFollower(image: number, id: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消跟随者
|
||||||
|
* @param follower 跟随者的 id
|
||||||
|
* @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格
|
||||||
|
*/
|
||||||
|
removeFollower(follower: string, animate: boolean): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除所有跟随者
|
||||||
|
*/
|
||||||
|
removeAllFollowers(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士位置
|
||||||
|
*/
|
||||||
|
setPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始移动,在移动前需要调用此方法切换勇士状态
|
||||||
|
*/
|
||||||
|
startMove(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待勇士移动停止后,将移动状态切换为停止
|
||||||
|
* @param waitFollower 是否也等待跟随者移动结束
|
||||||
|
*/
|
||||||
|
waitMoveEnd(waitFollower: boolean): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立刻停止移动,勇士和跟随者瞬移到目标点
|
||||||
|
*/
|
||||||
|
stopMove(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 勇士朝某个方向移动
|
||||||
|
* @param direction 移动方向
|
||||||
|
*/
|
||||||
|
move(direction: FaceDirection, time: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳跃勇士至目标点
|
||||||
|
* @param x 目标点横坐标
|
||||||
|
* @param y 目标点纵坐标
|
||||||
|
* @param time 跳跃时长
|
||||||
|
* @param waitFollower 是否等待跟随者也跳跃完毕
|
||||||
|
*/
|
||||||
|
jumpTo(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number,
|
||||||
|
waitFollower: boolean
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士不透明度
|
||||||
|
* @param alpha 不透明度
|
||||||
|
*/
|
||||||
|
setAlpha(alpha: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置跟随者的不透明度
|
||||||
|
* @param identifier 跟随者标识符
|
||||||
|
* @param alpha 跟随者不透明度
|
||||||
|
*/
|
||||||
|
setFollowerAlpha(identifier: string, alpha: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士移动的动画播放方向,一般后退会使用反向播放的动画,前进使用正向播放的动画
|
||||||
|
* @param direction 动画方向
|
||||||
|
*/
|
||||||
|
setHeroAnimateDirection(direction: HeroAnimateDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士朝向
|
||||||
|
* @param direction 勇士朝向,不填表示顺时针旋转
|
||||||
|
*/
|
||||||
|
turn(direction?: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摧毁这个勇士渲染拓展,释放相关资源
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDoorRenderer {
|
||||||
|
/**
|
||||||
|
* 开启指定位置的门,播放开门动画
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
openDoor(x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置执行关门动画
|
||||||
|
* @param num 门图块数字
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置开关门动画两帧之间的间隔
|
||||||
|
* @param interval 开门动画间隔
|
||||||
|
*/
|
||||||
|
setAnimateInterval(interval: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摧毁这个门动画拓展,释放相关资源
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapTextRenderable {
|
||||||
|
/** 文本内容 */
|
||||||
|
readonly text: string;
|
||||||
|
/** 文本字体 */
|
||||||
|
readonly font: Font;
|
||||||
|
/** 文本填充样式 */
|
||||||
|
readonly fillStyle: CanvasStyle;
|
||||||
|
/** 文本描边样式 */
|
||||||
|
readonly strokeStyle: CanvasStyle;
|
||||||
|
/** 文本横坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
|
||||||
|
readonly px: number;
|
||||||
|
/** 文本纵坐标,注意 {@link IMapTextArea.addTextRenderable} 的相对关系 */
|
||||||
|
readonly py: number;
|
||||||
|
/** 文本横向对齐方式 */
|
||||||
|
readonly textAlign: CanvasTextAlign;
|
||||||
|
/** 文本纵向对齐方式 */
|
||||||
|
readonly textBaseline: CanvasTextBaseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapTextRequested {
|
||||||
|
/**
|
||||||
|
* 申请更新指定的图块
|
||||||
|
* @param blocks 需要更新数据的图块列表
|
||||||
|
*/
|
||||||
|
requestBlocks(blocks: IMapTextArea[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapTextArea {
|
||||||
|
/** 图块在地图上的索引 */
|
||||||
|
index: number;
|
||||||
|
/** 图块横坐标 */
|
||||||
|
mapX: number;
|
||||||
|
/** 图块纵坐标 */
|
||||||
|
mapY: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加文字可渲染对象。可渲染对象的坐标相对于图块,而非地图。
|
||||||
|
* @param renderable 可渲染对象
|
||||||
|
*/
|
||||||
|
addTextRenderable(renderable: IMapTextRenderable): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定的文字可渲染对象
|
||||||
|
* @param renderable 可渲染对象
|
||||||
|
*/
|
||||||
|
removeTextRenderable(renderable: IMapTextRenderable): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除本图块的所有文字可渲染对象
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOnMapTextRenderer {
|
||||||
|
/**
|
||||||
|
* 渲染地图文字,返回的画布就是文字画到的画布
|
||||||
|
*/
|
||||||
|
render(): HTMLCanvasElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请指定图块坐标的文字管理对象
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
requireBlockArea(x: number, y: number): Readonly<IMapTextArea>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否需要更新
|
||||||
|
*/
|
||||||
|
needUpdate(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有文字内容
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摧毁这个文字渲染对象,释放相关资源
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
11
packages-user/client-modules/src/render/map/index.ts
Normal file
11
packages-user/client-modules/src/render/map/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export * from './extension';
|
||||||
|
|
||||||
|
export * from './block';
|
||||||
|
export * from './constant';
|
||||||
|
export * from './element';
|
||||||
|
export * from './moving';
|
||||||
|
export * from './renderer';
|
||||||
|
export * from './status';
|
||||||
|
export * from './types';
|
||||||
|
export * from './vertex';
|
||||||
|
export * from './viewport';
|
||||||
268
packages-user/client-modules/src/render/map/moving.ts
Normal file
268
packages-user/client-modules/src/render/map/moving.ts
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { linear, TimingFn } from 'mutate-animate';
|
||||||
|
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
|
||||||
|
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { IMapLayer } from '@user/data-state';
|
||||||
|
import { DynamicBlockStatus } from './status';
|
||||||
|
|
||||||
|
export interface IMovingRenderer {
|
||||||
|
/** 素材管理器 */
|
||||||
|
readonly manager: IMaterialManager;
|
||||||
|
/** 顶点数组生成器 */
|
||||||
|
readonly vertex: IMapVertexGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前时间戳
|
||||||
|
*/
|
||||||
|
getTimestamp(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的移动图块对象
|
||||||
|
* @param block 移动图块对象
|
||||||
|
*/
|
||||||
|
deleteMoving(block: IMovingBlock): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MovingBlock extends DynamicBlockStatus implements IMovingBlock {
|
||||||
|
readonly tile: number;
|
||||||
|
readonly renderer: IMovingRenderer;
|
||||||
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
|
texture: IMaterialFramedData;
|
||||||
|
index: number;
|
||||||
|
x: number = 0;
|
||||||
|
y: number = 0;
|
||||||
|
|
||||||
|
/** 当前动画开始的时刻 */
|
||||||
|
private startTime: number = 0;
|
||||||
|
|
||||||
|
/** 是否是直线动画 */
|
||||||
|
private line: boolean = false;
|
||||||
|
/** 是否是相对模式 */
|
||||||
|
private relative: boolean = false;
|
||||||
|
/** 目标横坐标 */
|
||||||
|
private targetX: number = 0;
|
||||||
|
/** 目标纵坐标 */
|
||||||
|
private targetY: number = 0;
|
||||||
|
/** 直线移动的横坐标增量 */
|
||||||
|
private dx: number = 0;
|
||||||
|
/** 直线移动的纵坐标增量 */
|
||||||
|
private dy: number = 0;
|
||||||
|
/** 动画时长 */
|
||||||
|
private time: number = 0;
|
||||||
|
/** 速率曲线 */
|
||||||
|
private timing: TimingFn = () => 0;
|
||||||
|
/** 移动轨迹曲线 */
|
||||||
|
private curve: TimingFn<2> = () => [0, 0];
|
||||||
|
|
||||||
|
/** 动画开始时横坐标 */
|
||||||
|
private startX: number = 0;
|
||||||
|
/** 动画开始时纵坐标 */
|
||||||
|
private startY: number = 0;
|
||||||
|
/** 当前动画是否已经结束 */
|
||||||
|
private end: boolean = true;
|
||||||
|
|
||||||
|
/** 是否通过 `setPos` 设置了位置 */
|
||||||
|
private posUpdated: boolean = false;
|
||||||
|
|
||||||
|
/** 兑现函数 */
|
||||||
|
private promiseFunc: () => void = () => {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: IMovingRenderer & IMapRenderer,
|
||||||
|
index: number,
|
||||||
|
layer: IMapLayer,
|
||||||
|
block: number | IMaterialFramedData
|
||||||
|
) {
|
||||||
|
super(layer, renderer.vertex, index);
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.index = index;
|
||||||
|
this.layer = layer;
|
||||||
|
if (typeof block === 'number') {
|
||||||
|
this.texture = renderer.manager.getTile(block)!;
|
||||||
|
this.tile = block;
|
||||||
|
} else {
|
||||||
|
if (!renderer.manager.assetContainsTexture(block.texture)) {
|
||||||
|
logger.error(34);
|
||||||
|
}
|
||||||
|
if (renderer.getOffsetIndex(block.offset) === -1) {
|
||||||
|
logger.error(41);
|
||||||
|
}
|
||||||
|
this.texture = block;
|
||||||
|
this.tile = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPos(x: number, y: number): void {
|
||||||
|
if (!this.end) return;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.posUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTexture(texture: IMaterialFramedData): void {
|
||||||
|
if (texture === this.texture) return;
|
||||||
|
this.texture = texture;
|
||||||
|
this.renderer.vertex.updateMoving(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lineTo(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number,
|
||||||
|
timing?: TimingFn
|
||||||
|
): Promise<this> {
|
||||||
|
if (!this.end) return Promise.resolve(this);
|
||||||
|
this.startX = this.x;
|
||||||
|
this.startY = this.y;
|
||||||
|
this.targetX = x;
|
||||||
|
this.targetY = y;
|
||||||
|
this.dx = x - this.x;
|
||||||
|
this.dy = y - this.y;
|
||||||
|
this.time = time;
|
||||||
|
this.relative = false;
|
||||||
|
this.startTime = this.renderer.getTimestamp();
|
||||||
|
if (time === 0) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.end = true;
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
this.end = false;
|
||||||
|
this.timing = timing ?? linear();
|
||||||
|
this.line = true;
|
||||||
|
return new Promise(res => {
|
||||||
|
this.promiseFunc = () => res(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise<this> {
|
||||||
|
if (!this.end) return Promise.resolve(this);
|
||||||
|
this.time = time;
|
||||||
|
this.line = false;
|
||||||
|
this.relative = false;
|
||||||
|
this.startX = this.x;
|
||||||
|
this.startY = this.y;
|
||||||
|
this.startTime = this.renderer.getTimestamp();
|
||||||
|
if (time === 0) {
|
||||||
|
const [tx, ty] = curve(1);
|
||||||
|
this.x = tx;
|
||||||
|
this.y = ty;
|
||||||
|
this.end = true;
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
this.end = false;
|
||||||
|
this.timing = timing ?? linear();
|
||||||
|
this.curve = curve;
|
||||||
|
return new Promise(res => {
|
||||||
|
this.promiseFunc = () => res(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
moveRelative(
|
||||||
|
curve: TimingFn<2>,
|
||||||
|
time: number,
|
||||||
|
timing?: TimingFn
|
||||||
|
): Promise<this> {
|
||||||
|
if (!this.end) return Promise.resolve(this);
|
||||||
|
this.time = time;
|
||||||
|
this.line = false;
|
||||||
|
this.relative = true;
|
||||||
|
this.startX = this.x;
|
||||||
|
this.startY = this.y;
|
||||||
|
this.startTime = this.renderer.getTimestamp();
|
||||||
|
if (time === 0) {
|
||||||
|
const [tx, ty] = curve(1);
|
||||||
|
this.x = tx + this.startX;
|
||||||
|
this.y = ty + this.startY;
|
||||||
|
this.end = true;
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
this.end = false;
|
||||||
|
this.timing = timing ?? linear();
|
||||||
|
this.curve = curve;
|
||||||
|
return new Promise(res => {
|
||||||
|
this.promiseFunc = () => res(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stepMoving(timestamp: number): boolean {
|
||||||
|
if (this.end) {
|
||||||
|
if (this.posUpdated) {
|
||||||
|
this.posUpdated = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const dt = timestamp - this.startTime;
|
||||||
|
if (this.line) {
|
||||||
|
if (dt > this.time) {
|
||||||
|
this.x = this.targetX;
|
||||||
|
this.y = this.targetY;
|
||||||
|
this.end = true;
|
||||||
|
this.promiseFunc();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const timeProgress = dt / this.time;
|
||||||
|
const progress = this.timing(timeProgress);
|
||||||
|
this.x = this.startX + progress * this.dx;
|
||||||
|
this.y = this.startY + progress * this.dy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dt > this.time) {
|
||||||
|
const [tx, ty] = this.curve(1);
|
||||||
|
if (this.relative) {
|
||||||
|
this.x = tx + this.startX;
|
||||||
|
this.y = ty + this.startY;
|
||||||
|
} else {
|
||||||
|
this.x = tx;
|
||||||
|
this.y = ty;
|
||||||
|
}
|
||||||
|
this.end = true;
|
||||||
|
this.promiseFunc();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const timeProgress = dt / this.time;
|
||||||
|
const progress = this.timing(timeProgress);
|
||||||
|
const [tx, ty] = this.curve(progress);
|
||||||
|
if (this.relative) {
|
||||||
|
this.x = tx + this.startX;
|
||||||
|
this.y = ty + this.startY;
|
||||||
|
} else {
|
||||||
|
this.x = tx;
|
||||||
|
this.y = ty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
endMoving(): void {
|
||||||
|
this.end = true;
|
||||||
|
if (this.line) {
|
||||||
|
this.x = this.targetX;
|
||||||
|
this.y = this.targetY;
|
||||||
|
} else {
|
||||||
|
const [x, y] = this.curve(1);
|
||||||
|
if (this.relative) {
|
||||||
|
this.x = x + this.startX;
|
||||||
|
this.y = y + this.startY;
|
||||||
|
} else {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.promiseFunc();
|
||||||
|
this.posUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useDefaultFrame(): void {
|
||||||
|
if (!this.renderer.manager.getTile(this.tile)) return;
|
||||||
|
const defaultFrame = this.renderer.manager.getDefaultFrame(this.tile);
|
||||||
|
this.useSpecifiedFrame(defaultFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.renderer.deleteMoving(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
1783
packages-user/client-modules/src/render/map/renderer.ts
Normal file
1783
packages-user/client-modules/src/render/map/renderer.ts
Normal file
File diff suppressed because it is too large
Load Diff
12
packages-user/client-modules/src/render/map/shader/back.frag
Normal file
12
packages-user/client-modules/src/render/map/shader/back.frag
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision highp sampler2DArray;
|
||||||
|
|
||||||
|
in vec3 v_texCoord;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_sampler;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
outColor = texture(u_sampler, v_texCoord);
|
||||||
|
}
|
||||||
16
packages-user/client-modules/src/render/map/shader/back.vert
Normal file
16
packages-user/client-modules/src/render/map/shader/back.vert
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 a_position;
|
||||||
|
layout(location = 1) in vec2 a_texCoord;
|
||||||
|
|
||||||
|
out vec3 v_texCoord;
|
||||||
|
|
||||||
|
uniform float u_nowFrame;
|
||||||
|
uniform mat3 u_transform;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 transformed = u_transform * vec3(a_position, 1.0);
|
||||||
|
v_texCoord = vec3(a_texCoord, u_nowFrame);
|
||||||
|
gl_Position = vec4(transformed.xy, 0.95, 1.0);
|
||||||
|
}
|
||||||
17
packages-user/client-modules/src/render/map/shader/map.frag
Normal file
17
packages-user/client-modules/src/render/map/shader/map.frag
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision highp sampler2DArray;
|
||||||
|
|
||||||
|
in vec4 v_texCoord;
|
||||||
|
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_sampler;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
|
||||||
|
float alpha = texColor.a * v_texCoord.a;
|
||||||
|
// todo: 透明像素应该如何解决??
|
||||||
|
if (alpha < 0.1) discard;
|
||||||
|
outColor = vec4(texColor.rgb, alpha);
|
||||||
|
}
|
||||||
36
packages-user/client-modules/src/render/map/shader/map.vert
Normal file
36
packages-user/client-modules/src/render/map/shader/map.vert
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision mediump int;
|
||||||
|
|
||||||
|
// 顶点坐标
|
||||||
|
layout(location = 0) in vec4 a_position;
|
||||||
|
// 实例化数据
|
||||||
|
// 图块坐标
|
||||||
|
layout(location = 1) in vec4 a_tilePos;
|
||||||
|
// 贴图坐标
|
||||||
|
layout(location = 2) in vec4 a_texCoord;
|
||||||
|
// x: 纵深,y: 不透明度
|
||||||
|
layout(location = 3) in vec4 a_tileData;
|
||||||
|
// x: 当前帧数,负数表示使用 u_nowFrame,y: 最大帧数,z: 偏移池索引,w: 纹理数组索引
|
||||||
|
layout(location = 4) in vec4 a_texData;
|
||||||
|
|
||||||
|
// x,y,z: 纹理坐标,w: 不透明度
|
||||||
|
out vec4 v_texCoord;
|
||||||
|
|
||||||
|
uniform float u_offsetPool[$1];
|
||||||
|
uniform float u_nowFrame;
|
||||||
|
uniform mat3 u_transform;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// 坐标
|
||||||
|
vec2 pos = a_position.xy * a_tilePos.zw + a_tilePos.xy;
|
||||||
|
vec2 texCoord = a_position.zw * a_texCoord.zw + a_texCoord.xy;
|
||||||
|
// 偏移量
|
||||||
|
float offset = mod(a_texData.x < 0.0 ? u_nowFrame : a_texData.x, a_texData.y);
|
||||||
|
int offsetIndex = int(a_texData.z);
|
||||||
|
// 贴图偏移
|
||||||
|
texCoord.x += u_offsetPool[offsetIndex] * offset;
|
||||||
|
v_texCoord = vec4(texCoord.xy, a_texData.w, a_tileData.y);
|
||||||
|
vec3 transformed = u_transform * vec3(pos.xy, 1.0);
|
||||||
|
gl_Position = vec4(transformed.xy, a_tileData.x, 1.0);
|
||||||
|
}
|
||||||
70
packages-user/client-modules/src/render/map/status.ts
Normal file
70
packages-user/client-modules/src/render/map/status.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { IMapLayer } from '@user/data-state';
|
||||||
|
import { IBlockStatus, IMapVertexStatus } from './types';
|
||||||
|
|
||||||
|
export class StaticBlockStatus implements IBlockStatus {
|
||||||
|
/**
|
||||||
|
* @param layer 图层对象
|
||||||
|
* @param vertex 顶点数组生成器对象
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly layer: IMapLayer,
|
||||||
|
readonly vertex: IMapVertexStatus,
|
||||||
|
readonly x: number,
|
||||||
|
readonly y: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
setAlpha(alpha: number): void {
|
||||||
|
this.vertex.setStaticAlpha(this.layer, alpha, this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlpha(): number {
|
||||||
|
return this.vertex.getStaticAlpha(this.layer, this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
useGlobalFrame(): void {
|
||||||
|
this.vertex.setStaticFrame(this.layer, this.x, this.y, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useSpecifiedFrame(frame: number): void {
|
||||||
|
this.vertex.setStaticFrame(this.layer, this.x, this.y, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrame(): number {
|
||||||
|
return this.vertex.getStaticFrame(this.layer, this.x, this.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicBlockStatus implements IBlockStatus {
|
||||||
|
/**
|
||||||
|
* @param layer 图层对象
|
||||||
|
* @param vertex 顶点数组生成器对象
|
||||||
|
* @param index 图块索引
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly layer: IMapLayer,
|
||||||
|
readonly vertex: IMapVertexStatus,
|
||||||
|
readonly index: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
setAlpha(alpha: number): void {
|
||||||
|
this.vertex.setDynamicAlpha(this.index, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAlpha(): number {
|
||||||
|
return this.vertex.getDynamicAlpha(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
useGlobalFrame(): void {
|
||||||
|
this.vertex.setDynamicFrame(this.index, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useSpecifiedFrame(frame: number): void {
|
||||||
|
this.vertex.setDynamicFrame(this.index, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrame(): number {
|
||||||
|
return this.vertex.getDynamicFrame(this.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
1126
packages-user/client-modules/src/render/map/types.ts
Normal file
1126
packages-user/client-modules/src/render/map/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
1166
packages-user/client-modules/src/render/map/vertex.ts
Normal file
1166
packages-user/client-modules/src/render/map/vertex.ts
Normal file
File diff suppressed because it is too large
Load Diff
138
packages-user/client-modules/src/render/map/viewport.ts
Normal file
138
packages-user/client-modules/src/render/map/viewport.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { Transform } from '@motajs/render-core';
|
||||||
|
import {
|
||||||
|
IBlockData,
|
||||||
|
IMapRenderArea,
|
||||||
|
IMapRenderData,
|
||||||
|
IMapRenderer,
|
||||||
|
IMapVertexBlock,
|
||||||
|
IMapVertexGenerator,
|
||||||
|
IMapViewportController
|
||||||
|
} from './types';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
|
||||||
|
export class MapViewport implements IMapViewportController {
|
||||||
|
transform: Transform = new Transform();
|
||||||
|
/** 顶点生成器 */
|
||||||
|
readonly vertex: IMapVertexGenerator;
|
||||||
|
|
||||||
|
constructor(readonly renderer: IMapRenderer) {
|
||||||
|
this.vertex = renderer.vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushBlock(
|
||||||
|
list: IMapRenderArea[],
|
||||||
|
start: IBlockData<IMapVertexBlock>,
|
||||||
|
end: IBlockData<IMapVertexBlock>
|
||||||
|
) {
|
||||||
|
const startIndex = start.data.startIndex;
|
||||||
|
const endIndex = end.data.endIndex;
|
||||||
|
list.push({
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
count: endIndex - startIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkDynamic(
|
||||||
|
list: IMapRenderArea[],
|
||||||
|
dynamicStart: number,
|
||||||
|
dynamicCount: number
|
||||||
|
) {
|
||||||
|
const last = list[list.length - 1];
|
||||||
|
if (!last || last.endIndex < dynamicStart) {
|
||||||
|
list.push({
|
||||||
|
startIndex: dynamicStart,
|
||||||
|
endIndex: dynamicStart + dynamicCount,
|
||||||
|
count: dynamicCount
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
last.endIndex = dynamicStart + dynamicCount;
|
||||||
|
last.count += dynamicCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderArea(): IMapRenderData {
|
||||||
|
const { cellWidth, cellHeight, renderWidth, renderHeight } =
|
||||||
|
this.renderer;
|
||||||
|
const { blockWidth, blockHeight, width, height } = this.vertex.block;
|
||||||
|
// 其实只需要算左上角和右下角就行了
|
||||||
|
const [left, top] = this.transform.untransformed(-1, -1);
|
||||||
|
const [right, bottom] = this.transform.untransformed(1, 1);
|
||||||
|
const cl = (left * renderWidth) / cellWidth;
|
||||||
|
const ct = (top * renderHeight) / cellHeight;
|
||||||
|
const cr = (right * renderWidth) / cellWidth;
|
||||||
|
const cb = (bottom * renderHeight) / cellHeight;
|
||||||
|
const blockLeft = clamp(Math.floor(cl / blockWidth), 0, width - 1);
|
||||||
|
const blockRight = clamp(Math.floor(cr / blockWidth), 0, width - 1);
|
||||||
|
const blockTop = clamp(Math.floor(ct / blockHeight), 0, height - 1);
|
||||||
|
const blockBottom = clamp(Math.floor(cb / blockHeight), 0, height - 1);
|
||||||
|
|
||||||
|
const renderArea: IMapRenderArea[] = [];
|
||||||
|
const updateArea: IMapRenderArea[] = [];
|
||||||
|
const blockList: IBlockData<IMapVertexBlock>[] = [];
|
||||||
|
|
||||||
|
// 内层横向外层纵向的话,索引在换行之前都是连续的,方便整合
|
||||||
|
for (let ny = blockTop; ny <= blockBottom; ny++) {
|
||||||
|
for (let nx = blockLeft; nx <= blockRight; nx++) {
|
||||||
|
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
|
||||||
|
blockList.push(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockList.length > 0) {
|
||||||
|
if (blockList.length === 1) {
|
||||||
|
const block = blockList[0];
|
||||||
|
if (block.data.renderDirty) {
|
||||||
|
this.pushBlock(updateArea, block, block);
|
||||||
|
}
|
||||||
|
this.pushBlock(renderArea, block, block);
|
||||||
|
} else {
|
||||||
|
// 更新区域
|
||||||
|
let updateStart: IBlockData<IMapVertexBlock> = blockList[0];
|
||||||
|
let updateEnd: IBlockData<IMapVertexBlock> = blockList[0];
|
||||||
|
let renderStart: IBlockData<IMapVertexBlock> = blockList[0];
|
||||||
|
let renderEnd: IBlockData<IMapVertexBlock> = blockList[0];
|
||||||
|
for (let i = 1; i < blockList.length; i++) {
|
||||||
|
const block = blockList[i];
|
||||||
|
const { renderDirty } = block.data;
|
||||||
|
// 连续则合并
|
||||||
|
// 渲染区域
|
||||||
|
if (block.index === renderEnd.index + 1) {
|
||||||
|
renderEnd = block;
|
||||||
|
} else {
|
||||||
|
this.pushBlock(renderArea, renderStart, renderEnd);
|
||||||
|
renderStart = block;
|
||||||
|
renderEnd = block;
|
||||||
|
}
|
||||||
|
// 缓冲区更新区域
|
||||||
|
if (renderDirty && block.index === updateEnd.index + 1) {
|
||||||
|
updateEnd = block;
|
||||||
|
} else {
|
||||||
|
this.pushBlock(updateArea, updateStart, updateEnd);
|
||||||
|
updateStart = block;
|
||||||
|
updateEnd = block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pushBlock(updateArea, updateStart, updateEnd);
|
||||||
|
this.pushBlock(renderArea, renderStart, renderEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicStart = this.vertex.dynamicStart;
|
||||||
|
const dynamicCount = this.vertex.dynamicCount;
|
||||||
|
this.checkDynamic(renderArea, dynamicStart, dynamicCount);
|
||||||
|
if (this.vertex.dynamicRenderDirty) {
|
||||||
|
this.checkDynamic(updateArea, dynamicStart, dynamicCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
render: renderArea,
|
||||||
|
dirty: updateArea,
|
||||||
|
blockList: blockList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bindTransform(transform: Transform): void {
|
||||||
|
this.transform = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,10 @@ import { Font } from '@motajs/render-style';
|
|||||||
|
|
||||||
//#region 地图
|
//#region 地图
|
||||||
|
|
||||||
|
/** 每个格子的默认宽度,现阶段用处不大 */
|
||||||
|
export const CELL_WIDTH = 32;
|
||||||
|
/** 每个格子的默认高度,现阶段用处不大 */
|
||||||
|
export const CELL_HEIGHT = 32;
|
||||||
/** 每个格子的宽高 */
|
/** 每个格子的宽高 */
|
||||||
export const CELL_SIZE = 32;
|
export const CELL_SIZE = 32;
|
||||||
/** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
|
/** 地图格子宽度,此处仅影响画面,不影响游戏内逻辑,游戏内逻辑地图大小请在 core.js 中修改 */
|
||||||
@ -19,6 +23,18 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
|
|||||||
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
|
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
|
||||||
/** 地图高度的一半 */
|
/** 地图高度的一半 */
|
||||||
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
|
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
|
||||||
|
/**
|
||||||
|
* 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小,
|
||||||
|
* 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。
|
||||||
|
*/
|
||||||
|
export const DYNAMIC_RESERVE = 16;
|
||||||
|
/**
|
||||||
|
* 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。
|
||||||
|
* 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。
|
||||||
|
*/
|
||||||
|
export const MOVING_TOLERANCE = 60;
|
||||||
|
/** 开关门动画的动画时长 */
|
||||||
|
export const DOOR_ANIMATE_INTERVAL = 50;
|
||||||
|
|
||||||
//#region 状态栏
|
//#region 状态栏
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export * from './controller';
|
|||||||
export * from './main';
|
export * from './main';
|
||||||
export * from './save';
|
export * from './save';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
|
export * from './statistics';
|
||||||
export * from './statusBar';
|
export * from './statusBar';
|
||||||
export * from './toolbar';
|
export * from './toolbar';
|
||||||
export * from './viewmap';
|
export * from './viewmap';
|
||||||
|
|||||||
@ -241,10 +241,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const startIndex = getPosIndex(core.saves.saveIndex - 1);
|
const startIndex = getPosIndex(core.saves.saveIndex);
|
||||||
selected.value = startIndex;
|
selected.value = startIndex - 1;
|
||||||
pageRef.value?.changePage(
|
pageRef.value?.changePage(
|
||||||
Math.floor((core.saves.saveIndex - 1) / (grid.value.count - 1))
|
Math.floor(core.saves.saveIndex / (grid.value.count - 1))
|
||||||
);
|
);
|
||||||
updateDataList(now.value);
|
updateDataList(now.value);
|
||||||
});
|
});
|
||||||
@ -380,10 +380,10 @@ export const Save = defineComponent<SaveProps, SaveEmits, keyof SaveEmits>(
|
|||||||
'@save_right',
|
'@save_right',
|
||||||
() => {
|
() => {
|
||||||
const count = grid.value.count;
|
const count = grid.value.count;
|
||||||
if (selected.value < count - 1) {
|
if (selected.value < count) {
|
||||||
selected.value++;
|
selected.value++;
|
||||||
} else {
|
} else {
|
||||||
selected.value = 1;
|
selected.value = 0;
|
||||||
pageRef.value?.movePage(1);
|
pageRef.value?.movePage(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { generateKeyboardEvent } from '@motajs/system-action';
|
|||||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||||
import { getAllSavesData, getSaveData, syncFromServer } from '../utils';
|
import { getAllSavesData, getSaveData, syncFromServer } from '../utils';
|
||||||
import { getInput } from '../components';
|
import { getInput } from '../components';
|
||||||
|
import { openStatistics } from './statistics';
|
||||||
import { saveWithExist } from './save';
|
import { saveWithExist } from './save';
|
||||||
import { compressToBase64 } from 'lz-string';
|
import { compressToBase64 } from 'lz-string';
|
||||||
import { ViewMapUI } from './viewmap';
|
import { ViewMapUI } from './viewmap';
|
||||||
@ -126,7 +127,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
|
|||||||
choices={choices}
|
choices={choices}
|
||||||
width={POP_BOX_WIDTH}
|
width={POP_BOX_WIDTH}
|
||||||
onChoose={choose}
|
onChoose={choose}
|
||||||
maxHeight={MAIN_HEIGHT - 32}
|
maxHeight={MAIN_HEIGHT - 64}
|
||||||
interval={8}
|
interval={8}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
/>
|
/>
|
||||||
@ -245,7 +246,6 @@ export const ReplaySettings = defineComponent<MainSettingsProps>(props => {
|
|||||||
onChoose={choose}
|
onChoose={choose}
|
||||||
interval={8}
|
interval={8}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
maxHeight={MAIN_HEIGHT - 32}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, mainSettingsProps);
|
}, mainSettingsProps);
|
||||||
@ -275,12 +275,7 @@ export const GameInfo = defineComponent<MainSettingsProps>(props => {
|
|||||||
const choose = async (key: ChoiceKey) => {
|
const choose = async (key: ChoiceKey) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case GameInfoChoice.Statistics: {
|
case GameInfoChoice.Statistics: {
|
||||||
getConfirm(
|
openStatistics(props.controller);
|
||||||
props.controller,
|
|
||||||
'数据统计尚未完工',
|
|
||||||
CENTER_LOC,
|
|
||||||
POP_BOX_WIDTH
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GameInfoChoice.Project: {
|
case GameInfoChoice.Project: {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { CloudLike } from './cloudLike';
|
import { CloudLike } from './cloudLike';
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-core';
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
|
|
||||||
export class CloudWeather extends CloudLike {
|
export class CloudWeather extends CloudLike {
|
||||||
getImage(): SizedCanvasImageSource | null {
|
getImage(): SizedCanvasImageSource | null {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core';
|
||||||
import { Weather } from '../weather';
|
import { Weather } from '../weather';
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-core';
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
|
|
||||||
export abstract class CloudLike extends Weather<Sprite> {
|
export abstract class CloudLike extends Weather<Sprite> {
|
||||||
/** 不透明度 */
|
/** 不透明度 */
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
import { MotaOffscreenCanvas2D } from '@motajs/render-core';
|
||||||
import { CloudLike } from './cloudLike';
|
import { CloudLike } from './cloudLike';
|
||||||
import { SizedCanvasImageSource } from '@motajs/render-core';
|
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||||
|
|
||||||
export class FogWeather extends CloudLike {
|
export class FogWeather extends CloudLike {
|
||||||
/** 雾天气的图像比较小,因此将四个进行合并 */
|
/** 雾天气的图像比较小,因此将四个进行合并 */
|
||||||
|
|||||||
@ -11,6 +11,8 @@ interface GameLoadEvent {
|
|||||||
coreInit: [];
|
coreInit: [];
|
||||||
/** 当所有启动必要资源加载完毕后触发 */
|
/** 当所有启动必要资源加载完毕后触发 */
|
||||||
loaded: [];
|
loaded: [];
|
||||||
|
/** 当资源构建完毕后触发,后续需要用新的加载系统替代 */
|
||||||
|
assetBuilt: [];
|
||||||
/** 当客户端(渲染端)和数据端都挂载完毕后触发 */
|
/** 当客户端(渲染端)和数据端都挂载完毕后触发 */
|
||||||
registered: [];
|
registered: [];
|
||||||
/** 当数据端挂载完毕后触发 */
|
/** 当数据端挂载完毕后触发 */
|
||||||
|
|||||||
63
packages-user/data-state/src/common/face.ts
Normal file
63
packages-user/data-state/src/common/face.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { IFaceData, IRoleFaceBinder } from './types';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { FaceDirection } from '.';
|
||||||
|
|
||||||
|
interface FaceInfo {
|
||||||
|
/** 此图块的朝向 */
|
||||||
|
readonly face: FaceDirection;
|
||||||
|
/** 此图块对应的映射 */
|
||||||
|
readonly map: Map<FaceDirection, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoleFaceBinder implements IRoleFaceBinder {
|
||||||
|
/** 每个图块对应的朝向信息 */
|
||||||
|
private faceMap: Map<number, FaceInfo> = new Map();
|
||||||
|
/** 主要朝向映射 */
|
||||||
|
private mainMap: Map<number, FaceDirection> = new Map();
|
||||||
|
|
||||||
|
malloc(identifier: number, main: FaceDirection): void {
|
||||||
|
this.mainMap.set(identifier, main);
|
||||||
|
const map = new Map<FaceDirection, number>();
|
||||||
|
map.set(main, identifier);
|
||||||
|
const info: FaceInfo = { face: main, map };
|
||||||
|
this.faceMap.set(identifier, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(identifier: number, main: number, face: FaceDirection): void {
|
||||||
|
const mainFace = this.mainMap.get(main);
|
||||||
|
if (isNil(mainFace)) {
|
||||||
|
logger.error(43, main.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mainFace === face) {
|
||||||
|
logger.error(44, main.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { map } = this.faceMap.get(main)!;
|
||||||
|
map.set(face, identifier);
|
||||||
|
const info: FaceInfo = { face, map };
|
||||||
|
this.faceMap.set(identifier, info);
|
||||||
|
this.mainMap.set(identifier, mainFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null {
|
||||||
|
const info = this.faceMap.get(identifier);
|
||||||
|
if (!info) return null;
|
||||||
|
const target = info.map.get(face);
|
||||||
|
if (isNil(target)) return null;
|
||||||
|
const data: IFaceData = { identifier: target, face };
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFaceDirection(identifier: number): FaceDirection | undefined {
|
||||||
|
return this.faceMap.get(identifier)?.face;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMainFace(identifier: number): IFaceData | null {
|
||||||
|
const face = this.mainMap.get(identifier);
|
||||||
|
if (isNil(face)) return null;
|
||||||
|
const data: IFaceData = { identifier, face };
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages-user/data-state/src/common/index.ts
Normal file
3
packages-user/data-state/src/common/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './face';
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
54
packages-user/data-state/src/common/types.ts
Normal file
54
packages-user/data-state/src/common/types.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
export const enum FaceDirection {
|
||||||
|
Unknown,
|
||||||
|
Left,
|
||||||
|
Up,
|
||||||
|
Right,
|
||||||
|
Down,
|
||||||
|
LeftUp,
|
||||||
|
RightUp,
|
||||||
|
LeftDown,
|
||||||
|
RightDown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFaceData {
|
||||||
|
/** 图块数字 */
|
||||||
|
readonly identifier: number;
|
||||||
|
/** 图块朝向 */
|
||||||
|
readonly face: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoleFaceBinder {
|
||||||
|
/**
|
||||||
|
* 给指定的图块分配朝向绑定
|
||||||
|
* @param identifier 图块数字
|
||||||
|
* @param main 主图块朝向,一般是朝下
|
||||||
|
*/
|
||||||
|
malloc(identifier: number, main: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一个图块与另一个图块绑定朝向,需要注意要先调用 {@link malloc} 分配朝向信息
|
||||||
|
* @param identifier 当前图块数字
|
||||||
|
* @param main 主图块数字,即当前图块与目标图块属于主图块的另一个朝向
|
||||||
|
* @param face 当前图块的朝向方向
|
||||||
|
*/
|
||||||
|
bind(identifier: number, main: number, face: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一个图块指定朝向的图块数字
|
||||||
|
* @param identifier 图块数字,可以是任意朝向的图块数字
|
||||||
|
* @param face 要获取的朝向
|
||||||
|
*/
|
||||||
|
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定图块数字是哪个朝向
|
||||||
|
* @param identifier 图块数字
|
||||||
|
*/
|
||||||
|
getFaceDirection(identifier: number): FaceDirection | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定图块数字绑定至的主朝向
|
||||||
|
* @param identifier 图块数字,可以是任意朝向的图块数字
|
||||||
|
*/
|
||||||
|
getMainFace(identifier: number): IFaceData | null;
|
||||||
|
}
|
||||||
182
packages-user/data-state/src/common/utils.ts
Normal file
182
packages-user/data-state/src/common/utils.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { FaceDirection } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定朝向的坐标偏移量
|
||||||
|
* @param dir 朝向
|
||||||
|
*/
|
||||||
|
export function getFaceMovement(dir: FaceDirection): Loc {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.Left:
|
||||||
|
return { x: -1, y: 0 };
|
||||||
|
case FaceDirection.Right:
|
||||||
|
return { x: 1, y: 0 };
|
||||||
|
case FaceDirection.Up:
|
||||||
|
return { x: 0, y: -1 };
|
||||||
|
case FaceDirection.Down:
|
||||||
|
return { x: 0, y: 1 };
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return { x: -1, y: -1 };
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return { x: 1, y: -1 };
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return { x: -1, y: 1 };
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return { x: 1, y: 1 };
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将八方向朝向降级为四方向朝向
|
||||||
|
* @param dir 朝向
|
||||||
|
* @param unknown 如果朝向是 `FaceDirection.Unknown`,那么会返回什么,默认还是未知
|
||||||
|
*/
|
||||||
|
export function degradeFace(
|
||||||
|
dir: FaceDirection,
|
||||||
|
unknown: FaceDirection = FaceDirection.Unknown
|
||||||
|
): FaceDirection {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return unknown;
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定朝向旋转后的朝向
|
||||||
|
* @param dir 当前朝向
|
||||||
|
* @param anticlockwise 是否逆时针旋转,默认顺时针
|
||||||
|
* @param face8 是否使用八朝向。为 `false` 时,旋转为九十度旋转,即 上->右->下->左,左上->右上->右下->左下。
|
||||||
|
* 为 `true` 时,旋转为四十五度旋转,即 上->右上->右->右下->下->左下->左->左上。逆时针反过来旋转。
|
||||||
|
*/
|
||||||
|
export function nextFaceDirection(
|
||||||
|
dir: FaceDirection,
|
||||||
|
anticlockwise: boolean = false,
|
||||||
|
face8: boolean = false
|
||||||
|
): FaceDirection {
|
||||||
|
if (face8) {
|
||||||
|
if (anticlockwise) {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.Left:
|
||||||
|
return FaceDirection.LeftDown;
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return FaceDirection.Down;
|
||||||
|
case FaceDirection.Down:
|
||||||
|
return FaceDirection.RightDown;
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.Right:
|
||||||
|
return FaceDirection.RightUp;
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return FaceDirection.Up;
|
||||||
|
case FaceDirection.Up:
|
||||||
|
return FaceDirection.LeftUp;
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.Left:
|
||||||
|
return FaceDirection.LeftUp;
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return FaceDirection.Up;
|
||||||
|
case FaceDirection.Up:
|
||||||
|
return FaceDirection.RightUp;
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.Right:
|
||||||
|
return FaceDirection.RightDown;
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return FaceDirection.Down;
|
||||||
|
case FaceDirection.Down:
|
||||||
|
return FaceDirection.LeftDown;
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (anticlockwise) {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.Left:
|
||||||
|
return FaceDirection.Down;
|
||||||
|
case FaceDirection.Down:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.Right:
|
||||||
|
return FaceDirection.Up;
|
||||||
|
case FaceDirection.Up:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return FaceDirection.LeftDown;
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return FaceDirection.RightDown;
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return FaceDirection.RightUp;
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return FaceDirection.LeftUp;
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (dir) {
|
||||||
|
case FaceDirection.Left:
|
||||||
|
return FaceDirection.Up;
|
||||||
|
case FaceDirection.Up:
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case FaceDirection.Right:
|
||||||
|
return FaceDirection.Down;
|
||||||
|
case FaceDirection.Down:
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case FaceDirection.LeftUp:
|
||||||
|
return FaceDirection.RightUp;
|
||||||
|
case FaceDirection.RightUp:
|
||||||
|
return FaceDirection.RightDown;
|
||||||
|
case FaceDirection.RightDown:
|
||||||
|
return FaceDirection.LeftDown;
|
||||||
|
case FaceDirection.LeftDown:
|
||||||
|
return FaceDirection.LeftUp;
|
||||||
|
case FaceDirection.Unknown:
|
||||||
|
return FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据朝向字符串获取朝向枚举值
|
||||||
|
* @param dir 朝向字符串
|
||||||
|
*/
|
||||||
|
export function fromDirectionString(dir: Dir2): FaceDirection {
|
||||||
|
switch (dir) {
|
||||||
|
case 'left':
|
||||||
|
return FaceDirection.Left;
|
||||||
|
case 'right':
|
||||||
|
return FaceDirection.Right;
|
||||||
|
case 'up':
|
||||||
|
return FaceDirection.Up;
|
||||||
|
case 'down':
|
||||||
|
return FaceDirection.Down;
|
||||||
|
case 'leftup':
|
||||||
|
return FaceDirection.LeftUp;
|
||||||
|
case 'rightup':
|
||||||
|
return FaceDirection.RightUp;
|
||||||
|
case 'leftdown':
|
||||||
|
return FaceDirection.LeftDown;
|
||||||
|
case 'rightdown':
|
||||||
|
return FaceDirection.RightDown;
|
||||||
|
default:
|
||||||
|
return FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages-user/data-state/src/core.ts
Normal file
33
packages-user/data-state/src/core.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ICoreState, IStateSaveData } from './types';
|
||||||
|
import { IHeroState, HeroState } from './hero';
|
||||||
|
import { ILayerState, LayerState } from './map';
|
||||||
|
import { IRoleFaceBinder, RoleFaceBinder } from './common';
|
||||||
|
|
||||||
|
export class CoreState implements ICoreState {
|
||||||
|
readonly layer: ILayerState;
|
||||||
|
readonly hero: IHeroState;
|
||||||
|
readonly roleFace: IRoleFaceBinder;
|
||||||
|
readonly idNumberMap: Map<string, number>;
|
||||||
|
readonly numberIdMap: Map<number, string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.layer = new LayerState();
|
||||||
|
this.hero = new HeroState();
|
||||||
|
this.roleFace = new RoleFaceBinder();
|
||||||
|
this.idNumberMap = new Map();
|
||||||
|
this.numberIdMap = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(): IStateSaveData {
|
||||||
|
return structuredClone({
|
||||||
|
followers: this.hero.followers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(data: IStateSaveData): void {
|
||||||
|
this.hero.removeAllFollowers();
|
||||||
|
data?.followers.forEach(v => {
|
||||||
|
this.hero.addFollower(v.num, v.identifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { getHeroStatusOf, getHeroStatusOn } from '../state/hero';
|
import { getHeroStatusOf, getHeroStatusOn } from '../legacy/hero';
|
||||||
import { Range, ensureArray, has, manhattan } from '@user/data-utils';
|
import { Range, ensureArray, has, manhattan } from '@user/data-utils';
|
||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { hook } from '@user/data-base';
|
import { hook } from '@user/data-base';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { EnemyInfo } from '@motajs/types';
|
import { EnemyInfo } from '@motajs/types';
|
||||||
import { getHeroStatusOn } from '../state/hero';
|
import { getHeroStatusOn } from '../legacy/hero';
|
||||||
|
|
||||||
export interface SpecialDeclaration {
|
export interface SpecialDeclaration {
|
||||||
code: number;
|
code: number;
|
||||||
|
|||||||
2
packages-user/data-state/src/hero/index.ts
Normal file
2
packages-user/data-state/src/hero/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './state';
|
||||||
|
export * from './types';
|
||||||
133
packages-user/data-state/src/hero/state.ts
Normal file
133
packages-user/data-state/src/hero/state.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { Hookable, HookController, IHookController } from '@motajs/common';
|
||||||
|
import { IHeroFollower, IHeroState, IHeroStateHooks } from './types';
|
||||||
|
import { FaceDirection, getFaceMovement, nextFaceDirection } from '../common';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { DEFAULT_HERO_IMAGE } from '../shared';
|
||||||
|
|
||||||
|
export class HeroState extends Hookable<IHeroStateHooks> implements IHeroState {
|
||||||
|
x: number = 0;
|
||||||
|
y: number = 0;
|
||||||
|
direction: FaceDirection = FaceDirection.Down;
|
||||||
|
image: ImageIds = DEFAULT_HERO_IMAGE;
|
||||||
|
|
||||||
|
/** 当前勇士是否正在移动 */
|
||||||
|
moving: boolean = false;
|
||||||
|
alpha: number = 1;
|
||||||
|
|
||||||
|
readonly followers: IHeroFollower[] = [];
|
||||||
|
|
||||||
|
protected createController(
|
||||||
|
hook: Partial<IHeroStateHooks>
|
||||||
|
): IHookController<IHeroStateHooks> {
|
||||||
|
return new HookController(this, hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(x: number, y: number): void {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onSetPosition?.(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
turn(direction?: FaceDirection): void {
|
||||||
|
const next = isNil(direction)
|
||||||
|
? nextFaceDirection(this.direction)
|
||||||
|
: direction;
|
||||||
|
this.direction = next;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onTurnHero?.(next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startMove(): void {
|
||||||
|
this.moving = true;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onStartMove?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(dir: FaceDirection, time: number = 100): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onMoveHero?.(dir, time);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { x, y } = getFaceMovement(dir);
|
||||||
|
this.x += x;
|
||||||
|
this.y += y;
|
||||||
|
}
|
||||||
|
|
||||||
|
async endMove(): Promise<void> {
|
||||||
|
if (!this.moving) return;
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onEndMove?.();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async jumpHero(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number = 500,
|
||||||
|
waitFollower: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onJumpHero?.(x, y, time, waitFollower);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(image: ImageIds): void {
|
||||||
|
this.image = image;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onSetImage?.(image);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlpha(alpha: number): void {
|
||||||
|
this.alpha = alpha;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onSetAlpha?.(alpha);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFollowerAlpha(identifier: string, alpha: number): void {
|
||||||
|
const follower = this.followers.find(v => v.identifier === identifier);
|
||||||
|
if (!follower) return;
|
||||||
|
follower.alpha = alpha;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onSetFollowerAlpha?.(identifier, alpha);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFollower(follower: number, identifier: string): void {
|
||||||
|
this.followers.push({ num: follower, identifier, alpha: 1 });
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onAddFollower?.(follower, identifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFollower(identifier: string, animate: boolean = false): void {
|
||||||
|
const index = this.followers.findIndex(
|
||||||
|
v => v.identifier === identifier
|
||||||
|
);
|
||||||
|
if (index === -1) return;
|
||||||
|
this.followers.splice(index, 1);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onRemoveFollower?.(identifier, animate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllFollowers(): void {
|
||||||
|
this.followers.length = 0;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onRemoveAllFollowers?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
204
packages-user/data-state/src/hero/types.ts
Normal file
204
packages-user/data-state/src/hero/types.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { IHookBase, IHookable } from '@motajs/common';
|
||||||
|
import { FaceDirection } from '../common/types';
|
||||||
|
|
||||||
|
export const enum HeroAnimateDirection {
|
||||||
|
/** 正向播放动画 */
|
||||||
|
Forward,
|
||||||
|
/** 反向播放动画 */
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeroFollower {
|
||||||
|
/** 跟随者的图块数字 */
|
||||||
|
readonly num: number;
|
||||||
|
/** 跟随者的标识符 */
|
||||||
|
readonly identifier: string;
|
||||||
|
/** 跟随者的不透明度 */
|
||||||
|
alpha: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeroStateHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当设置勇士的坐标时触发
|
||||||
|
* @param controller 钩子控制器
|
||||||
|
* @param x 勇士横坐标
|
||||||
|
* @param y 勇士纵坐标
|
||||||
|
*/
|
||||||
|
onSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当设置勇士朝向时触发
|
||||||
|
* @param direction 勇士朝向
|
||||||
|
*/
|
||||||
|
onTurnHero(direction: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当勇士开始移动时触发
|
||||||
|
*/
|
||||||
|
onStartMove(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移动勇士时触发
|
||||||
|
* @param controller 钩子控制器
|
||||||
|
* @param direction 移动方向
|
||||||
|
* @param time 移动动画时长
|
||||||
|
*/
|
||||||
|
onMoveHero(direction: FaceDirection, time: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当停止移动时触发
|
||||||
|
*/
|
||||||
|
onEndMove(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当勇士跳跃时触发
|
||||||
|
* @param x 目标点横坐标
|
||||||
|
* @param y 目标点纵坐标
|
||||||
|
* @param time 跳跃动画时长
|
||||||
|
* @param waitFollower 是否等待跟随者跳跃完毕
|
||||||
|
*/
|
||||||
|
onJumpHero(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time: number,
|
||||||
|
waitFollower: boolean
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当设置勇士图片时触发
|
||||||
|
* @param image 勇士图片 id
|
||||||
|
*/
|
||||||
|
onSetImage(image: ImageIds): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当设置勇士不透明度时执行
|
||||||
|
* @param alpha 不透明度
|
||||||
|
*/
|
||||||
|
onSetAlpha(alpha: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加跟随者时触发
|
||||||
|
* @param follower 跟随者的图块数字
|
||||||
|
* @param identifier 跟随者的标识符
|
||||||
|
*/
|
||||||
|
onAddFollower(follower: number, identifier: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移除跟随者时触发
|
||||||
|
* @param identifier 跟随者的标识符
|
||||||
|
* @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格
|
||||||
|
*/
|
||||||
|
onRemoveFollower(identifier: string, animate: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移除所有跟随者时触发
|
||||||
|
*/
|
||||||
|
onRemoveAllFollowers(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置跟随者的不透明度
|
||||||
|
* @param identifier 跟随者标识符
|
||||||
|
* @param alpha 跟随者不透明度
|
||||||
|
*/
|
||||||
|
onSetFollowerAlpha(identifier: string, alpha: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeroState extends IHookable<IHeroStateHooks> {
|
||||||
|
/** 勇士横坐标 */
|
||||||
|
readonly x: number;
|
||||||
|
/** 勇士纵坐标 */
|
||||||
|
readonly y: number;
|
||||||
|
/** 勇士朝向 */
|
||||||
|
readonly direction: FaceDirection;
|
||||||
|
/** 勇士图片 */
|
||||||
|
readonly image?: ImageIds;
|
||||||
|
/** 跟随者列表 */
|
||||||
|
readonly followers: readonly IHeroFollower[];
|
||||||
|
/** 勇士当前的不透明度 */
|
||||||
|
readonly alpha: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士位置
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
*/
|
||||||
|
setPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士朝向
|
||||||
|
* @param direction 勇士朝向,不填表示顺时针旋转
|
||||||
|
*/
|
||||||
|
turn(direction?: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始勇士移动,在移动前必须先调用此方法将勇士切换为移动状态
|
||||||
|
*/
|
||||||
|
startMove(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动勇士。能否移动的逻辑暂时不在这里,目前作为过渡作用,仅服务于渲染
|
||||||
|
* @param dir 移动方向
|
||||||
|
* @param time 移动动画时长,默认 100ms
|
||||||
|
* @returns 移动的 `Promise`,当相关的移动动画结束后兑现
|
||||||
|
*/
|
||||||
|
move(dir: FaceDirection, time?: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束勇士移动
|
||||||
|
* @returns 当移动动画结束后兑现的 `Promise`
|
||||||
|
*/
|
||||||
|
endMove(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳跃勇士至目标点
|
||||||
|
* @param x 目标点横坐标
|
||||||
|
* @param y 目标点纵坐标
|
||||||
|
* @param time 跳跃动画时长,默认 500ms
|
||||||
|
* @param waitFollower 是否等待跟随者跳跃完毕,默认不等待
|
||||||
|
* @returns 跳跃的 `Promise`,当相关的移动动画结束后兑现
|
||||||
|
*/
|
||||||
|
jumpHero(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
time?: number,
|
||||||
|
waitFollower?: boolean
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士图片
|
||||||
|
* @param image 图片 id
|
||||||
|
*/
|
||||||
|
setImage(image: ImageIds): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置勇士的不透明度
|
||||||
|
* @param alpha 不透明度
|
||||||
|
*/
|
||||||
|
setAlpha(alpha: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个跟随者
|
||||||
|
* @param follower 跟随者的图块数字
|
||||||
|
* @param identifier 跟随者的标识符,可以用来移除
|
||||||
|
*/
|
||||||
|
addFollower(follower: number, identifier: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定的跟随者
|
||||||
|
* @param identifier 跟随者的标识符
|
||||||
|
* @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格
|
||||||
|
*/
|
||||||
|
removeFollower(identifier: string, animate?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除所有跟随者
|
||||||
|
*/
|
||||||
|
removeAllFollowers(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定跟随者的不透明度
|
||||||
|
* @param identifier 跟随者标识符
|
||||||
|
* @param alpha 跟随者不透明度
|
||||||
|
*/
|
||||||
|
setFollowerAlpha(identifier: string, alpha: number): void;
|
||||||
|
}
|
||||||
@ -1,2 +1,75 @@
|
|||||||
|
import { loading } from '@user/data-base';
|
||||||
|
import { CoreState } from './core';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { FaceDirection } from './common';
|
||||||
|
|
||||||
|
function createCoreState() {
|
||||||
|
//#region 地图部分
|
||||||
|
|
||||||
|
const width = core._WIDTH_;
|
||||||
|
const height = core._HEIGHT_;
|
||||||
|
const bg = state.layer.addLayer(width, height);
|
||||||
|
const bg2 = state.layer.addLayer(width, height);
|
||||||
|
const event = state.layer.addLayer(width, height);
|
||||||
|
const fg = state.layer.addLayer(width, height);
|
||||||
|
const fg2 = state.layer.addLayer(width, height);
|
||||||
|
state.layer.setLayerAlias(bg, 'bg');
|
||||||
|
state.layer.setLayerAlias(bg2, 'bg2');
|
||||||
|
state.layer.setLayerAlias(event, 'event');
|
||||||
|
state.layer.setLayerAlias(fg, 'fg');
|
||||||
|
state.layer.setLayerAlias(fg2, 'fg2');
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 图块部分
|
||||||
|
|
||||||
|
const data = Object.entries(core.maps.blocksInfo);
|
||||||
|
for (const [key, block] of data) {
|
||||||
|
const num = Number(key);
|
||||||
|
state.idNumberMap.set(block.id, num);
|
||||||
|
state.numberIdMap.set(num, block.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, block] of data) {
|
||||||
|
if (!block.faceIds) continue;
|
||||||
|
const { down, up, left, right } = block.faceIds;
|
||||||
|
const downNum = state.idNumberMap.get(down);
|
||||||
|
if (downNum !== Number(key)) continue;
|
||||||
|
const upNum = state.idNumberMap.get(up);
|
||||||
|
const leftNum = state.idNumberMap.get(left);
|
||||||
|
const rightNum = state.idNumberMap.get(right);
|
||||||
|
state.roleFace.malloc(downNum, FaceDirection.Down);
|
||||||
|
if (!isNil(upNum)) {
|
||||||
|
state.roleFace.bind(upNum, downNum, FaceDirection.Up);
|
||||||
|
}
|
||||||
|
if (!isNil(leftNum)) {
|
||||||
|
state.roleFace.bind(leftNum, downNum, FaceDirection.Left);
|
||||||
|
}
|
||||||
|
if (!isNil(rightNum)) {
|
||||||
|
state.roleFace.bind(rightNum, downNum, FaceDirection.Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create() {
|
||||||
|
loading.once('loaded', () => {
|
||||||
|
// 加载后初始化全局状态
|
||||||
|
createCoreState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据端核心状态,目前处于过渡阶段,仅服务于渲染,不负责任何逻辑计算,会在后续把核心逻辑逐渐移动至此对象。
|
||||||
|
* 此对象是数据端状态,本身不负责任何渲染操作,仅会向渲染端发送数据让渲染端渲染,不要把渲染操作直接放到此对象上,
|
||||||
|
* 否则可能导致录像验证失败。
|
||||||
|
*/
|
||||||
|
export const state = new CoreState();
|
||||||
|
|
||||||
|
export * from './common';
|
||||||
|
export * from './core';
|
||||||
export * from './enemy';
|
export * from './enemy';
|
||||||
export * from './state';
|
export * from './hero';
|
||||||
|
export * from './map';
|
||||||
|
export * from './legacy';
|
||||||
|
|||||||
115
packages-user/data-state/src/legacy/hero.ts
Normal file
115
packages-user/data-state/src/legacy/hero.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* 获取勇士在某一点的属性
|
||||||
|
* @param name 要获取的勇士属性
|
||||||
|
* @param floorId 勇士所在楼层
|
||||||
|
*/
|
||||||
|
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
|
||||||
|
export function getHeroStatusOn(
|
||||||
|
name: (keyof HeroStatus)[],
|
||||||
|
floorId?: FloorIds
|
||||||
|
): Partial<HeroStatus>;
|
||||||
|
export function getHeroStatusOn<K extends keyof HeroStatus>(
|
||||||
|
name: K,
|
||||||
|
floorId?: FloorIds
|
||||||
|
): HeroStatus[K];
|
||||||
|
export function getHeroStatusOn(
|
||||||
|
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||||
|
floorId?: FloorIds
|
||||||
|
) {
|
||||||
|
// @ts-expect-error 暂时无法推导
|
||||||
|
return getHeroStatusOf(core.status.hero, name, floorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一定状态下的勇士在某一点的属性
|
||||||
|
* @param status 勇士的状态
|
||||||
|
* @param name 要获取的勇士属性
|
||||||
|
* @param floorId 勇士所在楼层
|
||||||
|
*/
|
||||||
|
export function getHeroStatusOf(
|
||||||
|
status: Partial<HeroStatus>,
|
||||||
|
name: 'all',
|
||||||
|
floorId?: FloorIds
|
||||||
|
): HeroStatus;
|
||||||
|
export function getHeroStatusOf(
|
||||||
|
status: Partial<HeroStatus>,
|
||||||
|
name: (keyof HeroStatus)[],
|
||||||
|
floorId?: FloorIds
|
||||||
|
): Partial<HeroStatus>;
|
||||||
|
export function getHeroStatusOf<K extends keyof HeroStatus>(
|
||||||
|
status: Partial<HeroStatus>,
|
||||||
|
name: K,
|
||||||
|
floorId?: FloorIds
|
||||||
|
): HeroStatus[K];
|
||||||
|
export function getHeroStatusOf(
|
||||||
|
status: DeepPartial<HeroStatus>,
|
||||||
|
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||||
|
floorId?: FloorIds
|
||||||
|
) {
|
||||||
|
return getRealStatus(status, name, floorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRealStatus(
|
||||||
|
status: DeepPartial<HeroStatus>,
|
||||||
|
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
||||||
|
floorId: FloorIds = core.status.floorId
|
||||||
|
): any {
|
||||||
|
if (name instanceof Array) {
|
||||||
|
const res: any = {};
|
||||||
|
name.forEach(v => {
|
||||||
|
res[v] = getRealStatus(status, v, floorId);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'all') {
|
||||||
|
const res: any = {};
|
||||||
|
for (const [key, value] of Object.entries(core.status.hero)) {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
res[key] = getRealStatus(
|
||||||
|
status,
|
||||||
|
key as keyof HeroStatus,
|
||||||
|
floorId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = (status[name] ?? core.status.hero[name]) as number;
|
||||||
|
if (s === null || s === void 0) {
|
||||||
|
throw new ReferenceError(
|
||||||
|
`Wrong hero status property name is delivered: ${name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof s !== 'number') return s;
|
||||||
|
|
||||||
|
// buff
|
||||||
|
s *= core.status.hero.buff[name] ?? 1;
|
||||||
|
s = Math.floor(s);
|
||||||
|
|
||||||
|
// 衰弱效果
|
||||||
|
if ((name === 'atk' || name === 'def') && flags.weak) {
|
||||||
|
const weak = core.values.weakValue;
|
||||||
|
if (weak < 1) {
|
||||||
|
// 百分比衰弱
|
||||||
|
s *= 1 - weak;
|
||||||
|
} else {
|
||||||
|
s -= weak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下面的内容暂时无用
|
||||||
|
|
||||||
|
export interface IHeroStatusDefault {
|
||||||
|
atk: number;
|
||||||
|
def: number;
|
||||||
|
hp: number;
|
||||||
|
}
|
||||||
3
packages-user/data-state/src/map/index.ts
Normal file
3
packages-user/data-state/src/map/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './layerState';
|
||||||
|
export * from './mapLayer';
|
||||||
|
export * from './types';
|
||||||
139
packages-user/data-state/src/map/layerState.ts
Normal file
139
packages-user/data-state/src/map/layerState.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
Hookable,
|
||||||
|
HookController,
|
||||||
|
IHookController,
|
||||||
|
logger
|
||||||
|
} from '@motajs/common';
|
||||||
|
import {
|
||||||
|
ILayerState,
|
||||||
|
ILayerStateHooks,
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerHookController,
|
||||||
|
IMapLayerHooks
|
||||||
|
} from './types';
|
||||||
|
import { MapLayer } from './mapLayer';
|
||||||
|
|
||||||
|
export class LayerState
|
||||||
|
extends Hookable<ILayerStateHooks>
|
||||||
|
implements ILayerState
|
||||||
|
{
|
||||||
|
readonly layerList: Set<IMapLayer> = new Set();
|
||||||
|
/** 图层到图层别名映射 */
|
||||||
|
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
|
||||||
|
/** 图层别名到图层的映射 */
|
||||||
|
readonly aliasLayerMap: Map<symbol, IMapLayer> = new Map();
|
||||||
|
|
||||||
|
/** 背景图块 */
|
||||||
|
private backgroundTile: number = 0;
|
||||||
|
|
||||||
|
/** 图层钩子映射 */
|
||||||
|
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
||||||
|
|
||||||
|
addLayer(width: number, height: number): IMapLayer {
|
||||||
|
const array = new Uint32Array(width * height);
|
||||||
|
const layer = new MapLayer(array, width, height);
|
||||||
|
this.layerList.add(layer);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateLayer?.(this.layerList);
|
||||||
|
});
|
||||||
|
const controller = layer.addHook(new StateMapLayerHook(this, layer));
|
||||||
|
this.layerHookMap.set(layer, controller);
|
||||||
|
controller.load();
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLayer(layer: IMapLayer): void {
|
||||||
|
this.layerList.delete(layer);
|
||||||
|
const alias = this.layerAliasMap.get(layer);
|
||||||
|
if (alias) {
|
||||||
|
const symbol = Symbol.for(alias);
|
||||||
|
this.aliasLayerMap.delete(symbol);
|
||||||
|
this.layerAliasMap.delete(layer);
|
||||||
|
}
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateLayer?.(this.layerList);
|
||||||
|
});
|
||||||
|
const controller = this.layerHookMap.get(layer);
|
||||||
|
if (!controller) return;
|
||||||
|
controller.unload();
|
||||||
|
this.layerHookMap.delete(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLayer(layer: IMapLayer): boolean {
|
||||||
|
return this.layerList.has(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayerAlias(layer: IMapLayer, alias: string): void {
|
||||||
|
const symbol = Symbol.for(alias);
|
||||||
|
if (this.aliasLayerMap.has(symbol)) {
|
||||||
|
logger.warn(84, alias);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.layerAliasMap.set(layer, alias);
|
||||||
|
this.aliasLayerMap.set(symbol, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerByAlias(alias: string): IMapLayer | null {
|
||||||
|
const symbol = Symbol.for(alias);
|
||||||
|
return this.aliasLayerMap.get(symbol) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerAlias(layer: IMapLayer): string | undefined {
|
||||||
|
return this.layerAliasMap.get(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeLayer(
|
||||||
|
layer: IMapLayer,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
keepBlock: boolean = false
|
||||||
|
): void {
|
||||||
|
if (keepBlock) {
|
||||||
|
layer.resize(width, height);
|
||||||
|
} else {
|
||||||
|
layer.resize2(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackground(tile: number): void {
|
||||||
|
this.backgroundTile = tile;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onChangeBackground?.(tile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getBackground(): number {
|
||||||
|
return this.backgroundTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createController(
|
||||||
|
hook: Partial<ILayerStateHooks>
|
||||||
|
): IHookController<ILayerStateHooks> {
|
||||||
|
return new HookController(this, hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateMapLayerHook implements Partial<IMapLayerHooks> {
|
||||||
|
constructor(
|
||||||
|
readonly state: LayerState,
|
||||||
|
readonly layer: IMapLayer
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
||||||
|
this.state.forEachHook(hook => {
|
||||||
|
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateBlock(block: number, x: number, y: number): void {
|
||||||
|
this.state.forEachHook(hook => {
|
||||||
|
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(width: number, height: number): void {
|
||||||
|
this.state.forEachHook(hook => {
|
||||||
|
hook.onResizeLayer?.(this.layer, width, height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
263
packages-user/data-state/src/map/mapLayer.ts
Normal file
263
packages-user/data-state/src/map/mapLayer.ts
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerData,
|
||||||
|
IMapLayerHookController,
|
||||||
|
IMapLayerHooks
|
||||||
|
} from './types';
|
||||||
|
import { Hookable, HookController, logger } from '@motajs/common';
|
||||||
|
|
||||||
|
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
||||||
|
|
||||||
|
export class MapLayer
|
||||||
|
extends Hookable<IMapLayerHooks, IMapLayerHookController>
|
||||||
|
implements IMapLayer
|
||||||
|
{
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
empty: boolean = true;
|
||||||
|
zIndex: number = 0;
|
||||||
|
|
||||||
|
/** 地图图块数组 */
|
||||||
|
private mapArray: Uint32Array;
|
||||||
|
/** 地图数据引用 */
|
||||||
|
private mapData: IMapLayerData;
|
||||||
|
|
||||||
|
constructor(array: Uint32Array, width: number, height: number) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
const area = width * height;
|
||||||
|
this.mapArray = new Uint32Array(area);
|
||||||
|
// 超出的裁剪,不足的补零
|
||||||
|
this.mapArray.set(array);
|
||||||
|
this.mapData = {
|
||||||
|
expired: false,
|
||||||
|
array: this.mapArray
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(width: number, height: number): void {
|
||||||
|
if (this.width === width && this.height === height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mapData.expired = true;
|
||||||
|
const before = this.mapArray;
|
||||||
|
const beforeWidth = this.width;
|
||||||
|
const beforeHeight = this.height;
|
||||||
|
const beforeArea = beforeWidth * beforeHeight;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
const area = width * height;
|
||||||
|
const newArray = new Uint32Array(area);
|
||||||
|
this.mapArray = newArray;
|
||||||
|
// 将原来的地图数组赋值给现在的
|
||||||
|
if (beforeArea > area) {
|
||||||
|
// 如果地图变小了,那么直接设置,不需要补零
|
||||||
|
for (let ny = 0; ny < height; ny++) {
|
||||||
|
const begin = ny * beforeWidth;
|
||||||
|
newArray.set(before.subarray(begin, begin + width), ny * width);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了
|
||||||
|
for (let ny = 0; ny < beforeHeight; ny++) {
|
||||||
|
const begin = ny * beforeWidth;
|
||||||
|
newArray.set(
|
||||||
|
before.subarray(begin, begin + beforeWidth),
|
||||||
|
ny * width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mapData = {
|
||||||
|
expired: false,
|
||||||
|
array: this.mapArray
|
||||||
|
};
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onResize?.(width, height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resize2(width: number, height: number): void {
|
||||||
|
if (this.width === width && this.height === height) {
|
||||||
|
this.mapArray.fill(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mapData.expired = true;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.mapArray = new Uint32Array(width * height);
|
||||||
|
this.mapData = {
|
||||||
|
expired: false,
|
||||||
|
array: this.mapArray
|
||||||
|
};
|
||||||
|
this.empty = true;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onResize?.(width, height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlock(block: number, x: number, y: number): void {
|
||||||
|
const index = y * this.width + x;
|
||||||
|
if (block === this.mapArray[index]) return;
|
||||||
|
this.mapArray[index] = block;
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateBlock?.(block, x, y);
|
||||||
|
});
|
||||||
|
if (block !== 0) {
|
||||||
|
this.empty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlock(x: number, y: number): number {
|
||||||
|
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||||
|
// 不在地图内,返回 -1
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.mapArray[y * this.width + x];
|
||||||
|
}
|
||||||
|
|
||||||
|
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
|
||||||
|
if (array.length % width !== 0) {
|
||||||
|
logger.warn(8);
|
||||||
|
}
|
||||||
|
const height = Math.ceil(array.length / width);
|
||||||
|
if (width === this.width && height === this.height) {
|
||||||
|
this.mapArray.set(array);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateArea?.(x, y, width, height);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const w = this.width;
|
||||||
|
const r = x + width;
|
||||||
|
const b = y + height;
|
||||||
|
if (x < 0 || y < 0 || r > w || b > this.height) {
|
||||||
|
logger.warn(9);
|
||||||
|
}
|
||||||
|
const nl = Math.max(x, 0);
|
||||||
|
const nt = Math.max(y, 0);
|
||||||
|
const nr = Math.min(r, w);
|
||||||
|
const nb = Math.min(b, this.height);
|
||||||
|
const nw = nr - nl;
|
||||||
|
const nh = nb - nt;
|
||||||
|
let empty = true;
|
||||||
|
for (let ny = 0; ny < nh; ny++) {
|
||||||
|
const start = ny * nw;
|
||||||
|
const offset = (ny + nt) * w + nl;
|
||||||
|
const sub = array.subarray(start, start + nw);
|
||||||
|
if (empty && sub.some(v => v !== 0)) {
|
||||||
|
// 空地图判断
|
||||||
|
empty = false;
|
||||||
|
}
|
||||||
|
this.mapArray.set(array.subarray(start, start + nw), offset);
|
||||||
|
}
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateArea?.(x, y, width, height);
|
||||||
|
});
|
||||||
|
this.empty &&= empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMapData(): Uint32Array;
|
||||||
|
getMapData(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Uint32Array;
|
||||||
|
getMapData(
|
||||||
|
x?: number,
|
||||||
|
y?: number,
|
||||||
|
width?: number,
|
||||||
|
height?: number
|
||||||
|
): Uint32Array {
|
||||||
|
if (isNil(x)) {
|
||||||
|
return new Uint32Array(this.mapArray);
|
||||||
|
}
|
||||||
|
if (isNil(y) || isNil(width) || isNil(height)) {
|
||||||
|
logger.warn(80);
|
||||||
|
return new Uint32Array();
|
||||||
|
}
|
||||||
|
const w = this.width;
|
||||||
|
const h = this.height;
|
||||||
|
const r = x + width;
|
||||||
|
const b = y + height;
|
||||||
|
if (x < 0 || y < 0 || r > w || b > h) {
|
||||||
|
logger.warn(81);
|
||||||
|
}
|
||||||
|
const res = new Uint32Array(width * height);
|
||||||
|
const arr = this.mapArray;
|
||||||
|
const nr = Math.min(r, w);
|
||||||
|
const nb = Math.min(b, h);
|
||||||
|
for (let nx = x; nx < nr; nx++) {
|
||||||
|
for (let ny = y; ny < nb; ny++) {
|
||||||
|
const origin = ny * w + nx;
|
||||||
|
const target = (ny - y) * width + (nx - x);
|
||||||
|
res[target] = arr[origin];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图数据的内部存储直接引用
|
||||||
|
*/
|
||||||
|
getMapRef(): IMapLayerData {
|
||||||
|
return this.mapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createController(
|
||||||
|
hook: Partial<IMapLayerHooks>
|
||||||
|
): IMapLayerHookController {
|
||||||
|
return new MapLayerHookController(this, hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
setZIndex(zIndex: number): void {
|
||||||
|
this.zIndex = zIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openDoor(x: number, y: number): Promise<void> {
|
||||||
|
const index = y * this.width + x;
|
||||||
|
const num = this.mapArray[index];
|
||||||
|
if (num === 0) return;
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onOpenDoor?.(x, y);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.setBlock(0, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeDoor(num: number, x: number, y: number): Promise<void> {
|
||||||
|
const index = y * this.width + x;
|
||||||
|
const nowNum = this.mapArray[index];
|
||||||
|
if (nowNum !== 0) {
|
||||||
|
logger.error(46, x.toString(), y.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onCloseDoor?.(num, x, y);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.setBlock(num, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLayerHookController
|
||||||
|
extends HookController<IMapLayerHooks>
|
||||||
|
implements IMapLayerHookController
|
||||||
|
{
|
||||||
|
hookable: MapLayer;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly layer: MapLayer,
|
||||||
|
hook: Partial<IMapLayerHooks>
|
||||||
|
) {
|
||||||
|
super(layer, hook);
|
||||||
|
this.hookable = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMapData(): Readonly<IMapLayerData> {
|
||||||
|
return this.layer.getMapRef();
|
||||||
|
}
|
||||||
|
}
|
||||||
278
packages-user/data-state/src/map/types.ts
Normal file
278
packages-user/data-state/src/map/types.ts
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
||||||
|
|
||||||
|
export interface IMapLayerData {
|
||||||
|
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||||
|
expired: boolean;
|
||||||
|
/** 地图图块数组,是对内部存储的直接引用 */
|
||||||
|
array: Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayerHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当地图大小发生变化时执行,如果调用了地图的 `resize` 方法,但是地图大小没变,则不会触发
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
onResize(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当更新某个区域的图块时执行
|
||||||
|
* @param x 更新区域左上角横坐标
|
||||||
|
* @param y 更新区域左上角纵坐标
|
||||||
|
* @param width 更新区域宽度
|
||||||
|
* @param height 更新区域高度
|
||||||
|
*/
|
||||||
|
onUpdateArea(x: number, y: number, width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法
|
||||||
|
* @param block 更新为的图块数字
|
||||||
|
* @param x 更新点横坐标
|
||||||
|
* @param y 更新点纵坐标
|
||||||
|
*/
|
||||||
|
onUpdateBlock(block: number, x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当开门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
onOpenDoor(x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当关门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
||||||
|
* @param num 门的图块数字
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayerHookController
|
||||||
|
extends IHookController<IMapLayerHooks> {
|
||||||
|
/** 拓展所属的图层对象 */
|
||||||
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图数据,是对内部存储的直接引用
|
||||||
|
*/
|
||||||
|
getMapData(): Readonly<IMapLayerData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayer
|
||||||
|
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
|
||||||
|
/** 地图宽度 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 地图高度 */
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* 地图是否全部空白,此值具有充分性,但不具有必要性,
|
||||||
|
* 即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白
|
||||||
|
*/
|
||||||
|
readonly empty: boolean;
|
||||||
|
/** 图层纵深 */
|
||||||
|
readonly zIndex: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整地图尺寸,维持原有图块。如果尺寸变大,那么会补零,如果尺寸变小,那么会将当前数组裁剪
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
resize(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整地图尺寸,但是将地图全部重置为零,不保留原地图数据
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
resize2(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置某一点的图块
|
||||||
|
* @param block 图块数字
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
setBlock(block: number, x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定点的图块
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
* @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1
|
||||||
|
*/
|
||||||
|
getBlock(x: number, y: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图图块
|
||||||
|
* @param array 地图图块数组
|
||||||
|
* @param x 数组第一项代表的横坐标
|
||||||
|
* @param y 数组第一项代表的纵坐标
|
||||||
|
* @param width 传入数组所表示的矩形范围的宽度
|
||||||
|
*/
|
||||||
|
putMapData(array: Uint32Array, x: number, y: number, width: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取整个地图的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
||||||
|
*/
|
||||||
|
getMapData(): Uint32Array;
|
||||||
|
/**
|
||||||
|
* 获取地图指定区域的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
||||||
|
* @param x 左上角横坐标
|
||||||
|
* @param y 左上角纵坐标
|
||||||
|
* @param width 获取区域的宽度
|
||||||
|
* @param height 获取区域的高度
|
||||||
|
*/
|
||||||
|
getMapData(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Uint32Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取整个地图的地图数组,是对内部数组的直接引用
|
||||||
|
*/
|
||||||
|
getMapRef(): IMapLayerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图纵深,会影响渲染的遮挡顺序
|
||||||
|
* @param zIndex 纵深
|
||||||
|
*/
|
||||||
|
setZIndex(zIndex: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启指定位置的门
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
openDoor(x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置关门,门的图块数字由参数指定
|
||||||
|
* @param num 门图块数字
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILayerStateHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当设置背景图块时执行,如果设置的背景图块与原先一样,则不会执行
|
||||||
|
* @param tile 背景图块
|
||||||
|
*/
|
||||||
|
onChangeBackground(tile: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图列表发生变化时执行
|
||||||
|
* @param layerList 地图图层列表
|
||||||
|
*/
|
||||||
|
onUpdateLayer(layerList: Set<IMapLayer>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层发生区域更新时执行
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param x 更新区域左上角横坐标
|
||||||
|
* @param y 更新区域左上角纵坐标
|
||||||
|
* @param width 更新区域宽度
|
||||||
|
* @param height 更新区域高度
|
||||||
|
*/
|
||||||
|
onUpdateLayerArea(
|
||||||
|
layer: IMapLayer,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层设置图块时执行,如果设置的图块与原先一样则不会触发
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param block 设置为的图块
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
onUpdateLayerBlock(
|
||||||
|
layer: IMapLayer,
|
||||||
|
block: number,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层大小发生变化时执行
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param width 地图的新宽度
|
||||||
|
* @param height 地图的新高度
|
||||||
|
*/
|
||||||
|
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||||
|
/** 地图列表 */
|
||||||
|
readonly layerList: Set<IMapLayer>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加图层
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
addLayer(width: number, height: number): IMapLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定图层
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
removeLayer(layer: IMapLayer): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前地图状态对象是否包含指定图层对象
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
hasLayer(layer: IMapLayer): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图层别名
|
||||||
|
* @param layer 图层对象
|
||||||
|
* @param alias 图层别名
|
||||||
|
*/
|
||||||
|
setLayerAlias(layer: IMapLayer, alias: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图层别名获取图层对象
|
||||||
|
* @param alias 图层别名
|
||||||
|
*/
|
||||||
|
getLayerByAlias(alias: string): IMapLayer | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图层对象的别名
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
getLayerAlias(layer: IMapLayer): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新设置图层的大小
|
||||||
|
* @param layer 图层对象
|
||||||
|
* @param width 新的图层宽度
|
||||||
|
* @param height 新的图层高度
|
||||||
|
* @param keepBlock 是否保留原有图块,默认不保留
|
||||||
|
*/
|
||||||
|
resizeLayer(
|
||||||
|
layer: IMapLayer,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
keepBlock?: boolean
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置背景图块
|
||||||
|
* @param tile 背景图块数字
|
||||||
|
*/
|
||||||
|
setBackground(tile: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取背景图块数字,如果没有设置过,则返回 0
|
||||||
|
*/
|
||||||
|
getBackground(): number;
|
||||||
|
}
|
||||||
2
packages-user/data-state/src/shared.ts
Normal file
2
packages-user/data-state/src/shared.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/** 默认的勇士图片 */
|
||||||
|
export const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png';
|
||||||
@ -1,332 +0,0 @@
|
|||||||
import { logger } from '@motajs/common';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取勇士在某一点的属性
|
|
||||||
* @param name 要获取的勇士属性
|
|
||||||
* @param floorId 勇士所在楼层
|
|
||||||
*/
|
|
||||||
export function getHeroStatusOn(name: 'all', floorId?: FloorIds): HeroStatus;
|
|
||||||
export function getHeroStatusOn(
|
|
||||||
name: (keyof HeroStatus)[],
|
|
||||||
floorId?: FloorIds
|
|
||||||
): Partial<HeroStatus>;
|
|
||||||
export function getHeroStatusOn<K extends keyof HeroStatus>(
|
|
||||||
name: K,
|
|
||||||
floorId?: FloorIds
|
|
||||||
): HeroStatus[K];
|
|
||||||
export function getHeroStatusOn(
|
|
||||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
|
||||||
floorId?: FloorIds
|
|
||||||
) {
|
|
||||||
// @ts-expect-error 暂时无法推导
|
|
||||||
return getHeroStatusOf(core.status.hero, name, floorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取一定状态下的勇士在某一点的属性
|
|
||||||
* @param status 勇士的状态
|
|
||||||
* @param name 要获取的勇士属性
|
|
||||||
* @param floorId 勇士所在楼层
|
|
||||||
*/
|
|
||||||
export function getHeroStatusOf(
|
|
||||||
status: Partial<HeroStatus>,
|
|
||||||
name: 'all',
|
|
||||||
floorId?: FloorIds
|
|
||||||
): HeroStatus;
|
|
||||||
export function getHeroStatusOf(
|
|
||||||
status: Partial<HeroStatus>,
|
|
||||||
name: (keyof HeroStatus)[],
|
|
||||||
floorId?: FloorIds
|
|
||||||
): Partial<HeroStatus>;
|
|
||||||
export function getHeroStatusOf<K extends keyof HeroStatus>(
|
|
||||||
status: Partial<HeroStatus>,
|
|
||||||
name: K,
|
|
||||||
floorId?: FloorIds
|
|
||||||
): HeroStatus[K];
|
|
||||||
export function getHeroStatusOf(
|
|
||||||
status: DeepPartial<HeroStatus>,
|
|
||||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
|
||||||
floorId?: FloorIds
|
|
||||||
) {
|
|
||||||
return getRealStatus(status, name, floorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRealStatus(
|
|
||||||
status: DeepPartial<HeroStatus>,
|
|
||||||
name: keyof HeroStatus | 'all' | (keyof HeroStatus)[],
|
|
||||||
floorId: FloorIds = core.status.floorId
|
|
||||||
): any {
|
|
||||||
if (name instanceof Array) {
|
|
||||||
const res: any = {};
|
|
||||||
name.forEach(v => {
|
|
||||||
res[v] = getRealStatus(status, v, floorId);
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'all') {
|
|
||||||
const res: any = {};
|
|
||||||
for (const [key, value] of Object.entries(core.status.hero)) {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
res[key] = getRealStatus(
|
|
||||||
status,
|
|
||||||
key as keyof HeroStatus,
|
|
||||||
floorId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
let s = (status[name] ?? core.status.hero[name]) as number;
|
|
||||||
if (s === null || s === void 0) {
|
|
||||||
throw new ReferenceError(
|
|
||||||
`Wrong hero status property name is delivered: ${name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof s !== 'number') return s;
|
|
||||||
|
|
||||||
// buff
|
|
||||||
s *= core.status.hero.buff[name] ?? 1;
|
|
||||||
s = Math.floor(s);
|
|
||||||
|
|
||||||
// 衰弱效果
|
|
||||||
if ((name === 'atk' || name === 'def') && flags.weak) {
|
|
||||||
const weak = core.values.weakValue;
|
|
||||||
if (weak < 1) {
|
|
||||||
// 百分比衰弱
|
|
||||||
s *= 1 - weak;
|
|
||||||
} else {
|
|
||||||
s -= weak;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下面的内容暂时无用
|
|
||||||
|
|
||||||
export interface IHeroStatusDefault {
|
|
||||||
atk: number;
|
|
||||||
def: number;
|
|
||||||
hp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeroStateEvent {
|
|
||||||
set: [key: string | number | symbol, value: any];
|
|
||||||
}
|
|
||||||
|
|
||||||
type HeroStatusCalculate = (
|
|
||||||
hero: HeroState<any>,
|
|
||||||
key: string | number | symbol,
|
|
||||||
value: any
|
|
||||||
) => any;
|
|
||||||
|
|
||||||
export class HeroState<
|
|
||||||
T extends object = IHeroStatusDefault
|
|
||||||
> extends EventEmitter<HeroStateEvent> {
|
|
||||||
readonly status: T;
|
|
||||||
readonly computedStatus: T;
|
|
||||||
|
|
||||||
readonly buffable: Set<keyof T> = new Set();
|
|
||||||
readonly buffMap: Map<keyof T, number> = new Map();
|
|
||||||
|
|
||||||
private static cal: HeroStatusCalculate = (_0, _1, value) => value;
|
|
||||||
|
|
||||||
constructor(init: T) {
|
|
||||||
super();
|
|
||||||
this.status = init;
|
|
||||||
this.computedStatus = cloneDeep(init);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置某个属性的值
|
|
||||||
* @param key 要设置的属性
|
|
||||||
* @param value 属性值
|
|
||||||
* @returns 是否设置成功
|
|
||||||
*/
|
|
||||||
setStatus<K extends keyof T>(key: K, value: T[K]): boolean {
|
|
||||||
this.status[key] = value;
|
|
||||||
this.emit('set', key, value);
|
|
||||||
return this.refreshStatus(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加或减少一个属性的值,只对数字型的属性有效
|
|
||||||
* @param key 要修改的属性
|
|
||||||
* @param value 属性的增量
|
|
||||||
* @returns 是否设置成功
|
|
||||||
*/
|
|
||||||
addStatus<K extends SelectKey<T, number>>(key: K, value: number): boolean {
|
|
||||||
if (typeof this.status[key] !== 'number') {
|
|
||||||
logger.warn(14, String(key));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.setStatus<K>(key, (this.status[key] + value) as T[K]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某个属性的原始值
|
|
||||||
* @param key 要获取的属性
|
|
||||||
* @returns 属性的值
|
|
||||||
*/
|
|
||||||
getStatus<K extends keyof T>(key: K): T[K] {
|
|
||||||
return this.status[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取一个属性计算后的值,也就是2.x所说的勇士真实属性
|
|
||||||
* @param key 要获取的属性值
|
|
||||||
*/
|
|
||||||
getComputedStatus<K extends keyof T>(key: K): T[K] {
|
|
||||||
return this.computedStatus[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标记某个属性为可以被buff加成
|
|
||||||
*/
|
|
||||||
markBuffable(key: SelectKey<T, number>): void {
|
|
||||||
if (typeof this.status[key] !== 'number') {
|
|
||||||
logger.warn(12, String(key));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.buffable.add(key);
|
|
||||||
this.buffMap.set(key, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置某个属性的buff值
|
|
||||||
* @param key 要设置buff的属性
|
|
||||||
* @param value buff值
|
|
||||||
* @returns 是否设置成功
|
|
||||||
*/
|
|
||||||
setBuff(key: SelectKey<T, number>, value: number): boolean {
|
|
||||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
|
||||||
logger.warn(13, String(key));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.buffMap.set(key, value);
|
|
||||||
return this.refreshStatus(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加或减少一个buff值
|
|
||||||
* @param key 要修改的buff属性
|
|
||||||
* @param value buff增量
|
|
||||||
* @returns 是否修改成功
|
|
||||||
*/
|
|
||||||
addBuff(key: SelectKey<T, number>, value: number): boolean {
|
|
||||||
if (!this.buffable.has(key) || typeof this.status[key] !== 'number') {
|
|
||||||
logger.warn(13, String(key));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.setBuff(key, this.buffMap.get(key)! + value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新某个或所有属性,重新进行计算
|
|
||||||
* @param key 要刷新的属性名,不填表示刷新所有属性
|
|
||||||
* @returns 是否计算成功
|
|
||||||
*/
|
|
||||||
refreshStatus(key?: keyof T): boolean {
|
|
||||||
if (key === void 0) {
|
|
||||||
for (const [key, value] of Object.entries(this.status)) {
|
|
||||||
// @ts-expect-error 暂时无法推导
|
|
||||||
this.computedStatus[key] = HeroState.cal(this, key, value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
this.computedStatus[key] = HeroState.cal(this, key, this.status[key]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算所有可以buff加成的属性
|
|
||||||
* @returns 是否计算成功
|
|
||||||
*/
|
|
||||||
refreshBuffable(): boolean {
|
|
||||||
for (const key of this.buffable) {
|
|
||||||
this.computedStatus[key] = HeroState.cal(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
this.status[key]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复写属性计算函数,默认函数不进行计算,直接将原属性返回
|
|
||||||
* @param fn 计算函数,传入两个参数,key表示属性名,value表示属性值,返回值表示计算结果
|
|
||||||
*/
|
|
||||||
static overrideCalculate(fn: HeroStatusCalculate) {
|
|
||||||
this.cal = fn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface _IHeroItem {
|
|
||||||
items: Map<AllIdsOf<'items'>, number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置勇士拥有的物品数量
|
|
||||||
* @param item 物品id
|
|
||||||
* @param value 物品数量
|
|
||||||
* @returns 是否设置成功
|
|
||||||
*/
|
|
||||||
setItem(item: AllIdsOf<'items'>, value: number): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增加或减少勇士拥有的物品数量
|
|
||||||
* @param item 物品id
|
|
||||||
* @param value 物品数量增量
|
|
||||||
* @returns 是否设置成功
|
|
||||||
*/
|
|
||||||
addItem(item: AllIdsOf<'items'>, value: number): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用一个物品
|
|
||||||
* @param item 物品id
|
|
||||||
* @returns 是否使用成功
|
|
||||||
*/
|
|
||||||
useItem(item: AllIdsOf<'items'>, x?: number, y?: number): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得一个物品
|
|
||||||
* @param item 物品id
|
|
||||||
* @param num 获得的数量
|
|
||||||
*/
|
|
||||||
getItem(item: AllIdsOf<'items'>, num: number): void;
|
|
||||||
/**
|
|
||||||
* 获得一个物品
|
|
||||||
* @param item 物品id
|
|
||||||
* @param x 物品所在x坐标
|
|
||||||
* @param y 物品所在y坐标
|
|
||||||
* @param floorId 物品所在楼层
|
|
||||||
* @param num 获得的数量
|
|
||||||
*/
|
|
||||||
getItem(
|
|
||||||
item: AllIdsOf<'items'>,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
floorId?: FloorIds,
|
|
||||||
num?: number
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取某个物品的数量
|
|
||||||
* @param item 物品id
|
|
||||||
*/
|
|
||||||
itemCount(item: AllIdsOf<'items'>): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断勇士是否拥有这个物品
|
|
||||||
* @param item 物品id
|
|
||||||
*/
|
|
||||||
hasItem(item: AllIdsOf<'items'>): boolean;
|
|
||||||
}
|
|
||||||
32
packages-user/data-state/src/types.ts
Normal file
32
packages-user/data-state/src/types.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ILayerState } from './map';
|
||||||
|
import { IHeroFollower, IHeroState } from './hero';
|
||||||
|
import { IRoleFaceBinder } from './common';
|
||||||
|
|
||||||
|
export interface IStateSaveData {
|
||||||
|
/** 跟随者列表 */
|
||||||
|
readonly followers: readonly IHeroFollower[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICoreState {
|
||||||
|
/** 地图状态 */
|
||||||
|
readonly layer: ILayerState;
|
||||||
|
/** 勇士状态 */
|
||||||
|
readonly hero: IHeroState;
|
||||||
|
/** 朝向绑定 */
|
||||||
|
readonly roleFace: IRoleFaceBinder;
|
||||||
|
/** id 到图块数字的映射 */
|
||||||
|
readonly idNumberMap: Map<string, number>;
|
||||||
|
/** 图块数字到 id 的映射 */
|
||||||
|
readonly numberIdMap: Map<number, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存状态
|
||||||
|
*/
|
||||||
|
saveState(): IStateSaveData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载状态
|
||||||
|
* @param data 状态对象
|
||||||
|
*/
|
||||||
|
loadState(data: IStateSaveData): void;
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
"@motajs/client-base": "workspace:*",
|
"@motajs/client-base": "workspace:*",
|
||||||
"@motajs/common": "workspace:*",
|
"@motajs/common": "workspace:*",
|
||||||
"@motajs/render": "workspace:*",
|
"@motajs/render": "workspace:*",
|
||||||
|
"@motajs/render-assets": "workspace:*",
|
||||||
"@motajs/render-core": "workspace:*",
|
"@motajs/render-core": "workspace:*",
|
||||||
"@motajs/render-elements": "workspace:*",
|
"@motajs/render-elements": "workspace:*",
|
||||||
"@motajs/render-style": "workspace:*",
|
"@motajs/render-style": "workspace:*",
|
||||||
@ -17,6 +18,7 @@
|
|||||||
"@motajs/legacy-data": "workspace:*",
|
"@motajs/legacy-data": "workspace:*",
|
||||||
"@motajs/legacy-ui": "workspace:*",
|
"@motajs/legacy-ui": "workspace:*",
|
||||||
"@motajs/legacy-system": "workspace:*",
|
"@motajs/legacy-system": "workspace:*",
|
||||||
|
"@user/client-base": "workspace:*",
|
||||||
"@user/client-modules": "workspace:*",
|
"@user/client-modules": "workspace:*",
|
||||||
"@user/legacy-plugin-client": "workspace:*"
|
"@user/legacy-plugin-client": "workspace:*"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import * as LegacyClient from '@motajs/legacy-client';
|
|||||||
import * as LegacySystem from '@motajs/legacy-system';
|
import * as LegacySystem from '@motajs/legacy-system';
|
||||||
import * as LegacyUI from '@motajs/legacy-ui';
|
import * as LegacyUI from '@motajs/legacy-ui';
|
||||||
import * as Render from '@motajs/render';
|
import * as Render from '@motajs/render';
|
||||||
|
import * as RenderAssets from '@motajs/render-assets';
|
||||||
import * as RenderCore from '@motajs/render-core';
|
import * as RenderCore from '@motajs/render-core';
|
||||||
import * as RenderElements from '@motajs/render-elements';
|
import * as RenderElements from '@motajs/render-elements';
|
||||||
import * as RenderStyle from '@motajs/render-style';
|
import * as RenderStyle from '@motajs/render-style';
|
||||||
@ -12,6 +13,7 @@ import * as RenderVue from '@motajs/render-vue';
|
|||||||
import * as System from '@motajs/system';
|
import * as System from '@motajs/system';
|
||||||
import * as SystemAction from '@motajs/system-action';
|
import * as SystemAction from '@motajs/system-action';
|
||||||
import * as SystemUI from '@motajs/system-ui';
|
import * as SystemUI from '@motajs/system-ui';
|
||||||
|
import * as UserClientBase from '@user/client-base';
|
||||||
import * as ClientModules from '@user/client-modules';
|
import * as ClientModules from '@user/client-modules';
|
||||||
import * as LegacyPluginClient from '@user/legacy-plugin-client';
|
import * as LegacyPluginClient from '@user/legacy-plugin-client';
|
||||||
import * as MutateAnimate from 'mutate-animate';
|
import * as MutateAnimate from 'mutate-animate';
|
||||||
@ -28,6 +30,7 @@ export function create() {
|
|||||||
Mota.register('@motajs/legacy-system', LegacySystem);
|
Mota.register('@motajs/legacy-system', LegacySystem);
|
||||||
Mota.register('@motajs/legacy-ui', LegacyUI);
|
Mota.register('@motajs/legacy-ui', LegacyUI);
|
||||||
Mota.register('@motajs/render', Render);
|
Mota.register('@motajs/render', Render);
|
||||||
|
Mota.register('@motajs/render-assets', RenderAssets);
|
||||||
Mota.register('@motajs/render-core', RenderCore);
|
Mota.register('@motajs/render-core', RenderCore);
|
||||||
Mota.register('@motajs/render-elements', RenderElements);
|
Mota.register('@motajs/render-elements', RenderElements);
|
||||||
Mota.register('@motajs/render-style', RenderStyle);
|
Mota.register('@motajs/render-style', RenderStyle);
|
||||||
@ -35,6 +38,7 @@ export function create() {
|
|||||||
Mota.register('@motajs/system', System);
|
Mota.register('@motajs/system', System);
|
||||||
Mota.register('@motajs/system-action', SystemAction);
|
Mota.register('@motajs/system-action', SystemAction);
|
||||||
Mota.register('@motajs/system-ui', SystemUI);
|
Mota.register('@motajs/system-ui', SystemUI);
|
||||||
|
Mota.register('@user/client-base', UserClientBase);
|
||||||
Mota.register('@user/client-modules', ClientModules);
|
Mota.register('@user/client-modules', ClientModules);
|
||||||
Mota.register('@user/legacy-plugin-client', LegacyPluginClient);
|
Mota.register('@user/legacy-plugin-client', LegacyPluginClient);
|
||||||
Mota.register('MutateAnimate', MutateAnimate);
|
Mota.register('MutateAnimate', MutateAnimate);
|
||||||
@ -45,8 +49,9 @@ export function create() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createModule() {
|
async function createModule() {
|
||||||
LegacyUI.create();
|
UserClientBase.create();
|
||||||
ClientModules.create();
|
ClientModules.create();
|
||||||
|
LegacyUI.create();
|
||||||
|
|
||||||
await import('ant-design-vue/dist/antd.dark.css');
|
await import('ant-design-vue/dist/antd.dark.css');
|
||||||
main.renderLoaded = true;
|
main.renderLoaded = true;
|
||||||
|
|||||||
@ -3,12 +3,20 @@ import { create } from './create';
|
|||||||
import { patchAll } from '@user/data-fallback';
|
import { patchAll } from '@user/data-fallback';
|
||||||
import { loading } from '@user/data-base';
|
import { loading } from '@user/data-base';
|
||||||
import { Patch } from '@motajs/legacy-common';
|
import { Patch } from '@motajs/legacy-common';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
|
||||||
export function createData() {
|
export function createData() {
|
||||||
createMota();
|
createMota();
|
||||||
patchAll();
|
patchAll();
|
||||||
create();
|
create();
|
||||||
|
|
||||||
|
if (main.replayChecking) {
|
||||||
|
logger.log(
|
||||||
|
`如果需要调试录像验证,请在 script/build-game.ts 中将 DEBUG_REPLAY 设为 true,` +
|
||||||
|
`此时录像验证中可以看到完整正确的报错栈。调试完毕后,记得将它重新设为 false`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loading.once('coreInit', () => {
|
loading.once('coreInit', () => {
|
||||||
Patch.patchAll();
|
Patch.patchAll();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,25 +1,27 @@
|
|||||||
import type { RenderAdapter } from '@motajs/render';
|
import type { RenderAdapter } from '@motajs/render';
|
||||||
import type { TimingFn } from 'mutate-animate';
|
import type { TimingFn } from 'mutate-animate';
|
||||||
import { BlockMover, heroMoveCollection, MoveStep } from '@user/data-state';
|
import {
|
||||||
|
BlockMover,
|
||||||
|
fromDirectionString,
|
||||||
|
heroMoveCollection,
|
||||||
|
MoveStep,
|
||||||
|
state
|
||||||
|
} from '@user/data-state';
|
||||||
import { hook, loading } from '@user/data-base';
|
import { hook, loading } from '@user/data-base';
|
||||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||||
import type {
|
import type {
|
||||||
HeroRenderer,
|
|
||||||
LayerDoorAnimate,
|
LayerDoorAnimate,
|
||||||
LayerGroupAnimate,
|
LayerGroupAnimate,
|
||||||
Layer,
|
|
||||||
FloorViewport,
|
FloorViewport,
|
||||||
LayerFloorBinder,
|
|
||||||
LayerGroup
|
LayerGroup
|
||||||
} from '@user/client-modules';
|
} from '@user/client-modules';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
// 向后兼容用,会充当两个版本间过渡的作用
|
// 向后兼容用,会充当两个版本间过渡的作用
|
||||||
|
|
||||||
interface Adapters {
|
interface Adapters {
|
||||||
'hero-adapter'?: RenderAdapter<HeroRenderer>;
|
|
||||||
'door-animate'?: RenderAdapter<LayerDoorAnimate>;
|
'door-animate'?: RenderAdapter<LayerDoorAnimate>;
|
||||||
animate?: RenderAdapter<LayerGroupAnimate>;
|
animate?: RenderAdapter<LayerGroupAnimate>;
|
||||||
layer?: RenderAdapter<Layer>;
|
|
||||||
viewport?: RenderAdapter<FloorViewport>;
|
viewport?: RenderAdapter<FloorViewport>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,16 +32,12 @@ export function initFallback() {
|
|||||||
|
|
||||||
if (!main.replayChecking && main.mode === 'play') {
|
if (!main.replayChecking && main.mode === 'play') {
|
||||||
const Adapter = Mota.require('@motajs/render').RenderAdapter;
|
const Adapter = Mota.require('@motajs/render').RenderAdapter;
|
||||||
const hero = Adapter.get<HeroRenderer>('hero-adapter');
|
|
||||||
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
|
const doorAnimate = Adapter.get<LayerDoorAnimate>('door-animate');
|
||||||
const animate = Adapter.get<LayerGroupAnimate>('animate');
|
const animate = Adapter.get<LayerGroupAnimate>('animate');
|
||||||
const layer = Adapter.get<Layer>('layer');
|
|
||||||
const viewport = Adapter.get<FloorViewport>('viewport');
|
const viewport = Adapter.get<FloorViewport>('viewport');
|
||||||
|
|
||||||
adapters['hero-adapter'] = hero;
|
|
||||||
adapters['door-animate'] = doorAnimate;
|
adapters['door-animate'] = doorAnimate;
|
||||||
adapters['animate'] = animate;
|
adapters['animate'] = animate;
|
||||||
adapters['layer'] = layer;
|
|
||||||
adapters['viewport'] = viewport;
|
adapters['viewport'] = viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +72,7 @@ export function initFallback() {
|
|||||||
/**
|
/**
|
||||||
* 生成跳跃函数
|
* 生成跳跃函数
|
||||||
*/
|
*/
|
||||||
function generateJumpFn(dx: number, dy: number): TimingFn<3> {
|
function generateJumpFn(dx: number, dy: number): TimingFn<2> {
|
||||||
const distance = Math.hypot(dx, dy);
|
const distance = Math.hypot(dx, dy);
|
||||||
const peak = 3 + distance;
|
const peak = 3 + distance;
|
||||||
|
|
||||||
@ -82,7 +80,7 @@ export function initFallback() {
|
|||||||
const x = dx * progress;
|
const x = dx * progress;
|
||||||
const y = progress * dy + (progress ** 2 - progress) * peak;
|
const y = progress * dy + (progress ** 2 - progress) * peak;
|
||||||
|
|
||||||
return [x, y, Math.ceil(y)];
|
return [x, y];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,31 +110,28 @@ export function initFallback() {
|
|||||||
|
|
||||||
patch.add('_moveAction_moving', () => {});
|
patch.add('_moveAction_moving', () => {});
|
||||||
|
|
||||||
patch2.add(
|
patch2.add('_action_moveAction', function () {
|
||||||
'_action_moveAction',
|
if (core.canMoveHero()) {
|
||||||
function (data: any, x: number, y: number, prefix: any) {
|
const nx = core.nextX(),
|
||||||
if (core.canMoveHero()) {
|
ny = core.nextY();
|
||||||
const nx = core.nextX(),
|
// 检查noPass决定是撞击还是移动
|
||||||
ny = core.nextY();
|
if (core.noPass(nx, ny)) {
|
||||||
// 检查noPass决定是撞击还是移动
|
core.insertAction([{ type: 'trigger', loc: [nx, ny] }]);
|
||||||
if (core.noPass(nx, ny)) {
|
} else {
|
||||||
core.insertAction([{ type: 'trigger', loc: [nx, ny] }]);
|
// 先移动一格,然后尝试触发事件
|
||||||
} else {
|
core.insertAction([
|
||||||
// 先移动一格,然后尝试触发事件
|
{
|
||||||
core.insertAction([
|
type: 'function',
|
||||||
{
|
function:
|
||||||
type: 'function',
|
'function() { core.moveAction(core.doAction); }',
|
||||||
function:
|
async: true
|
||||||
'function() { core.moveAction(core.doAction); }',
|
},
|
||||||
async: true
|
{ type: '_label' }
|
||||||
},
|
]);
|
||||||
{ type: '_label' }
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
core.doAction();
|
|
||||||
}
|
}
|
||||||
);
|
core.doAction();
|
||||||
|
});
|
||||||
|
|
||||||
patch2.add(
|
patch2.add(
|
||||||
'eventMoveHero',
|
'eventMoveHero',
|
||||||
@ -184,21 +179,28 @@ export function initFallback() {
|
|||||||
if (!core.status.hero) return;
|
if (!core.status.hero) return;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
core.status.hero.loc[name] = value;
|
core.status.hero.loc[name] = value;
|
||||||
if ((name === 'x' || name === 'y') && !noGather) {
|
|
||||||
core.control.gatherFollowers();
|
|
||||||
}
|
|
||||||
if (name === 'direction') {
|
if (name === 'direction') {
|
||||||
adapters['hero-adapter']?.sync('turn', value);
|
const dir = fromDirectionString(value as Dir);
|
||||||
adapters['hero-adapter']?.sync('setAnimateDir', value);
|
state.hero.turn(dir);
|
||||||
setHeroDirection(value as Dir);
|
setHeroDirection(value as Dir);
|
||||||
} else if (name === 'x') {
|
} else if (name === 'x') {
|
||||||
// 为了防止逆天样板出问题
|
// 为了防止逆天样板出问题
|
||||||
core.bigmap.posX = value as number;
|
core.bigmap.posX = value as number;
|
||||||
adapters['hero-adapter']?.sync('setHeroLoc', value);
|
if (!noGather) {
|
||||||
|
state.hero.setPosition(
|
||||||
|
value as number,
|
||||||
|
core.status.hero.loc.y
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 为了防止逆天样板出问题
|
// 为了防止逆天样板出问题
|
||||||
core.bigmap.posY = value as number;
|
core.bigmap.posY = value as number;
|
||||||
adapters['hero-adapter']?.sync('setHeroLoc', void 0, value);
|
if (!noGather) {
|
||||||
|
state.hero.setPosition(
|
||||||
|
core.status.hero.loc.x,
|
||||||
|
value as number
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -243,10 +245,8 @@ export function initFallback() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
patch2.add('setHeroIcon', function (name: ImageIds) {
|
patch2.add('setHeroIcon', function (name: ImageIds) {
|
||||||
const img = core.material.images.images[name];
|
|
||||||
if (!img) return;
|
|
||||||
core.status.hero.image = name;
|
core.status.hero.image = name;
|
||||||
adapters['hero-adapter']?.sync('setImage', img);
|
state.hero.setImage(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
patch.add('isMoving', function () {
|
patch.add('isMoving', function () {
|
||||||
@ -279,10 +279,10 @@ export function initFallback() {
|
|||||||
// 找寻自动寻路路线
|
// 找寻自动寻路路线
|
||||||
const moveStep = core.automaticRoute(destX, destY);
|
const moveStep = core.automaticRoute(destX, destY);
|
||||||
if (
|
if (
|
||||||
moveStep.length == 0 &&
|
moveStep.length === 0 &&
|
||||||
(destX != core.status.hero.loc.x ||
|
(destX !== core.status.hero.loc.x ||
|
||||||
destY != core.status.hero.loc.y ||
|
destY !== core.status.hero.loc.y ||
|
||||||
stepPostfix.length == 0)
|
stepPostfix.length === 0)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
moveStep.push(...stepPostfix);
|
moveStep.push(...stepPostfix);
|
||||||
@ -341,9 +341,9 @@ export function initFallback() {
|
|||||||
const locked = core.status.lockControl;
|
const locked = core.status.lockControl;
|
||||||
core.lockControl();
|
core.lockControl();
|
||||||
core.status.replay.animate = true;
|
core.status.replay.animate = true;
|
||||||
core.removeBlock(x, y);
|
|
||||||
|
|
||||||
const cb = () => {
|
const cb = () => {
|
||||||
|
core.removeBlock(x, y);
|
||||||
core.maps._removeBlockFromMap(
|
core.maps._removeBlockFromMap(
|
||||||
core.status.floorId,
|
core.status.floorId,
|
||||||
block
|
block
|
||||||
@ -357,10 +357,10 @@ export function initFallback() {
|
|||||||
y
|
y
|
||||||
);
|
);
|
||||||
callback?.();
|
callback?.();
|
||||||
delete core.animateFrame.asyncId[animate];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
adapters['door-animate']?.all('openDoor', block).then(cb);
|
const layer = state.layer.getLayerByAlias('event')!;
|
||||||
|
layer.openDoor(x, y).then(cb);
|
||||||
|
|
||||||
const animate = fallbackIds++;
|
const animate = fallbackIds++;
|
||||||
core.animateFrame.lastAsyncId = animate;
|
core.animateFrame.lastAsyncId = animate;
|
||||||
@ -376,10 +376,10 @@ export function initFallback() {
|
|||||||
id = id || '';
|
id = id || '';
|
||||||
if (
|
if (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(core.material.icons.animates[id] == null &&
|
(isNil(core.material.icons.animates[id]) &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
core.material.icons.npc48[id] == null) ||
|
isNil(core.material.icons.npc48[id])) ||
|
||||||
core.getBlock(x, y) != null
|
!isNil(core.getBlock(x, y))
|
||||||
) {
|
) {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
return;
|
return;
|
||||||
@ -407,11 +407,9 @@ export function initFallback() {
|
|||||||
if (core.status.replay.speed === 24) {
|
if (core.status.replay.speed === 24) {
|
||||||
cb();
|
cb();
|
||||||
} else {
|
} else {
|
||||||
adapters['door-animate']
|
const num = state.idNumberMap.get(id)!;
|
||||||
?.all('closeDoor', block)
|
const layer = state.layer.getLayerByAlias('event')!;
|
||||||
.then(() => {
|
layer.closeDoor(num, x, y).then(cb);
|
||||||
cb();
|
|
||||||
});
|
|
||||||
|
|
||||||
const animate = fallbackIds++;
|
const animate = fallbackIds++;
|
||||||
core.animateFrame.lastAsyncId = animate;
|
core.animateFrame.lastAsyncId = animate;
|
||||||
@ -439,10 +437,10 @@ export function initFallback() {
|
|||||||
if (
|
if (
|
||||||
core.isReplaying() ||
|
core.isReplaying() ||
|
||||||
!core.material.animates[name] ||
|
!core.material.animates[name] ||
|
||||||
x == null ||
|
isNil(x) ||
|
||||||
y == null
|
isNil(y)
|
||||||
) {
|
) {
|
||||||
if (callback) callback();
|
callback?.();
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,19 +513,10 @@ export function initFallback() {
|
|||||||
mover.insertMove(...[start, ...resolved]);
|
mover.insertMove(...[start, ...resolved]);
|
||||||
const controller = mover.startMove();
|
const controller = mover.startMove();
|
||||||
|
|
||||||
const id = fallbackIds++;
|
|
||||||
core.animateFrame.asyncId[id] = () => {
|
|
||||||
if (!keep) {
|
|
||||||
core.removeBlock(mover.x, mover.y);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (controller) {
|
if (controller) {
|
||||||
await controller.onEnd;
|
await controller.onEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete core.animateFrame.asyncId[id];
|
|
||||||
|
|
||||||
if (!keep) {
|
if (!keep) {
|
||||||
core.removeBlock(mover.x, mover.y);
|
core.removeBlock(mover.x, mover.y);
|
||||||
}
|
}
|
||||||
@ -557,36 +546,24 @@ export function initFallback() {
|
|||||||
const dy = ey - sy;
|
const dy = ey - sy;
|
||||||
|
|
||||||
const fn = generateJumpFn(dx, dy);
|
const fn = generateJumpFn(dx, dy);
|
||||||
|
// 先使用 mainMapRenderer 妥协
|
||||||
const list = adapters.layer?.items ?? [];
|
const { mainMapRenderer: renderer } = Mota.require(
|
||||||
const items = [...list].filter(v => {
|
'@user/client-modules'
|
||||||
if (v.layer !== 'event') return false;
|
|
||||||
const ex = v.getExtends('floor-binder') as LayerFloorBinder;
|
|
||||||
if (!ex) return false;
|
|
||||||
return ex.getFloor() === core.status.floorId;
|
|
||||||
});
|
|
||||||
const width = core.status.thisMap.width;
|
|
||||||
const index = sx + sy * width;
|
|
||||||
|
|
||||||
const promise = Promise.all(
|
|
||||||
items.map(v => {
|
|
||||||
return v.moveAs(index, ex, ey, fn, time, keep);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
if (renderer.layerState !== state.layer) {
|
||||||
core.updateStatusBar();
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = state.layer.getLayerByAlias('event');
|
||||||
|
if (!layer) {
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
core.removeBlock(sx, sy);
|
core.removeBlock(sx, sy);
|
||||||
|
const moving = renderer.addMovingBlock(layer, block.id, sx, sy);
|
||||||
const id = fallbackIds++;
|
core.updateStatusBar();
|
||||||
core.animateFrame.asyncId[id] = () => {
|
await moving.moveRelative(fn, time);
|
||||||
if (keep) {
|
moving.destroy();
|
||||||
core.setBlock(block.id, ex, ey);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
|
|
||||||
delete core.animateFrame.asyncId[id];
|
|
||||||
|
|
||||||
if (keep) {
|
if (keep) {
|
||||||
core.setBlock(block.id, ex, ey);
|
core.setBlock(block.id, ex, ey);
|
||||||
@ -607,30 +584,15 @@ export function initFallback() {
|
|||||||
) {
|
) {
|
||||||
if (heroMover.moving) return;
|
if (heroMover.moving) return;
|
||||||
|
|
||||||
const sx = core.getHeroLoc('x');
|
|
||||||
const sy = core.getHeroLoc('y');
|
|
||||||
adapters.viewport?.all('mutateTo', ex, ey, time);
|
adapters.viewport?.all('mutateTo', ex, ey, time);
|
||||||
|
|
||||||
const locked = core.status.lockControl;
|
const locked = core.status.lockControl;
|
||||||
core.lockControl();
|
core.lockControl();
|
||||||
const list = adapters['hero-adapter']?.items ?? [];
|
|
||||||
const items = [...list];
|
|
||||||
|
|
||||||
time /= core.status.replay.speed;
|
time /= core.status.replay.speed;
|
||||||
if (core.status.replay.speed === 24) time = 1;
|
if (core.status.replay.speed === 24) time = 1;
|
||||||
const fn = generateJumpFn(ex - sx, ey - sy);
|
|
||||||
await Promise.all(
|
await state.hero.jumpHero(ex, ey, time);
|
||||||
items.map(v => {
|
|
||||||
if (!v.renderable) return Promise.reject();
|
|
||||||
return v.layer.moveRenderable(
|
|
||||||
v.renderable,
|
|
||||||
sx,
|
|
||||||
sy,
|
|
||||||
fn,
|
|
||||||
time
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!locked) core.unlockControl();
|
if (!locked) core.unlockControl();
|
||||||
core.setHeroLoc('x', ex);
|
core.setHeroLoc('x', ex);
|
||||||
|
|||||||
86
packages/client-base/src/glUtils.ts
Normal file
86
packages/client-base/src/glUtils.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
|
||||||
|
export interface ICompiledProgram {
|
||||||
|
/** 着色器程序 */
|
||||||
|
readonly program: WebGLProgram;
|
||||||
|
/** 顶点着色器对象 */
|
||||||
|
readonly vertexShader: WebGLShader;
|
||||||
|
/** 片段着色器对象 */
|
||||||
|
readonly fragmentShader: WebGLShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译着色器
|
||||||
|
* @param gl WebGL2 上下文
|
||||||
|
* @param type 着色器类型
|
||||||
|
* @param source 着色器代码
|
||||||
|
*/
|
||||||
|
export function compileShader(
|
||||||
|
gl: WebGL2RenderingContext,
|
||||||
|
type: number,
|
||||||
|
source: string
|
||||||
|
): WebGLShader | null {
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
if (!shader) return null;
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
const info = gl.getShaderInfoLog(shader);
|
||||||
|
const typeStr = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment';
|
||||||
|
logger.error(10, typeStr, info ?? '');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编译链接着色器程序
|
||||||
|
* @param gl WebGL2 上下文
|
||||||
|
* @param vs 顶点着色器对象
|
||||||
|
* @param fs 片段着色器对象
|
||||||
|
*/
|
||||||
|
export function compileProgram(
|
||||||
|
gl: WebGL2RenderingContext,
|
||||||
|
vs: WebGLShader,
|
||||||
|
fs: WebGLShader
|
||||||
|
) {
|
||||||
|
const program = gl.createProgram();
|
||||||
|
gl.attachShader(program, vs);
|
||||||
|
gl.attachShader(program, fs);
|
||||||
|
gl.linkProgram(program);
|
||||||
|
|
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
const info = gl.getProgramInfoLog(program);
|
||||||
|
logger.error(9, info ?? '');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定着色器代码编译链接程序
|
||||||
|
* @param gl WebGL2 上下文
|
||||||
|
* @param vs 顶点着色器代码
|
||||||
|
* @param fs 片段着色器代码
|
||||||
|
*/
|
||||||
|
export function compileProgramWith(
|
||||||
|
gl: WebGL2RenderingContext,
|
||||||
|
vs: string,
|
||||||
|
fs: string
|
||||||
|
): ICompiledProgram | null {
|
||||||
|
const vsShader = compileShader(gl, gl.VERTEX_SHADER, vs);
|
||||||
|
const fsShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
|
||||||
|
|
||||||
|
if (!vsShader || !fsShader) return null;
|
||||||
|
const program = compileProgram(gl, vsShader, fsShader);
|
||||||
|
if (!program) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
program,
|
||||||
|
vertexShader: vsShader,
|
||||||
|
fragmentShader: fsShader
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
|
export * from './glUtils';
|
||||||
export * from './keyCodes';
|
export * from './keyCodes';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
133
packages/common/src/dirtyTracker.ts
Normal file
133
packages/common/src/dirtyTracker.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { IDirtyMark, IDirtyTracker } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 布尔类型的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回 `true`
|
||||||
|
*/
|
||||||
|
export class PrivateBooleanDirtyTracker implements IDirtyTracker<boolean> {
|
||||||
|
/** 标记映射 */
|
||||||
|
private markMap: WeakMap<IDirtyMark, number> = new WeakMap();
|
||||||
|
/** 脏标记 */
|
||||||
|
private dirtyFlag: number = 0;
|
||||||
|
|
||||||
|
mark(): IDirtyMark {
|
||||||
|
const symbol = {};
|
||||||
|
this.markMap.set(symbol, this.dirtyFlag);
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmark(mark: IDirtyMark): void {
|
||||||
|
this.markMap.delete(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirtySince(mark: IDirtyMark): boolean {
|
||||||
|
const num = this.markMap.get(mark);
|
||||||
|
if (isNil(num)) return true;
|
||||||
|
return num < this.dirtyFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMark(symbol: IDirtyMark): boolean {
|
||||||
|
return this.markMap.has(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据标记为脏
|
||||||
|
*/
|
||||||
|
protected dirty() {
|
||||||
|
this.dirtyFlag++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回空集合
|
||||||
|
*/
|
||||||
|
export class PrivateListDirtyTracker<T extends number>
|
||||||
|
implements IDirtyTracker<Set<T>>
|
||||||
|
{
|
||||||
|
/** 标记映射,键表示在索引,值表示其对应的标记数字 */
|
||||||
|
private readonly markMap: Map<T, number> = new Map();
|
||||||
|
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
|
||||||
|
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
|
||||||
|
|
||||||
|
/** 脏标记数字 */
|
||||||
|
private dirtyFlag: number = 0;
|
||||||
|
|
||||||
|
constructor(protected length: number) {}
|
||||||
|
|
||||||
|
mark(): IDirtyMark {
|
||||||
|
const symbol = {};
|
||||||
|
this.symbolMap.set(symbol, this.dirtyFlag);
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmark(mark: IDirtyMark): void {
|
||||||
|
this.symbolMap.delete(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirtySince(mark: IDirtyMark): Set<T> {
|
||||||
|
const num = this.symbolMap.get(mark);
|
||||||
|
const res = new Set<T>();
|
||||||
|
if (isNil(num)) return res;
|
||||||
|
this.markMap.forEach((v, k) => {
|
||||||
|
if (v > num) res.add(k);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMark(symbol: IDirtyMark): boolean {
|
||||||
|
return this.symbolMap.has(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected dirty(data: T): void {
|
||||||
|
if (data >= this.length) return;
|
||||||
|
this.dirtyFlag++;
|
||||||
|
this.markMap.set(data, this.dirtyFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateLength(length: number) {
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PrivateMapDirtyTracker<T extends string>
|
||||||
|
implements IDirtyTracker<Record<T, boolean>>
|
||||||
|
{
|
||||||
|
/** 标记映射,键表示名称,值表示其对应的标记数字 */
|
||||||
|
private readonly markMap: Map<T, number> = new Map();
|
||||||
|
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
|
||||||
|
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
|
||||||
|
|
||||||
|
/** 脏标记数字 */
|
||||||
|
private dirtyFlag: number = 0;
|
||||||
|
|
||||||
|
constructor(protected length: number) {}
|
||||||
|
|
||||||
|
mark(): IDirtyMark {
|
||||||
|
const symbol = {};
|
||||||
|
this.symbolMap.set(symbol, this.dirtyFlag);
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmark(mark: IDirtyMark): void {
|
||||||
|
this.symbolMap.delete(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
dirtySince(mark: IDirtyMark): Record<T, boolean> {
|
||||||
|
const num = this.symbolMap.get(mark) ?? 0;
|
||||||
|
const obj: Partial<Record<T, boolean>> = {};
|
||||||
|
this.markMap.forEach((v, k) => {
|
||||||
|
if (v > num) obj[k] = true;
|
||||||
|
else obj[k] = false;
|
||||||
|
});
|
||||||
|
return obj as Record<T, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMark(symbol: IDirtyMark): boolean {
|
||||||
|
return this.symbolMap.has(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected dirty(data: T): void {
|
||||||
|
this.dirtyFlag++;
|
||||||
|
this.markMap.set(data, this.dirtyFlag);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
packages/common/src/hook.ts
Normal file
86
packages/common/src/hook.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { logger } from './logger';
|
||||||
|
import { IHookable, IHookBase, IHookController, IHookObject } from './types';
|
||||||
|
|
||||||
|
export abstract class Hookable<
|
||||||
|
H extends IHookBase = IHookBase,
|
||||||
|
C extends IHookController<H> = IHookController<H>
|
||||||
|
> implements IHookable<H, C>
|
||||||
|
{
|
||||||
|
/** 加载完成的钩子列表 */
|
||||||
|
protected readonly loadedList: Set<IHookObject<H, C>> = new Set();
|
||||||
|
|
||||||
|
/** 钩子列表 */
|
||||||
|
private readonly hookList: Set<IHookObject<H, C>> = new Set();
|
||||||
|
/** 钩子对象到钩子存储对象的映射 */
|
||||||
|
private readonly hookMap: Map<Partial<H>, IHookObject<H, C>> = new Map();
|
||||||
|
/** 钩子控制器到钩子存储对象的映射 */
|
||||||
|
private readonly controllerMap: Map<C, IHookObject<H, C>> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建钩子对象的控制器
|
||||||
|
* @param hook 钩子对象
|
||||||
|
*/
|
||||||
|
protected abstract createController(hook: Partial<H>): C;
|
||||||
|
|
||||||
|
addHook(hook: Partial<H>): C {
|
||||||
|
const controller = this.createController(hook);
|
||||||
|
const obj: IHookObject<H, C> = { hook, controller };
|
||||||
|
this.hookMap.set(hook, obj);
|
||||||
|
this.controllerMap.set(controller, obj);
|
||||||
|
this.hookList.add(obj);
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadHook(hook: Partial<H>): void {
|
||||||
|
const obj = this.hookMap.get(hook);
|
||||||
|
if (!obj) {
|
||||||
|
logger.warn(85);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hook.awake?.(obj.controller);
|
||||||
|
this.loadedList.add(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHook(hook: Partial<H>): void {
|
||||||
|
const obj = this.hookMap.get(hook);
|
||||||
|
if (!obj) return;
|
||||||
|
obj.hook.destroy?.(obj.controller);
|
||||||
|
this.hookList.delete(obj);
|
||||||
|
this.loadedList.delete(obj);
|
||||||
|
this.hookMap.delete(hook);
|
||||||
|
this.controllerMap.delete(obj.controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeHookByController(hook: C): void {
|
||||||
|
const obj = this.controllerMap.get(hook);
|
||||||
|
if (!obj) return;
|
||||||
|
obj.hook.destroy?.(obj.controller);
|
||||||
|
this.hookList.delete(obj);
|
||||||
|
this.loadedList.delete(obj);
|
||||||
|
this.controllerMap.delete(hook);
|
||||||
|
this.hookMap.delete(obj.hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[] {
|
||||||
|
const arr: T[] = [];
|
||||||
|
this.loadedList.forEach(v => {
|
||||||
|
arr.push(fn(v.hook, v.controller));
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HookController<H extends IHookBase> implements IHookController<H> {
|
||||||
|
constructor(
|
||||||
|
readonly hookable: IHookable<H, IHookController<H>>,
|
||||||
|
readonly hook: Partial<H>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
this.hookable.loadHook(this.hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
unload(): void {
|
||||||
|
this.hookable.removeHookByController(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,5 @@
|
|||||||
|
export * from './dirtyTracker';
|
||||||
|
export * from './hook';
|
||||||
export * from './logger';
|
export * from './logger';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
@ -26,6 +26,26 @@
|
|||||||
"24": "Cannot decode source type of '$1', since there is no registered decoder for that type.",
|
"24": "Cannot decode source type of '$1', since there is no registered decoder for that type.",
|
||||||
"25": "Unknown audio type. Header: '$1'",
|
"25": "Unknown audio type. Header: '$1'",
|
||||||
"26": "Uncaught error when fetching stream data from '$1'. Error info: $2.",
|
"26": "Uncaught error when fetching stream data from '$1'. Error info: $2.",
|
||||||
|
"27": "No autotile connection data, please ensure you have created autotile connection map.",
|
||||||
|
"28": "Cannot compile map render shader.",
|
||||||
|
"29": "Cannot get uniform location of map render shader.",
|
||||||
|
"30": "",
|
||||||
|
"31": "No asset data is specified when rending map.",
|
||||||
|
"32": "Every layer added to map renderer must share the same size. Different layer: $1.",
|
||||||
|
"33": "Map layer transfered to vertex generator must belong to the renderer that the generator use.",
|
||||||
|
"34": "The texture of moving block must be a part of built asset.",
|
||||||
|
"35": "Tile background transfered to map renderer does not exists.",
|
||||||
|
"36": "Tile background transfered to map renderer has no frame data.",
|
||||||
|
"37": "Frame of tile background transfered to map renderer must share the same size.",
|
||||||
|
"38": "Cached texture cannot be convert to asset. This is likely an internal bug, please contact us.",
|
||||||
|
"39": "Offset pool size exceeds WebGL2 limitation, ensure size type of your big image is less than $1.",
|
||||||
|
"40": "Material used by map block $1 is not found in built asset. Please ensure you have pack it into asset.",
|
||||||
|
"41": "You are trying to use a texture on moving block whose offset is not in the offset pool, please build it into asset after loading.",
|
||||||
|
"42": "The '$1' property of map-render element is required.",
|
||||||
|
"43": "Cannot bind face direction to main block $1, please call malloc in advance.",
|
||||||
|
"44": "Cannot bind face direction to main block $1, since main direction cannot be override.",
|
||||||
|
"45": "Cannot add $1 map renderer extension, since $1 already exists for the given state.",
|
||||||
|
"46": "Cannot execute close door action on $1,$2, since the given position is not empty.",
|
||||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
||||||
},
|
},
|
||||||
"warn": {
|
"warn": {
|
||||||
@ -94,6 +114,34 @@
|
|||||||
"63": "Uncaught promise error in waiting box component. Error reason: $1",
|
"63": "Uncaught promise error in waiting box component. Error reason: $1",
|
||||||
"64": "Text node type and block type mismatch: '$1' vs '$2'",
|
"64": "Text node type and block type mismatch: '$1' vs '$2'",
|
||||||
"65": "Cannot bind a weather controller twice.",
|
"65": "Cannot bind a weather controller twice.",
|
||||||
|
"66": "Texture identifier repeated: $1",
|
||||||
|
"67": "Cannot set alias '$1' for texture $2, since there is already an alias '$3' for it.",
|
||||||
|
"68": "Cannot set alias '$1' for texture $2, since '$1' is already an alias for $3.",
|
||||||
|
"69": "Clip totally exceeds texture's dimensions, no operation will be alppied.",
|
||||||
|
"70": "An TextureAnimater can only be created once.",
|
||||||
|
"71": "Cannot start animate when animater not created on texture.",
|
||||||
|
"72": "Frame count delivered to frame-based animater need to exactly divide texture's height.",
|
||||||
|
"73": "Consistent size is needed when using WebGL2 composer.",
|
||||||
|
"74": "Frame size not match texture's size, incomplete data will be skipped.",
|
||||||
|
"75": "Material count splitted by grid does not match alias count, some material may have no alias or some alias may not map to a texture. Splitted: $1, alias: $2.",
|
||||||
|
"76": "Cannot pipe texture store when asset builder has been started.",
|
||||||
|
"77": "Texture is clipped to an area of 0. Ensure that the texture contains your clip rect and clip rect's area is not zero.",
|
||||||
|
"78": "Adding tileset source must follow index order.",
|
||||||
|
"79": "Assets can only be built once.",
|
||||||
|
"80": "Parameter count of MapLayer.getMapData must be 0 or 4.",
|
||||||
|
"81": "Map data to get is partially (or totally) out of range. Overflowed area will be filled with zero.",
|
||||||
|
"82": "Big image offset size is larger than 64. Considier reduce big image offset bucket.",
|
||||||
|
"83": "It seems that you call '$1' too frequently. This will extremely affect game's performance, so considering integrate them into one 'updateArea' or 'updateBlockList' call.",
|
||||||
|
"84": "Cannot set alias '$1' for layer, since '$1' is already an alias for another layer.",
|
||||||
|
"85": "Hook to load does not belong to current hookable object.",
|
||||||
|
"86": "Cannot restore vertex data since delivered state does not belong to current vertex generator instance.",
|
||||||
|
"87": "Map texture asset cache not hit. This has no effect on rendering, but may extremely affect performance, please see the following link to find solution.",
|
||||||
|
"88": "No hero image is specified, please set image in advance.",
|
||||||
|
"89": "Cannot set hero image, since there is no image named '$1'.",
|
||||||
|
"90": "Cannot set hero image, since delivered image has incorrect format or size.",
|
||||||
|
"91": "Cannot add follower, since specified follower number $1 does not exist.",
|
||||||
|
"92": "Followers can only be added when the last follower is not moving.",
|
||||||
|
"93": "Followers can only be removed when the last follower is not moving.",
|
||||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,8 +72,15 @@ export class Logger {
|
|||||||
let paramNum = '';
|
let paramNum = '';
|
||||||
while (++pointer < text.length) {
|
while (++pointer < text.length) {
|
||||||
const char = text[pointer];
|
const char = text[pointer];
|
||||||
|
const next = text[pointer + 1];
|
||||||
|
|
||||||
if (char === '$' && text[pointer - 1] !== '\\') {
|
if (char === '\\' && next === '$') {
|
||||||
|
str += '$';
|
||||||
|
pointer++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '$' && nums.has(next)) {
|
||||||
inParam = true;
|
inParam = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -82,7 +89,7 @@ export class Logger {
|
|||||||
if (nums.has(char)) {
|
if (nums.has(char)) {
|
||||||
paramNum += char;
|
paramNum += char;
|
||||||
}
|
}
|
||||||
if (!nums.has(text[pointer + 1])) {
|
if (!nums.has(next)) {
|
||||||
inParam = false;
|
inParam = false;
|
||||||
const num = Number(paramNum);
|
const num = Number(paramNum);
|
||||||
paramNum = '';
|
paramNum = '';
|
||||||
@ -125,8 +132,7 @@ export class Logger {
|
|||||||
hideTipText();
|
hideTipText();
|
||||||
}
|
}
|
||||||
const n = Math.floor(code / 50) + 1;
|
const n = Math.floor(code / 50) + 1;
|
||||||
const n2 = code % 50;
|
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${code}`;
|
||||||
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${n2}`;
|
|
||||||
console.error(`[ERROR Code ${code}] ${text} See ${url}`);
|
console.error(`[ERROR Code ${code}] ${text} See ${url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,8 +165,7 @@ export class Logger {
|
|||||||
hideTipText();
|
hideTipText();
|
||||||
}
|
}
|
||||||
const n = Math.floor(code / 50) + 1;
|
const n = Math.floor(code / 50) + 1;
|
||||||
const n2 = code % 50;
|
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${code}`;
|
||||||
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${n2}`;
|
|
||||||
console.warn(`[WARNING Code ${code}] ${text} See ${url}`);
|
console.warn(`[WARNING Code ${code}] ${text} See ${url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
packages/common/src/types.ts
Normal file
117
packages/common/src/types.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//#region 脏标记
|
||||||
|
|
||||||
|
export interface IDirtyMarker<T> {
|
||||||
|
/**
|
||||||
|
* 标记为脏,即进行了一次更新
|
||||||
|
* @param data 传递给追踪器的数据
|
||||||
|
*/
|
||||||
|
dirty(data: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDirtyMark {}
|
||||||
|
|
||||||
|
export interface IDirtyTracker<T> {
|
||||||
|
/**
|
||||||
|
* 对状态进行标记
|
||||||
|
*/
|
||||||
|
mark(): IDirtyMark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消指定标记符号
|
||||||
|
* @param mark 标记符号
|
||||||
|
*/
|
||||||
|
unmark(mark: IDirtyMark): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从指定标记符号开始,数据是否发生了变动
|
||||||
|
* @param mark 标记符号
|
||||||
|
*/
|
||||||
|
dirtySince(mark: IDirtyMark): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前追踪器是否包含指定标记符号
|
||||||
|
* @param symbol 标记符号
|
||||||
|
*/
|
||||||
|
hasMark(symbol: IDirtyMark): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 钩子
|
||||||
|
|
||||||
|
export interface IHookObject<
|
||||||
|
H extends IHookBase,
|
||||||
|
C extends IHookController<H>
|
||||||
|
> {
|
||||||
|
/** 钩子对象 */
|
||||||
|
readonly hook: Partial<H>;
|
||||||
|
/** 钩子控制器 */
|
||||||
|
readonly controller: C;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHookController<H extends IHookBase = IHookBase> {
|
||||||
|
/** 控制器的钩子对象 */
|
||||||
|
readonly hook: Partial<H>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载此控制器对应的钩子对象
|
||||||
|
*/
|
||||||
|
load(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载此控制器对应的钩子对象,之后此钩子将不会再被触发
|
||||||
|
*/
|
||||||
|
unload(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHookBase {
|
||||||
|
/**
|
||||||
|
* 加载此钩子对象
|
||||||
|
* @param controller 钩子控制器对象
|
||||||
|
*/
|
||||||
|
awake(controller: IHookController<this>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 摧毁此钩子对象
|
||||||
|
* @param controller 钩子控制器对象
|
||||||
|
*/
|
||||||
|
destroy(controller: IHookController<this>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHookable<
|
||||||
|
H extends IHookBase = IHookBase,
|
||||||
|
C extends IHookController<H> = IHookController<H>
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* 添加钩子对象,返回控制该钩子对象的控制器
|
||||||
|
* @param hook 钩子对象
|
||||||
|
*/
|
||||||
|
addHook(hook: Partial<H>): C;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载指定的钩子对象
|
||||||
|
* @param hook 钩子对象
|
||||||
|
*/
|
||||||
|
loadHook(hook: Partial<H>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除钩子对象,会调用钩子对象的 `destroy` 方法
|
||||||
|
* @param hook 钩子对象
|
||||||
|
*/
|
||||||
|
removeHook(hook: Partial<H>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传入钩子控制器,移除对应的钩子对象
|
||||||
|
* @param hook 钩子控制器
|
||||||
|
*/
|
||||||
|
removeHookByController(hook: C): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历每个钩子,执行顺序不固定
|
||||||
|
* @param fn 对每个钩子执行的函数
|
||||||
|
* @returns 每个钩子的返回值组成的数组,顺序不固定
|
||||||
|
*/
|
||||||
|
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
6
packages/render-assets/package.json
Normal file
6
packages/render-assets/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@motajs/render-assets",
|
||||||
|
"dependencies": {
|
||||||
|
"@motajs/client-base": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
134
packages/render-assets/src/animater.ts
Normal file
134
packages/render-assets/src/animater.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,动画传入的参数代表帧数
|
||||||
|
*/
|
||||||
|
export class TextureRowAnimater implements ITextureAnimater<number> {
|
||||||
|
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
|
||||||
|
if (frames <= 0) return;
|
||||||
|
const renderable = texture.render();
|
||||||
|
const { x: ox, y: oy } = renderable.rect;
|
||||||
|
const { width: w, height } = texture!;
|
||||||
|
const h = height / frames;
|
||||||
|
for (let i = 0; i < frames; i++) {
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
|
||||||
|
if (frames <= 0) return;
|
||||||
|
const renderable = texture.render();
|
||||||
|
const { x: ox, y: oy } = renderable.rect;
|
||||||
|
const { width: w, height } = texture;
|
||||||
|
const h = height / frames;
|
||||||
|
let i = 0;
|
||||||
|
while (true) {
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
i++;
|
||||||
|
if (i === frames) i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,动画传入的参数代表帧数
|
||||||
|
*/
|
||||||
|
export class TextureColumnAnimater implements ITextureAnimater<number> {
|
||||||
|
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
|
||||||
|
if (frames <= 0) return;
|
||||||
|
const renderable = texture.render();
|
||||||
|
const { x: ox, y: oy, w: width, h } = renderable.rect;
|
||||||
|
const w = width / frames;
|
||||||
|
for (let i = 0; i < frames; i++) {
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
|
||||||
|
if (frames <= 0) return null;
|
||||||
|
const renderable = texture.render();
|
||||||
|
const { x: ox, y: oy } = renderable.rect;
|
||||||
|
const { width, height: h } = texture;
|
||||||
|
const w = width / frames;
|
||||||
|
let i = 0;
|
||||||
|
while (true) {
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
i++;
|
||||||
|
if (i === frames) i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IScanAnimaterData {
|
||||||
|
/** 每帧的宽度 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 每帧的高度 */
|
||||||
|
readonly height: number;
|
||||||
|
/** 总帧数 */
|
||||||
|
readonly frames: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描动画控制器,会按照先从左到右,再从上到下的顺序依次输出,可以用于动画精灵图等
|
||||||
|
*/
|
||||||
|
export class TextureScanAnimater
|
||||||
|
implements ITextureAnimater<IScanAnimaterData>
|
||||||
|
{
|
||||||
|
*once(
|
||||||
|
texture: ITexture,
|
||||||
|
data: IScanAnimaterData
|
||||||
|
): Generator<ITextureRenderable, void> {
|
||||||
|
const w = texture.width;
|
||||||
|
const h = texture.height;
|
||||||
|
|
||||||
|
let frame = 0;
|
||||||
|
for (let y = 0; y < data.width; y++) {
|
||||||
|
for (let x = 0; x < data.height; x++) {
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
frame++;
|
||||||
|
if (frame === data.frames) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*cycled(
|
||||||
|
texture: ITexture,
|
||||||
|
data: IScanAnimaterData
|
||||||
|
): Generator<ITextureRenderable, void> {
|
||||||
|
const w = texture.width;
|
||||||
|
const h = texture.height;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
while (true) {
|
||||||
|
const x = index % data.width;
|
||||||
|
const y = Math.floor(index / data.height);
|
||||||
|
const renderable: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
|
||||||
|
};
|
||||||
|
yield renderable;
|
||||||
|
index++;
|
||||||
|
if (index === data.frames) index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
451
packages/render-assets/src/composer.ts
Normal file
451
packages/render-assets/src/composer.ts
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
import {
|
||||||
|
IOption,
|
||||||
|
IRectangle,
|
||||||
|
MaxRectsPacker,
|
||||||
|
Rectangle
|
||||||
|
} from 'maxrects-packer';
|
||||||
|
import { Texture } from './texture';
|
||||||
|
import {
|
||||||
|
IRect,
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureComposer,
|
||||||
|
SizedCanvasImageSource
|
||||||
|
} from './types';
|
||||||
|
import vert from './shader/pack.vert?raw';
|
||||||
|
import frag from './shader/pack.frag?raw';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { compileProgramWith } from '@motajs/client-base';
|
||||||
|
|
||||||
|
interface IndexMarkedComposedData {
|
||||||
|
/** 组合数据 */
|
||||||
|
readonly asset: ITextureComposedData;
|
||||||
|
/** 组合时最后一个用到的贴图的索引 */
|
||||||
|
readonly index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGridComposerData {
|
||||||
|
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
|
||||||
|
readonly height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextureGridComposer
|
||||||
|
implements ITextureComposer<IGridComposerData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 网格组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
|
||||||
|
* 组合时按照先从左到右,再从上到下的顺序组合。
|
||||||
|
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
|
||||||
|
* @param maxHeight 图集最大高度,也是输出纹理的高度
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly maxWidth: number,
|
||||||
|
readonly maxHeight: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private nextAsset(
|
||||||
|
tex: ITexture[],
|
||||||
|
start: number,
|
||||||
|
data: IGridComposerData,
|
||||||
|
rows: number,
|
||||||
|
cols: number,
|
||||||
|
index: number
|
||||||
|
): IndexMarkedComposedData {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
canvas.width = this.maxWidth;
|
||||||
|
canvas.height = this.maxHeight;
|
||||||
|
|
||||||
|
const count = Math.min(rows * cols, tex.length - start);
|
||||||
|
const map = new Map<ITexture, IRect>();
|
||||||
|
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const dx = x * data.width;
|
||||||
|
const dy = y * data.height;
|
||||||
|
const texture = tex[i + start];
|
||||||
|
const renderable = texture.render();
|
||||||
|
const { x: sx, y: sy, w: sw, h: sh } = renderable.rect;
|
||||||
|
ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh);
|
||||||
|
map.set(texture, { x: dx, y: dy, w: sw, h: sh });
|
||||||
|
x++;
|
||||||
|
if (x === cols) {
|
||||||
|
y++;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const texture = new Texture(canvas);
|
||||||
|
const composed: ITextureComposedData = {
|
||||||
|
index,
|
||||||
|
texture,
|
||||||
|
assetMap: map
|
||||||
|
};
|
||||||
|
|
||||||
|
return { asset: composed, index: start + count };
|
||||||
|
}
|
||||||
|
|
||||||
|
*compose(
|
||||||
|
input: Iterable<ITexture>,
|
||||||
|
data: IGridComposerData
|
||||||
|
): Generator<ITextureComposedData, void> {
|
||||||
|
const arr = [...input];
|
||||||
|
|
||||||
|
const rows = Math.floor(this.maxWidth / data.width);
|
||||||
|
const cols = Math.floor(this.maxHeight / data.height);
|
||||||
|
|
||||||
|
let arrindex = 0;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (arrindex < arr.length) {
|
||||||
|
const res = this.nextAsset(arr, arrindex, data, rows, cols, i);
|
||||||
|
arrindex = res.index + 1;
|
||||||
|
i++;
|
||||||
|
yield res.asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMaxRectsComposerData extends IOption {
|
||||||
|
/** 贴图之间的间距 */
|
||||||
|
readonly padding: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MaxRectsRectangle extends IRectangle {
|
||||||
|
/** 这个矩形对应的贴图对象 */
|
||||||
|
readonly data: ITexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextureMaxRectsComposer
|
||||||
|
implements ITextureComposer<IMaxRectsComposerData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData},
|
||||||
|
* 输出的纹理的图像源将会是不同的画布,注意与 {@link TextureMaxRectsWebGL2Composer} 区分
|
||||||
|
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
|
||||||
|
* @param maxHeight 图集最大高度,也是输出纹理的高度
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public readonly maxWidth: number,
|
||||||
|
public readonly maxHeight: number
|
||||||
|
) {}
|
||||||
|
|
||||||
|
*compose(
|
||||||
|
input: Iterable<ITexture>,
|
||||||
|
data: IMaxRectsComposerData
|
||||||
|
): Generator<ITextureComposedData, void> {
|
||||||
|
const packer = new MaxRectsPacker<MaxRectsRectangle>(
|
||||||
|
this.maxWidth,
|
||||||
|
this.maxHeight,
|
||||||
|
data.padding,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
const arr = [...input];
|
||||||
|
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||||
|
const rect = v.render().rect;
|
||||||
|
const toPack = new Rectangle(rect.w, rect.h);
|
||||||
|
toPack.data = v;
|
||||||
|
return toPack;
|
||||||
|
});
|
||||||
|
packer.addArray(rects);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (const bin of packer.bins) {
|
||||||
|
const map = new Map<ITexture, IRect>();
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
canvas.width = this.maxWidth;
|
||||||
|
canvas.height = this.maxHeight;
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
bin.rects.forEach(v => {
|
||||||
|
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
|
||||||
|
map.set(v.data, rect);
|
||||||
|
const renderable = v.data.render();
|
||||||
|
const { x, y, w, h } = renderable.rect;
|
||||||
|
const source = renderable.source;
|
||||||
|
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
|
||||||
|
});
|
||||||
|
const texture = new Texture(canvas);
|
||||||
|
const data: ITextureComposedData = {
|
||||||
|
index: index++,
|
||||||
|
texture,
|
||||||
|
assetMap: map
|
||||||
|
};
|
||||||
|
yield data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RectProcessed {
|
||||||
|
/** 贴图位置映射 */
|
||||||
|
readonly texMap: Map<ITexture, Readonly<IRect>>;
|
||||||
|
/** 顶点数组 */
|
||||||
|
readonly attrib: Float32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextureMaxRectsWebGL2Composer
|
||||||
|
implements ITextureComposer<IMaxRectsComposerData>
|
||||||
|
{
|
||||||
|
/** 使用的画布 */
|
||||||
|
readonly canvas: HTMLCanvasElement;
|
||||||
|
/** 画布上下文 */
|
||||||
|
readonly gl: WebGL2RenderingContext;
|
||||||
|
/** WebGL2 程序 */
|
||||||
|
readonly program: WebGLProgram;
|
||||||
|
/** 顶点数组缓冲区 */
|
||||||
|
readonly vertBuffer: WebGLBuffer;
|
||||||
|
/** 纹理数组对象 */
|
||||||
|
readonly texArray: WebGLTexture;
|
||||||
|
/** 纹理数组对象的位置 */
|
||||||
|
readonly texArrayPos: WebGLUniformLocation;
|
||||||
|
/** `a_position` 的索引 */
|
||||||
|
readonly posPos: number;
|
||||||
|
/** `a_texCoord` 的索引 */
|
||||||
|
readonly texPos: number;
|
||||||
|
|
||||||
|
/** 本次处理的贴图宽度 */
|
||||||
|
private opWidth: number = 0;
|
||||||
|
/** 本次处理的贴图高度 */
|
||||||
|
private opHeight: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Max Rects 算法执行贴图整合,使用 WebGL2 执行组合操作。
|
||||||
|
* 输入数据参考 {@link IMaxRectsComposerData},要求每个贴图的图像源尺寸一致,贴图大小可以不同。
|
||||||
|
* 注意,本组合器同时只能处理一个组合操作,上一个没执行完的时候再次调用 `compose` 会出现问题。
|
||||||
|
* 所有输出的内容中,贴图对象的图像源都是同一个画布,因此获取后要么直接使用,要么立刻调用 `toBitmap`,
|
||||||
|
* 否则在下一次调用 `next` 时,图像源将会被覆盖。
|
||||||
|
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
|
||||||
|
* @param maxHeight 图集最大高度,也是输出纹理的高度
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public readonly maxWidth: number,
|
||||||
|
public readonly maxHeight: number
|
||||||
|
) {
|
||||||
|
this.canvas = document.createElement('canvas');
|
||||||
|
this.canvas.width = maxWidth;
|
||||||
|
this.canvas.height = maxHeight;
|
||||||
|
this.gl = this.canvas.getContext('webgl2')!;
|
||||||
|
const { program } = compileProgramWith(this.gl, vert, frag)!;
|
||||||
|
this.program = program;
|
||||||
|
|
||||||
|
// 初始化画布数据
|
||||||
|
|
||||||
|
const texture = this.gl.createTexture();
|
||||||
|
this.texArray = texture;
|
||||||
|
const location = this.gl.getUniformLocation(program, 'u_textArray')!;
|
||||||
|
this.texArrayPos = location;
|
||||||
|
|
||||||
|
this.posPos = this.gl.getAttribLocation(program, 'a_position');
|
||||||
|
this.texPos = this.gl.getAttribLocation(program, 'a_texCoord');
|
||||||
|
|
||||||
|
this.vertBuffer = this.gl.createBuffer();
|
||||||
|
|
||||||
|
this.gl.useProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对贴图进行索引
|
||||||
|
* @param textures 贴图数组
|
||||||
|
*/
|
||||||
|
private mapTextures(
|
||||||
|
textures: ITexture[]
|
||||||
|
): Map<SizedCanvasImageSource, number> {
|
||||||
|
const map = new Map<SizedCanvasImageSource, number>();
|
||||||
|
const { width, height } = textures[0].source;
|
||||||
|
|
||||||
|
textures.forEach(v => {
|
||||||
|
const source = v.source;
|
||||||
|
if (map.has(source)) return;
|
||||||
|
if (source.width !== width || source.height !== height) {
|
||||||
|
logger.warn(73);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
map.set(source, map.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.opWidth = width;
|
||||||
|
this.opHeight = height;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传递贴图
|
||||||
|
* @param texMap 纹理映射
|
||||||
|
*/
|
||||||
|
private setTexture(texMap: Map<SizedCanvasImageSource, number>) {
|
||||||
|
const gl = this.gl;
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texArray);
|
||||||
|
|
||||||
|
gl.texStorage3D(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
1,
|
||||||
|
gl.RGBA8,
|
||||||
|
this.opWidth,
|
||||||
|
this.opHeight,
|
||||||
|
texMap.size
|
||||||
|
);
|
||||||
|
texMap.forEach((index, source) => {
|
||||||
|
gl.texSubImage3D(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
index,
|
||||||
|
this.opWidth,
|
||||||
|
this.opHeight,
|
||||||
|
1,
|
||||||
|
gl.RGBA,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
source
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gl.texParameteri(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
gl.TEXTURE_MAG_FILTER,
|
||||||
|
gl.NEAREST
|
||||||
|
);
|
||||||
|
gl.texParameteri(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
gl.TEXTURE_MIN_FILTER,
|
||||||
|
gl.NEAREST
|
||||||
|
);
|
||||||
|
gl.texParameteri(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
gl.TEXTURE_WRAP_S,
|
||||||
|
gl.CLAMP_TO_EDGE
|
||||||
|
);
|
||||||
|
gl.texParameteri(
|
||||||
|
gl.TEXTURE_2D_ARRAY,
|
||||||
|
gl.TEXTURE_WRAP_T,
|
||||||
|
gl.CLAMP_TO_EDGE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理矩形数组,生成 WebGL2 顶点数据
|
||||||
|
* @param rects 要处理的矩形数组
|
||||||
|
* @param texMap 贴图到贴图数组索引的映射
|
||||||
|
*/
|
||||||
|
private processRects(
|
||||||
|
rects: MaxRectsRectangle[],
|
||||||
|
texMap: Map<SizedCanvasImageSource, number>
|
||||||
|
): RectProcessed {
|
||||||
|
const { width: ow, height: oh } = this.canvas;
|
||||||
|
const map = new Map<ITexture, IRect>();
|
||||||
|
const attrib = new Float32Array(rects.length * 5 * 6);
|
||||||
|
rects.forEach((v, i) => {
|
||||||
|
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
|
||||||
|
map.set(v.data, rect);
|
||||||
|
const renderable = v.data.render();
|
||||||
|
const { width: tw, height: th } = v.data.source;
|
||||||
|
const { x, y, w, h } = renderable.rect;
|
||||||
|
// 画到目标画布上的位置
|
||||||
|
const ol = (v.x / ow) * 2 - 1;
|
||||||
|
const ob = (v.y / oh) * 2 - 1;
|
||||||
|
const or = ((v.x + v.width) / ow) * 2 - 1;
|
||||||
|
const ot = ((v.y + v.height) / oh) * 2 - 1;
|
||||||
|
// 原始贴图位置
|
||||||
|
const tl = x / tw;
|
||||||
|
const tt = y / tw;
|
||||||
|
const tr = (x + w) / tw;
|
||||||
|
const tb = (y + h) / th;
|
||||||
|
// 贴图索引
|
||||||
|
const ti = texMap.get(v.data.source);
|
||||||
|
|
||||||
|
if (isNil(ti)) return;
|
||||||
|
|
||||||
|
// Benchmark https://www.measurethat.net/Benchmarks/Show/35246/2/different-method-to-write-a-typedarray
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const data = [
|
||||||
|
// x y u v i
|
||||||
|
ol, -ot, tl, tt, ti, // 左上角
|
||||||
|
ol, -ob, tl, tb, ti, // 左下角
|
||||||
|
or, -ot, tr, tt, ti, // 右上角
|
||||||
|
or, -ot, tr, tt, ti, // 右上角
|
||||||
|
ol, -ob, tl, tb, ti, // 左下角
|
||||||
|
or, -ob, tr, tb, ti // 右下角
|
||||||
|
];
|
||||||
|
|
||||||
|
attrib.set(data, i * 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: RectProcessed = {
|
||||||
|
texMap: map,
|
||||||
|
attrib
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行渲染操作
|
||||||
|
* @param attrib 顶点数组
|
||||||
|
*/
|
||||||
|
private renderAtlas(attrib: Float32Array) {
|
||||||
|
const gl = this.gl;
|
||||||
|
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
gl.clearDepth(1);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, attrib, gl.DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
gl.vertexAttribPointer(this.posPos, 2, gl.FLOAT, false, 5 * 4, 0);
|
||||||
|
gl.vertexAttribPointer(this.texPos, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
|
||||||
|
gl.enableVertexAttribArray(this.posPos);
|
||||||
|
gl.enableVertexAttribArray(this.texPos);
|
||||||
|
|
||||||
|
gl.uniform1i(this.texArrayPos, 0);
|
||||||
|
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, attrib.length / 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
*compose(
|
||||||
|
input: Iterable<ITexture>,
|
||||||
|
data: IMaxRectsComposerData
|
||||||
|
): Generator<ITextureComposedData, void> {
|
||||||
|
this.opWidth = 0;
|
||||||
|
this.opHeight = 0;
|
||||||
|
|
||||||
|
const packer = new MaxRectsPacker<MaxRectsRectangle>(
|
||||||
|
this.maxWidth,
|
||||||
|
this.maxHeight,
|
||||||
|
data.padding,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
const arr = [...input];
|
||||||
|
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||||
|
const rect = v.render().rect;
|
||||||
|
const toPack = new Rectangle(rect.w, rect.h);
|
||||||
|
toPack.data = v;
|
||||||
|
return toPack;
|
||||||
|
});
|
||||||
|
packer.addArray(rects);
|
||||||
|
|
||||||
|
const indexMap = this.mapTextures(arr);
|
||||||
|
this.setTexture(indexMap);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (const bin of packer.bins) {
|
||||||
|
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
|
||||||
|
this.renderAtlas(attrib);
|
||||||
|
const texture = new Texture(this.canvas);
|
||||||
|
const data: ITextureComposedData = {
|
||||||
|
index: index++,
|
||||||
|
texture,
|
||||||
|
assetMap: texMap
|
||||||
|
};
|
||||||
|
yield data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.disableVertexAttribArray(this.posPos);
|
||||||
|
this.gl.disableVertexAttribArray(this.texPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/render-assets/src/index.ts
Normal file
7
packages/render-assets/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from './animater';
|
||||||
|
export * from './composer';
|
||||||
|
export * from './splitter';
|
||||||
|
export * from './store';
|
||||||
|
export * from './streamComposer';
|
||||||
|
export * from './texture';
|
||||||
|
export * from './types';
|
||||||
12
packages/render-assets/src/shader/pack.frag
Normal file
12
packages/render-assets/src/shader/pack.frag
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision highp sampler2DArray;
|
||||||
|
|
||||||
|
in vec3 v_texCoord;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
uniform sampler2DArray u_texArray;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
outColor = texture(u_texArray, v_texCoord);
|
||||||
|
}
|
||||||
12
packages/render-assets/src/shader/pack.vert
Normal file
12
packages/render-assets/src/shader/pack.vert
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
in vec2 a_position;
|
||||||
|
in vec3 a_texCoord;
|
||||||
|
|
||||||
|
out vec3 v_texCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
v_texCoord = a_texCoord;
|
||||||
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||||
|
}
|
||||||
68
packages/render-assets/src/splitter.ts
Normal file
68
packages/render-assets/src/splitter.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Texture } from './texture';
|
||||||
|
import { ITexture, ITextureSplitter, IRect } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按行分割贴图,即分割成一行行的贴图,按从上到下的顺序输出
|
||||||
|
* 输入参数代表每一行的高度
|
||||||
|
*/
|
||||||
|
export class TextureRowSplitter implements ITextureSplitter<number> {
|
||||||
|
*split(texture: ITexture, data: number): Generator<ITexture> {
|
||||||
|
const lines = Math.ceil(texture.height / data);
|
||||||
|
const { x, y } = texture.render().rect;
|
||||||
|
for (let i = 0; i < lines; i++) {
|
||||||
|
const tex = new Texture(texture.source);
|
||||||
|
tex.clip(x, y + i * data, texture.width, data);
|
||||||
|
yield tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按列分割贴图,即分割成一列列的贴图,按从左到右的顺序输出
|
||||||
|
* 输入参数代表每一列的宽度
|
||||||
|
*/
|
||||||
|
export class TextureColumnSplitter implements ITextureSplitter<number> {
|
||||||
|
*split(texture: ITexture, data: number): Generator<ITexture> {
|
||||||
|
const lines = Math.ceil(texture.width / data);
|
||||||
|
const { x, y } = texture.render().rect;
|
||||||
|
for (let i = 0; i < lines; i++) {
|
||||||
|
const tex = new Texture(texture.source);
|
||||||
|
tex.clip(x + i * data, y, data, texture.height);
|
||||||
|
yield tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按照网格分割贴图,按照先从左到右,再从上到下的顺序输出
|
||||||
|
* 输入参数代表每一列的宽度和高度
|
||||||
|
*/
|
||||||
|
export class TextureGridSplitter implements ITextureSplitter<[number, number]> {
|
||||||
|
*split(texture: ITexture, data: [number, number]): Generator<ITexture> {
|
||||||
|
const [w, h] = data;
|
||||||
|
const rows = Math.ceil(texture.width / w);
|
||||||
|
const lines = Math.ceil(texture.height / h);
|
||||||
|
const { x, y } = texture.render().rect;
|
||||||
|
for (let ny = 0; ny < lines; ny++) {
|
||||||
|
for (let nx = 0; nx < rows; nx++) {
|
||||||
|
const tex = new Texture(texture.source);
|
||||||
|
tex.clip(x + nx * w, y + ny * h, w, h);
|
||||||
|
yield tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图集信息分割贴图,按照传入的矩形数组的顺序输出
|
||||||
|
* 输入参数代表每个贴图对应到图集上的矩形位置
|
||||||
|
*/
|
||||||
|
export class TextureAssetSplitter implements ITextureSplitter<IRect[]> {
|
||||||
|
*split(texture: ITexture, data: IRect[]): Generator<ITexture> {
|
||||||
|
for (const { x, y, w, h } of data) {
|
||||||
|
const tex = new Texture(texture.source);
|
||||||
|
tex.clip(x, y, w, h);
|
||||||
|
yield tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
packages/render-assets/src/store.ts
Normal file
107
packages/render-assets/src/store.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import { ITexture, ITextureStore } from './types';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
|
||||||
|
export class TextureStore<T extends ITexture = ITexture>
|
||||||
|
implements ITextureStore<T>
|
||||||
|
{
|
||||||
|
private readonly texMap: Map<number, T> = new Map();
|
||||||
|
private readonly invMap: Map<T, number> = new Map();
|
||||||
|
private readonly aliasMap: Map<string, number> = new Map();
|
||||||
|
private readonly aliasInvMap: Map<number, string> = new Map();
|
||||||
|
|
||||||
|
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
|
||||||
|
return this.texMap.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
entries(): Iterable<[key: number, tex: T]> {
|
||||||
|
return this.texMap.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): Iterable<number> {
|
||||||
|
return this.texMap.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
values(): Iterable<T> {
|
||||||
|
return this.texMap.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
addTexture(identifier: number, texture: T): void {
|
||||||
|
if (this.texMap.has(identifier)) {
|
||||||
|
logger.warn(66, identifier.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.texMap.set(identifier, texture);
|
||||||
|
this.invMap.set(texture, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeBy(id: number, tex: T, alias?: string) {
|
||||||
|
this.texMap.delete(id);
|
||||||
|
this.invMap.delete(tex);
|
||||||
|
if (alias) {
|
||||||
|
this.aliasMap.delete(alias);
|
||||||
|
this.aliasInvMap.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTexture(identifier: number | string | T): void {
|
||||||
|
if (typeof identifier === 'string') {
|
||||||
|
const id = this.aliasMap.get(identifier);
|
||||||
|
if (isNil(id)) return;
|
||||||
|
const tex = this.texMap.get(id);
|
||||||
|
if (isNil(tex)) return;
|
||||||
|
this.removeBy(id, tex, identifier);
|
||||||
|
} else if (typeof identifier === 'number') {
|
||||||
|
const tex = this.texMap.get(identifier);
|
||||||
|
if (isNil(tex)) return;
|
||||||
|
const alias = this.aliasInvMap.get(identifier);
|
||||||
|
this.removeBy(identifier, tex, alias);
|
||||||
|
} else {
|
||||||
|
const id = this.invMap.get(identifier);
|
||||||
|
if (isNil(id)) return;
|
||||||
|
const alias = this.aliasInvMap.get(id);
|
||||||
|
this.removeBy(id, identifier, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTexture(identifier: number): boolean {
|
||||||
|
return this.texMap.has(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTexture(identifier: number): T | null {
|
||||||
|
return this.texMap.get(identifier) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
alias(identifier: number, alias: string): void {
|
||||||
|
const id = this.aliasMap.get(alias);
|
||||||
|
const al = this.aliasInvMap.get(identifier);
|
||||||
|
if (!isNil(al)) {
|
||||||
|
logger.warn(67, alias, identifier.toString(), al);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isNil(id)) {
|
||||||
|
logger.warn(68, alias, identifier.toString(), id.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.aliasMap.set(alias, identifier);
|
||||||
|
this.aliasInvMap.set(identifier, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAlias(alias: string): ITexture | null {
|
||||||
|
const id = this.aliasMap.get(alias);
|
||||||
|
if (isNil(id)) return null;
|
||||||
|
return this.texMap.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
idOf(texture: T): number | undefined {
|
||||||
|
return this.invMap.get(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasOf(identifier: number): string | undefined {
|
||||||
|
return this.aliasInvMap.get(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
identifierOf(alias: string): number | undefined {
|
||||||
|
return this.aliasMap.get(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
packages/render-assets/src/streamComposer.ts
Normal file
201
packages/render-assets/src/streamComposer.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import {
|
||||||
|
Bin,
|
||||||
|
IOption,
|
||||||
|
IRectangle,
|
||||||
|
MaxRectsPacker,
|
||||||
|
Rectangle
|
||||||
|
} from 'maxrects-packer';
|
||||||
|
import { Texture } from './texture';
|
||||||
|
import {
|
||||||
|
IRect,
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureStreamComposer
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
|
||||||
|
readonly rows: number;
|
||||||
|
readonly cols: number;
|
||||||
|
|
||||||
|
private nowIndex: number = 0;
|
||||||
|
private outputIndex: number = -1;
|
||||||
|
|
||||||
|
private nowTexture: ITexture;
|
||||||
|
private nowCanvas: HTMLCanvasElement;
|
||||||
|
private nowCtx: CanvasRenderingContext2D;
|
||||||
|
private nowMap: Map<ITexture, Readonly<IRect>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网格流式贴图组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
|
||||||
|
* 组合时按照先从左到右,再从上到下的顺序组合。
|
||||||
|
* @param width 单个贴图的宽度
|
||||||
|
* @param height 单个贴图的高度
|
||||||
|
* @param maxWidth 图集的最大宽度,也是输出贴图对象的宽度
|
||||||
|
* @param maxHeight 图集的最大高度,也是输出贴图对象的高度
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly width: number,
|
||||||
|
readonly height: number,
|
||||||
|
readonly maxWidth: number,
|
||||||
|
readonly maxHeight: number
|
||||||
|
) {
|
||||||
|
this.rows = Math.floor(maxHeight / height);
|
||||||
|
this.cols = Math.floor(maxWidth / width);
|
||||||
|
|
||||||
|
this.nowCanvas = document.createElement('canvas');
|
||||||
|
this.nowCtx = this.nowCanvas.getContext('2d')!;
|
||||||
|
this.nowMap = new Map();
|
||||||
|
this.nowTexture = new Texture(this.nowCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextCanvas() {
|
||||||
|
this.nowCanvas = document.createElement('canvas');
|
||||||
|
this.nowCtx = this.nowCanvas.getContext('2d')!;
|
||||||
|
this.nowMap = new Map();
|
||||||
|
this.nowIndex = 0;
|
||||||
|
this.nowTexture = new Texture(this.nowCanvas);
|
||||||
|
this.outputIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
|
||||||
|
let index = this.nowIndex;
|
||||||
|
const max = this.cols * this.rows;
|
||||||
|
|
||||||
|
for (const tex of textures) {
|
||||||
|
const nowRow = Math.floor(index / this.cols);
|
||||||
|
const nowCol = index % this.cols;
|
||||||
|
|
||||||
|
const { source, rect } = tex.render();
|
||||||
|
const { x: cx, y: cy, w: cw, h: ch } = rect;
|
||||||
|
const x = nowRow * this.width;
|
||||||
|
const y = nowCol * this.height;
|
||||||
|
this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch);
|
||||||
|
this.nowMap.set(tex, { x, y, w: cw, h: ch });
|
||||||
|
|
||||||
|
if (++index === max) {
|
||||||
|
const data: ITextureComposedData = {
|
||||||
|
index: this.outputIndex,
|
||||||
|
texture: this.nowTexture,
|
||||||
|
assetMap: this.nowMap
|
||||||
|
};
|
||||||
|
yield data;
|
||||||
|
this.nextCanvas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: ITextureComposedData = {
|
||||||
|
index: this.outputIndex,
|
||||||
|
texture: this.nowTexture,
|
||||||
|
assetMap: this.nowMap
|
||||||
|
};
|
||||||
|
yield data;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
// We need to do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MaxRectsRectangle extends IRectangle {
|
||||||
|
/** 这个矩形对应的贴图对象 */
|
||||||
|
readonly data: ITexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextureMaxRectsStreamComposer
|
||||||
|
implements ITextureStreamComposer<void>
|
||||||
|
{
|
||||||
|
/** Max Rects 打包器 */
|
||||||
|
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
|
||||||
|
|
||||||
|
private outputIndex: number = -1;
|
||||||
|
private nowTexture!: ITexture;
|
||||||
|
|
||||||
|
private nowCanvas!: HTMLCanvasElement;
|
||||||
|
private nowCtx!: CanvasRenderingContext2D;
|
||||||
|
private nowMap!: Map<ITexture, Readonly<IRect>>;
|
||||||
|
private nowBin: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Max Rects 算法执行贴图整合。输出的纹理的图像源将会是不同的画布。
|
||||||
|
* @param maxWidth 图集最大宽度,也是输出贴图对象的宽度
|
||||||
|
* @param maxHeight 图集最大高度,也是输出贴图对象的高度
|
||||||
|
* @param padding 每个贴图之间的间距
|
||||||
|
* @param options 传递给打包器对象的参数
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly maxWidth: number,
|
||||||
|
readonly maxHeight: number,
|
||||||
|
readonly padding: number,
|
||||||
|
options?: IOption
|
||||||
|
) {
|
||||||
|
this.packer = new MaxRectsPacker<MaxRectsRectangle>(
|
||||||
|
this.maxWidth,
|
||||||
|
this.maxHeight,
|
||||||
|
padding,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
this.nextCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextCanvas() {
|
||||||
|
this.nowCanvas = document.createElement('canvas');
|
||||||
|
this.nowCtx = this.nowCanvas.getContext('2d')!;
|
||||||
|
this.nowCanvas.width = this.maxWidth;
|
||||||
|
this.nowCanvas.height = this.maxHeight;
|
||||||
|
this.nowMap = new Map();
|
||||||
|
this.outputIndex++;
|
||||||
|
this.nowTexture = new Texture(this.nowCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
|
||||||
|
const arr = [...textures];
|
||||||
|
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||||
|
const rect = v.render().rect;
|
||||||
|
const toPack = new Rectangle(rect.w, rect.h);
|
||||||
|
toPack.data = v;
|
||||||
|
return toPack;
|
||||||
|
});
|
||||||
|
this.packer.addArray(rects);
|
||||||
|
|
||||||
|
const bins = this.packer.bins;
|
||||||
|
if (bins.length === 0) return;
|
||||||
|
const toYield: Bin<MaxRectsRectangle>[] = [bins[bins.length - 1]];
|
||||||
|
if (bins.length > this.nowBin) {
|
||||||
|
toYield.push(...bins.slice(this.nowBin));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this.nowBin; i < bins.length; i++) {
|
||||||
|
const rects = bins[i].rects;
|
||||||
|
rects.forEach(v => {
|
||||||
|
if (this.nowMap.has(v.data)) return;
|
||||||
|
const target: IRect = {
|
||||||
|
x: v.x,
|
||||||
|
y: v.y,
|
||||||
|
w: v.width,
|
||||||
|
h: v.height
|
||||||
|
};
|
||||||
|
this.nowMap.set(v.data, target);
|
||||||
|
const { source, rect } = v.data.render();
|
||||||
|
const { x: cx, y: cy, w: cw, h: ch } = rect;
|
||||||
|
this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: ITextureComposedData = {
|
||||||
|
index: this.outputIndex,
|
||||||
|
texture: this.nowTexture,
|
||||||
|
assetMap: this.nowMap
|
||||||
|
};
|
||||||
|
yield data;
|
||||||
|
if (i < bins.length - 1) {
|
||||||
|
this.nextCanvas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nowBin = bins.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
// We need to do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
193
packages/render-assets/src/texture.ts
Normal file
193
packages/render-assets/src/texture.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
IRect,
|
||||||
|
ITexture,
|
||||||
|
ITextureComposedData,
|
||||||
|
ITextureRenderable,
|
||||||
|
ITextureSplitter,
|
||||||
|
SizedCanvasImageSource
|
||||||
|
} from './types';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
|
||||||
|
export class Texture implements ITexture {
|
||||||
|
source: SizedCanvasImageSource;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
isBitmap: boolean = false;
|
||||||
|
|
||||||
|
/** 裁剪矩形的左边框位置 */
|
||||||
|
private cl: number;
|
||||||
|
/** 裁剪矩形的上边框位置 */
|
||||||
|
private ct: number;
|
||||||
|
/** 裁剪矩形的右边框位置 */
|
||||||
|
private cr: number;
|
||||||
|
/** 裁剪矩形的下边框位置 */
|
||||||
|
private cb: number;
|
||||||
|
|
||||||
|
constructor(source: SizedCanvasImageSource) {
|
||||||
|
this.source = source;
|
||||||
|
this.width = source.width;
|
||||||
|
this.height = source.height;
|
||||||
|
this.cl = 0;
|
||||||
|
this.ct = 0;
|
||||||
|
this.cr = source.width;
|
||||||
|
this.cb = source.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对纹理进行裁剪操作,不会改变图像源
|
||||||
|
* @param x 裁剪左上角横坐标
|
||||||
|
* @param y 裁剪左上角纵坐标
|
||||||
|
* @param w 裁剪宽度
|
||||||
|
* @param h 裁剪高度
|
||||||
|
*/
|
||||||
|
clip(x: number, y: number, w: number, h: number) {
|
||||||
|
const left = clamp(this.cl + x, this.cl, this.cr);
|
||||||
|
const top = clamp(this.ct + y, this.ct, this.cb);
|
||||||
|
const right = clamp(this.cl + x + w, this.cl, this.cr);
|
||||||
|
const bottom = clamp(this.ct + y + h, this.ct, this.cb);
|
||||||
|
if (left === right || top === bottom) {
|
||||||
|
logger.warn(69);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const width = right - left;
|
||||||
|
const height = bottom - top;
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
logger.warn(77);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cl = left;
|
||||||
|
this.ct = top;
|
||||||
|
this.cr = right;
|
||||||
|
this.cb = bottom;
|
||||||
|
this.width = right - left;
|
||||||
|
this.height = bottom - top;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toBitmap(): Promise<void> {
|
||||||
|
if (this.source instanceof ImageBitmap) return;
|
||||||
|
this.source = await createImageBitmap(
|
||||||
|
this.source,
|
||||||
|
this.cl,
|
||||||
|
this.ct,
|
||||||
|
this.width,
|
||||||
|
this.height
|
||||||
|
);
|
||||||
|
this.cl = 0;
|
||||||
|
this.ct = 0;
|
||||||
|
this.cr = this.width;
|
||||||
|
this.cb = this.height;
|
||||||
|
this.isBitmap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
split<U>(splitter: ITextureSplitter<U>, data: U): Generator<ITexture> {
|
||||||
|
return splitter.split(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ITextureRenderable {
|
||||||
|
return {
|
||||||
|
source: this.source,
|
||||||
|
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clampRect(rect: Readonly<IRect>): Readonly<IRect> {
|
||||||
|
const l = clamp(rect.x, this.cl, this.cr);
|
||||||
|
const t = clamp(rect.y, this.ct, this.cb);
|
||||||
|
const r = clamp(rect.x + rect.w, this.cl, this.cr);
|
||||||
|
const b = clamp(rect.y + rect.h, this.ct, this.cb);
|
||||||
|
return { x: l, y: t, w: r - l, h: b - t };
|
||||||
|
}
|
||||||
|
|
||||||
|
clipped(rect: Readonly<IRect>): ITextureRenderable {
|
||||||
|
return {
|
||||||
|
source: this.source,
|
||||||
|
rect: this.clampRect(rect)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
if (this.source instanceof ImageBitmap) {
|
||||||
|
this.source.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toAsset(asset: ITextureComposedData): boolean {
|
||||||
|
const rect = asset.assetMap.get(this);
|
||||||
|
if (!rect) return false;
|
||||||
|
if (this.isBitmap && this.source instanceof ImageBitmap) {
|
||||||
|
this.source.close();
|
||||||
|
}
|
||||||
|
this.isBitmap = false;
|
||||||
|
this.source = asset.texture.source;
|
||||||
|
this.cl = rect.x;
|
||||||
|
this.ct = rect.y;
|
||||||
|
this.width = rect.w;
|
||||||
|
this.height = rect.h;
|
||||||
|
this.cr = rect.x + rect.w;
|
||||||
|
this.cb = rect.y + rect.h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对贴图的动画执行偏移效果。动画效果可以不来自传入的贴图对象,
|
||||||
|
* 输出结果的图像源会是传入的贴图对象的图像源而非动画效果对应的图像源
|
||||||
|
* @param texture 贴图对象
|
||||||
|
* @param animate 动画效果
|
||||||
|
* @param ox 偏移横坐标
|
||||||
|
* @param oy 偏移纵坐标
|
||||||
|
*/
|
||||||
|
static *translated(
|
||||||
|
texture: ITexture,
|
||||||
|
animate: Generator<ITextureRenderable, void> | null,
|
||||||
|
ox: number,
|
||||||
|
oy: number
|
||||||
|
): Generator<ITextureRenderable, void> | null {
|
||||||
|
if (!animate) return null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const next = animate.next();
|
||||||
|
if (next.done) break;
|
||||||
|
const renderable = next.value;
|
||||||
|
const { x, y, w, h } = renderable.rect;
|
||||||
|
const translated: IRect = { x: x + ox, y: y + oy, w, h };
|
||||||
|
const res: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect(translated)
|
||||||
|
};
|
||||||
|
yield res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对贴图的动画重定位。最终输出的动画会以传入的 `fx` `fy` 为左上角坐标计算
|
||||||
|
* @param texture 贴图对象
|
||||||
|
* @param origin 动画效果来自的贴图对象
|
||||||
|
* @param animate 动画效果
|
||||||
|
* @param fx 左上角横坐标
|
||||||
|
* @param fy 左上角纵坐标
|
||||||
|
*/
|
||||||
|
static *fixed(
|
||||||
|
texture: ITexture,
|
||||||
|
origin: ITexture,
|
||||||
|
animate: Generator<ITextureRenderable, void> | null,
|
||||||
|
fx: number,
|
||||||
|
fy: number
|
||||||
|
): Generator<ITextureRenderable, void> | null {
|
||||||
|
if (!animate) return null;
|
||||||
|
const { x: ox, y: oy } = origin.render().rect;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const next = animate.next();
|
||||||
|
if (next.done) break;
|
||||||
|
const renderable = next.value;
|
||||||
|
const { x, y, w, h } = renderable.rect;
|
||||||
|
const translated: IRect = { x: x - ox + fx, y: y - oy + fy, w, h };
|
||||||
|
const res: ITextureRenderable = {
|
||||||
|
source: texture.source,
|
||||||
|
rect: texture.clampRect(translated)
|
||||||
|
};
|
||||||
|
yield res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user