mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-11-28 22:42:59 +08:00
Compare commits
2 Commits
ad469cf1ff
...
639a0d0f68
| Author | SHA1 | Date | |
|---|---|---|---|
| 639a0d0f68 | |||
| 0d9bef70b1 |
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@user/client-base",
|
||||
"dependencies": {
|
||||
"@motajs/render-asset": "workspace:*"
|
||||
"@motajs/render-asset": "workspace:*",
|
||||
"@motajs/client-base": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { IMaterialAsset } from './types';
|
||||
|
||||
export class MaterialAsset implements IMaterialAsset {
|
||||
/** 标记列表 */
|
||||
private readonly marks: Map<symbol, number> = new Map();
|
||||
private readonly marks: WeakMap<symbol, number> = new WeakMap();
|
||||
/** 脏标记,所有值小于此标记的都视为需要更新 */
|
||||
private dirtyFlag: number = 0;
|
||||
|
||||
@ -27,4 +27,8 @@ export class MaterialAsset implements IMaterialAsset {
|
||||
const value = this.marks.get(mark) ?? -1;
|
||||
return value < this.dirtyFlag;
|
||||
}
|
||||
|
||||
hasMark(symbol: symbol): boolean {
|
||||
return this.marks.has(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { IRect, ITexture, ITextureRenderable } from '@motajs/render-assets';
|
||||
import {
|
||||
IRect,
|
||||
ITextureRenderable,
|
||||
SizedCanvasImageSource
|
||||
} from '@motajs/render-assets';
|
||||
import {
|
||||
AutotileConnection,
|
||||
AutotileType,
|
||||
BlockCls,
|
||||
IAutotileConnection,
|
||||
IAutotileProcessor,
|
||||
IAutotileRenderable,
|
||||
IMaterialFramedData,
|
||||
IMaterialManager
|
||||
} from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
interface ConnectedAutotile {
|
||||
readonly lt: Readonly<IRect>;
|
||||
@ -16,19 +21,23 @@ interface ConnectedAutotile {
|
||||
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 自动元件各方向连接矩形映射 */
|
||||
/** 3x4 自动元件各方向连接的矩形映射 */
|
||||
const rectMap3x4 = new Map<number, ConnectedAutotile>();
|
||||
/** 2x3 自动元件各方向连接的矩形映射 */
|
||||
const rectMap2x3 = new Map<number, ConnectedAutotile>();
|
||||
|
||||
interface AutotileFrameList {
|
||||
type: AutotileType;
|
||||
rects: Readonly<IRect>[];
|
||||
}
|
||||
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
|
||||
const distinctConnectionMap = new Map<number, number>();
|
||||
|
||||
export class AutotileProcessor implements IAutotileProcessor {
|
||||
/** 自动元件父子关系映射,子元件 -> 父元件 */
|
||||
@ -46,7 +55,7 @@ export class AutotileProcessor implements IAutotileProcessor {
|
||||
return ensure;
|
||||
}
|
||||
|
||||
setParent(autotile: number, parent: number): void {
|
||||
setConnection(autotile: number, parent: number): void {
|
||||
this.parentMap.set(autotile, parent);
|
||||
const child = this.ensureChildSet(parent);
|
||||
child.add(autotile);
|
||||
@ -116,8 +125,14 @@ export class AutotileProcessor implements IAutotileProcessor {
|
||||
index: number,
|
||||
width: number
|
||||
): IAutotileConnection {
|
||||
let res: number = this.connectEdge(array.length, index, width);
|
||||
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);
|
||||
|
||||
// 最高位表示左上,低位依次顺时针旋转
|
||||
@ -132,7 +147,7 @@ export class AutotileProcessor implements IAutotileProcessor {
|
||||
|
||||
// Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number
|
||||
|
||||
if (!childList) {
|
||||
if (!childList || childList.size === 0) {
|
||||
// 不包含子元件,那么直接跟相同的连接
|
||||
res |=
|
||||
+(a0 === block) |
|
||||
@ -161,157 +176,129 @@ export class AutotileProcessor implements IAutotileProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
render(
|
||||
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 === 128 ? 32 : 48;
|
||||
const index = distinctConnectionMap.get(connection);
|
||||
if (isNil(index)) return null;
|
||||
return {
|
||||
source: texture.source,
|
||||
rect: { x: 0, y: size * index, w: size, h: size }
|
||||
};
|
||||
}
|
||||
|
||||
*renderAnimated(
|
||||
autotile: number,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null {
|
||||
const cls = this.manager.getBlockCls(autotile);
|
||||
if (cls !== BlockCls.Autotile) return null;
|
||||
const tile = this.manager.getTile(autotile)!;
|
||||
return this.fromStaticRenderable(tile.static(), connection);
|
||||
): Generator<ITextureRenderable, void> {
|
||||
const tile = this.manager.getTile(autotile);
|
||||
if (!tile) return;
|
||||
yield* this.renderAnimatedWith(tile, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据静态可渲染对象获取自动元件的帧列表
|
||||
* @param renderable 静态可渲染对象
|
||||
*/
|
||||
private getStaticRectList(
|
||||
renderable: ITextureRenderable
|
||||
): AutotileFrameList {
|
||||
const { x, y, w, h } = renderable.rect;
|
||||
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
|
||||
if (w === 96) {
|
||||
return {
|
||||
type,
|
||||
rects: [renderable.rect]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type,
|
||||
rects: [
|
||||
{ x: x + 0, y, w, h },
|
||||
{ x: x + 96, y, w, h },
|
||||
{ x: x + 192, y, w, h },
|
||||
{ x: x + 288, y, w, h }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对自动元件连接执行偏移操作,偏移至自动元件在图像源中的所在矩形范围
|
||||
* @param ox 横向偏移量
|
||||
* @param oy 纵向偏移量
|
||||
* @param connection 自动元件连接信息
|
||||
*/
|
||||
private getConnectedRect(
|
||||
ox: number,
|
||||
oy: number,
|
||||
connection: ConnectedAutotile
|
||||
): ConnectedAutotile {
|
||||
const { lt, rt, rb, lb } = connection;
|
||||
|
||||
return {
|
||||
lt: { x: ox + lt.x, y: oy + lt.y, w: lt.w, h: lt.h },
|
||||
rt: { x: ox + rt.x, y: oy + rt.y, w: rt.w, h: rt.h },
|
||||
rb: { x: ox + rb.x, y: oy + rb.y, w: rb.w, h: rb.h },
|
||||
lb: { x: ox + lb.x, y: oy + lb.y, w: lb.w, h: lb.h }
|
||||
};
|
||||
}
|
||||
|
||||
*fromStaticRenderable(
|
||||
renderable: ITextureRenderable,
|
||||
*renderAnimatedWith(
|
||||
tile: IMaterialFramedData,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null {
|
||||
const { type, rects } = this.getStaticRectList(renderable);
|
||||
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
|
||||
const data = map.get(connection);
|
||||
if (!data) {
|
||||
logger.error(27);
|
||||
return null;
|
||||
}
|
||||
if (rects.length === 1) {
|
||||
const { x, y } = rects[0];
|
||||
const connected = this.getConnectedRect(x, y, data);
|
||||
if (!connected) return null;
|
||||
const res: IAutotileRenderable = {
|
||||
source: renderable.source,
|
||||
lt: connected.lt,
|
||||
rt: connected.rt,
|
||||
rb: connected.rb,
|
||||
lb: connected.lb
|
||||
): 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 }
|
||||
};
|
||||
yield res;
|
||||
} else {
|
||||
for (const { x, y } of rects) {
|
||||
const connected = this.getConnectedRect(x, y, data);
|
||||
if (!connected) return null;
|
||||
const res: IAutotileRenderable = {
|
||||
source: renderable.source,
|
||||
lt: connected.lt,
|
||||
rt: connected.rt,
|
||||
rb: connected.rb,
|
||||
lb: connected.lb
|
||||
};
|
||||
yield res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将自动元件图片展平,平铺存储 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fromAnimatedRenderable(
|
||||
renderable: ITextureRenderable,
|
||||
connection: number
|
||||
): IAutotileRenderable | null {
|
||||
const { x, y, h } = renderable.rect;
|
||||
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
|
||||
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
|
||||
const data = map.get(connection);
|
||||
if (!data) {
|
||||
logger.error(27);
|
||||
return null;
|
||||
}
|
||||
const connected = this.getConnectedRect(x, y, data);
|
||||
if (!connected) return null;
|
||||
const res: IAutotileRenderable = {
|
||||
source: renderable.source,
|
||||
lt: connected.lt,
|
||||
rt: connected.rt,
|
||||
rb: connected.rb,
|
||||
lb: connected.lb
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
*fromAnimatedGenerator(
|
||||
texture: ITexture,
|
||||
generator: Generator<ITextureRenderable> | null,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null {
|
||||
if (!generator) return null;
|
||||
const h = texture.height;
|
||||
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
|
||||
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
|
||||
const data = map.get(connection);
|
||||
if (!data) {
|
||||
logger.error(27);
|
||||
return null;
|
||||
}
|
||||
while (true) {
|
||||
const value = generator.next();
|
||||
if (value.done) break;
|
||||
const renderable = value.value;
|
||||
const { x, y } = renderable.rect;
|
||||
const connected = this.getConnectedRect(x, y, data);
|
||||
if (!connected) return null;
|
||||
const res: IAutotileRenderable = {
|
||||
source: renderable.source,
|
||||
lt: connected.lt,
|
||||
rt: connected.rt,
|
||||
rb: connected.rb,
|
||||
lb: connected.lb
|
||||
};
|
||||
yield res;
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
@ -444,4 +431,23 @@ export function createAutotile() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,6 +31,16 @@ function addTileset(set: Set<number>, map?: readonly (readonly number[])[]) {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版加载
|
||||
*/
|
||||
@ -57,11 +67,11 @@ export function fallbackLoad() {
|
||||
materials.addGrid(images.items, items);
|
||||
|
||||
// Row Animates
|
||||
materials.addRowAnimate(images.animates, animates, 4, 32);
|
||||
materials.addRowAnimate(images.enemys, enemys, 2, 32);
|
||||
materials.addRowAnimate(images.npcs, npcs, 2, 32);
|
||||
materials.addRowAnimate(images.enemy48, enemy48, 4, 48);
|
||||
materials.addRowAnimate(images.npc48, npc48, 4, 48);
|
||||
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)) {
|
||||
@ -91,10 +101,9 @@ export function fallbackLoad() {
|
||||
materials.addImage(img, { index: i, alias: v });
|
||||
});
|
||||
|
||||
materials.buildAssets();
|
||||
|
||||
// 地图上出现过的 tileset
|
||||
const tilesetSet = new Set<number>();
|
||||
const autotileSet = new Set<number>();
|
||||
core.floorIds.forEach(v => {
|
||||
const floor = core.floors[v];
|
||||
addTileset(tilesetSet, floor.bgmap);
|
||||
@ -102,7 +111,14 @@ export function fallbackLoad() {
|
||||
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);
|
||||
});
|
||||
|
||||
materials.cacheTilesetList(tilesetSet);
|
||||
materials.cacheTilesetList(tilesetSet.union(autotileSet));
|
||||
|
||||
materials.buildAssets();
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { loading } from '@user/data-base';
|
||||
import { fallbackLoad } from './fallback';
|
||||
import { createAutotile } from './autotile';
|
||||
|
||||
export function createMaterial() {
|
||||
createAutotile();
|
||||
loading.once('loaded', () => {
|
||||
fallbackLoad();
|
||||
});
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
ITextureStore,
|
||||
SizedCanvasImageSource,
|
||||
Texture,
|
||||
TextureColumnAnimater,
|
||||
TextureGridSplitter,
|
||||
TextureRowSplitter,
|
||||
TextureStore
|
||||
@ -18,15 +17,17 @@ import {
|
||||
IIndexedIdentifier,
|
||||
IMaterialAssetData,
|
||||
BlockCls,
|
||||
IBigImageData,
|
||||
IBigImageReturn,
|
||||
IAssetBuilder,
|
||||
IMaterialAsset
|
||||
IMaterialAsset,
|
||||
IMaterialFramedData
|
||||
} from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
import { getClsByString } from './utils';
|
||||
import { getClsByString, getTextureFrame } from './utils';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { AssetBuilder } from './builder';
|
||||
import { MaterialAsset } from './asset';
|
||||
import { AutotileProcessor } from './autotile';
|
||||
|
||||
export class MaterialManager implements IMaterialManager {
|
||||
readonly tileStore: ITextureStore = new TextureStore();
|
||||
@ -35,18 +36,26 @@ export class MaterialManager implements IMaterialManager {
|
||||
readonly assetStore: ITextureStore = new TextureStore();
|
||||
readonly bigImageStore: ITextureStore = new TextureStore();
|
||||
|
||||
/** 自动元件图像源映射 */
|
||||
readonly autotileSource: Map<number, SizedCanvasImageSource> = new Map();
|
||||
|
||||
/** 图集信息存储 */
|
||||
readonly assetDataStore: Map<number, IMaterialAsset> = new Map();
|
||||
/** 贴图到图集索引的映射 */
|
||||
readonly assetMap: Map<ITexture, number> = new Map();
|
||||
|
||||
/** 大怪物数据 */
|
||||
readonly bigImageData: Map<number, ITexture> = new Map();
|
||||
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 = new AssetBuilder();
|
||||
|
||||
/** 图块 id 到图块数字的映射 */
|
||||
readonly idNumMap: Map<string, number> = new Map();
|
||||
/** 图块数字到图块 id 的映射 */
|
||||
readonly numIdMap: Map<number, string> = new Map();
|
||||
/** 图块数字到图块类型的映射 */
|
||||
readonly clsMap: Map<number, BlockCls> = new Map();
|
||||
|
||||
/** 网格切分器 */
|
||||
@ -63,9 +72,6 @@ export class MaterialManager implements IMaterialManager {
|
||||
/** 是否已经构建过素材 */
|
||||
private built: boolean = false;
|
||||
|
||||
/** 标记列表 */
|
||||
private readonly markList: symbol[] = [];
|
||||
|
||||
constructor() {
|
||||
this.assetBuilder.pipe(this.assetStore);
|
||||
}
|
||||
@ -125,7 +131,6 @@ export class MaterialManager implements IMaterialManager {
|
||||
addRowAnimate(
|
||||
source: SizedCanvasImageSource,
|
||||
map: ArrayLike<IBlockIdentifier>,
|
||||
frames: number,
|
||||
height: number
|
||||
): Iterable<IMaterialData> {
|
||||
return this.addMappedSource(
|
||||
@ -133,18 +138,18 @@ export class MaterialManager implements IMaterialManager {
|
||||
map,
|
||||
this.tileStore,
|
||||
this.rowSplitter,
|
||||
height,
|
||||
(tex: ITexture<number>) => {
|
||||
tex.animated(new TextureColumnAnimater(), frames);
|
||||
}
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
addAutotile(
|
||||
source: SizedCanvasImageSource,
|
||||
identifier: IBlockIdentifier
|
||||
): IMaterialData {
|
||||
const texture = new Texture(source);
|
||||
): IMaterialData | null {
|
||||
const frames = source.width === 96 ? 1 : 4;
|
||||
const flattened = AutotileProcessor.flatten({ source, frames });
|
||||
if (!flattened) return null;
|
||||
const texture = new Texture(flattened);
|
||||
this.tileStore.addTexture(identifier.num, texture);
|
||||
this.tileStore.alias(identifier.num, identifier.id);
|
||||
this.clsMap.set(identifier.num, BlockCls.Autotile);
|
||||
@ -177,6 +182,7 @@ export class MaterialManager implements IMaterialManager {
|
||||
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;
|
||||
@ -213,11 +219,26 @@ export class MaterialManager implements IMaterialManager {
|
||||
return data;
|
||||
}
|
||||
|
||||
getTile(identifier: number): ITexture | null {
|
||||
getTile(identifier: number): IMaterialFramedData | null {
|
||||
if (identifier < 10000) {
|
||||
return this.tileStore.getTexture(identifier);
|
||||
const texture = this.tileStore.getTexture(identifier);
|
||||
if (!texture) return null;
|
||||
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
|
||||
return {
|
||||
texture,
|
||||
cls,
|
||||
offset: 32,
|
||||
frames: getTextureFrame(cls, texture)
|
||||
};
|
||||
} else {
|
||||
return this.cacheTileset(identifier);
|
||||
const texture = this.cacheTileset(identifier);
|
||||
if (!texture) return null;
|
||||
return {
|
||||
texture,
|
||||
cls: BlockCls.Tileset,
|
||||
offset: 32,
|
||||
frames: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,11 +250,13 @@ export class MaterialManager implements IMaterialManager {
|
||||
return this.imageStore.getTexture(identifier);
|
||||
}
|
||||
|
||||
getTileByAlias(alias: string): ITexture | null {
|
||||
getTileByAlias(alias: string): IMaterialFramedData | null {
|
||||
if (/X\d{5,}/.test(alias)) {
|
||||
return this.cacheTileset(parseInt(alias.slice(1)));
|
||||
return this.getTile(parseInt(alias.slice(1)));
|
||||
} else {
|
||||
return this.tileStore.fromAlias(alias);
|
||||
const identifier = this.tileStore.identifierOf(alias);
|
||||
if (isNil(identifier)) return null;
|
||||
return this.getTile(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,11 +305,33 @@ export class MaterialManager implements IMaterialManager {
|
||||
} else {
|
||||
// 如果有新图集,需要添加
|
||||
const alias = `asset-${data.index}`;
|
||||
const newAsset = new MaterialAsset(data);
|
||||
newAsset.dirty();
|
||||
this.assetStore.alias(data.index, alias);
|
||||
this.assetDataStore.set(data.index, new MaterialAsset(data));
|
||||
this.assetDataStore.set(data.index, newAsset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定的贴图列表转换至指定的图集数据中
|
||||
* @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;
|
||||
@ -315,16 +360,59 @@ export class MaterialManager implements IMaterialManager {
|
||||
this.numIdMap.set(v, `X${v}`);
|
||||
});
|
||||
|
||||
const set = new Set(toAdd);
|
||||
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.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);
|
||||
});
|
||||
|
||||
const data = this.assetBuilder.addTextureList(toAdd);
|
||||
const res = [...data];
|
||||
res.forEach(data => {
|
||||
data.assetMap.keys().forEach(tex => {
|
||||
if (set.has(tex)) tex.toAsset(data);
|
||||
});
|
||||
this.checkAssetDirty(data);
|
||||
});
|
||||
this.cacheToAsset(res, toAdd);
|
||||
|
||||
return toAdd;
|
||||
}
|
||||
@ -380,7 +468,7 @@ export class MaterialManager implements IMaterialManager {
|
||||
if (isNil(cls)) return null;
|
||||
const texture = this.getTextureOf(identifier, cls);
|
||||
if (!texture) return null;
|
||||
return texture.static();
|
||||
return texture.render();
|
||||
}
|
||||
|
||||
getRenderableByAlias(alias: string): ITextureRenderable | null {
|
||||
@ -407,11 +495,22 @@ export class MaterialManager implements IMaterialManager {
|
||||
return this.numIdMap.get(identifier);
|
||||
}
|
||||
|
||||
setBigImage(identifier: number, image: ITexture): IBigImageData {
|
||||
setBigImage(
|
||||
identifier: number,
|
||||
image: ITexture,
|
||||
frames: number
|
||||
): IBigImageReturn {
|
||||
const bigImageId = this.bigImageId++;
|
||||
this.bigImageStore.addTexture(bigImageId, image);
|
||||
this.bigImageData.set(identifier, image);
|
||||
const data: IBigImageData = {
|
||||
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
|
||||
const store: IMaterialFramedData = {
|
||||
texture: image,
|
||||
cls,
|
||||
offset: image.width / 4,
|
||||
frames
|
||||
};
|
||||
this.bigImageData.set(identifier, store);
|
||||
const data: IBigImageReturn = {
|
||||
identifier: bigImageId,
|
||||
store: this.bigImageStore
|
||||
};
|
||||
@ -422,23 +521,27 @@ export class MaterialManager implements IMaterialManager {
|
||||
return this.bigImageData.has(identifier);
|
||||
}
|
||||
|
||||
getBigImage(identifier: number): ITexture | null {
|
||||
getBigImage(identifier: number): IMaterialFramedData | null {
|
||||
return this.bigImageData.get(identifier) ?? null;
|
||||
}
|
||||
|
||||
getBigImageByAlias(alias: string): ITexture | null {
|
||||
getBigImageByAlias(alias: string): IMaterialFramedData | null {
|
||||
const identifier = this.idNumMap.get(alias);
|
||||
if (isNil(identifier)) return null;
|
||||
return this.bigImageData.get(identifier) ?? null;
|
||||
}
|
||||
|
||||
getIfBigImage(identifier: number): ITexture | null {
|
||||
getIfBigImage(identifier: number): IMaterialFramedData | null {
|
||||
const bigImage = this.bigImageData.get(identifier) ?? null;
|
||||
if (bigImage) return bigImage;
|
||||
if (identifier < 10000) {
|
||||
return this.tileStore.getTexture(identifier);
|
||||
} else {
|
||||
return this.cacheTileset(identifier);
|
||||
}
|
||||
else return this.getTile(identifier);
|
||||
}
|
||||
|
||||
assetContainsTexture(texture: ITexture): boolean {
|
||||
return this.assetMap.has(texture);
|
||||
}
|
||||
|
||||
getTextureAsset(texture: ITexture): number | undefined {
|
||||
return this.assetMap.get(texture);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { IDirtyTracker, IDirtyMarker } from '@motajs/common';
|
||||
import {
|
||||
IRect,
|
||||
ITexture,
|
||||
ITextureComposedData,
|
||||
ITextureRenderable,
|
||||
@ -25,6 +25,17 @@ export const enum AutotileType {
|
||||
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;
|
||||
@ -70,53 +81,27 @@ export interface IAutotileConnection {
|
||||
readonly center: number;
|
||||
}
|
||||
|
||||
export interface IAutotileRenderable {
|
||||
/** 自动元件的图像源 */
|
||||
readonly source: SizedCanvasImageSource;
|
||||
/** 左上渲染的矩形范围 */
|
||||
readonly lt: Readonly<IRect>;
|
||||
/** 右上渲染的矩形范围 */
|
||||
readonly rt: Readonly<IRect>;
|
||||
/** 右下渲染的矩形范围 */
|
||||
readonly rb: Readonly<IRect>;
|
||||
/** 左下渲染的矩形范围 */
|
||||
readonly lb: Readonly<IRect>;
|
||||
}
|
||||
|
||||
export interface IBigImageData {
|
||||
export interface IBigImageReturn {
|
||||
/** 大怪物贴图在 store 中的标识符 */
|
||||
readonly identifier: number;
|
||||
/** 存储大怪物贴图的存储对象 */
|
||||
readonly store: ITextureStore;
|
||||
}
|
||||
|
||||
export interface IAssetDirtyMarker {
|
||||
/**
|
||||
* 标记为脏,即进行了一次更新
|
||||
*/
|
||||
dirty(): void;
|
||||
export interface IMaterialFramedData {
|
||||
/** 贴图对象 */
|
||||
readonly texture: ITexture;
|
||||
/** 图块类型 */
|
||||
readonly cls: BlockCls;
|
||||
/** 贴图总帧数 */
|
||||
readonly frames: number;
|
||||
/** 每帧的横向偏移量 */
|
||||
readonly offset: number;
|
||||
}
|
||||
|
||||
export interface IAssetDirtyTracker {
|
||||
/**
|
||||
* 对图集状态进行标记
|
||||
*/
|
||||
mark(): symbol;
|
||||
|
||||
/**
|
||||
* 取消指定标记符号
|
||||
* @param mark 标记符号
|
||||
*/
|
||||
unmark(mark: symbol): void;
|
||||
|
||||
/**
|
||||
* 从指定标记符号开始,图集是否发生了变动
|
||||
* @param mark 标记符号
|
||||
*/
|
||||
dirtySince(mark: symbol): boolean;
|
||||
}
|
||||
|
||||
export interface IMaterialAsset extends IAssetDirtyTracker, IAssetDirtyMarker {
|
||||
export interface IMaterialAsset
|
||||
extends IDirtyTracker<boolean>,
|
||||
IDirtyMarker<void> {
|
||||
/** 图集的贴图数据 */
|
||||
readonly data: ITextureComposedData;
|
||||
}
|
||||
@ -126,11 +111,12 @@ export interface IAutotileProcessor {
|
||||
readonly manager: IMaterialManager;
|
||||
|
||||
/**
|
||||
* 设置一个自动元件的父元件,一个自动元件可以有多个父元件
|
||||
* 设置一个自动元件的特殊连接方式,设置后当前自动元件将会单方面与目标元件连接,
|
||||
* 一个自动元件可以与多个自动元件有特殊连接
|
||||
* @param autotile 自动元件
|
||||
* @param parent 自动元件的父元件
|
||||
* @param target 当前自动元件将会连接至的自动元件
|
||||
*/
|
||||
setParent(autotile: number, parent: number): void;
|
||||
setConnection(autotile: number, target: number): void;
|
||||
|
||||
/**
|
||||
* 获取自动元件的连接情况
|
||||
@ -145,52 +131,71 @@ export interface IAutotileProcessor {
|
||||
): 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: IMaterialFramedData,
|
||||
connection: number
|
||||
): ITextureRenderable | null;
|
||||
|
||||
/**
|
||||
* 根据图块贴图对象,获取指定自动元件经过连接的可渲染对象,但是会假设传入的图块就是自动元件,不做不必要的判断
|
||||
* @param tile 自动元件的图块贴图数据
|
||||
* @param connection 连接方式,上方连接是第一位,顺时针旋转位次依次升高
|
||||
* @returns 连接方式的可渲染对象,可以通过偏移量依次获取其他帧
|
||||
*/
|
||||
renderWithoutCheck(
|
||||
tile: IMaterialFramedData,
|
||||
connection: number
|
||||
): ITextureRenderable | null;
|
||||
|
||||
/**
|
||||
* 根据图块数字,获取指定自动元件经过链接的动态可渲染对象
|
||||
* @param autotile 自动元件的图块数字
|
||||
* @param connection 自动元件的连接方式
|
||||
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
|
||||
*/
|
||||
render(
|
||||
renderAnimated(
|
||||
autotile: number,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null;
|
||||
): Generator<ITextureRenderable, void>;
|
||||
|
||||
/**
|
||||
* 通过静态可渲染对象(由 {@link ITexture.static} 输出的可渲染对象)输出自动元件经过连接的可渲染对象生成器
|
||||
* @param renderable 自动元件的原始可渲染对象
|
||||
* 根据图块贴图对象,获取指定自动元件经过链接的动态可渲染对象
|
||||
* @param autotile 自动元件的图块数字
|
||||
* @param connection 自动元件的连接方式
|
||||
* @returns 生成器,每一个输出代表每一帧的渲染对象,不同自动元件的帧数可能不同
|
||||
*/
|
||||
fromStaticRenderable(
|
||||
renderable: ITextureRenderable,
|
||||
renderAnimatedWith(
|
||||
tile: IMaterialFramedData,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null;
|
||||
|
||||
/**
|
||||
* 通过动画可渲染对象(由 {@link ITexture.dynamic} 或 {@link ITexture.cycled} 输出的单个可渲染对象)
|
||||
* 输出自动元件经过连接的可渲染对象
|
||||
* @param renderable 自动元件的原始可渲染对象
|
||||
* @param connection 自动元件的连接方式
|
||||
* @returns 这一帧的可渲染对象
|
||||
*/
|
||||
fromAnimatedRenderable(
|
||||
renderable: ITextureRenderable,
|
||||
connection: number
|
||||
): IAutotileRenderable | null;
|
||||
|
||||
/**
|
||||
* 通过动画生成器(由 {@link ITexture.dynamic} 或 {@link ITexture.cycled} 输出的生成器)
|
||||
* 输出自动元件经过连接的可渲染对象生成器
|
||||
* @param texture 生成动画的纹理对象
|
||||
* @param generator 自动元件的动画生成器
|
||||
* @param connection 自动元件的连接方式
|
||||
* @returns 生成器,每一个输出代表每一帧的渲染对象
|
||||
*/
|
||||
fromAnimatedGenerator(
|
||||
texture: ITexture,
|
||||
generator: Generator<ITextureRenderable> | null,
|
||||
connection: number
|
||||
): Generator<IAutotileRenderable, void> | null;
|
||||
): Generator<ITextureRenderable, void>;
|
||||
}
|
||||
|
||||
export interface IMaterialGetter {
|
||||
@ -198,7 +203,7 @@ export interface IMaterialGetter {
|
||||
* 根据图块数字获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
|
||||
* @param identifier 图块的图块数字
|
||||
*/
|
||||
getTile(identifier: number): ITexture | null;
|
||||
getTile(identifier: number): IMaterialFramedData | null;
|
||||
|
||||
/**
|
||||
* 根据图块标识符获取图块类型
|
||||
@ -216,14 +221,14 @@ export interface IMaterialGetter {
|
||||
* 根据图块标识符获取一个图块的 `bigImage` 贴图
|
||||
* @param identifier 图块标识符,即图块数字
|
||||
*/
|
||||
getBigImage(identifier: number): ITexture | null;
|
||||
getBigImage(identifier: number): IMaterialFramedData | null;
|
||||
|
||||
/**
|
||||
* 根据图块标识符,首先判断是否是 `bigImage` 贴图,如果是,则返回 `bigImage` 贴图,
|
||||
* 否则返回普通贴图。如果图块不存在,则返回 `null`
|
||||
* @param identifier 图块标识符,即图块数字
|
||||
*/
|
||||
getIfBigImage(identifier: number): ITexture | null;
|
||||
getIfBigImage(identifier: number): IMaterialFramedData | null;
|
||||
|
||||
/**
|
||||
* 根据标识符获取图集信息
|
||||
@ -249,7 +254,7 @@ export interface IMaterialAliasGetter {
|
||||
* 根据图块 id 获取图块,可以获取额外素材,会自动将未缓存的额外素材缓存
|
||||
* @param alias 图块 id
|
||||
*/
|
||||
getTileByAlias(alias: string): ITexture | null;
|
||||
getTileByAlias(alias: string): IMaterialFramedData | null;
|
||||
|
||||
/**
|
||||
* 根据额外素材名称获取额外素材
|
||||
@ -279,7 +284,7 @@ export interface IMaterialAliasGetter {
|
||||
* 根据图块别名获取一个图块的 `bigImage` 贴图
|
||||
* @param alias 图块别名,即图块的 id
|
||||
*/
|
||||
getBigImageByAlias(alias: string): ITexture | null;
|
||||
getBigImageByAlias(alias: string): IMaterialFramedData | null;
|
||||
}
|
||||
|
||||
export interface IMaterialManager
|
||||
@ -330,11 +335,12 @@ export interface IMaterialManager
|
||||
* 添加自动元件
|
||||
* @param source 图像源
|
||||
* @param identifier 自动元件的字符串 id 及图块数字
|
||||
* @returns 由于自动元件是懒加载的,因此不会返回任何东西
|
||||
*/
|
||||
addAutotile(
|
||||
source: SizedCanvasImageSource,
|
||||
identifier: IBlockIdentifier
|
||||
): IMaterialData;
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 添加一个 tileset 类型的素材
|
||||
@ -357,7 +363,7 @@ export interface IMaterialManager
|
||||
): IMaterialData;
|
||||
|
||||
/**
|
||||
* 缓存某个 tileset
|
||||
* 缓存某个 tileset,当需要缓存多个时,请使用 {@link cacheTilesetList} 方法
|
||||
* @param identifier tileset 的标识符,即图块数字
|
||||
*/
|
||||
cacheTileset(identifier: number): ITexture | null;
|
||||
@ -370,6 +376,20 @@ export interface IMaterialManager
|
||||
identifierList: Iterable<number>
|
||||
): Iterable<ITexture | null>;
|
||||
|
||||
/**
|
||||
* 缓存某个自动元件,当需要缓存多个时,请使用 {@link cacheAutotileList} 方法
|
||||
* @param identifier 自动元件标识符,即图块数字
|
||||
*/
|
||||
cacheAutotile(identifier: number): ITexture | null;
|
||||
|
||||
/**
|
||||
* 缓存一系列自动元件
|
||||
* @param identifierList 自动元件标识符列表,即图块数字列表
|
||||
*/
|
||||
cacheAutotileList(
|
||||
identifierList: Iterable<number>
|
||||
): Iterable<ITexture | null>;
|
||||
|
||||
/**
|
||||
* 把常用素材打包成为图集形式供后续使用
|
||||
*/
|
||||
@ -403,8 +423,25 @@ export interface IMaterialManager
|
||||
* 设置一个图块的 `bigImage` 贴图,即大怪物贴图,但不止怪物能用
|
||||
* @param identifier 图块标识符,即图块数字
|
||||
* @param image `bigImage` 对应的贴图对象
|
||||
* @param frames `bigImage` 的帧数,即贴图有多少帧
|
||||
*/
|
||||
setBigImage(identifier: number, image: ITexture): IBigImageData;
|
||||
setBigImage(
|
||||
identifier: number,
|
||||
image: ITexture,
|
||||
frames: number
|
||||
): IBigImageReturn;
|
||||
|
||||
/**
|
||||
* 当前的所有图集中是否包含指定的贴图对象
|
||||
* @param texture 贴图对象
|
||||
*/
|
||||
assetContainsTexture(texture: ITexture): boolean;
|
||||
|
||||
/**
|
||||
* 获取指定贴图对象所属的图集索引
|
||||
* @param texture 贴图对象
|
||||
*/
|
||||
getTextureAsset(texture: ITexture): number | undefined;
|
||||
}
|
||||
|
||||
export interface IAssetBuilder {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ITexture } from '@motajs/render-assets';
|
||||
import { BlockCls } from './types';
|
||||
|
||||
export function getClsByString(cls: Cls): BlockCls {
|
||||
@ -24,3 +25,23 @@ export function getClsByString(cls: Cls): BlockCls {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import { createViewport } from './viewport';
|
||||
import { Icon, Winskin } from './misc';
|
||||
import { Animate } from './animate';
|
||||
import { createItemDetail } from './itemDetail';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MapRender } from '../map/element';
|
||||
|
||||
export function createElements() {
|
||||
createCache();
|
||||
@ -66,6 +68,18 @@ export function createElements() {
|
||||
return new Animate();
|
||||
});
|
||||
tagMap.register('icon', standardElementNoCache(Icon));
|
||||
tagMap.register('map-render', (_0, _1, props) => {
|
||||
if (!props) {
|
||||
logger.error(42);
|
||||
return new MapRender([]);
|
||||
}
|
||||
const { layerList } = props;
|
||||
if (!layerList) {
|
||||
logger.error(42);
|
||||
return new MapRender([]);
|
||||
}
|
||||
return new MapRender(layerList);
|
||||
});
|
||||
}
|
||||
|
||||
export * from './animate';
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
import { EAnimateEvent } from './animate';
|
||||
import { EIconEvent, EWinskinEvent } from './misc';
|
||||
import { IEnemyCollection } from '@motajs/types';
|
||||
import { IRenderLayerData } from '../map/element';
|
||||
|
||||
export interface AnimateProps extends BaseProps {}
|
||||
|
||||
@ -60,6 +61,10 @@ export interface LayerProps extends BaseProps {
|
||||
ex?: readonly ILayerRenderExtends[];
|
||||
}
|
||||
|
||||
export interface MapRenderProps extends BaseProps {
|
||||
layerList: IRenderLayerData;
|
||||
}
|
||||
|
||||
declare module 'vue/jsx-runtime' {
|
||||
namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
|
||||
31
packages-user/client-modules/src/render/map/asset.ts
Normal file
31
packages-user/client-modules/src/render/map/asset.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { SizedCanvasImageSource } from '@motajs/render-assets';
|
||||
import {
|
||||
IMaterialGetter,
|
||||
IMaterialManager,
|
||||
materials
|
||||
} from '@user/client-base';
|
||||
import { IMapAssetData, IMapAssetManager } from './types';
|
||||
import { PrivateListDirtyTracker } from '@motajs/common';
|
||||
|
||||
export class MapAssetManager implements IMapAssetManager {
|
||||
materials: IMaterialManager = materials;
|
||||
|
||||
generateAsset(): IMapAssetData {
|
||||
const data = new MapAssetData();
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MapAssetData
|
||||
extends PrivateListDirtyTracker<number>
|
||||
implements IMapAssetData
|
||||
{
|
||||
sourceList: ImageBitmap[] = [];
|
||||
skipRef: Map<SizedCanvasImageSource, number> = new Map();
|
||||
materials: IMaterialGetter = materials;
|
||||
|
||||
constructor() {
|
||||
super(0);
|
||||
}
|
||||
}
|
||||
347
packages-user/client-modules/src/render/map/block.ts
Normal file
347
packages-user/client-modules/src/render/map/block.ts
Normal file
@ -0,0 +1,347 @@
|
||||
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 = 0;
|
||||
height: number = 0;
|
||||
|
||||
/** 分块映射 */
|
||||
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 = y * this.width + x;
|
||||
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.splitBlockWidth = config.blockWidth;
|
||||
this.splitBlockHeight = config.blockHeight;
|
||||
this.splitDataWidth = config.dataWidth;
|
||||
this.splitBlockHeight = config.dataHeight;
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
114
packages-user/client-modules/src/render/map/element.ts
Normal file
114
packages-user/client-modules/src/render/map/element.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import {
|
||||
MotaOffscreenCanvas2D,
|
||||
RenderItem,
|
||||
Transform
|
||||
} from '@motajs/render-core';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
import { IMapRenderer } from './types';
|
||||
import { MapRenderer } from './renderer';
|
||||
import { materials } from '@user/client-base';
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
|
||||
export interface IRenderLayerData {
|
||||
/** 图层对象 */
|
||||
readonly layer: IMapLayer;
|
||||
/** 图层纵深 */
|
||||
readonly zIndex: number;
|
||||
/** 图层别名 */
|
||||
readonly alias?: string;
|
||||
}
|
||||
|
||||
export class MapRender extends RenderItem {
|
||||
/** 地图渲染器 */
|
||||
readonly renderer: IMapRenderer;
|
||||
/** 地图视角变换矩阵 */
|
||||
readonly camera: Transform = new Transform();
|
||||
|
||||
/** 地图画布 */
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
/** 画布上下文 */
|
||||
readonly gl: WebGL2RenderingContext;
|
||||
|
||||
constructor(layerList: Iterable<IRenderLayerData>) {
|
||||
super('static');
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
const gl = this.canvas.getContext('webgl2')!;
|
||||
this.gl = gl;
|
||||
|
||||
this.renderer = new MapRenderer(materials, this.gl, this.camera);
|
||||
for (const layer of layerList) {
|
||||
this.renderer.addLayer(layer.layer, layer.alias);
|
||||
this.renderer.setZIndex(layer.layer, layer.zIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图层列表
|
||||
* @param layerList 图层列表
|
||||
*/
|
||||
updateLayerList(layerList: Iterable<IRenderLayerData>) {
|
||||
this.renderer
|
||||
.getSortedLayer()
|
||||
.forEach(v => this.renderer.removeLayer(v));
|
||||
for (const layer of layerList) {
|
||||
this.renderer.addLayer(layer.layer, layer.alias);
|
||||
this.renderer.setZIndex(layer.layer, layer.zIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private sizeGL(width: number, height: number) {
|
||||
const ratio = this.highResolution ? devicePixelRatio : 1;
|
||||
const scale = ratio * this.scale;
|
||||
this.canvas.width = width * scale;
|
||||
this.canvas.height = height * scale;
|
||||
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
updateTransform(transform: Transform): void {
|
||||
super.updateTransform(transform);
|
||||
if (transform === this.camera) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(canvas: MotaOffscreenCanvas2D): void {
|
||||
console.time('map-element-render');
|
||||
this.renderer.render(this.gl);
|
||||
|
||||
canvas.ctx.drawImage(
|
||||
this.canvas,
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
);
|
||||
console.timeEnd('map-element-render');
|
||||
}
|
||||
|
||||
patchProp(
|
||||
key: string,
|
||||
prevValue: any,
|
||||
nextValue: any,
|
||||
namespace?: ElementNamespace,
|
||||
parentComponent?: ComponentInternalInstance | null
|
||||
): void {
|
||||
switch (key) {
|
||||
case 'layerList': {
|
||||
this.updateLayerList(nextValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
|
||||
}
|
||||
}
|
||||
3
packages-user/client-modules/src/render/map/index.ts
Normal file
3
packages-user/client-modules/src/render/map/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './asset';
|
||||
export * from './renderer';
|
||||
export * from './types';
|
||||
229
packages-user/client-modules/src/render/map/moving.ts
Normal file
229
packages-user/client-modules/src/render/map/moving.ts
Normal file
@ -0,0 +1,229 @@
|
||||
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';
|
||||
|
||||
export interface IMovingRenderer {
|
||||
/** 素材管理器 */
|
||||
readonly manager: IMaterialManager;
|
||||
/** 顶点数组生成器 */
|
||||
readonly vertex: IMapVertexGenerator;
|
||||
|
||||
/**
|
||||
* 获得当前时间戳
|
||||
*/
|
||||
getTimestamp(): number;
|
||||
|
||||
/**
|
||||
* 删除指定的移动图块对象
|
||||
* @param block 移动图块对象
|
||||
*/
|
||||
deleteMoving(block: IMovingBlock): void;
|
||||
}
|
||||
|
||||
export class MovingBlock implements IMovingBlock {
|
||||
readonly texture: IMaterialFramedData;
|
||||
readonly tile: number;
|
||||
readonly renderer: IMovingRenderer;
|
||||
readonly index: number;
|
||||
readonly layer: IMapLayer;
|
||||
|
||||
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 = false;
|
||||
|
||||
/** 兑现函数 */
|
||||
private promiseFunc: () => void = () => {};
|
||||
|
||||
constructor(
|
||||
renderer: IMovingRenderer & IMapRenderer,
|
||||
index: number,
|
||||
layer: IMapLayer,
|
||||
block: number | IMaterialFramedData,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
this.renderer = renderer;
|
||||
this.index = index;
|
||||
this.layer = layer;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
lineTo(
|
||||
x: number,
|
||||
y: number,
|
||||
time: number,
|
||||
timing?: TimingFn
|
||||
): Promise<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;
|
||||
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> {
|
||||
this.time = time;
|
||||
this.line = false;
|
||||
this.relative = false;
|
||||
this.startX = this.x;
|
||||
this.startY = this.y;
|
||||
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> {
|
||||
this.time = time;
|
||||
this.line = false;
|
||||
this.relative = false;
|
||||
this.startX = this.x;
|
||||
this.startY = this.y;
|
||||
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) 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 false;
|
||||
} 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 false;
|
||||
} 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;
|
||||
}
|
||||
|
||||
enableFrameAnimate(): void {
|
||||
this.renderer.vertex.enableDynamicFrameAnimate(this);
|
||||
}
|
||||
|
||||
disableFrameAnimate(): void {
|
||||
this.renderer.vertex.disableDynamicFrameAnimate(this);
|
||||
}
|
||||
|
||||
setAlpha(alpha: number): void {
|
||||
this.renderer.vertex.setDynamicAlpha(this, alpha);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.renderer.deleteMoving(this);
|
||||
}
|
||||
}
|
||||
1488
packages-user/client-modules/src/render/map/renderer.ts
Normal file
1488
packages-user/client-modules/src/render/map/renderer.ts
Normal file
File diff suppressed because it is too large
Load Diff
11
packages-user/client-modules/src/render/map/shader/back.frag
Normal file
11
packages-user/client-modules/src/render/map/shader/back.frag
Normal file
@ -0,0 +1,11 @@
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec3 v_texCoord;
|
||||
out vec4 outColor;
|
||||
|
||||
uniform sampler2DArray u_sampler;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_sampler, v_texCoord);
|
||||
}
|
||||
18
packages-user/client-modules/src/render/map/shader/back.vert
Normal file
18
packages-user/client-modules/src/render/map/shader/back.vert
Normal file
@ -0,0 +1,18 @@
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec2 a_position;
|
||||
in vec2 a_texCoord;
|
||||
|
||||
out vec3 v_texCoord;
|
||||
|
||||
uniform float u_nowFrame;
|
||||
uniform mat3 u_transform;
|
||||
|
||||
void main() {
|
||||
// 背景图永远是全图都画,因此变换矩阵应该作用于纹理坐标
|
||||
vec3 texCoord = vec3(a_texCoord, 1.0);
|
||||
vec3 transformed = u_transform * texCoord;
|
||||
v_texCoord = vec3(transformed.xy, u_nowFrame);
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
14
packages-user/client-modules/src/render/map/shader/map.frag
Normal file
14
packages-user/client-modules/src/render/map/shader/map.frag
Normal file
@ -0,0 +1,14 @@
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
precision mediump uint;
|
||||
|
||||
in vec4 v_texCoord;
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
uniform sampler2DArray u_sampler;
|
||||
|
||||
void main() {
|
||||
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
|
||||
outColor = vec4(texColor.rgb, texColor.a * v_texCoord.a);
|
||||
}
|
||||
26
packages-user/client-modules/src/render/map/shader/map.vert
Normal file
26
packages-user/client-modules/src/render/map/shader/map.vert
Normal file
@ -0,0 +1,26 @@
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
precision mediump uint;
|
||||
|
||||
in vec3 a_position;
|
||||
in vec3 a_texCoord;
|
||||
// x: 最大帧数,y: 偏移池索引,实例化绘制传入
|
||||
in ivec2 a_offsetData;
|
||||
// 不透明度,用于前景层虚化
|
||||
in float a_alpha;
|
||||
|
||||
// x,y,z: 纹理坐标,w: 不透明度
|
||||
out vec4 v_texCoord;
|
||||
|
||||
uniform vec2 u_offsetPool[$1];
|
||||
uniform uint u_nowFrame;
|
||||
uniform mat3 u_transform;
|
||||
|
||||
void main() {
|
||||
// 偏移量
|
||||
uint offset = mod(u_nowFrame, a_offsetData.x);
|
||||
float fOffset = float(offset);
|
||||
// 贴图坐标
|
||||
v_texCoord = vec4(a_texCoord.xy + u_offsetPool[a_offsetData.y] * fOffset, a_texCoord.z, a_alpha);
|
||||
gl_Position = vec4(u_transform * a_position, 1.0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1243
packages-user/client-modules/src/render/map/vertex.ts
Normal file
1243
packages-user/client-modules/src/render/map/vertex.ts
Normal file
File diff suppressed because it is too large
Load Diff
105
packages-user/client-modules/src/render/map/viewport.ts
Normal file
105
packages-user/client-modules/src/render/map/viewport.ts
Normal file
@ -0,0 +1,105 @@
|
||||
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;
|
||||
}
|
||||
|
||||
getRenderArea(): IMapRenderData {
|
||||
const { cellWidth, cellHeight, renderWidth, renderHeight } =
|
||||
this.renderer;
|
||||
const { blockWidth, blockHeight, width, height } = this.vertex.block;
|
||||
// 减一是因为第一个像素是 0,所以最后一个像素就是宽度减一
|
||||
const r = renderWidth - 1;
|
||||
const b = renderHeight - 1;
|
||||
// 其实只需要算左上角和右下角就行了
|
||||
const [left, top] = this.transform.transformed(0, 0);
|
||||
const [right, bottom] = this.transform.transformed(r, b);
|
||||
const cl = left / cellWidth;
|
||||
const ct = top / cellHeight;
|
||||
const cr = right / cellWidth;
|
||||
const cb = bottom / 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++) {
|
||||
const first = this.vertex.block.getBlockByLoc(blockLeft, ny)!;
|
||||
const last = this.vertex.block.getBlockByLoc(blockRight, ny)!;
|
||||
if (first.data.dirty) {
|
||||
blockList.push(first);
|
||||
}
|
||||
if (last.data.dirty) {
|
||||
blockList.push(last);
|
||||
}
|
||||
renderArea.push({
|
||||
startIndex: first.data.startIndex,
|
||||
endIndex: last.data.endIndex,
|
||||
count: last.data.endIndex - first.data.startIndex
|
||||
});
|
||||
for (let nx = blockLeft + 1; nx < blockRight; nx++) {
|
||||
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
|
||||
if (block.data.dirty) {
|
||||
blockList.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockList.length > 0) {
|
||||
if (blockList.length === 1) {
|
||||
const block = blockList[0];
|
||||
updateArea.push(block.data);
|
||||
} else {
|
||||
let continuousStart: IBlockData<IMapVertexBlock> = blockList[0];
|
||||
let continuousLast: IBlockData<IMapVertexBlock> = blockList[0];
|
||||
for (let i = 1; i < blockList.length; i++) {
|
||||
const block = blockList[i];
|
||||
if (block.index === continuousLast.index + 1) {
|
||||
// 连续则合并
|
||||
continuousLast = block;
|
||||
} else {
|
||||
const start = continuousStart.data.startIndex;
|
||||
const end = continuousLast.data.endIndex;
|
||||
updateArea.push({
|
||||
startIndex: start,
|
||||
endIndex: end,
|
||||
count: end - start
|
||||
});
|
||||
continuousStart = block;
|
||||
continuousLast = block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
render: renderArea,
|
||||
dirty: updateArea,
|
||||
blockList
|
||||
};
|
||||
}
|
||||
|
||||
bindTransform(transform: Transform): void {
|
||||
this.transform = transform;
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,20 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
|
||||
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
|
||||
/** 地图高度的一半 */
|
||||
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
|
||||
/**
|
||||
* 动态内容预留,不明白含义的话不要动。地图上所有正在移动的图块称为动态内容,这些内容的数量无法预测,因此需要预留数组大小,
|
||||
* 如果不够再临时扩充。如果你的塔中有大量的移动操作,可以适当提高此值,避免频繁的内存扩充行为,可以一定程度上提高性能表现。
|
||||
*/
|
||||
export const DYNAMIC_RESERVE = 16;
|
||||
/**
|
||||
* 移动图块容忍度,不明白含义的话不要动。如果移动图块的数量长期小于当前预留数量,那么将会降低预留数量,提升性能表现。
|
||||
* 调整此值可以调整频率,值越大,越不容易因为数量小于预留数量而减小预留。
|
||||
*/
|
||||
export const MOVING_TOLERANCE = 60;
|
||||
/** 每个格子的默认宽度,现阶段用处不大 */
|
||||
export const CELL_WIDTH = 32;
|
||||
/** 每个格子的默认高度,现阶段用处不大 */
|
||||
export const CELL_HEIGHT = 32;
|
||||
|
||||
//#region 状态栏
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ import {
|
||||
LayerGroup
|
||||
} from '../elements';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { materials } from '@user/client-base';
|
||||
|
||||
const MainScene = defineComponent(() => {
|
||||
//#region 基本定义
|
||||
@ -267,8 +268,25 @@ const MainScene = defineComponent(() => {
|
||||
core.doRegisteredAction('onmove', bx, by, ev.offsetX, ev.offsetY);
|
||||
};
|
||||
|
||||
const testRender = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const tileset = materials.getAsset(0);
|
||||
if (!tileset) return;
|
||||
canvas.ctx.drawImage(
|
||||
tileset.data.texture.source,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container id="main-scene" width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
||||
<sprite
|
||||
render={testRender}
|
||||
loc={[180, 0, 480, 480]}
|
||||
zIndex={1000}
|
||||
/>
|
||||
<LeftStatusBar
|
||||
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
||||
status={leftStatus}
|
||||
|
||||
10
packages-user/data-state/src/core/core.ts
Normal file
10
packages-user/data-state/src/core/core.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { LayerState } from './layerState';
|
||||
import { ICoreState, ILayerState } from './types';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
readonly layer: ILayerState;
|
||||
|
||||
constructor() {
|
||||
this.layer = new LayerState();
|
||||
}
|
||||
}
|
||||
22
packages-user/data-state/src/core/index.ts
Normal file
22
packages-user/data-state/src/core/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { CoreState } from './core';
|
||||
|
||||
export function createCoreState() {
|
||||
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');
|
||||
}
|
||||
|
||||
export const state = new CoreState();
|
||||
|
||||
export * from './core';
|
||||
export * from './layerState';
|
||||
export * from './types';
|
||||
60
packages-user/data-state/src/core/layerState.ts
Normal file
60
packages-user/data-state/src/core/layerState.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { IMapLayer, MapLayer } from '../map';
|
||||
import { ILayerState } from './types';
|
||||
|
||||
export class LayerState implements ILayerState {
|
||||
readonly layerList: WeakSet<IMapLayer> = new WeakSet();
|
||||
/** 图层到图层别名映射 */
|
||||
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
|
||||
/** 图层别名到图层的映射 */
|
||||
readonly aliasLayerMap: WeakMap<symbol, IMapLayer> = new WeakMap();
|
||||
|
||||
addLayer(width: number, height: number): IMapLayer {
|
||||
const array = new Uint32Array(width * height);
|
||||
const layer = new MapLayer(array, width, height);
|
||||
this.layerList.add(layer);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
): void {
|
||||
if (keepBlock) {
|
||||
layer.resize(width, height);
|
||||
} else {
|
||||
layer.resize2(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
packages-user/data-state/src/core/types.ts
Normal file
57
packages-user/data-state/src/core/types.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { IMapLayer } from '../map';
|
||||
|
||||
export interface ILayerState {
|
||||
/** 地图列表 */
|
||||
readonly layerList: WeakSet<IMapLayer>;
|
||||
|
||||
/**
|
||||
* 添加图层
|
||||
* @param width 地图宽度
|
||||
* @param height 地图高度
|
||||
*/
|
||||
addLayer(width: number, height: number): IMapLayer;
|
||||
|
||||
/**
|
||||
* 移除指定图层
|
||||
* @param layer 图层对象
|
||||
*/
|
||||
removeLayer(layer: IMapLayer): void;
|
||||
|
||||
/**
|
||||
* 设置图层别名
|
||||
* @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;
|
||||
}
|
||||
|
||||
export interface ICoreState {
|
||||
/** 地图状态 */
|
||||
readonly layer: ILayerState;
|
||||
}
|
||||
@ -1,9 +1,17 @@
|
||||
import { loading } from '@user/data-base';
|
||||
import { createMechanism } from './mechanism';
|
||||
import { createCoreState } from './core';
|
||||
|
||||
export function create() {
|
||||
createMechanism();
|
||||
loading.once('loaded', () => {
|
||||
// 加载后初始化全局状态
|
||||
createCoreState();
|
||||
});
|
||||
}
|
||||
|
||||
export * from './core';
|
||||
export * from './enemy';
|
||||
export * from './map';
|
||||
export * from './mechanism';
|
||||
export * from './state';
|
||||
|
||||
2
packages-user/data-state/src/map/index.ts
Normal file
2
packages-user/data-state/src/map/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './mapLayer';
|
||||
export * from './types';
|
||||
260
packages-user/data-state/src/map/mapLayer.ts
Normal file
260
packages-user/data-state/src/map/mapLayer.ts
Normal file
@ -0,0 +1,260 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import {
|
||||
IMapLayer,
|
||||
IMapLayerData,
|
||||
IMapLayerExtends,
|
||||
IMapLayerExtendsController
|
||||
} from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
interface IExtendsData {
|
||||
readonly ex: IMapLayerExtends;
|
||||
readonly controller: IMapLayerExtendsController;
|
||||
}
|
||||
|
||||
export class MapLayer implements IMapLayer {
|
||||
width: number;
|
||||
height: number;
|
||||
empty: boolean = true;
|
||||
|
||||
/** 已经加载完毕的图层拓展 */
|
||||
private loadedExtends: Set<IExtendsData> = new Set();
|
||||
/** 添加的图层拓展 */
|
||||
private addedExtends: Map<string, IExtendsData> = new Map();
|
||||
|
||||
/** 地图图块数组 */
|
||||
private mapArray: Uint32Array;
|
||||
/** 地图数据引用 */
|
||||
private mapData: IMapLayerData;
|
||||
|
||||
constructor(array: Uint32Array, width: number, height: number) {
|
||||
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) {
|
||||
this.loadedExtends.forEach(v => {
|
||||
v.ex.onResize?.(v.controller, width, 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.loadedExtends.forEach(v => {
|
||||
v.ex.onResize?.(v.controller, width, height);
|
||||
});
|
||||
}
|
||||
|
||||
resize2(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
this.mapArray.fill(0);
|
||||
this.loadedExtends.forEach(v => {
|
||||
v.ex.onResize?.(v.controller, width, height);
|
||||
});
|
||||
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.loadedExtends.forEach(v => {
|
||||
v.ex.onResize?.(v.controller, width, height);
|
||||
});
|
||||
this.empty = true;
|
||||
}
|
||||
|
||||
setBlock(block: number, x: number, y: number): void {
|
||||
const index = y * this.width + x;
|
||||
this.mapArray[index] = block;
|
||||
this.loadedExtends.forEach(v => {
|
||||
v.ex.onUpdateBlock?.(v.controller, 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);
|
||||
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.loadedExtends.forEach(v => {
|
||||
v.ex.onUpdateArea?.(v.controller, 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;
|
||||
}
|
||||
|
||||
loadExtends(ex: IMapLayerExtends): boolean {
|
||||
if (!this.addedExtends.has(ex.id)) return false;
|
||||
ex.awake?.();
|
||||
const data = this.addedExtends.get(ex.id)!;
|
||||
this.loadedExtends.add(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
addExtends(ex: IMapLayerExtends): IMapLayerExtendsController {
|
||||
const controller = new MapLayerExtendsController(this, ex);
|
||||
this.addedExtends.set(ex.id, {
|
||||
ex,
|
||||
controller
|
||||
});
|
||||
return controller;
|
||||
}
|
||||
|
||||
removeExtends(ex: IMapLayerExtends | string): void {
|
||||
const id = typeof ex === 'string' ? ex : ex.id;
|
||||
const data = this.addedExtends.get(id);
|
||||
if (!data) return;
|
||||
data.ex.destroy?.();
|
||||
this.addedExtends.delete(id);
|
||||
this.loadedExtends.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
class MapLayerExtendsController implements IMapLayerExtendsController {
|
||||
loaded: boolean = false;
|
||||
|
||||
constructor(
|
||||
readonly layer: MapLayer,
|
||||
readonly ex: IMapLayerExtends
|
||||
) {}
|
||||
|
||||
load(): void {
|
||||
this.loaded = this.layer.loadExtends(this.ex);
|
||||
}
|
||||
|
||||
getMapData(): Readonly<IMapLayerData> {
|
||||
return this.layer.getMapRef();
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
this.layer.removeExtends(this.ex);
|
||||
this.loaded = false;
|
||||
}
|
||||
}
|
||||
167
packages-user/data-state/src/map/types.ts
Normal file
167
packages-user/data-state/src/map/types.ts
Normal file
@ -0,0 +1,167 @@
|
||||
export interface IMapLayerData {
|
||||
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||
expired: boolean;
|
||||
/** 地图图块数组,是对内部存储的直接引用 */
|
||||
array: Uint32Array;
|
||||
}
|
||||
|
||||
export interface IMapLayerHooks {
|
||||
/**
|
||||
* 当钩子准备完毕时执行,会自动分析依赖,并把依赖实例作为参数传入,遵循依赖列表的顺序
|
||||
* @param dependencies 依赖列表
|
||||
*/
|
||||
awake(): void;
|
||||
|
||||
/**
|
||||
* 当拓展被移除之前执行,可以用来清理相关内容
|
||||
*/
|
||||
destroy(): void;
|
||||
|
||||
/**
|
||||
* 当地图大小发生变化时执行
|
||||
* @param controller 拓展控制器
|
||||
* @param width 地图宽度
|
||||
* @param height 地图高度
|
||||
*/
|
||||
onResize(
|
||||
controller: IMapLayerExtendsController,
|
||||
width: number,
|
||||
height: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 当更新某个区域的图块时执行
|
||||
* @param controller 拓展控制器
|
||||
* @param x 更新区域左上角横坐标
|
||||
* @param y 更新区域左上角纵坐标
|
||||
* @param width 更新区域宽度
|
||||
* @param height 更新区域高度
|
||||
*/
|
||||
onUpdateArea(
|
||||
controller: IMapLayerExtendsController,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法
|
||||
* @param controller 拓展控制器
|
||||
* @param block 更新为的图块数字
|
||||
* @param x 更新点横坐标
|
||||
* @param y 更新点纵坐标
|
||||
*/
|
||||
onUpdateBlock(
|
||||
controller: IMapLayerExtendsController,
|
||||
block: number,
|
||||
x: number,
|
||||
y: number
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IMapLayerExtends extends Partial<IMapLayerHooks> {
|
||||
/** 这个拓展对象的标识符 */
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface IMapLayerExtendsController {
|
||||
/** 当前图层拓展是否已经被加载 */
|
||||
readonly loaded: boolean;
|
||||
/** 拓展所属的图层对象 */
|
||||
readonly layer: IMapLayer;
|
||||
|
||||
/**
|
||||
* 加载此图层拓展,如果拓展依赖了其他拓展并且已经添加,将会自动加载其他拓展
|
||||
*/
|
||||
load(): void;
|
||||
|
||||
/**
|
||||
* 获取地图数据,是对内部存储的直接引用
|
||||
*/
|
||||
getMapData(): Readonly<IMapLayerData>;
|
||||
|
||||
/**
|
||||
* 结束此拓展的生命周期,释放相关资源
|
||||
*/
|
||||
unload(): void;
|
||||
}
|
||||
|
||||
export interface IMapLayer {
|
||||
/** 地图宽度 */
|
||||
readonly width: number;
|
||||
/** 地图高度 */
|
||||
readonly height: number;
|
||||
/** 地图是否全部空白,此值具有保守性,即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白 */
|
||||
readonly empty: boolean;
|
||||
|
||||
/**
|
||||
* 调整地图尺寸,维持原有图块。如果尺寸变大,那么会补零,如果尺寸变小,那么会将当前数组裁剪
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 添加图层拓展,使用一系列钩子与图层本身通讯。不同图层拓展没有顺序关系。
|
||||
* @param ex 图层拓展对象
|
||||
* @returns 图层拓展控制对象,可以通过它来控制拓展的生命周期,也可以用于获取图层内的一些数据
|
||||
*/
|
||||
addExtends(ex: IMapLayerExtends): IMapLayerExtendsController;
|
||||
|
||||
/**
|
||||
* 移除指定的图层拓展对象
|
||||
* @param ex 要移除的图层拓展对象,也可以填拓展对象的标识符
|
||||
*/
|
||||
removeExtends(ex: IMapLayerExtends | string): void;
|
||||
}
|
||||
@ -1,5 +1,14 @@
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export interface ICompiledProgram {
|
||||
/** 着色器程序 */
|
||||
readonly program: WebGLProgram;
|
||||
/** 顶点着色器对象 */
|
||||
readonly vertexShader: WebGLShader;
|
||||
/** 片段着色器对象 */
|
||||
readonly fragmentShader: WebGLShader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编译着色器
|
||||
* @param gl WebGL2 上下文
|
||||
@ -61,11 +70,17 @@ export function compileProgramWith(
|
||||
gl: WebGL2RenderingContext,
|
||||
vs: string,
|
||||
fs: string
|
||||
): WebGLProgram | null {
|
||||
): 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 compileProgram(gl, vsShader, fsShader);
|
||||
return {
|
||||
program,
|
||||
vertexShader: vsShader,
|
||||
fragmentShader: fsShader
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './glUtils';
|
||||
export * from './keyCodes';
|
||||
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 { IDirtyTracker } from './types';
|
||||
|
||||
/**
|
||||
* 布尔类型的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回 `true`
|
||||
*/
|
||||
export class PrivateBooleanDirtyTracker implements IDirtyTracker<boolean> {
|
||||
/** 标记映射 */
|
||||
private markMap: WeakMap<symbol, number> = new WeakMap();
|
||||
/** 脏标记 */
|
||||
private dirtyFlag: number = 0;
|
||||
|
||||
mark(): symbol {
|
||||
const symbol = Symbol();
|
||||
this.markMap.set(symbol, this.dirtyFlag);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
unmark(mark: symbol): void {
|
||||
this.markMap.delete(mark);
|
||||
}
|
||||
|
||||
dirtySince(mark: symbol): boolean {
|
||||
const num = this.markMap.get(mark);
|
||||
if (isNil(num)) return true;
|
||||
return num < this.dirtyFlag;
|
||||
}
|
||||
|
||||
hasMark(symbol: symbol): 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<symbol, number> = new WeakMap();
|
||||
|
||||
/** 脏标记数字 */
|
||||
private dirtyFlag: number = 0;
|
||||
|
||||
constructor(protected length: number) {}
|
||||
|
||||
mark(): symbol {
|
||||
const symbol = Symbol();
|
||||
this.symbolMap.set(symbol, this.dirtyFlag);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
unmark(mark: symbol): void {
|
||||
this.symbolMap.delete(mark);
|
||||
}
|
||||
|
||||
dirtySince(mark: symbol): 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: symbol): 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<symbol, number> = new WeakMap();
|
||||
|
||||
/** 脏标记数字 */
|
||||
private dirtyFlag: number = 0;
|
||||
|
||||
constructor(protected length: number) {}
|
||||
|
||||
mark(): symbol {
|
||||
const symbol = Symbol();
|
||||
this.symbolMap.set(symbol, this.dirtyFlag);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
unmark(mark: symbol): void {
|
||||
this.symbolMap.delete(mark);
|
||||
}
|
||||
|
||||
dirtySince(mark: symbol): 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: symbol): boolean {
|
||||
return this.symbolMap.has(symbol);
|
||||
}
|
||||
|
||||
protected dirty(data: T): void {
|
||||
this.dirtyFlag++;
|
||||
this.markMap.set(data, this.dirtyFlag);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './dirtyTracker';
|
||||
export * from './logger';
|
||||
export * from './utils';
|
||||
export * from './types';
|
||||
|
||||
@ -27,6 +27,21 @@
|
||||
"25": "Unknown audio type. Header: '$1'",
|
||||
"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 layerList property of map-render element is required.",
|
||||
"1101": "Shadow extension needs 'floor-hero' extension as dependency.",
|
||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency.",
|
||||
"1301": "Portal extension need 'floor-binder' extension as dependency.",
|
||||
@ -112,6 +127,11 @@
|
||||
"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 'updateBlock' 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.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
|
||||
32
packages/common/src/types.ts
Normal file
32
packages/common/src/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface IDirtyMarker<T> {
|
||||
/**
|
||||
* 标记为脏,即进行了一次更新
|
||||
* @param data 传递给追踪器的数据
|
||||
*/
|
||||
dirty(data: T): void;
|
||||
}
|
||||
|
||||
export interface IDirtyTracker<T> {
|
||||
/**
|
||||
* 对状态进行标记
|
||||
*/
|
||||
mark(): symbol;
|
||||
|
||||
/**
|
||||
* 取消指定标记符号
|
||||
* @param mark 标记符号
|
||||
*/
|
||||
unmark(mark: symbol): void;
|
||||
|
||||
/**
|
||||
* 从指定标记符号开始,数据是否发生了变动
|
||||
* @param mark 标记符号
|
||||
*/
|
||||
dirtySince(mark: symbol): T;
|
||||
|
||||
/**
|
||||
* 当前追踪器是否包含指定标记符号
|
||||
* @param symbol 标记符号
|
||||
*/
|
||||
hasMark(symbol: symbol): boolean;
|
||||
}
|
||||
@ -1,123 +1,82 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
|
||||
|
||||
/**
|
||||
* 基于帧的动画控制器,创建时传入的参数代表帧数,生成动画时传入的参数自定义
|
||||
* 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,动画传入的参数代表帧数
|
||||
*/
|
||||
export abstract class FrameBasedAnimater<T>
|
||||
implements ITextureAnimater<number, T>
|
||||
{
|
||||
texture: ITexture<number, T> | null = null;
|
||||
|
||||
/** 动画的总帧数 */
|
||||
protected frames: number = 0;
|
||||
|
||||
create(texture: ITexture, data: number): void {
|
||||
if (this.texture) {
|
||||
logger.warn(70);
|
||||
return;
|
||||
}
|
||||
this.texture = texture;
|
||||
this.frames = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前动画控制器是否已经初始化完毕并开始生成动画
|
||||
*/
|
||||
protected check(): boolean {
|
||||
if (!this.texture || this.frames === 0) {
|
||||
logger.warn(71);
|
||||
return false;
|
||||
}
|
||||
if (this.texture.height % this.frames !== 0) {
|
||||
logger.warn(72);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract open(init: T): Generator<ITextureRenderable> | null;
|
||||
|
||||
abstract cycled(init: T): Generator<ITextureRenderable> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,创建时传入的参数代表帧数
|
||||
*/
|
||||
export class TextureRowAnimater extends FrameBasedAnimater<void> {
|
||||
*open(): Generator<ITextureRenderable> | null {
|
||||
if (!this.check()) return null;
|
||||
const renderable = this.texture!.static();
|
||||
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 } = this.texture!;
|
||||
const h = height / this.frames;
|
||||
for (let i = 0; i < this.frames; i++) {
|
||||
const { width: w, height } = texture!;
|
||||
const h = height / frames;
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const renderable: ITextureRenderable = {
|
||||
source: this.texture!.source,
|
||||
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
source: texture.source,
|
||||
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
};
|
||||
yield renderable;
|
||||
}
|
||||
}
|
||||
|
||||
*cycled(): Generator<ITextureRenderable> | null {
|
||||
if (!this.check()) return null;
|
||||
const renderable = this.texture!.static();
|
||||
*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 } = this.texture!;
|
||||
const h = height / this.frames;
|
||||
const { width: w, height } = texture;
|
||||
const h = height / frames;
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const renderable: ITextureRenderable = {
|
||||
source: this.texture!.source,
|
||||
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
source: texture.source,
|
||||
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
};
|
||||
yield renderable;
|
||||
i++;
|
||||
if (i === this.frames) i = 0;
|
||||
if (i === frames) i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,创建时传入的参数代表帧数
|
||||
* 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,动画传入的参数代表帧数
|
||||
*/
|
||||
export class TextureColumnAnimater extends FrameBasedAnimater<void> {
|
||||
*open(): Generator<ITextureRenderable> | null {
|
||||
if (!this.check()) return null;
|
||||
const renderable = this.texture!.static();
|
||||
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 } = renderable.rect;
|
||||
const { width, height: h } = this.texture!;
|
||||
const w = width / this.frames;
|
||||
for (let i = 0; i < this.frames; i++) {
|
||||
const { width, height: h } = texture!;
|
||||
const w = width / frames;
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const renderable: ITextureRenderable = {
|
||||
source: this.texture!.source,
|
||||
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
source: texture.source,
|
||||
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
};
|
||||
yield renderable;
|
||||
}
|
||||
}
|
||||
|
||||
*cycled(): Generator<ITextureRenderable> | null {
|
||||
if (!this.check()) return null;
|
||||
const renderable = this.texture!.static();
|
||||
*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 } = this.texture!;
|
||||
const w = width / this.frames;
|
||||
const { width, height: h } = texture;
|
||||
const w = width / frames;
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const renderable: ITextureRenderable = {
|
||||
source: this.texture!.source,
|
||||
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
source: texture.source,
|
||||
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
|
||||
};
|
||||
yield renderable;
|
||||
i++;
|
||||
if (i === this.frames) i = 0;
|
||||
if (i === frames) i = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IScanAnimaterCreate {
|
||||
export interface IScanAnimaterData {
|
||||
/** 每帧的宽度 */
|
||||
readonly width: number;
|
||||
/** 每帧的高度 */
|
||||
@ -130,82 +89,47 @@ export interface IScanAnimaterCreate {
|
||||
* 扫描动画控制器,会按照先从左到右,再从上到下的顺序依次输出,可以用于动画精灵图等
|
||||
*/
|
||||
export class TextureScanAnimater
|
||||
implements ITextureAnimater<IScanAnimaterCreate, void>
|
||||
implements ITextureAnimater<IScanAnimaterData>
|
||||
{
|
||||
texture: ITexture<IScanAnimaterCreate, void> | null = null;
|
||||
*once(
|
||||
texture: ITexture,
|
||||
data: IScanAnimaterData
|
||||
): Generator<ITextureRenderable, void> {
|
||||
const w = texture.width;
|
||||
const h = texture.height;
|
||||
|
||||
private width: number = 0;
|
||||
private height: number = 0;
|
||||
|
||||
private frames: number = 0;
|
||||
private frameX: number = 0;
|
||||
private frameY: number = 0;
|
||||
|
||||
create(texture: ITexture, data: IScanAnimaterCreate): void {
|
||||
if (this.texture) {
|
||||
logger.warn(70);
|
||||
return;
|
||||
}
|
||||
this.texture = texture;
|
||||
|
||||
this.width = data.width;
|
||||
this.height = data.height;
|
||||
this.frames = data.frames;
|
||||
|
||||
// 如果尺寸不匹配
|
||||
if (
|
||||
texture.width % data.width !== 0 ||
|
||||
texture.height % data.height !== 0
|
||||
) {
|
||||
logger.warn(74);
|
||||
}
|
||||
|
||||
const frameX = Math.floor(texture.width / data.width);
|
||||
const frameY = Math.floor(texture.height / data.height);
|
||||
const possibleFrames = frameX * frameY;
|
||||
|
||||
// 如果传入的帧数超出了可能的帧数上限
|
||||
if (this.frames > possibleFrames) {
|
||||
this.frames = possibleFrames;
|
||||
}
|
||||
}
|
||||
|
||||
*open(): Generator<ITextureRenderable, void> | null {
|
||||
const texture = this.texture;
|
||||
if (!texture) return null;
|
||||
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
|
||||
for (let y = 0; y < this.frameY; y++) {
|
||||
for (let x = 0; x < this.frameX; x++) {
|
||||
const data: ITextureRenderable = {
|
||||
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 data;
|
||||
yield renderable;
|
||||
frame++;
|
||||
if (frame === data.frames) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*cycled(): Generator<ITextureRenderable, void> | null {
|
||||
const texture = this.texture;
|
||||
if (!texture) return null;
|
||||
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
*cycled(
|
||||
texture: ITexture,
|
||||
data: IScanAnimaterData
|
||||
): Generator<ITextureRenderable, void> {
|
||||
const w = texture.width;
|
||||
const h = texture.height;
|
||||
|
||||
let index = 0;
|
||||
while (true) {
|
||||
const x = index % this.frameX;
|
||||
const y = Math.floor(index / this.frameX);
|
||||
const data: ITextureRenderable = {
|
||||
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 data;
|
||||
yield renderable;
|
||||
index++;
|
||||
if (index === this.frames) index = 0;
|
||||
if (index === data.frames) index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ 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 'packages/client-base/src/glUtils';
|
||||
import { compileProgramWith } from '@motajs/client-base';
|
||||
|
||||
interface IndexMarkedComposedData {
|
||||
/** 组合数据 */
|
||||
@ -68,7 +68,7 @@ export class TextureGridComposer
|
||||
const dx = x * data.width;
|
||||
const dy = y * data.height;
|
||||
const texture = tex[i + start];
|
||||
const renderable = texture.static();
|
||||
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 });
|
||||
@ -146,7 +146,7 @@ export class TextureMaxRectsComposer
|
||||
);
|
||||
const arr = [...input];
|
||||
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||
const rect = v.static().rect;
|
||||
const rect = v.render().rect;
|
||||
const toPack = new Rectangle(rect.w, rect.h);
|
||||
toPack.data = v;
|
||||
return toPack;
|
||||
@ -164,7 +164,7 @@ export class TextureMaxRectsComposer
|
||||
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.static();
|
||||
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);
|
||||
@ -229,7 +229,7 @@ export class TextureMaxRectsWebGL2Composer
|
||||
this.canvas.width = maxWidth;
|
||||
this.canvas.height = maxHeight;
|
||||
this.gl = this.canvas.getContext('webgl2')!;
|
||||
const program = compileProgramWith(this.gl, vert, frag)!;
|
||||
const { program } = compileProgramWith(this.gl, vert, frag)!;
|
||||
this.program = program;
|
||||
|
||||
// 初始化画布数据
|
||||
@ -342,7 +342,7 @@ export class TextureMaxRectsWebGL2Composer
|
||||
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.static();
|
||||
const renderable = v.data.render();
|
||||
const { width: tw, height: th } = v.data.source;
|
||||
const { x, y, w, h } = renderable.rect;
|
||||
// 画到目标画布上的位置
|
||||
@ -422,7 +422,7 @@ export class TextureMaxRectsWebGL2Composer
|
||||
);
|
||||
const arr = [...input];
|
||||
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||
const rect = v.static().rect;
|
||||
const rect = v.render().rect;
|
||||
const toPack = new Rectangle(rect.w, rect.h);
|
||||
toPack.data = v;
|
||||
return toPack;
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Texture } from './texture';
|
||||
import { ITexture, ITextureStore, SizedCanvasImageSource } from './types';
|
||||
import { ITexture, ITextureStore } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class TextureStore implements ITextureStore {
|
||||
private readonly texMap: Map<number, ITexture> = new Map();
|
||||
private readonly invMap: Map<ITexture, number> = new Map();
|
||||
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: ITexture]> {
|
||||
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
|
||||
return this.texMap.entries();
|
||||
}
|
||||
|
||||
entries(): Iterable<[key: number, tex: ITexture]> {
|
||||
entries(): Iterable<[key: number, tex: T]> {
|
||||
return this.texMap.entries();
|
||||
}
|
||||
|
||||
@ -21,15 +22,11 @@ export class TextureStore implements ITextureStore {
|
||||
return this.texMap.keys();
|
||||
}
|
||||
|
||||
values(): Iterable<ITexture> {
|
||||
values(): Iterable<T> {
|
||||
return this.texMap.values();
|
||||
}
|
||||
|
||||
createTexture(source: SizedCanvasImageSource): ITexture {
|
||||
return new Texture(source);
|
||||
}
|
||||
|
||||
addTexture(identifier: number, texture: ITexture): void {
|
||||
addTexture(identifier: number, texture: T): void {
|
||||
if (this.texMap.has(identifier)) {
|
||||
logger.warn(66, identifier.toString());
|
||||
return;
|
||||
@ -38,7 +35,7 @@ export class TextureStore implements ITextureStore {
|
||||
this.invMap.set(texture, identifier);
|
||||
}
|
||||
|
||||
private removeBy(id: number, tex: ITexture, alias?: string) {
|
||||
private removeBy(id: number, tex: T, alias?: string) {
|
||||
this.texMap.delete(id);
|
||||
this.invMap.delete(tex);
|
||||
if (alias) {
|
||||
@ -47,7 +44,7 @@ export class TextureStore implements ITextureStore {
|
||||
}
|
||||
}
|
||||
|
||||
removeTexture(identifier: number | string | ITexture): void {
|
||||
removeTexture(identifier: number | string | T): void {
|
||||
if (typeof identifier === 'string') {
|
||||
const id = this.aliasMap.get(identifier);
|
||||
if (isNil(id)) return;
|
||||
@ -67,7 +64,7 @@ export class TextureStore implements ITextureStore {
|
||||
}
|
||||
}
|
||||
|
||||
getTexture(identifier: number): ITexture | null {
|
||||
getTexture(identifier: number): T | null {
|
||||
return this.texMap.get(identifier) ?? null;
|
||||
}
|
||||
|
||||
@ -92,7 +89,7 @@ export class TextureStore implements ITextureStore {
|
||||
return this.texMap.get(id) ?? null;
|
||||
}
|
||||
|
||||
idOf(texture: ITexture): number | undefined {
|
||||
idOf(texture: T): number | undefined {
|
||||
return this.invMap.get(texture);
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
|
||||
readonly cols: number;
|
||||
|
||||
private nowIndex: number = 0;
|
||||
private outputIndex: number = 0;
|
||||
private outputIndex: number = -1;
|
||||
|
||||
private nowTexture: ITexture;
|
||||
private nowCanvas: HTMLCanvasElement;
|
||||
@ -65,7 +65,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
|
||||
const nowRow = Math.floor(index / this.cols);
|
||||
const nowCol = index % this.cols;
|
||||
|
||||
const { source, rect } = tex.static();
|
||||
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;
|
||||
@ -107,7 +107,7 @@ export class TextureMaxRectsStreamComposer
|
||||
/** Max Rects 打包器 */
|
||||
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
|
||||
|
||||
private outputIndex: number = 0;
|
||||
private outputIndex: number = -1;
|
||||
private nowTexture!: ITexture;
|
||||
|
||||
private nowCanvas!: HTMLCanvasElement;
|
||||
@ -151,7 +151,7 @@ export class TextureMaxRectsStreamComposer
|
||||
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
|
||||
const arr = [...textures];
|
||||
const rects = arr.map<MaxRectsRectangle>(v => {
|
||||
const rect = v.static().rect;
|
||||
const rect = v.render().rect;
|
||||
const toPack = new Rectangle(rect.w, rect.h);
|
||||
toPack.data = v;
|
||||
return toPack;
|
||||
@ -176,7 +176,7 @@ export class TextureMaxRectsStreamComposer
|
||||
h: v.height
|
||||
};
|
||||
this.nowMap.set(v.data, target);
|
||||
const { source, rect } = v.data.static();
|
||||
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);
|
||||
});
|
||||
|
||||
@ -2,16 +2,14 @@ import { logger } from '@motajs/common';
|
||||
import {
|
||||
IRect,
|
||||
ITexture,
|
||||
ITextureAnimater,
|
||||
ITextureComposedData,
|
||||
ITextureRenderable,
|
||||
ITextureSplitter,
|
||||
SizedCanvasImageSource
|
||||
} from './types';
|
||||
|
||||
export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
|
||||
export class Texture implements ITexture {
|
||||
source: SizedCanvasImageSource;
|
||||
animater: ITextureAnimater<T, A> | null = null;
|
||||
width: number;
|
||||
height: number;
|
||||
isBitmap: boolean = false;
|
||||
@ -85,12 +83,7 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
|
||||
return splitter.split(this, data);
|
||||
}
|
||||
|
||||
animated(animater: ITextureAnimater<T, A>, data: T): void {
|
||||
this.animater = animater;
|
||||
animater.create(this, data);
|
||||
}
|
||||
|
||||
static(): ITextureRenderable {
|
||||
render(): ITextureRenderable {
|
||||
return {
|
||||
source: this.source,
|
||||
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
|
||||
@ -112,21 +105,10 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
|
||||
};
|
||||
}
|
||||
|
||||
dynamic(data: A): Generator<ITextureRenderable, void> | null {
|
||||
if (!this.animater) return null;
|
||||
return this.animater.open(data);
|
||||
}
|
||||
|
||||
cycled(data: A): Generator<ITextureRenderable, void> | null {
|
||||
if (!this.animater) return null;
|
||||
return this.animater.cycled(data);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.source instanceof ImageBitmap) {
|
||||
this.source.close();
|
||||
}
|
||||
this.animater = null;
|
||||
}
|
||||
|
||||
toAsset(asset: ITextureComposedData): boolean {
|
||||
@ -192,7 +174,7 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
|
||||
fy: number
|
||||
): Generator<ITextureRenderable, void> | null {
|
||||
if (!animate) return null;
|
||||
const { x: ox, y: oy } = origin.static().rect;
|
||||
const { x: ox, y: oy } = origin.render().rect;
|
||||
|
||||
while (true) {
|
||||
const next = animate.next();
|
||||
|
||||
@ -66,40 +66,29 @@ export interface ITextureSplitter<T> {
|
||||
split(texture: ITexture, data: T): Generator<ITexture, void>;
|
||||
}
|
||||
|
||||
export interface ITextureAnimater<T, I> {
|
||||
/** 此动画控制器所控制的贴图 */
|
||||
readonly texture: ITexture<T, I> | null;
|
||||
|
||||
/**
|
||||
* 对一个贴图对象创建动画控制器
|
||||
* @param texture 要绑定的贴图对象
|
||||
* @param data 传递给动画控制器的参数
|
||||
*/
|
||||
create(texture: ITexture, data: T): void;
|
||||
|
||||
export interface ITextureAnimater<T> {
|
||||
/**
|
||||
* 开始动画序列
|
||||
* @param init 动画初始化参数
|
||||
* @param texture 贴图对象
|
||||
* @param data 动画初始化参数
|
||||
*/
|
||||
open(init: I): Generator<ITextureRenderable, void> | null;
|
||||
once(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
|
||||
|
||||
/**
|
||||
* 开始循环动画序列
|
||||
* @param init 动画初始化参数
|
||||
* @param texture 贴图对象
|
||||
* @param data 动画初始化参数
|
||||
*/
|
||||
cycled(init: I): Generator<ITextureRenderable, void> | null;
|
||||
cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
|
||||
}
|
||||
|
||||
export interface ITexture<T = unknown, A = unknown> {
|
||||
export interface ITexture {
|
||||
/** 贴图的图像源 */
|
||||
readonly source: SizedCanvasImageSource;
|
||||
/** 此贴图使用的动画控制器 */
|
||||
readonly animater: ITextureAnimater<T, A> | null;
|
||||
/** 贴图宽度 */
|
||||
readonly width: number;
|
||||
/** 贴图高度 */
|
||||
readonly height: number;
|
||||
|
||||
/** 当前贴图是否是完整 bitmap 图像 */
|
||||
readonly isBitmap: boolean;
|
||||
|
||||
@ -116,17 +105,10 @@ export interface ITexture<T = unknown, A = unknown> {
|
||||
*/
|
||||
split<T>(splitter: ITextureSplitter<T>, data: T): Generator<ITexture>;
|
||||
|
||||
/**
|
||||
* 将此贴图标记为可动画贴图,使用传入的动画控制器描述动画。每个贴图只能绑定一个动画控制器,反之同理
|
||||
* @param animater 动画控制器
|
||||
* @param data 传递给动画控制器的参数
|
||||
*/
|
||||
animated(animater: ITextureAnimater<T, A>, data: T): void;
|
||||
|
||||
/**
|
||||
* 获取整张图的可渲染对象
|
||||
*/
|
||||
static(): ITextureRenderable;
|
||||
render(): ITextureRenderable;
|
||||
|
||||
/**
|
||||
* 限制矩形范围至当前贴图对象范围
|
||||
@ -135,23 +117,11 @@ export interface ITexture<T = unknown, A = unknown> {
|
||||
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
|
||||
|
||||
/**
|
||||
* 获取贴图经过矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况
|
||||
* 获取贴图经过指定矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况
|
||||
* @param rect 裁剪矩形
|
||||
*/
|
||||
clipped(rect: Readonly<IRect>): ITextureRenderable;
|
||||
|
||||
/**
|
||||
* 获取一系列动画可渲染对象,不循环,按帧数依次排列
|
||||
* @param data 传递给动画控制器的初始化参数
|
||||
*/
|
||||
dynamic(data: A): Generator<ITextureRenderable, void> | null;
|
||||
|
||||
/**
|
||||
* 获取无限循环的动画可渲染对象
|
||||
* @param data 传递给动画控制器的初始化参数
|
||||
*/
|
||||
cycled(data: A): Generator<ITextureRenderable, void> | null;
|
||||
|
||||
/**
|
||||
* 释放此贴图的资源,将不能再被使用
|
||||
*/
|
||||
@ -165,13 +135,20 @@ export interface ITexture<T = unknown, A = unknown> {
|
||||
toAsset(asset: ITextureComposedData): boolean;
|
||||
}
|
||||
|
||||
export interface ITextureStore {
|
||||
[Symbol.iterator](): Iterator<[key: number, tex: ITexture]>;
|
||||
export const enum TextureOffsetDirection {
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
TopToBottom,
|
||||
BottomToTop
|
||||
}
|
||||
|
||||
export interface ITextureStore<T extends ITexture = ITexture> {
|
||||
[Symbol.iterator](): Iterator<[key: number, tex: T]>;
|
||||
|
||||
/**
|
||||
* 获取纹理对象键值对的可迭代对象
|
||||
*/
|
||||
entries(): Iterable<[key: number, tex: ITexture]>;
|
||||
entries(): Iterable<[key: number, tex: T]>;
|
||||
|
||||
/**
|
||||
* 获取纹理对象的键的可迭代对象
|
||||
@ -181,35 +158,29 @@ export interface ITextureStore {
|
||||
/**
|
||||
* 获取纹理对象的值的可迭代对象
|
||||
*/
|
||||
values(): Iterable<ITexture>;
|
||||
|
||||
/**
|
||||
* 通过图像源创建贴图对象
|
||||
* @param source 贴图使用的图像源
|
||||
*/
|
||||
createTexture(source: SizedCanvasImageSource): ITexture;
|
||||
values(): Iterable<T>;
|
||||
|
||||
/**
|
||||
* 添加一个贴图
|
||||
* @param identifier 贴图 id
|
||||
* @param texture 贴图对象
|
||||
*/
|
||||
addTexture(identifier: number, texture: ITexture): void;
|
||||
addTexture(identifier: number, texture: T): void;
|
||||
|
||||
/**
|
||||
* 移除一个贴图
|
||||
* @param identifier 要移除的贴图对象 id 或 别名 或 贴图对象
|
||||
*/
|
||||
removeTexture(identifier: number | string | ITexture): void;
|
||||
removeTexture(identifier: number | string | T): void;
|
||||
|
||||
/**
|
||||
* 根据贴图对象 id 获取贴图
|
||||
* @param identifier 贴图对象 id
|
||||
*/
|
||||
getTexture(identifier: number): ITexture | null;
|
||||
getTexture(identifier: number): T | null;
|
||||
|
||||
/**
|
||||
* 给贴图对象命名一个别名
|
||||
* 给贴图对象命名一个别名,如果贴图对象不存在也可以设置
|
||||
* @param identifier 贴图对象 id
|
||||
* @param alias 要命名的别名
|
||||
*/
|
||||
|
||||
@ -225,7 +225,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
IRenderTickerSupport,
|
||||
IRenderChildable,
|
||||
IRenderVueSupport,
|
||||
ITransformUpdatable,
|
||||
ITransformUpdatable<Transform>,
|
||||
IRenderEvent
|
||||
{
|
||||
/** 渲染的全局ticker */
|
||||
@ -671,10 +671,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
|
||||
}
|
||||
}
|
||||
|
||||
updateTransform() {
|
||||
updateTransform(transform: Transform) {
|
||||
// 更新变换矩阵时,不需要更新自身的缓存,直接调用父元素的更新即可
|
||||
this._parent?.update();
|
||||
this.emit('transform', this, this._transform);
|
||||
if (transform === this.transform) {
|
||||
this._parent?.update();
|
||||
this.emit('transform', this, this._transform);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
|
||||
|
||||
export interface ITransformUpdatable {
|
||||
updateTransform?(): void;
|
||||
export interface ITransformUpdatable<T> {
|
||||
updateTransform?(transform: T): void;
|
||||
}
|
||||
|
||||
export class Transform {
|
||||
@ -17,13 +17,13 @@ export class Transform {
|
||||
private modified: boolean = false;
|
||||
|
||||
/** 绑定的可更新元素 */
|
||||
bindedObject?: ITransformUpdatable;
|
||||
bindedObject?: ITransformUpdatable<Transform>;
|
||||
|
||||
/**
|
||||
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
|
||||
* @param obj 要绑定的对象
|
||||
*/
|
||||
bind(obj?: ITransformUpdatable) {
|
||||
bind(obj?: ITransformUpdatable<Transform>) {
|
||||
this.bindedObject = obj;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export class Transform {
|
||||
this.scaleY = 1;
|
||||
this.rad = 0;
|
||||
this.modified = false;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,7 +49,7 @@ export class Transform {
|
||||
this.scaleX *= x;
|
||||
this.scaleY *= y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ export class Transform {
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export class Transform {
|
||||
this.rad -= n * Math.PI * 2;
|
||||
}
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ export class Transform {
|
||||
this.scaleX = x;
|
||||
this.scaleY = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ export class Transform {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ export class Transform {
|
||||
mat3.rotate(this.mat, this.mat, rad - this.rad);
|
||||
this.rad = rad;
|
||||
this.modified = true;
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -138,7 +138,7 @@ export class Transform {
|
||||
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
|
||||
);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@ export class Transform {
|
||||
): this {
|
||||
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
|
||||
this.calAttributes();
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -285,13 +285,13 @@ export class Transform3D {
|
||||
mat: mat4 = mat4.create();
|
||||
|
||||
/** 绑定的可更新元素 */
|
||||
bindedObject?: ITransformUpdatable;
|
||||
bindedObject?: ITransformUpdatable<Transform3D>;
|
||||
|
||||
/**
|
||||
* 绑定可更新对象
|
||||
* @param obj 要绑定的对象
|
||||
*/
|
||||
bind(obj?: ITransformUpdatable) {
|
||||
bind(obj?: ITransformUpdatable<Transform3D>) {
|
||||
this.bindedObject = obj;
|
||||
}
|
||||
|
||||
@ -300,7 +300,7 @@ export class Transform3D {
|
||||
*/
|
||||
reset(): this {
|
||||
mat4.identity(this.mat);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -312,7 +312,7 @@ export class Transform3D {
|
||||
*/
|
||||
scale(x: number, y: number, z: number): this {
|
||||
mat4.scale(this.mat, this.mat, [x, y, z]);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -324,7 +324,7 @@ export class Transform3D {
|
||||
*/
|
||||
translate(x: number, y: number, z: number): this {
|
||||
mat4.translate(this.mat, this.mat, [x, y, z]);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -335,7 +335,7 @@ export class Transform3D {
|
||||
*/
|
||||
rotate(rad: number, axis: vec3): this {
|
||||
mat4.rotate(this.mat, this.mat, rad, axis);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -371,7 +371,7 @@ export class Transform3D {
|
||||
*/
|
||||
lookAt(eye: vec3, center: vec3, up: vec3): this {
|
||||
mat4.lookAt(this.mat, eye, center, up);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -384,7 +384,7 @@ export class Transform3D {
|
||||
*/
|
||||
perspective(fovy: number, aspect: number, near: number, far: number): this {
|
||||
mat4.perspective(this.mat, fovy, aspect, near, far);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -406,7 +406,7 @@ export class Transform3D {
|
||||
far: number
|
||||
): this {
|
||||
mat4.ortho(this.mat, left, right, bottom, top, near, far);
|
||||
this.bindedObject?.updateTransform?.();
|
||||
this.bindedObject?.updateTransform?.(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@ -25,9 +25,16 @@ import { formatSize } from './utils.js';
|
||||
'.vue',
|
||||
'.less',
|
||||
'.css',
|
||||
'.html'
|
||||
'.html',
|
||||
'.vert',
|
||||
'.frag'
|
||||
];
|
||||
|
||||
const mapExt = (ext: string) => {
|
||||
if (ext === '.vert' || ext === '.frag') return '.shader';
|
||||
else return ext;
|
||||
};
|
||||
|
||||
const check = async (dir: string) => {
|
||||
if (ignoreDir.some(v => dir.includes(v))) return;
|
||||
const d = await fs.readdir(dir);
|
||||
@ -38,7 +45,7 @@ import { formatSize } from './utils.js';
|
||||
if (exts.some(v => one.endsWith(v))) {
|
||||
const file = await fs.readFile(resolve(dir, one), 'utf-8');
|
||||
const lines = file.split('\n').length;
|
||||
const ext = extname(one);
|
||||
const ext = mapExt(extname(one));
|
||||
list[ext] ??= [0, 0, 0];
|
||||
list[ext][0]++;
|
||||
list[ext][1] += lines;
|
||||
@ -59,7 +66,7 @@ import { formatSize } from './utils.js';
|
||||
});
|
||||
for (const [ext, [file, lines, size]] of sorted) {
|
||||
console.log(
|
||||
`${ext.slice(1).padEnd(7, ' ')}files: ${file
|
||||
`${ext.slice(1).padEnd(9, ' ')}files: ${file
|
||||
.toString()
|
||||
.padEnd(6, ' ')}lines: ${lines
|
||||
.toString()
|
||||
@ -67,7 +74,7 @@ import { formatSize } from './utils.js';
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`\x1b[33mtotal files: ${totalFiles
|
||||
`\x1b[33mtotal files: ${totalFiles
|
||||
.toString()
|
||||
.padEnd(6, ' ')}lines: ${totalLines
|
||||
.toString()
|
||||
|
||||
@ -8,7 +8,7 @@ import * as glob from 'glob';
|
||||
const custom = [
|
||||
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
|
||||
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin',
|
||||
'container-custom'
|
||||
'container-custom', 'map-render'
|
||||
];
|
||||
|
||||
const aliases = glob.sync('packages/*/src').map((srcPath) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user